From 4c281fe09d9d188e8f9ffbfe693389c69a7bab5e Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 3 Jul 2025 02:26:34 -0700 Subject: [PATCH] chore: update to ai sdk v5 beta (#1074) --- app/(chat)/api/chat/[id]/stream/route.ts | 112 ++++++ app/(chat)/api/chat/route.ts | 207 +++-------- app/(chat)/api/chat/schema.ts | 24 +- app/(chat)/chat/[id]/page.tsx | 24 +- app/(chat)/layout.tsx | 11 +- app/(chat)/page.tsx | 4 +- artifacts/code/client.tsx | 30 +- artifacts/code/server.ts | 14 +- artifacts/image/client.tsx | 4 +- artifacts/image/server.ts | 14 +- artifacts/sheet/client.tsx | 24 +- artifacts/sheet/server.ts | 21 +- artifacts/text/client.tsx | 41 ++- artifacts/text/server.ts | 29 +- components/artifact-messages.tsx | 14 +- components/artifact.tsx | 33 +- components/chat.tsx | 72 ++-- components/create-artifact.tsx | 17 +- components/data-stream-handler.tsx | 41 +-- components/data-stream-provider.tsx | 40 +++ components/document-preview.tsx | 8 +- components/document.tsx | 15 +- components/message-actions.tsx | 4 +- components/message-editor.tsx | 31 +- components/message.tsx | 240 +++++++++---- components/messages.tsx | 20 +- components/multimodal-input.tsx | 44 ++- components/preview-attachment.tsx | 3 +- components/suggested-actions.tsx | 9 +- components/toolbar.tsx | 56 +-- hooks/use-auto-resume.ts | 30 +- hooks/use-messages.tsx | 3 +- lib/ai/models.test.ts | 37 +- lib/ai/providers.ts | 4 +- lib/ai/tools/create-document.ts | 39 ++- lib/ai/tools/get-weather.ts | 2 +- lib/ai/tools/request-suggestions.ts | 21 +- lib/ai/tools/update-document.ts | 20 +- lib/artifacts/server.ts | 13 +- lib/db/helpers/01-core-to-parts.ts | 424 ++++++++++++----------- lib/db/utils.ts | 2 +- lib/types.ts | 56 +++ lib/utils.ts | 29 +- package.json | 11 +- playwright.config.ts | 4 +- pnpm-lock.yaml | 194 +++++------ tests/e2e/artifacts.test.ts | 3 + tests/e2e/chat.test.ts | 2 + tests/e2e/session.test.ts | 1 + tests/helpers.ts | 4 +- tests/prompts/basic.ts | 131 +++---- tests/prompts/routes.ts | 33 +- tests/prompts/utils.ts | 122 ++++--- tests/routes/chat.test.ts | 41 ++- 54 files changed, 1372 insertions(+), 1060 deletions(-) create mode 100644 app/(chat)/api/chat/[id]/stream/route.ts create mode 100644 components/data-stream-provider.tsx diff --git a/app/(chat)/api/chat/[id]/stream/route.ts b/app/(chat)/api/chat/[id]/stream/route.ts new file mode 100644 index 0000000..23e8488 --- /dev/null +++ b/app/(chat)/api/chat/[id]/stream/route.ts @@ -0,0 +1,112 @@ +import { auth } from '@/app/(auth)/auth'; +import { + getChatById, + getMessagesByChatId, + getStreamIdsByChatId, +} from '@/lib/db/queries'; +import type { Chat } from '@/lib/db/schema'; +import { ChatSDKError } from '@/lib/errors'; +import type { ChatMessage } from '@/lib/types'; +import { createUIMessageStream, JsonToSseTransformStream } from 'ai'; +import { getStreamContext } from '../../route'; +import { differenceInSeconds } from 'date-fns'; + +export async function GET( + _: Request, + { params }: { params: Promise<{ id: string }> }, +) { + const { id: chatId } = await params; + + const streamContext = getStreamContext(); + const resumeRequestedAt = new Date(); + + if (!streamContext) { + return new Response(null, { status: 204 }); + } + + if (!chatId) { + return new ChatSDKError('bad_request:api').toResponse(); + } + + const session = await auth(); + + if (!session?.user) { + return new ChatSDKError('unauthorized:chat').toResponse(); + } + + let chat: Chat; + + try { + chat = await getChatById({ id: chatId }); + } catch { + return new ChatSDKError('not_found:chat').toResponse(); + } + + if (!chat) { + return new ChatSDKError('not_found:chat').toResponse(); + } + + if (chat.visibility === 'private' && chat.userId !== session.user.id) { + return new ChatSDKError('forbidden:chat').toResponse(); + } + + const streamIds = await getStreamIdsByChatId({ chatId }); + + if (!streamIds.length) { + return new ChatSDKError('not_found:stream').toResponse(); + } + + const recentStreamId = streamIds.at(-1); + + if (!recentStreamId) { + return new ChatSDKError('not_found:stream').toResponse(); + } + + const emptyDataStream = createUIMessageStream({ + execute: () => {}, + }); + + const stream = await streamContext.resumableStream(recentStreamId, () => + emptyDataStream.pipeThrough(new JsonToSseTransformStream()), + ); + + /* + * For when the generation is streaming during SSR + * but the resumable stream has concluded at this point. + */ + if (!stream) { + const messages = await getMessagesByChatId({ id: chatId }); + const mostRecentMessage = messages.at(-1); + + if (!mostRecentMessage) { + return new Response(emptyDataStream, { status: 200 }); + } + + if (mostRecentMessage.role !== 'assistant') { + return new Response(emptyDataStream, { status: 200 }); + } + + const messageCreatedAt = new Date(mostRecentMessage.createdAt); + + if (differenceInSeconds(resumeRequestedAt, messageCreatedAt) > 15) { + return new Response(emptyDataStream, { status: 200 }); + } + + const restoredStream = createUIMessageStream({ + execute: ({ writer }) => { + writer.write({ + type: 'data-appendMessage', + data: JSON.stringify(mostRecentMessage), + transient: true, + }); + }, + }); + + return new Response( + restoredStream.pipeThrough(new JsonToSseTransformStream()), + { status: 200 }, + ); + } + + return new Response(stream, { status: 200 }); +} diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 1526e96..8c61e76 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -1,8 +1,9 @@ import { - appendClientMessage, - appendResponseMessages, - createDataStream, + convertToModelMessages, + createUIMessageStream, + JsonToSseTransformStream, smoothStream, + stepCountIs, streamText, } from 'ai'; import { auth, type UserType } from '@/app/(auth)/auth'; @@ -13,11 +14,10 @@ import { getChatById, getMessageCountByUserId, getMessagesByChatId, - getStreamIdsByChatId, saveChat, saveMessages, } from '@/lib/db/queries'; -import { generateUUID, getTrailingMessageId } from '@/lib/utils'; +import { convertToUIMessages, generateUUID } from '@/lib/utils'; import { generateTitleFromUserMessage } from '../../actions'; import { createDocument } from '@/lib/ai/tools/create-document'; import { updateDocument } from '@/lib/ai/tools/update-document'; @@ -33,15 +33,16 @@ import { type ResumableStreamContext, } from 'resumable-stream'; import { after } from 'next/server'; -import type { Chat } from '@/lib/db/schema'; -import { differenceInSeconds } from 'date-fns'; import { ChatSDKError } from '@/lib/errors'; +import type { ChatMessage } from '@/lib/types'; +import type { ChatModel } from '@/lib/ai/models'; +import type { VisibilityType } from '@/components/visibility-selector'; export const maxDuration = 60; let globalStreamContext: ResumableStreamContext | null = null; -function getStreamContext() { +export function getStreamContext() { if (!globalStreamContext) { try { globalStreamContext = createResumableStreamContext({ @@ -72,8 +73,17 @@ export async function POST(request: Request) { } try { - const { id, message, selectedChatModel, selectedVisibilityType } = - requestBody; + const { + id, + message, + selectedChatModel, + selectedVisibilityType, + }: { + id: string; + message: ChatMessage; + selectedChatModel: ChatModel['id']; + selectedVisibilityType: VisibilityType; + } = requestBody; const session = await auth(); @@ -111,13 +121,8 @@ export async function POST(request: Request) { } } - const previousMessages = await getMessagesByChatId({ id }); - - const messages = appendClientMessage({ - // @ts-expect-error: todo add type conversion from DBMessage[] to UIMessage[] - messages: previousMessages, - message, - }); + const messagesFromDb = await getMessagesByChatId({ id }); + const uiMessages = [message, ...convertToUIMessages(messagesFromDb)]; const { longitude, latitude, city, country } = geolocation(request); @@ -135,7 +140,7 @@ export async function POST(request: Request) { id: message.id, role: 'user', parts: message.parts, - attachments: message.experimental_attachments ?? [], + attachments: [], createdAt: new Date(), }, ], @@ -144,13 +149,13 @@ export async function POST(request: Request) { const streamId = generateUUID(); await createStreamId({ streamId, chatId: id }); - const stream = createDataStream({ - execute: (dataStream) => { + const stream = createUIMessageStream({ + execute: ({ writer: dataStream }) => { const result = streamText({ model: myProvider.languageModel(selectedChatModel), system: systemPrompt({ selectedChatModel, requestHints }), - messages, - maxSteps: 5, + messages: convertToModelMessages(uiMessages), + stopWhen: stepCountIs(5), experimental_activeTools: selectedChatModel === 'chat-model-reasoning' ? [] @@ -161,7 +166,6 @@ export async function POST(request: Request) { 'requestSuggestions', ], experimental_transform: smoothStream({ chunking: 'word' }), - experimental_generateMessageId: generateUUID, tools: { getWeather, createDocument: createDocument({ session, dataStream }), @@ -171,42 +175,6 @@ export async function POST(request: Request) { dataStream, }), }, - onFinish: async ({ response }) => { - if (session.user?.id) { - try { - const assistantId = getTrailingMessageId({ - messages: response.messages.filter( - (message) => message.role === 'assistant', - ), - }); - - if (!assistantId) { - throw new Error('No assistant message found!'); - } - - const [, assistantMessage] = appendResponseMessages({ - messages: [message], - responseMessages: response.messages, - }); - - await saveMessages({ - messages: [ - { - id: assistantId, - chatId: id, - role: assistantMessage.role, - parts: assistantMessage.parts, - attachments: - assistantMessage.experimental_attachments ?? [], - createdAt: new Date(), - }, - ], - }); - } catch (_) { - console.error('Failed to save chat'); - } - } - }, experimental_telemetry: { isEnabled: isProductionEnvironment, functionId: 'stream-text', @@ -215,11 +183,27 @@ export async function POST(request: Request) { result.consumeStream(); - result.mergeIntoDataStream(dataStream, { - sendReasoning: true, + dataStream.merge( + result.toUIMessageStream({ + sendReasoning: true, + }), + ); + }, + generateId: generateUUID, + onFinish: async ({ messages }) => { + await saveMessages({ + messages: messages.map((message) => ({ + id: message.id, + role: message.role, + parts: message.parts, + createdAt: new Date(), + attachments: [], + chatId: id, + })), }); }, - onError: () => { + onError: (error) => { + console.log(error); return 'Oops, an error occurred!'; }, }); @@ -228,7 +212,9 @@ export async function POST(request: Request) { if (streamContext) { return new Response( - await streamContext.resumableStream(streamId, () => stream), + await streamContext.resumableStream(streamId, () => + stream.pipeThrough(new JsonToSseTransformStream()), + ), ); } else { return new Response(stream); @@ -240,101 +226,6 @@ export async function POST(request: Request) { } } -export async function GET(request: Request) { - const streamContext = getStreamContext(); - const resumeRequestedAt = new Date(); - - if (!streamContext) { - return new Response(null, { status: 204 }); - } - - const { searchParams } = new URL(request.url); - const chatId = searchParams.get('chatId'); - - if (!chatId) { - return new ChatSDKError('bad_request:api').toResponse(); - } - - const session = await auth(); - - if (!session?.user) { - return new ChatSDKError('unauthorized:chat').toResponse(); - } - - let chat: Chat; - - try { - chat = await getChatById({ id: chatId }); - } catch { - return new ChatSDKError('not_found:chat').toResponse(); - } - - if (!chat) { - return new ChatSDKError('not_found:chat').toResponse(); - } - - if (chat.visibility === 'private' && chat.userId !== session.user.id) { - return new ChatSDKError('forbidden:chat').toResponse(); - } - - const streamIds = await getStreamIdsByChatId({ chatId }); - - if (!streamIds.length) { - return new ChatSDKError('not_found:stream').toResponse(); - } - - const recentStreamId = streamIds.at(-1); - - if (!recentStreamId) { - return new ChatSDKError('not_found:stream').toResponse(); - } - - const emptyDataStream = createDataStream({ - execute: () => {}, - }); - - const stream = await streamContext.resumableStream( - recentStreamId, - () => emptyDataStream, - ); - - /* - * For when the generation is streaming during SSR - * but the resumable stream has concluded at this point. - */ - if (!stream) { - const messages = await getMessagesByChatId({ id: chatId }); - const mostRecentMessage = messages.at(-1); - - if (!mostRecentMessage) { - return new Response(emptyDataStream, { status: 200 }); - } - - if (mostRecentMessage.role !== 'assistant') { - return new Response(emptyDataStream, { status: 200 }); - } - - const messageCreatedAt = new Date(mostRecentMessage.createdAt); - - if (differenceInSeconds(resumeRequestedAt, messageCreatedAt) > 15) { - return new Response(emptyDataStream, { status: 200 }); - } - - const restoredStream = createDataStream({ - execute: (buffer) => { - buffer.writeData({ - type: 'append-message', - message: JSON.stringify(mostRecentMessage), - }); - }, - }); - - return new Response(restoredStream, { status: 200 }); - } - - return new Response(stream, { status: 200 }); -} - export async function DELETE(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get('id'); diff --git a/app/(chat)/api/chat/schema.ts b/app/(chat)/api/chat/schema.ts index a452dc4..555ef8b 100644 --- a/app/(chat)/api/chat/schema.ts +++ b/app/(chat)/api/chat/schema.ts @@ -1,27 +1,25 @@ import { z } from 'zod'; const textPartSchema = z.object({ - text: z.string().min(1).max(2000), type: z.enum(['text']), + text: z.string().min(1).max(2000), }); +const filePartSchema = z.object({ + type: z.enum(['file']), + mediaType: z.enum(['image/jpeg', 'image/png']), + name: z.string().min(1).max(100), + url: z.string().url(), +}); + +const partSchema = z.union([textPartSchema, filePartSchema]); + export const postRequestBodySchema = z.object({ id: z.string().uuid(), message: z.object({ id: z.string().uuid(), - createdAt: z.coerce.date(), role: z.enum(['user']), - content: z.string().min(1).max(2000), - parts: z.array(textPartSchema), - experimental_attachments: z - .array( - z.object({ - url: z.string().url(), - name: z.string().min(1).max(2000), - contentType: z.enum(['image/png', 'image/jpg', 'image/jpeg']), - }), - ) - .optional(), + parts: z.array(partSchema), }), selectedChatModel: z.enum(['chat-model', 'chat-model-reasoning']), selectedVisibilityType: z.enum(['public', 'private']), diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx index c0024dc..b9dc99d 100644 --- a/app/(chat)/chat/[id]/page.tsx +++ b/app/(chat)/chat/[id]/page.tsx @@ -6,8 +6,7 @@ import { Chat } from '@/components/chat'; import { getChatById, getMessagesByChatId } from '@/lib/db/queries'; import { DataStreamHandler } from '@/components/data-stream-handler'; import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models'; -import type { DBMessage } from '@/lib/db/schema'; -import type { Attachment, UIMessage } from 'ai'; +import { convertToUIMessages } from '@/lib/utils'; export default async function Page(props: { params: Promise<{ id: string }> }) { const params = await props.params; @@ -38,18 +37,7 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { id, }); - function convertToUIMessages(messages: Array): Array { - return messages.map((message) => ({ - id: message.id, - parts: message.parts as UIMessage['parts'], - role: message.role as UIMessage['role'], - // Note: content will soon be deprecated in @ai-sdk/react - content: '', - createdAt: message.createdAt, - experimental_attachments: - (message.attachments as Array) ?? [], - })); - } + const uiMessages = convertToUIMessages(messagesFromDb); const cookieStore = await cookies(); const chatModelFromCookie = cookieStore.get('chat-model'); @@ -59,14 +47,14 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { <> - + ); } @@ -75,14 +63,14 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { <> - + ); } diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx index 9310a10..dfd9da2 100644 --- a/app/(chat)/layout.tsx +++ b/app/(chat)/layout.tsx @@ -4,6 +4,7 @@ import { AppSidebar } from '@/components/app-sidebar'; import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; import { auth } from '../(auth)/auth'; import Script from 'next/script'; +import { DataStreamProvider } from '@/components/data-stream-provider'; export const experimental_ppr = true; @@ -21,10 +22,12 @@ export default async function Layout({ src="https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js" strategy="beforeInteractive" /> - - - {children} - + + + + {children} + + ); } diff --git a/app/(chat)/page.tsx b/app/(chat)/page.tsx index 6ffd4da..41328cb 100644 --- a/app/(chat)/page.tsx +++ b/app/(chat)/page.tsx @@ -32,7 +32,7 @@ export default async function Page() { session={session} autoResume={false} /> - + ); } @@ -49,7 +49,7 @@ export default async function Page() { session={session} autoResume={false} /> - + ); } diff --git a/artifacts/code/client.tsx b/artifacts/code/client.tsx index 8223c9a..25a1597 100644 --- a/artifacts/code/client.tsx +++ b/artifacts/code/client.tsx @@ -12,8 +12,8 @@ import { toast } from 'sonner'; import { generateUUID } from '@/lib/utils'; import { Console, - ConsoleOutput, - ConsoleOutputContent, + type ConsoleOutput, + type ConsoleOutputContent, } from '@/components/console'; const OUTPUT_HANDLERS = { @@ -76,10 +76,10 @@ export const codeArtifact = new Artifact<'code', Metadata>({ }); }, onStreamPart: ({ streamPart, setArtifact }) => { - if (streamPart.type === 'code-delta') { + if (streamPart.type === 'data-codeDelta') { setArtifact((draftArtifact) => ({ ...draftArtifact, - content: streamPart.content as string, + content: streamPart.data, isVisible: draftArtifact.status === 'streaming' && draftArtifact.content.length > 300 && @@ -249,20 +249,30 @@ export const codeArtifact = new Artifact<'code', Metadata>({ { icon: , description: 'Add comments', - onClick: ({ appendMessage }) => { - appendMessage({ + onClick: ({ sendMessage }) => { + sendMessage({ role: 'user', - content: 'Add comments to the code snippet for understanding', + parts: [ + { + type: 'text', + text: 'Add comments to the code snippet for understanding', + }, + ], }); }, }, { icon: , description: 'Add logs', - onClick: ({ appendMessage }) => { - appendMessage({ + onClick: ({ sendMessage }) => { + sendMessage({ role: 'user', - content: 'Add logs to the code snippet for debugging', + parts: [ + { + type: 'text', + text: 'Add logs to the code snippet for debugging', + }, + ], }); }, }, diff --git a/artifacts/code/server.ts b/artifacts/code/server.ts index 0b74019..5b397bf 100644 --- a/artifacts/code/server.ts +++ b/artifacts/code/server.ts @@ -26,9 +26,10 @@ export const codeDocumentHandler = createDocumentHandler<'code'>({ const { code } = object; if (code) { - dataStream.writeData({ - type: 'code-delta', - content: code ?? '', + dataStream.write({ + type: 'data-codeDelta', + data: code ?? '', + transient: true, }); draftContent = code; @@ -58,9 +59,10 @@ export const codeDocumentHandler = createDocumentHandler<'code'>({ const { code } = object; if (code) { - dataStream.writeData({ - type: 'code-delta', - content: code ?? '', + dataStream.write({ + type: 'data-codeDelta', + data: code ?? '', + transient: true, }); draftContent = code; diff --git a/artifacts/image/client.tsx b/artifacts/image/client.tsx index a8f8827..7b88b9d 100644 --- a/artifacts/image/client.tsx +++ b/artifacts/image/client.tsx @@ -7,10 +7,10 @@ export const imageArtifact = new Artifact({ kind: 'image', description: 'Useful for image generation', onStreamPart: ({ streamPart, setArtifact }) => { - if (streamPart.type === 'image-delta') { + if (streamPart.type === 'data-imageDelta') { setArtifact((draftArtifact) => ({ ...draftArtifact, - content: streamPart.content as string, + content: streamPart.data, isVisible: true, status: 'streaming', })); diff --git a/artifacts/image/server.ts b/artifacts/image/server.ts index 96c2bc5..14a994c 100644 --- a/artifacts/image/server.ts +++ b/artifacts/image/server.ts @@ -15,9 +15,10 @@ export const imageDocumentHandler = createDocumentHandler<'image'>({ draftContent = image.base64; - dataStream.writeData({ - type: 'image-delta', - content: image.base64, + dataStream.write({ + type: 'data-imageDelta', + data: image.base64, + transient: true, }); return draftContent; @@ -33,9 +34,10 @@ export const imageDocumentHandler = createDocumentHandler<'image'>({ draftContent = image.base64; - dataStream.writeData({ - type: 'image-delta', - content: image.base64, + dataStream.write({ + type: 'data-imageDelta', + data: image.base64, + transient: true, }); return draftContent; diff --git a/artifacts/sheet/client.tsx b/artifacts/sheet/client.tsx index c73b2ca..3e7dd75 100644 --- a/artifacts/sheet/client.tsx +++ b/artifacts/sheet/client.tsx @@ -17,10 +17,10 @@ export const sheetArtifact = new Artifact<'sheet', Metadata>({ description: 'Useful for working with spreadsheets', initialize: async () => {}, onStreamPart: ({ setArtifact, streamPart }) => { - if (streamPart.type === 'sheet-delta') { + if (streamPart.type === 'data-sheetDelta') { setArtifact((draftArtifact) => ({ ...draftArtifact, - content: streamPart.content as string, + content: streamPart.data, isVisible: true, status: 'streaming', })); @@ -93,21 +93,27 @@ export const sheetArtifact = new Artifact<'sheet', Metadata>({ { description: 'Format and clean data', icon: , - onClick: ({ appendMessage }) => { - appendMessage({ + onClick: ({ sendMessage }) => { + sendMessage({ role: 'user', - content: 'Can you please format and clean the data?', + parts: [ + { type: 'text', text: 'Can you please format and clean the data?' }, + ], }); }, }, { description: 'Analyze and visualize data', icon: , - onClick: ({ appendMessage }) => { - appendMessage({ + onClick: ({ sendMessage }) => { + sendMessage({ role: 'user', - content: - 'Can you please analyze and visualize the data by creating a new code artifact in python?', + parts: [ + { + type: 'text', + text: 'Can you please analyze and visualize the data by creating a new code artifact in python?', + }, + ], }); }, }, diff --git a/artifacts/sheet/server.ts b/artifacts/sheet/server.ts index 7a557bd..5b19e99 100644 --- a/artifacts/sheet/server.ts +++ b/artifacts/sheet/server.ts @@ -26,9 +26,10 @@ export const sheetDocumentHandler = createDocumentHandler<'sheet'>({ const { csv } = object; if (csv) { - dataStream.writeData({ - type: 'sheet-delta', - content: csv, + dataStream.write({ + type: 'data-sheetDelta', + data: csv, + transient: true, }); draftContent = csv; @@ -36,9 +37,10 @@ export const sheetDocumentHandler = createDocumentHandler<'sheet'>({ } } - dataStream.writeData({ - type: 'sheet-delta', - content: draftContent, + dataStream.write({ + type: 'data-sheetDelta', + data: draftContent, + transient: true, }); return draftContent; @@ -63,9 +65,10 @@ export const sheetDocumentHandler = createDocumentHandler<'sheet'>({ const { csv } = object; if (csv) { - dataStream.writeData({ - type: 'sheet-delta', - content: csv, + dataStream.write({ + type: 'data-sheetDelta', + data: csv, + transient: true, }); draftContent = csv; diff --git a/artifacts/text/client.tsx b/artifacts/text/client.tsx index b5f4170..f2371ac 100644 --- a/artifacts/text/client.tsx +++ b/artifacts/text/client.tsx @@ -10,7 +10,7 @@ import { RedoIcon, UndoIcon, } from '@/components/icons'; -import { Suggestion } from '@/lib/db/schema'; +import type { Suggestion } from '@/lib/db/schema'; import { toast } from 'sonner'; import { getSuggestions } from '../actions'; @@ -29,22 +29,19 @@ export const textArtifact = new Artifact<'text', TextArtifactMetadata>({ }); }, onStreamPart: ({ streamPart, setMetadata, setArtifact }) => { - if (streamPart.type === 'suggestion') { + if (streamPart.type === 'data-suggestion') { setMetadata((metadata) => { return { - suggestions: [ - ...metadata.suggestions, - streamPart.content as Suggestion, - ], + suggestions: [...metadata.suggestions, streamPart.data], }; }); } - if (streamPart.type === 'text-delta') { + if (streamPart.type === 'data-textDelta') { setArtifact((draftArtifact) => { return { ...draftArtifact, - content: draftArtifact.content + (streamPart.content as string), + content: draftArtifact.content + streamPart.data, isVisible: draftArtifact.status === 'streaming' && draftArtifact.content.length > 400 && @@ -90,9 +87,7 @@ export const textArtifact = new Artifact<'text', TextArtifactMetadata>({ onSaveContent={onSaveContent} /> - {metadata && - metadata.suggestions && - metadata.suggestions.length > 0 ? ( + {metadata?.suggestions && metadata.suggestions.length > 0 ? (
) : null}
@@ -155,22 +150,30 @@ export const textArtifact = new Artifact<'text', TextArtifactMetadata>({ { icon: , description: 'Add final polish', - onClick: ({ appendMessage }) => { - appendMessage({ + onClick: ({ sendMessage }) => { + sendMessage({ role: 'user', - content: - 'Please add final polish and check for grammar, add section titles for better structure, and ensure everything reads smoothly.', + parts: [ + { + type: 'text', + text: 'Please add final polish and check for grammar, add section titles for better structure, and ensure everything reads smoothly.', + }, + ], }); }, }, { icon: , description: 'Request suggestions', - onClick: ({ appendMessage }) => { - appendMessage({ + onClick: ({ sendMessage }) => { + sendMessage({ role: 'user', - content: - 'Please add suggestions you have that could improve the writing.', + parts: [ + { + type: 'text', + text: 'Please add suggestions you have that could improve the writing.', + }, + ], }); }, }, diff --git a/artifacts/text/server.ts b/artifacts/text/server.ts index 58578bf..1a403d8 100644 --- a/artifacts/text/server.ts +++ b/artifacts/text/server.ts @@ -19,14 +19,15 @@ export const textDocumentHandler = createDocumentHandler<'text'>({ for await (const delta of fullStream) { const { type } = delta; - if (type === 'text-delta') { - const { textDelta } = delta; + if (type === 'text') { + const { text } = delta; - draftContent += textDelta; + draftContent += text; - dataStream.writeData({ - type: 'text-delta', - content: textDelta, + dataStream.write({ + type: 'data-textDelta', + data: text, + transient: true, }); } } @@ -41,7 +42,7 @@ export const textDocumentHandler = createDocumentHandler<'text'>({ system: updateDocumentPrompt(document.content, 'text'), experimental_transform: smoothStream({ chunking: 'word' }), prompt: description, - experimental_providerMetadata: { + providerOptions: { openai: { prediction: { type: 'content', @@ -54,13 +55,15 @@ export const textDocumentHandler = createDocumentHandler<'text'>({ for await (const delta of fullStream) { const { type } = delta; - if (type === 'text-delta') { - const { textDelta } = delta; + if (type === 'text') { + const { text } = delta; - draftContent += textDelta; - dataStream.writeData({ - type: 'text-delta', - content: textDelta, + draftContent += text; + + dataStream.write({ + type: 'data-textDelta', + data: text, + transient: true, }); } } diff --git a/components/artifact-messages.tsx b/components/artifact-messages.tsx index 82b30da..4d71435 100644 --- a/components/artifact-messages.tsx +++ b/components/artifact-messages.tsx @@ -1,20 +1,20 @@ import { PreviewMessage, ThinkingMessage } from './message'; import type { Vote } from '@/lib/db/schema'; -import type { UIMessage } from 'ai'; import { memo } from 'react'; import equal from 'fast-deep-equal'; import type { UIArtifact } from './artifact'; import type { UseChatHelpers } from '@ai-sdk/react'; import { motion } from 'framer-motion'; import { useMessages } from '@/hooks/use-messages'; +import type { ChatMessage } from '@/lib/types'; interface ArtifactMessagesProps { chatId: string; - status: UseChatHelpers['status']; + status: UseChatHelpers['status']; votes: Array | undefined; - messages: Array; - setMessages: UseChatHelpers['setMessages']; - reload: UseChatHelpers['reload']; + messages: ChatMessage[]; + setMessages: UseChatHelpers['setMessages']; + regenerate: UseChatHelpers['regenerate']; isReadonly: boolean; artifactStatus: UIArtifact['status']; } @@ -25,7 +25,7 @@ function PureArtifactMessages({ votes, messages, setMessages, - reload, + regenerate, isReadonly, }: ArtifactMessagesProps) { const { @@ -56,7 +56,7 @@ function PureArtifactMessages({ : undefined } setMessages={setMessages} - reload={reload} + regenerate={regenerate} isReadonly={isReadonly} requiresScrollPadding={ hasSentMessage && index === messages.length - 1 diff --git a/components/artifact.tsx b/components/artifact.tsx index 67e713b..bf3bfd6 100644 --- a/components/artifact.tsx +++ b/components/artifact.tsx @@ -1,4 +1,3 @@ -import type { Attachment, UIMessage } from 'ai'; import { formatDistance } from 'date-fns'; import { AnimatePresence, motion } from 'framer-motion'; import { @@ -28,6 +27,7 @@ import { textArtifact } from '@/artifacts/text/client'; import equal from 'fast-deep-equal'; import type { UseChatHelpers } from '@ai-sdk/react'; import type { VisibilityType } from './visibility-selector'; +import type { Attachment, ChatMessage } from '@/lib/types'; export const artifactDefinitions = [ textArtifact, @@ -56,32 +56,30 @@ function PureArtifact({ chatId, input, setInput, - handleSubmit, status, stop, attachments, setAttachments, - append, + sendMessage, messages, setMessages, - reload, + regenerate, votes, isReadonly, selectedVisibilityType, }: { chatId: string; input: string; - setInput: UseChatHelpers['setInput']; - status: UseChatHelpers['status']; - stop: UseChatHelpers['stop']; - attachments: Array; - setAttachments: Dispatch>>; - messages: Array; - setMessages: UseChatHelpers['setMessages']; + setInput: Dispatch>; + status: UseChatHelpers['status']; + stop: UseChatHelpers['stop']; + attachments: Attachment[]; + setAttachments: Dispatch>; + messages: ChatMessage[]; + setMessages: UseChatHelpers['setMessages']; votes: Array | undefined; - append: UseChatHelpers['append']; - handleSubmit: UseChatHelpers['handleSubmit']; - reload: UseChatHelpers['reload']; + sendMessage: UseChatHelpers['sendMessage']; + regenerate: UseChatHelpers['regenerate']; isReadonly: boolean; selectedVisibilityType: VisibilityType; }) { @@ -319,7 +317,7 @@ function PureArtifact({ votes={votes} messages={messages} setMessages={setMessages} - reload={reload} + regenerate={regenerate} isReadonly={isReadonly} artifactStatus={artifact.status} /> @@ -329,13 +327,12 @@ function PureArtifact({ chatId={chatId} input={input} setInput={setInput} - handleSubmit={handleSubmit} status={status} stop={stop} attachments={attachments} setAttachments={setAttachments} messages={messages} - append={append} + sendMessage={sendMessage} className="bg-background dark:bg-muted" setMessages={setMessages} selectedVisibilityType={selectedVisibilityType} @@ -476,7 +473,7 @@ function PureArtifact({ ; + initialMessages: ChatMessage[]; initialChatModel: string; initialVisibilityType: VisibilityType; isReadonly: boolean; session: Session; autoResume: boolean; }) { - const { mutate } = useSWRConfig(); - const { visibilityType } = useChatVisibility({ chatId: id, initialVisibilityType, }); + const { mutate } = useSWRConfig(); + const { setDataStream } = useDataStream(); + + const [input, setInput] = useState(''); + const { messages, setMessages, - handleSubmit, - input, - setInput, - append, + sendMessage, status, stop, - reload, - experimental_resume, - data, - } = useChat({ + regenerate, + resumeStream, + } = useChat({ id, - initialMessages, + messages: initialMessages, experimental_throttle: 100, - sendExtraMessageFields: true, generateId: generateUUID, - fetch: fetchWithErrorHandlers, - experimental_prepareRequestBody: (body) => ({ - id, - message: body.messages.at(-1), - selectedChatModel: initialChatModel, - selectedVisibilityType: visibilityType, + transport: new DefaultChatTransport({ + api: '/api/chat', + fetch: fetchWithErrorHandlers, + prepareSendMessagesRequest({ messages, id, body }) { + return { + body: { + id, + message: messages.at(-1), + selectedChatModel: initialChatModel, + selectedVisibilityType: visibilityType, + ...body, + }, + }; + }, }), + onData: (dataPart) => { + setDataStream((ds) => (ds ? [...ds, dataPart] : [])); + }, onFinish: () => { mutate(unstable_serialize(getChatHistoryPaginationKey)); }, @@ -90,15 +101,15 @@ export function Chat({ useEffect(() => { if (query && !hasAppendedQuery) { - append({ - role: 'user', - content: query, + sendMessage({ + role: 'user' as const, + parts: [{ type: 'text', text: query }], }); setHasAppendedQuery(true); window.history.replaceState({}, '', `/chat/${id}`); } - }, [query, append, hasAppendedQuery, id]); + }, [query, sendMessage, hasAppendedQuery, id]); const { data: votes } = useSWR>( messages.length >= 2 ? `/api/vote?chatId=${id}` : null, @@ -111,8 +122,7 @@ export function Chat({ useAutoResume({ autoResume, initialMessages, - experimental_resume, - data, + resumeStream, setMessages, }); @@ -133,7 +143,7 @@ export function Chat({ votes={votes} messages={messages} setMessages={setMessages} - reload={reload} + regenerate={regenerate} isReadonly={isReadonly} isArtifactVisible={isArtifactVisible} /> @@ -144,14 +154,13 @@ export function Chat({ chatId={id} input={input} setInput={setInput} - handleSubmit={handleSubmit} status={status} stop={stop} attachments={attachments} setAttachments={setAttachments} messages={messages} setMessages={setMessages} - append={append} + sendMessage={sendMessage} selectedVisibilityType={visibilityType} /> )} @@ -162,15 +171,14 @@ export function Chat({ chatId={id} input={input} setInput={setInput} - handleSubmit={handleSubmit} status={status} stop={stop} attachments={attachments} setAttachments={setAttachments} - append={append} + sendMessage={sendMessage} messages={messages} setMessages={setMessages} - reload={reload} + regenerate={regenerate} votes={votes} isReadonly={isReadonly} selectedVisibilityType={visibilityType} diff --git a/components/create-artifact.tsx b/components/create-artifact.tsx index b555914..eda62e2 100644 --- a/components/create-artifact.tsx +++ b/components/create-artifact.tsx @@ -1,8 +1,9 @@ -import { Suggestion } from '@/lib/db/schema'; -import { UseChatHelpers } from '@ai-sdk/react'; -import { ComponentType, Dispatch, ReactNode, SetStateAction } from 'react'; -import { DataStreamDelta } from './data-stream-handler'; -import { UIArtifact } from './artifact'; +import type { Suggestion } from '@/lib/db/schema'; +import type { UseChatHelpers } from '@ai-sdk/react'; +import type { ComponentType, Dispatch, ReactNode, SetStateAction } from 'react'; +import type { UIArtifact } from './artifact'; +import type { ChatMessage, CustomUIDataTypes } from '@/lib/types'; +import type { DataUIPart } from 'ai'; export type ArtifactActionContext = { content: string; @@ -23,7 +24,7 @@ type ArtifactAction = { }; export type ArtifactToolbarContext = { - appendMessage: UseChatHelpers['append']; + sendMessage: UseChatHelpers['sendMessage']; }; export type ArtifactToolbarItem = { @@ -63,7 +64,7 @@ type ArtifactConfig = { onStreamPart: (args: { setMetadata: Dispatch>; setArtifact: Dispatch>; - streamPart: DataStreamDelta; + streamPart: DataUIPart; }) => void; }; @@ -77,7 +78,7 @@ export class Artifact { readonly onStreamPart: (args: { setMetadata: Dispatch>; setArtifact: Dispatch>; - streamPart: DataStreamDelta; + streamPart: DataUIPart; }) => void; constructor(config: ArtifactConfig) { diff --git a/components/data-stream-handler.tsx b/components/data-stream-handler.tsx index d4c49b7..a42ffbe 100644 --- a/components/data-stream-handler.tsx +++ b/components/data-stream-handler.tsx @@ -1,28 +1,13 @@ 'use client'; -import { useChat } from '@ai-sdk/react'; import { useEffect, useRef } from 'react'; -import { artifactDefinitions, ArtifactKind } from './artifact'; -import { Suggestion } from '@/lib/db/schema'; +import { artifactDefinitions } from './artifact'; import { initialArtifactData, useArtifact } from '@/hooks/use-artifact'; +import { useDataStream } from './data-stream-provider'; -export type DataStreamDelta = { - type: - | 'text-delta' - | 'code-delta' - | 'sheet-delta' - | 'image-delta' - | 'title' - | 'id' - | 'suggestion' - | 'clear' - | 'finish' - | 'kind'; - content: string | Suggestion; -}; +export function DataStreamHandler() { + const { dataStream } = useDataStream(); -export function DataStreamHandler({ id }: { id: string }) { - const { data: dataStream } = useChat({ id }); const { artifact, setArtifact, setMetadata } = useArtifact(); const lastProcessedIndex = useRef(-1); @@ -32,7 +17,7 @@ export function DataStreamHandler({ id }: { id: string }) { const newDeltas = dataStream.slice(lastProcessedIndex.current + 1); lastProcessedIndex.current = dataStream.length - 1; - (newDeltas as DataStreamDelta[]).forEach((delta: DataStreamDelta) => { + newDeltas.forEach((delta) => { const artifactDefinition = artifactDefinitions.find( (artifactDefinition) => artifactDefinition.kind === artifact.kind, ); @@ -51,35 +36,35 @@ export function DataStreamHandler({ id }: { id: string }) { } switch (delta.type) { - case 'id': + case 'data-id': return { ...draftArtifact, - documentId: delta.content as string, + documentId: delta.data, status: 'streaming', }; - case 'title': + case 'data-title': return { ...draftArtifact, - title: delta.content as string, + title: delta.data, status: 'streaming', }; - case 'kind': + case 'data-kind': return { ...draftArtifact, - kind: delta.content as ArtifactKind, + kind: delta.data, status: 'streaming', }; - case 'clear': + case 'data-clear': return { ...draftArtifact, content: '', status: 'streaming', }; - case 'finish': + case 'data-finish': return { ...draftArtifact, status: 'idle', diff --git a/components/data-stream-provider.tsx b/components/data-stream-provider.tsx new file mode 100644 index 0000000..0f7c367 --- /dev/null +++ b/components/data-stream-provider.tsx @@ -0,0 +1,40 @@ +'use client'; + +import React, { createContext, useContext, useMemo, useState } from 'react'; +import type { DataUIPart } from 'ai'; +import type { CustomUIDataTypes } from '@/lib/types'; + +interface DataStreamContextValue { + dataStream: DataUIPart[]; + setDataStream: React.Dispatch< + React.SetStateAction[]> + >; +} + +const DataStreamContext = createContext(null); + +export function DataStreamProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [dataStream, setDataStream] = useState[]>( + [], + ); + + const value = useMemo(() => ({ dataStream, setDataStream }), [dataStream]); + + return ( + + {children} + + ); +} + +export function useDataStream() { + const context = useContext(DataStreamContext); + if (!context) { + throw new Error('useDataStream must be used within a DataStreamProvider'); + } + return context; +} diff --git a/components/document-preview.tsx b/components/document-preview.tsx index 6f28190..a1b7065 100644 --- a/components/document-preview.tsx +++ b/components/document-preview.tsx @@ -2,16 +2,16 @@ import { memo, - MouseEvent, + type MouseEvent, useCallback, useEffect, useMemo, useRef, } from 'react'; -import { ArtifactKind, UIArtifact } from './artifact'; +import type { ArtifactKind, UIArtifact } from './artifact'; import { FileIcon, FullscreenIcon, ImageIcon, LoaderIcon } from './icons'; import { cn, fetcher } from '@/lib/utils'; -import { Document } from '@/lib/db/schema'; +import type { Document } from '@/lib/db/schema'; import { InlineDocumentSkeleton } from './document-skeleton'; import useSWR from 'swr'; import { Editor } from './text-editor'; @@ -73,7 +73,7 @@ export function DocumentPreview({ return ( ); diff --git a/components/document.tsx b/components/document.tsx index 9ebc1de..6b5ffe6 100644 --- a/components/document.tsx +++ b/components/document.tsx @@ -88,7 +88,10 @@ export const DocumentToolResult = memo(PureDocumentToolResult, () => true); interface DocumentToolCallProps { type: 'create' | 'update' | 'request-suggestions'; - args: { title: string }; + args: + | { title: string; kind: ArtifactKind } // for create + | { id: string; description: string } // for update + | { documentId: string }; // for request-suggestions isReadonly: boolean; } @@ -139,7 +142,15 @@ function PureDocumentToolCall({
- {`${getActionText(type, 'present')} ${args.title ? `"${args.title}"` : ''}`} + {`${getActionText(type, 'present')} ${ + type === 'create' && 'title' in args && args.title + ? `"${args.title}"` + : type === 'update' && 'description' in args + ? `"${args.description}"` + : type === 'request-suggestions' + ? 'for document' + : '' + }`}
diff --git a/components/message-actions.tsx b/components/message-actions.tsx index 1a92407..8c0b68d 100644 --- a/components/message-actions.tsx +++ b/components/message-actions.tsx @@ -1,4 +1,3 @@ -import type { Message } from 'ai'; import { useSWRConfig } from 'swr'; import { useCopyToClipboard } from 'usehooks-ts'; @@ -15,6 +14,7 @@ import { import { memo } from 'react'; import equal from 'fast-deep-equal'; import { toast } from 'sonner'; +import type { ChatMessage } from '@/lib/types'; export function PureMessageActions({ chatId, @@ -23,7 +23,7 @@ export function PureMessageActions({ isLoading, }: { chatId: string; - message: Message; + message: ChatMessage; vote: Vote | undefined; isLoading: boolean; }) { diff --git a/components/message-editor.tsx b/components/message-editor.tsx index bf8db12..ca0f071 100644 --- a/components/message-editor.tsx +++ b/components/message-editor.tsx @@ -1,28 +1,37 @@ 'use client'; -import { ChatRequestOptions, Message } from 'ai'; import { Button } from './ui/button'; -import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; +import { + type Dispatch, + type SetStateAction, + useEffect, + useRef, + useState, +} from 'react'; import { Textarea } from './ui/textarea'; import { deleteTrailingMessages } from '@/app/(chat)/actions'; -import { UseChatHelpers } from '@ai-sdk/react'; +import type { UseChatHelpers } from '@ai-sdk/react'; +import type { ChatMessage } from '@/lib/types'; +import { getTextFromMessage } from '@/lib/utils'; export type MessageEditorProps = { - message: Message; + message: ChatMessage; setMode: Dispatch>; - setMessages: UseChatHelpers['setMessages']; - reload: UseChatHelpers['reload']; + setMessages: UseChatHelpers['setMessages']; + regenerate: UseChatHelpers['regenerate']; }; export function MessageEditor({ message, setMode, setMessages, - reload, + regenerate, }: MessageEditorProps) { const [isSubmitting, setIsSubmitting] = useState(false); - const [draftContent, setDraftContent] = useState(message.content); + const [draftContent, setDraftContent] = useState( + getTextFromMessage(message), + ); const textareaRef = useRef(null); useEffect(() => { @@ -75,14 +84,12 @@ export function MessageEditor({ id: message.id, }); - // @ts-expect-error todo: support UIMessage in setMessages setMessages((messages) => { const index = messages.findIndex((m) => m.id === message.id); if (index !== -1) { - const updatedMessage = { + const updatedMessage: ChatMessage = { ...message, - content: draftContent, parts: [{ type: 'text', text: draftContent }], }; @@ -93,7 +100,7 @@ export function MessageEditor({ }); setMode('view'); - reload(); + regenerate(); }} > {isSubmitting ? 'Sending...' : 'Send'} diff --git a/components/message.tsx b/components/message.tsx index 7661d14..02649bb 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -1,6 +1,4 @@ 'use client'; - -import type { UIMessage } from 'ai'; import cx from 'classnames'; import { AnimatePresence, motion } from 'framer-motion'; import { memo, useState } from 'react'; @@ -19,6 +17,11 @@ import { MessageEditor } from './message-editor'; import { DocumentPreview } from './document-preview'; import { MessageReasoning } from './message-reasoning'; import type { UseChatHelpers } from '@ai-sdk/react'; +import type { ChatMessage } from '@/lib/types'; +import { useDataStream } from './data-stream-provider'; + +// Type narrowing is handled by TypeScript's control flow analysis +// The AI SDK provides proper discriminated unions for tool calls const PurePreviewMessage = ({ chatId, @@ -26,21 +29,27 @@ const PurePreviewMessage = ({ vote, isLoading, setMessages, - reload, + regenerate, isReadonly, requiresScrollPadding, }: { chatId: string; - message: UIMessage; + message: ChatMessage; vote: Vote | undefined; isLoading: boolean; - setMessages: UseChatHelpers['setMessages']; - reload: UseChatHelpers['reload']; + setMessages: UseChatHelpers['setMessages']; + regenerate: UseChatHelpers['regenerate']; isReadonly: boolean; requiresScrollPadding: boolean; }) => { const [mode, setMode] = useState<'view' | 'edit'>('view'); + const attachmentsFromMessage = message.parts.filter( + (part) => part.type === 'file', + ); + + useDataStream(); + return ( - {message.experimental_attachments && - message.experimental_attachments.length > 0 && ( -
- {message.experimental_attachments.map((attachment) => ( - - ))} -
- )} + {attachmentsFromMessage.length > 0 && ( +
+ {attachmentsFromMessage.map((attachment) => ( + + ))} +
+ )} {message.parts?.map((part, index) => { const { type } = part; @@ -96,7 +108,7 @@ const PurePreviewMessage = ({ ); } @@ -146,75 +158,151 @@ const PurePreviewMessage = ({ message={message} setMode={setMode} setMessages={setMessages} - reload={reload} + regenerate={regenerate} /> ); } } - if (type === 'tool-invocation') { - const { toolInvocation } = part; - const { toolName, toolCallId, state } = toolInvocation; - - if (state === 'call') { - const { args } = toolInvocation; + if (type === 'tool-getWeather') { + const { toolCallId, state } = part; + if (state === 'input-available') { return ( -
- {toolName === 'getWeather' ? ( - - ) : toolName === 'createDocument' ? ( - - ) : toolName === 'updateDocument' ? ( - - ) : toolName === 'requestSuggestions' ? ( - - ) : null} +
+
); } - if (state === 'result') { - const { result } = toolInvocation; + if (state === 'output-available') { + const { output } = part; + return ( +
+ +
+ ); + } + } + + if (type === 'tool-createDocument') { + const { toolCallId, state } = part; + + if (state === 'input-available') { + const { input } = part; + return ( +
+ +
+ ); + } + + if (state === 'output-available') { + const { output } = part; + + if ('error' in output) { + return ( +
+ Error: {String(output.error)} +
+ ); + } return (
- {toolName === 'getWeather' ? ( - - ) : toolName === 'createDocument' ? ( - - ) : toolName === 'updateDocument' ? ( - - ) : toolName === 'requestSuggestions' ? ( - - ) : ( -
{JSON.stringify(result, null, 2)}
- )} + +
+ ); + } + } + + if (type === 'tool-updateDocument') { + const { toolCallId, state } = part; + + if (state === 'input-available') { + const { input } = part; + + return ( +
+ +
+ ); + } + + if (state === 'output-available') { + const { output } = part; + + if ('error' in output) { + return ( +
+ Error: {String(output.error)} +
+ ); + } + + return ( +
+ +
+ ); + } + } + + if (type === 'tool-requestSuggestions') { + const { toolCallId, state } = part; + + if (state === 'input-available') { + const { input } = part; + return ( +
+ +
+ ); + } + + if (state === 'output-available') { + const { output } = part; + + if ('error' in output) { + return ( +
+ Error: {String(output.error)} +
+ ); + } + + return ( +
+
); } @@ -247,7 +335,7 @@ export const PreviewMessage = memo( if (!equal(prevProps.message.parts, nextProps.message.parts)) return false; if (!equal(prevProps.vote, nextProps.vote)) return false; - return true; + return false; }, ); diff --git a/components/messages.tsx b/components/messages.tsx index d4e1a02..fc4bbf8 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -1,4 +1,3 @@ -import type { UIMessage } from 'ai'; import { PreviewMessage, ThinkingMessage } from './message'; import { Greeting } from './greeting'; import { memo } from 'react'; @@ -7,14 +6,16 @@ import equal from 'fast-deep-equal'; import type { UseChatHelpers } from '@ai-sdk/react'; import { motion } from 'framer-motion'; import { useMessages } from '@/hooks/use-messages'; +import type { ChatMessage } from '@/lib/types'; +import { useDataStream } from './data-stream-provider'; interface MessagesProps { chatId: string; - status: UseChatHelpers['status']; + status: UseChatHelpers['status']; votes: Array | undefined; - messages: Array; - setMessages: UseChatHelpers['setMessages']; - reload: UseChatHelpers['reload']; + messages: ChatMessage[]; + setMessages: UseChatHelpers['setMessages']; + regenerate: UseChatHelpers['regenerate']; isReadonly: boolean; isArtifactVisible: boolean; } @@ -25,7 +26,7 @@ function PureMessages({ votes, messages, setMessages, - reload, + regenerate, isReadonly, }: MessagesProps) { const { @@ -39,6 +40,8 @@ function PureMessages({ status, }); + useDataStream(); + return (
{ if (prevProps.isArtifactVisible && nextProps.isArtifactVisible) return true; if (prevProps.status !== nextProps.status) return false; - if (prevProps.status && nextProps.status) return false; if (prevProps.messages.length !== nextProps.messages.length) return false; if (!equal(prevProps.messages, nextProps.messages)) return false; if (!equal(prevProps.votes, nextProps.votes)) return false; - return true; + return false; }); diff --git a/components/multimodal-input.tsx b/components/multimodal-input.tsx index f17372b..980f252 100644 --- a/components/multimodal-input.tsx +++ b/components/multimodal-input.tsx @@ -1,6 +1,6 @@ 'use client'; -import type { Attachment, UIMessage } from 'ai'; +import type { UIMessage } from 'ai'; import cx from 'classnames'; import type React from 'react'; import { @@ -27,6 +27,7 @@ import { AnimatePresence, motion } from 'framer-motion'; import { ArrowDown } from 'lucide-react'; import { useScrollToBottom } from '@/hooks/use-scroll-to-bottom'; import type { VisibilityType } from './visibility-selector'; +import type { Attachment, ChatMessage } from '@/lib/types'; function PureMultimodalInput({ chatId, @@ -38,22 +39,20 @@ function PureMultimodalInput({ setAttachments, messages, setMessages, - append, - handleSubmit, + sendMessage, className, selectedVisibilityType, }: { chatId: string; - input: UseChatHelpers['input']; - setInput: UseChatHelpers['setInput']; - status: UseChatHelpers['status']; + input: string; + setInput: Dispatch>; + status: UseChatHelpers['status']; stop: () => void; attachments: Array; setAttachments: Dispatch>>; messages: Array; - setMessages: UseChatHelpers['setMessages']; - append: UseChatHelpers['append']; - handleSubmit: UseChatHelpers['handleSubmit']; + setMessages: UseChatHelpers['setMessages']; + sendMessage: UseChatHelpers['sendMessage']; className?: string; selectedVisibilityType: VisibilityType; }) { @@ -112,20 +111,35 @@ function PureMultimodalInput({ const submitForm = useCallback(() => { window.history.replaceState({}, '', `/chat/${chatId}`); - handleSubmit(undefined, { - experimental_attachments: attachments, + sendMessage({ + role: 'user', + parts: [ + ...attachments.map((attachment) => ({ + type: 'file' as const, + url: attachment.url, + name: attachment.name, + mediaType: attachment.contentType, + })), + { + type: 'text', + text: input, + }, + ], }); setAttachments([]); setLocalStorageInput(''); resetHeight(); + setInput(''); if (width && width > 768) { textareaRef.current?.focus(); } }, [ + input, + setInput, attachments, - handleSubmit, + sendMessage, setAttachments, setLocalStorageInput, width, @@ -224,7 +238,7 @@ function PureMultimodalInput({ attachments.length === 0 && uploadQueue.length === 0 && ( @@ -328,7 +342,7 @@ function PureAttachmentsButton({ status, }: { fileInputRef: React.MutableRefObject; - status: UseChatHelpers['status']; + status: UseChatHelpers['status']; }) { return (