Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions app/pages/chat/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,6 @@ const { messages, status, error, sendMessage, regenerate, stop } = useChat({
model: model.value
}
}),
onData: async (dataPart) => {
if (dataPart.type === 'data-chat-title') {
await refreshNuxtData('chats')
const chatsCache = useNuxtData<{ id: string, label: string }[]>('chats')
const updated = chatsCache.data.value?.find(c => c.id === data.value!.id)
if (updated && updated.label !== 'Untitled') {
title.value = updated.label
}
}
},
onError(error) {
let message = error.message
if (typeof message === 'string' && message[0] === '{') {
Expand All @@ -77,6 +67,20 @@ const { messages, status, error, sendMessage, regenerate, stop } = useChat({
}
})

// The title is generated server-side (and persisted) before streaming starts on
// the first message; there's no in-stream signal, so refresh the sidebar/title as
// soon as the response begins streaming.
watch(status, async (value) => {
if (value !== 'streaming' || title.value || !isOwner.value) return

await refreshNuxtData('chats')
const chatsCache = useNuxtData<{ id: string, label: string }[]>('chats')
const updated = chatsCache.data.value?.find(c => c.id === data.value!.id)
if (updated && updated.label !== 'Untitled') {
title.value = updated.label
}
})

async function handleSubmit(e: Event) {
e.preventDefault()
if (input.value.trim() && !uploading.value) {
Expand Down
102 changes: 45 additions & 57 deletions server/api/chats/[id].post.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { UIMessage } from 'ai'
import { convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse, generateText, isStepCount, smoothStream, streamText, toUIMessageStream } from 'ai'
import { convertToModelMessages, createUIMessageStreamResponse, generateText, isStepCount, smoothStream, streamText, toUIMessageStream } from 'ai'
import { db, schema } from 'hub:db'
import { and, eq } from 'drizzle-orm'
import { z } from 'zod'
Expand Down Expand Up @@ -72,12 +72,10 @@ export default defineEventHandler(async (event) => {
const abortController = new AbortController()
event.node.req.on('close', () => abortController.abort())

const stream = createUIMessageStream({
execute: async ({ writer }) => {
const result = streamText({
abortSignal: abortController.signal,
model,
instructions: `You are a knowledgeable and helpful AI assistant. ${session.user?.username ? `The user's name is ${session.user.username}.` : ''} Your goal is to provide clear, accurate, and well-structured responses.
const result = streamText({
abortSignal: abortController.signal,
model,
instructions: `You are a knowledgeable and helpful AI assistant. ${session.user?.username ? `The user's name is ${session.user.username}.` : ''} Your goal is to provide clear, accurate, and well-structured responses.

**FORMATTING RULES (CRITICAL):**
- ABSOLUTELY NO MARKDOWN HEADINGS: Never use #, ##, ###, ####, #####, or ######
Expand All @@ -99,58 +97,48 @@ export default defineEventHandler(async (event) => {
- Use examples when helpful
- Break down complex topics into digestible parts
- Maintain a friendly, professional tone`,
messages: await convertToModelMessages(messages),
tools: {
chart: chartTool,
weather: weatherTool,
...(model.startsWith('anthropic/') && { web_search: anthropic.tools.webSearch_20250305() }),
...(model.startsWith('openai/') && { web_search: openai.tools.webSearch() })
// TODO: enable once AI SDK supports combining provider-defined tools with custom tools
// ...(model.startsWith('google/') && { google_search: google.tools.googleSearch({}) })
},
providerOptions: {
anthropic: {
thinking: {
type: 'enabled',
budgetTokens: 2048
}
} satisfies AnthropicLanguageModelOptions,
google: {
thinkingConfig: {
includeThoughts: true,
thinkingLevel: 'low'
}
} satisfies GoogleLanguageModelOptions,
openai: {
reasoningEffort: 'low',
reasoningSummary: 'detailed'
} satisfies OpenAILanguageModelResponsesOptions
},
stopWhen: isStepCount(5),
experimental_transform: smoothStream()
})

if (!chat.title) {
writer.write({
type: 'data-chat-title',
data: { message: 'Generating title...' },
transient: true
})
}

writer.merge(toUIMessageStream({
stream: result.stream,
sendSources: true,
sendReasoning: true
}))
messages: await convertToModelMessages(messages),
tools: {
chart: chartTool,
weather: weatherTool,
...(model.startsWith('anthropic/') && { web_search: anthropic.tools.webSearch_20250305() }),
...(model.startsWith('openai/') && { web_search: openai.tools.webSearch() })
// TODO: enable once AI SDK supports combining provider-defined tools with custom tools
// ...(model.startsWith('google/') && { google_search: google.tools.googleSearch({}) })
},
providerOptions: {
anthropic: {
thinking: {
type: 'enabled',
budgetTokens: 2048
}
} satisfies AnthropicLanguageModelOptions,
google: {
thinkingConfig: {
includeThoughts: true,
thinkingLevel: 'low'
}
} satisfies GoogleLanguageModelOptions,
openai: {
reasoningEffort: 'low',
reasoningSummary: 'detailed'
} satisfies OpenAILanguageModelResponsesOptions
},
onEnd: async ({ messages }) => {
await db.insert(schema.messages).values(messages.map(message => ({
id: message.id,
stopWhen: isStepCount(5),
experimental_transform: smoothStream()
})

const stream = toUIMessageStream({
stream: result.stream,
sendSources: true,
sendReasoning: true,
onEnd: async ({ responseMessage }) => {
await db.insert(schema.messages).values([{
id: responseMessage.id,
chatId: chat.id,
role: message.role as 'user' | 'assistant',
parts: message.parts
}))).onConflictDoNothing()
role: responseMessage.role as 'user' | 'assistant',
parts: responseMessage.parts
}]).onConflictDoNothing()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
})

Expand Down