From b3bf148474458d49e2553e2895171746d352382c Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Wed, 14 Jan 2026 23:03:37 +0100 Subject: [PATCH] changes --- app/api/chat/title/route.ts | 68 ++++++++++++++++++++++++++++++++++ components/Chat/ChatLayout.tsx | 49 +++++++++++++++++++++++- 2 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 app/api/chat/title/route.ts diff --git a/app/api/chat/title/route.ts b/app/api/chat/title/route.ts new file mode 100644 index 0000000..b1a696f --- /dev/null +++ b/app/api/chat/title/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from 'next/server'; +import ollama from '@/lib/ollama'; + +interface Message { + role: 'user' | 'assistant'; + content: string; +} + +/** + * Generate a short, descriptive title for a chat based on the conversation + */ +export async function POST(request: NextRequest) { + try { + const { model, messages } = await request.json(); + + if (!model || !messages || !Array.isArray(messages) || messages.length === 0) { + return NextResponse.json({ error: 'Model and messages array are required' }, { status: 400 }); + } + + // Format the conversation for context + const conversationContext = messages + .map((msg: Message) => `${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}`) + .join('\n\n'); + + // Create a prompt asking for a short title with full context + const systemPrompt = `You are a helpful assistant that generates very short, descriptive titles for conversations. Based on the conversation below, generate a concise title (3-6 words maximum) that captures the main topic or intent. Reply with ONLY the title text, nothing else - no quotes, no prefixes, no explanation.`; + + const response = await ollama.chat({ + model, + messages: [ + { role: 'system', content: systemPrompt }, + { + role: 'user', + content: `Generate a short title for this conversation:\n\n${conversationContext}`, + }, + ], + options: { + temperature: 0.7, + num_predict: 25, // Limit output length + }, + }); + + // Clean up the response - remove quotes, extra whitespace, etc. + let title = response.message.content + .trim() + .replace(/^["']|["']$/g, '') // Remove surrounding quotes + .replace(/^Title:\s*/i, '') // Remove "Title:" prefix if present + .replace(/\n.*/g, '') // Only keep first line + .trim(); + + // Fallback if title is empty or too long + if (!title || title.length > 50) { + const firstUserMessage = messages.find((m: Message) => m.role === 'user')?.content || ''; + title = firstUserMessage.slice(0, 30) + (firstUserMessage.length > 30 ? '...' : ''); + } + + return NextResponse.json({ title }); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Generate title error:', error); + + // Return a fallback - don't fail the request + return NextResponse.json({ + title: 'New Chat', + error: error instanceof Error ? error.message : 'Failed to generate title', + }); + } +} diff --git a/components/Chat/ChatLayout.tsx b/components/Chat/ChatLayout.tsx index 485cfa4..6c2adb7 100644 --- a/components/Chat/ChatLayout.tsx +++ b/components/Chat/ChatLayout.tsx @@ -414,6 +414,9 @@ export default function ChatLayout() { content: fullContent, }; + // Track if this is a new chat (for title generation) + const isNewChat = !activeChatId; + // Save to both localStorage and database try { // Save user message to database @@ -427,10 +430,9 @@ export default function ChatLayout() { }); const userSaveData = await userSaveRes.json(); const savedChatId = userSaveData.chatId; - const chatTitle = userSaveData.title || userMessage.content.slice(0, 50); // Update activeChatId if this was a new chat - if (!activeChatId && savedChatId) { + if (isNewChat && savedChatId) { setActiveChatId(savedChatId); } @@ -444,6 +446,48 @@ export default function ChatLayout() { }), }); + // Generate a title for new chats using the model + let chatTitle = `${userMessage.content.slice(0, 30)}...`; // Fallback title + + if (isNewChat && selectedModel) { + try { + // Build conversation context (excluding the initial greeting) + const conversationForTitle = [...newMessages, responseMessage] + .filter((m) => !(m.role === 'assistant' && m.content.includes('How can I help'))) + .map((m) => ({ role: m.role, content: m.content })); + + const titleRes = await fetch('/api/chat/title', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: selectedModel, + messages: conversationForTitle, + }), + }); + const titleData = await titleRes.json(); + + if (titleData.title) { + chatTitle = titleData.title; + + // Update title in database + await fetch(`/api/chats/${savedChatId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: chatTitle }), + }); + + // Update title in local state + setChats((prev) => + prev.map((c) => (c.id === savedChatId ? { ...c, title: chatTitle } : c)) + ); + } + } catch (titleError) { + // eslint-disable-next-line no-console + console.error('Failed to generate title:', titleError); + // Continue with fallback title + } + } + // Save to localStorage const finalMessages = [...newMessages, responseMessage]; saveLocalChat({ @@ -456,6 +500,7 @@ export default function ChatLayout() { // Refresh chat list fetchChats(); } catch (saveError) { + // eslint-disable-next-line no-console console.error('Failed to save messages:', saveError); } } catch (e) {