chore: update to ai sdk v5 beta (#1074)

This commit is contained in:
Jeremy 2025-07-03 02:26:34 -07:00 committed by GitHub
parent 7d8e71383f
commit 4c281fe09d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 1372 additions and 1060 deletions

View file

@ -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<ChatMessage>['status'];
votes: Array<Vote> | undefined;
messages: Array<UIMessage>;
setMessages: UseChatHelpers['setMessages'];
reload: UseChatHelpers['reload'];
messages: ChatMessage[];
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
regenerate: UseChatHelpers<ChatMessage>['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

View file

@ -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<Attachment>;
setAttachments: Dispatch<SetStateAction<Array<Attachment>>>;
messages: Array<UIMessage>;
setMessages: UseChatHelpers['setMessages'];
setInput: Dispatch<SetStateAction<string>>;
status: UseChatHelpers<ChatMessage>['status'];
stop: UseChatHelpers<ChatMessage>['stop'];
attachments: Attachment[];
setAttachments: Dispatch<SetStateAction<Attachment[]>>;
messages: ChatMessage[];
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
votes: Array<Vote> | undefined;
append: UseChatHelpers['append'];
handleSubmit: UseChatHelpers['handleSubmit'];
reload: UseChatHelpers['reload'];
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
regenerate: UseChatHelpers<ChatMessage>['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({
<Toolbar
isToolbarVisible={isToolbarVisible}
setIsToolbarVisible={setIsToolbarVisible}
append={append}
sendMessage={sendMessage}
status={status}
stop={stop}
setMessages={setMessages}

View file

@ -1,6 +1,6 @@
'use client';
import type { Attachment, UIMessage } from 'ai';
import { DefaultChatTransport } from 'ai';
import { useChat } from '@ai-sdk/react';
import { useEffect, useState } from 'react';
import useSWR, { useSWRConfig } from 'swr';
@ -20,6 +20,8 @@ import { useSearchParams } from 'next/navigation';
import { useChatVisibility } from '@/hooks/use-chat-visibility';
import { useAutoResume } from '@/hooks/use-auto-resume';
import { ChatSDKError } from '@/lib/errors';
import type { Attachment, ChatMessage } from '@/lib/types';
import { useDataStream } from './data-stream-provider';
export function Chat({
id,
@ -31,45 +33,54 @@ export function Chat({
autoResume,
}: {
id: string;
initialMessages: Array<UIMessage>;
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<string>('');
const {
messages,
setMessages,
handleSubmit,
input,
setInput,
append,
sendMessage,
status,
stop,
reload,
experimental_resume,
data,
} = useChat({
regenerate,
resumeStream,
} = useChat<ChatMessage>({
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<Array<Vote>>(
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}

View file

@ -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<M = any> = {
content: string;
@ -23,7 +24,7 @@ type ArtifactAction<M = any> = {
};
export type ArtifactToolbarContext = {
appendMessage: UseChatHelpers['append'];
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
};
export type ArtifactToolbarItem = {
@ -63,7 +64,7 @@ type ArtifactConfig<T extends string, M = any> = {
onStreamPart: (args: {
setMetadata: Dispatch<SetStateAction<M>>;
setArtifact: Dispatch<SetStateAction<UIArtifact>>;
streamPart: DataStreamDelta;
streamPart: DataUIPart<CustomUIDataTypes>;
}) => void;
};
@ -77,7 +78,7 @@ export class Artifact<T extends string, M = any> {
readonly onStreamPart: (args: {
setMetadata: Dispatch<SetStateAction<M>>;
setArtifact: Dispatch<SetStateAction<UIArtifact>>;
streamPart: DataStreamDelta;
streamPart: DataUIPart<CustomUIDataTypes>;
}) => void;
constructor(config: ArtifactConfig<T, M>) {

View file

@ -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',

View file

@ -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<CustomUIDataTypes>[];
setDataStream: React.Dispatch<
React.SetStateAction<DataUIPart<CustomUIDataTypes>[]>
>;
}
const DataStreamContext = createContext<DataStreamContextValue | null>(null);
export function DataStreamProvider({
children,
}: {
children: React.ReactNode;
}) {
const [dataStream, setDataStream] = useState<DataUIPart<CustomUIDataTypes>[]>(
[],
);
const value = useMemo(() => ({ dataStream, setDataStream }), [dataStream]);
return (
<DataStreamContext.Provider value={value}>
{children}
</DataStreamContext.Provider>
);
}
export function useDataStream() {
const context = useContext(DataStreamContext);
if (!context) {
throw new Error('useDataStream must be used within a DataStreamProvider');
}
return context;
}

View file

@ -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 (
<DocumentToolCall
type="create"
args={{ title: args.title }}
args={{ title: args.title, kind: args.kind }}
isReadonly={isReadonly}
/>
);

View file

@ -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({
</div>
<div className="text-left">
{`${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'
: ''
}`}
</div>
</div>

View file

@ -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;
}) {

View file

@ -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<SetStateAction<'view' | 'edit'>>;
setMessages: UseChatHelpers['setMessages'];
reload: UseChatHelpers['reload'];
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
regenerate: UseChatHelpers<ChatMessage>['regenerate'];
};
export function MessageEditor({
message,
setMode,
setMessages,
reload,
regenerate,
}: MessageEditorProps) {
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [draftContent, setDraftContent] = useState<string>(message.content);
const [draftContent, setDraftContent] = useState<string>(
getTextFromMessage(message),
);
const textareaRef = useRef<HTMLTextAreaElement>(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'}

View file

@ -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<ChatMessage>['setMessages'];
regenerate: UseChatHelpers<ChatMessage>['regenerate'];
isReadonly: boolean;
requiresScrollPadding: boolean;
}) => {
const [mode, setMode] = useState<'view' | 'edit'>('view');
const attachmentsFromMessage = message.parts.filter(
(part) => part.type === 'file',
);
useDataStream();
return (
<AnimatePresence>
<motion.div
@ -72,20 +81,23 @@ const PurePreviewMessage = ({
'min-h-96': message.role === 'assistant' && requiresScrollPadding,
})}
>
{message.experimental_attachments &&
message.experimental_attachments.length > 0 && (
<div
data-testid={`message-attachments`}
className="flex flex-row justify-end gap-2"
>
{message.experimental_attachments.map((attachment) => (
<PreviewAttachment
key={attachment.url}
attachment={attachment}
/>
))}
</div>
)}
{attachmentsFromMessage.length > 0 && (
<div
data-testid={`message-attachments`}
className="flex flex-row justify-end gap-2"
>
{attachmentsFromMessage.map((attachment) => (
<PreviewAttachment
key={attachment.url}
attachment={{
name: attachment.filename ?? 'file',
contentType: attachment.mediaType,
url: attachment.url,
}}
/>
))}
</div>
)}
{message.parts?.map((part, index) => {
const { type } = part;
@ -96,7 +108,7 @@ const PurePreviewMessage = ({
<MessageReasoning
key={key}
isLoading={isLoading}
reasoning={part.reasoning}
reasoning={part.text}
/>
);
}
@ -146,75 +158,151 @@ const PurePreviewMessage = ({
message={message}
setMode={setMode}
setMessages={setMessages}
reload={reload}
regenerate={regenerate}
/>
</div>
);
}
}
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 (
<div
key={toolCallId}
className={cx({
skeleton: ['getWeather'].includes(toolName),
})}
>
{toolName === 'getWeather' ? (
<Weather />
) : toolName === 'createDocument' ? (
<DocumentPreview isReadonly={isReadonly} args={args} />
) : toolName === 'updateDocument' ? (
<DocumentToolCall
type="update"
args={args}
isReadonly={isReadonly}
/>
) : toolName === 'requestSuggestions' ? (
<DocumentToolCall
type="request-suggestions"
args={args}
isReadonly={isReadonly}
/>
) : null}
<div key={toolCallId} className="skeleton">
<Weather />
</div>
);
}
if (state === 'result') {
const { result } = toolInvocation;
if (state === 'output-available') {
const { output } = part;
return (
<div key={toolCallId}>
<Weather weatherAtLocation={output} />
</div>
);
}
}
if (type === 'tool-createDocument') {
const { toolCallId, state } = part;
if (state === 'input-available') {
const { input } = part;
return (
<div key={toolCallId}>
<DocumentPreview isReadonly={isReadonly} args={input} />
</div>
);
}
if (state === 'output-available') {
const { output } = part;
if ('error' in output) {
return (
<div
key={toolCallId}
className="text-red-500 p-2 border rounded"
>
Error: {String(output.error)}
</div>
);
}
return (
<div key={toolCallId}>
{toolName === 'getWeather' ? (
<Weather weatherAtLocation={result} />
) : toolName === 'createDocument' ? (
<DocumentPreview
isReadonly={isReadonly}
result={result}
/>
) : toolName === 'updateDocument' ? (
<DocumentToolResult
type="update"
result={result}
isReadonly={isReadonly}
/>
) : toolName === 'requestSuggestions' ? (
<DocumentToolResult
type="request-suggestions"
result={result}
isReadonly={isReadonly}
/>
) : (
<pre>{JSON.stringify(result, null, 2)}</pre>
)}
<DocumentPreview
isReadonly={isReadonly}
result={output}
/>
</div>
);
}
}
if (type === 'tool-updateDocument') {
const { toolCallId, state } = part;
if (state === 'input-available') {
const { input } = part;
return (
<div key={toolCallId}>
<DocumentToolCall
type="update"
args={input}
isReadonly={isReadonly}
/>
</div>
);
}
if (state === 'output-available') {
const { output } = part;
if ('error' in output) {
return (
<div
key={toolCallId}
className="text-red-500 p-2 border rounded"
>
Error: {String(output.error)}
</div>
);
}
return (
<div key={toolCallId}>
<DocumentToolResult
type="update"
result={output}
isReadonly={isReadonly}
/>
</div>
);
}
}
if (type === 'tool-requestSuggestions') {
const { toolCallId, state } = part;
if (state === 'input-available') {
const { input } = part;
return (
<div key={toolCallId}>
<DocumentToolCall
type="request-suggestions"
args={input}
isReadonly={isReadonly}
/>
</div>
);
}
if (state === 'output-available') {
const { output } = part;
if ('error' in output) {
return (
<div
key={toolCallId}
className="text-red-500 p-2 border rounded"
>
Error: {String(output.error)}
</div>
);
}
return (
<div key={toolCallId}>
<DocumentToolResult
type="request-suggestions"
result={output}
isReadonly={isReadonly}
/>
</div>
);
}
@ -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;
},
);

View file

@ -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<ChatMessage>['status'];
votes: Array<Vote> | undefined;
messages: Array<UIMessage>;
setMessages: UseChatHelpers['setMessages'];
reload: UseChatHelpers['reload'];
messages: ChatMessage[];
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
regenerate: UseChatHelpers<ChatMessage>['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 (
<div
ref={messagesContainerRef}
@ -58,7 +61,7 @@ function PureMessages({
: undefined
}
setMessages={setMessages}
reload={reload}
regenerate={regenerate}
isReadonly={isReadonly}
requiresScrollPadding={
hasSentMessage && index === messages.length - 1
@ -84,10 +87,9 @@ export const Messages = memo(PureMessages, (prevProps, nextProps) => {
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;
});

View file

@ -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<SetStateAction<string>>;
status: UseChatHelpers<ChatMessage>['status'];
stop: () => void;
attachments: Array<Attachment>;
setAttachments: Dispatch<SetStateAction<Array<Attachment>>>;
messages: Array<UIMessage>;
setMessages: UseChatHelpers['setMessages'];
append: UseChatHelpers['append'];
handleSubmit: UseChatHelpers['handleSubmit'];
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
sendMessage: UseChatHelpers<ChatMessage>['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 && (
<SuggestedActions
append={append}
sendMessage={sendMessage}
chatId={chatId}
selectedVisibilityType={selectedVisibilityType}
/>
@ -328,7 +342,7 @@ function PureAttachmentsButton({
status,
}: {
fileInputRef: React.MutableRefObject<HTMLInputElement | null>;
status: UseChatHelpers['status'];
status: UseChatHelpers<ChatMessage>['status'];
}) {
return (
<Button
@ -353,7 +367,7 @@ function PureStopButton({
setMessages,
}: {
stop: () => void;
setMessages: UseChatHelpers['setMessages'];
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
}) {
return (
<Button

View file

@ -1,5 +1,4 @@
import type { Attachment } from 'ai';
import type { Attachment } from '@/lib/types';
import { LoaderIcon } from './icons';
export const PreviewAttachment = ({

View file

@ -5,16 +5,17 @@ import { Button } from './ui/button';
import { memo } from 'react';
import type { UseChatHelpers } from '@ai-sdk/react';
import type { VisibilityType } from './visibility-selector';
import type { ChatMessage } from '@/lib/types';
interface SuggestedActionsProps {
chatId: string;
append: UseChatHelpers['append'];
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
selectedVisibilityType: VisibilityType;
}
function PureSuggestedActions({
chatId,
append,
sendMessage,
selectedVisibilityType,
}: SuggestedActionsProps) {
const suggestedActions = [
@ -59,9 +60,9 @@ function PureSuggestedActions({
onClick={async () => {
window.history.replaceState({}, '', `/chat/${chatId}`);
append({
sendMessage({
role: 'user',
content: suggestedAction.action,
parts: [{ type: 'text', text: suggestedAction.action }],
});
}}
className="text-left border rounded-xl px-4 py-3.5 text-sm flex-1 gap-1 sm:flex-col w-full h-auto justify-start items-start"

View file

@ -1,6 +1,4 @@
'use client';
import type { Message } from 'ai';
import cx from 'classnames';
import {
AnimatePresence,
@ -11,7 +9,7 @@ import {
import {
type Dispatch,
memo,
ReactNode,
type ReactNode,
type SetStateAction,
useEffect,
useRef,
@ -27,9 +25,10 @@ import {
} from '@/components/ui/tooltip';
import { ArrowUpIcon, StopIcon, SummarizeIcon } from './icons';
import { artifactDefinitions, ArtifactKind } from './artifact';
import { ArtifactToolbarItem } from './create-artifact';
import { UseChatHelpers } from '@ai-sdk/react';
import { artifactDefinitions, type ArtifactKind } from './artifact';
import type { ArtifactToolbarItem } from './create-artifact';
import type { UseChatHelpers } from '@ai-sdk/react';
import type { ChatMessage } from '@/lib/types';
type ToolProps = {
description: string;
@ -39,11 +38,11 @@ type ToolProps = {
isToolbarVisible?: boolean;
setIsToolbarVisible?: Dispatch<SetStateAction<boolean>>;
isAnimating: boolean;
append: UseChatHelpers['append'];
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
onClick: ({
appendMessage,
sendMessage,
}: {
appendMessage: UseChatHelpers['append'];
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
}) => void;
};
@ -55,7 +54,7 @@ const Tool = ({
isToolbarVisible,
setIsToolbarVisible,
isAnimating,
append,
sendMessage,
onClick,
}: ToolProps) => {
const [isHovered, setIsHovered] = useState(false);
@ -82,7 +81,7 @@ const Tool = ({
setSelectedTool(description);
} else {
setSelectedTool(null);
onClick({ appendMessage: append });
onClick({ sendMessage });
}
};
@ -135,12 +134,12 @@ const randomArr = [...Array(6)].map((x) => nanoid(5));
const ReadingLevelSelector = ({
setSelectedTool,
append,
sendMessage,
isAnimating,
}: {
setSelectedTool: Dispatch<SetStateAction<string | null>>;
isAnimating: boolean;
append: UseChatHelpers['append'];
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
}) => {
const LEVELS = [
'Elementary',
@ -214,9 +213,14 @@ const ReadingLevelSelector = ({
}}
onClick={() => {
if (currentLevel !== 2 && hasUserSelectedLevel) {
append({
sendMessage({
role: 'user',
content: `Please adjust the reading level to ${LEVELS[currentLevel]} level.`,
parts: [
{
type: 'text',
text: `Please adjust the reading level to ${LEVELS[currentLevel]} level.`,
},
],
});
setSelectedTool(null);
@ -243,7 +247,7 @@ export const Tools = ({
isToolbarVisible,
selectedTool,
setSelectedTool,
append,
sendMessage,
isAnimating,
setIsToolbarVisible,
tools,
@ -251,7 +255,7 @@ export const Tools = ({
isToolbarVisible: boolean;
selectedTool: string | null;
setSelectedTool: Dispatch<SetStateAction<string | null>>;
append: UseChatHelpers['append'];
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
isAnimating: boolean;
setIsToolbarVisible: Dispatch<SetStateAction<boolean>>;
tools: Array<ArtifactToolbarItem>;
@ -274,7 +278,7 @@ export const Tools = ({
icon={secondaryTool.icon}
selectedTool={selectedTool}
setSelectedTool={setSelectedTool}
append={append}
sendMessage={sendMessage}
isAnimating={isAnimating}
onClick={secondaryTool.onClick}
/>
@ -288,7 +292,7 @@ export const Tools = ({
setSelectedTool={setSelectedTool}
isToolbarVisible={isToolbarVisible}
setIsToolbarVisible={setIsToolbarVisible}
append={append}
sendMessage={sendMessage}
isAnimating={isAnimating}
onClick={primaryTool.onClick}
/>
@ -299,7 +303,7 @@ export const Tools = ({
const PureToolbar = ({
isToolbarVisible,
setIsToolbarVisible,
append,
sendMessage,
status,
stop,
setMessages,
@ -307,10 +311,10 @@ const PureToolbar = ({
}: {
isToolbarVisible: boolean;
setIsToolbarVisible: Dispatch<SetStateAction<boolean>>;
status: UseChatHelpers['status'];
append: UseChatHelpers['append'];
stop: UseChatHelpers['stop'];
setMessages: UseChatHelpers['setMessages'];
status: UseChatHelpers<ChatMessage>['status'];
sendMessage: UseChatHelpers<ChatMessage>['sendMessage'];
stop: UseChatHelpers<ChatMessage>['stop'];
setMessages: UseChatHelpers<ChatMessage>['setMessages'];
artifactKind: ArtifactKind;
}) => {
const toolbarRef = useRef<HTMLDivElement>(null);
@ -431,14 +435,14 @@ const PureToolbar = ({
) : selectedTool === 'adjust-reading-level' ? (
<ReadingLevelSelector
key="reading-level-selector"
append={append}
sendMessage={sendMessage}
setSelectedTool={setSelectedTool}
isAnimating={isAnimating}
/>
) : (
<Tools
key="tools"
append={append}
sendMessage={sendMessage}
isAnimating={isAnimating}
isToolbarVisible={isToolbarVisible}
selectedTool={selectedTool}