refactor: replace message.content with message.parts (#868)

This commit is contained in:
Jeremy 2025-03-16 18:42:29 -07:00 committed by GitHub
parent 553a3d825a
commit 47a630fd53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1311 additions and 311 deletions

View file

@ -1,6 +1,6 @@
'use client';
import type { ChatRequestOptions, Message } from 'ai';
import type { UIMessage } from 'ai';
import cx from 'classnames';
import { AnimatePresence, motion } from 'framer-motion';
import { memo, useState } from 'react';
@ -18,6 +18,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { MessageEditor } from './message-editor';
import { DocumentPreview } from './document-preview';
import { MessageReasoning } from './message-reasoning';
import { UseChatHelpers } from '@ai-sdk/react';
const PurePreviewMessage = ({
chatId,
@ -29,15 +30,11 @@ const PurePreviewMessage = ({
isReadonly,
}: {
chatId: string;
message: Message;
message: UIMessage;
vote: Vote | undefined;
isLoading: boolean;
setMessages: (
messages: Message[] | ((messages: Message[]) => Message[]),
) => void;
reload: (
chatRequestOptions?: ChatRequestOptions,
) => Promise<string | null | undefined>;
setMessages: UseChatHelpers['setMessages'];
reload: UseChatHelpers['reload'];
isReadonly: boolean;
}) => {
const [mode, setMode] = useState<'view' | 'edit'>('view');
@ -83,96 +80,79 @@ const PurePreviewMessage = ({
</div>
)}
{message.reasoning && (
<MessageReasoning
isLoading={isLoading}
reasoning={message.reasoning}
/>
)}
{message.parts?.map((part, index) => {
const { type } = part;
const key = `message-${message.id}-part-${index}`;
{(message.content || message.reasoning) && mode === 'view' && (
<div
data-testid="message-content"
className="flex flex-row gap-2 items-start"
>
{message.role === 'user' && !isReadonly && (
<Tooltip>
<TooltipTrigger asChild>
<Button
data-testid={`message-edit`}
variant="ghost"
className="px-2 h-fit rounded-full text-muted-foreground opacity-0 group-hover/message:opacity-100"
onClick={() => {
setMode('edit');
}}
if (type === 'reasoning') {
return (
<MessageReasoning
key={key}
isLoading={isLoading}
reasoning={part.reasoning}
/>
);
}
if (type === 'text') {
if (mode === 'view') {
return (
<div key={key} className="flex flex-row gap-2 items-start">
{message.role === 'user' && !isReadonly && (
<Tooltip>
<TooltipTrigger asChild>
<Button
data-testid="message-edit-button"
variant="ghost"
className="px-2 h-fit rounded-full text-muted-foreground opacity-0 group-hover/message:opacity-100"
onClick={() => {
setMode('edit');
}}
>
<PencilEditIcon />
</Button>
</TooltipTrigger>
<TooltipContent>Edit message</TooltipContent>
</Tooltip>
)}
<div
data-testid="message-content"
className={cn('flex flex-col gap-4', {
'bg-primary text-primary-foreground px-3 py-2 rounded-xl':
message.role === 'user',
})}
>
<PencilEditIcon />
</Button>
</TooltipTrigger>
<TooltipContent>Edit message</TooltipContent>
</Tooltip>
)}
<div
className={cn('flex flex-col gap-4', {
'bg-primary text-primary-foreground px-3 py-2 rounded-xl':
message.role === 'user',
})}
>
<Markdown>{message.content as string}</Markdown>
</div>
</div>
)}
{message.content && mode === 'edit' && (
<div className="flex flex-row gap-2 items-start">
<div className="size-8" />
<MessageEditor
key={message.id}
message={message}
setMode={setMode}
setMessages={setMessages}
reload={reload}
/>
</div>
)}
{message.toolInvocations && message.toolInvocations.length > 0 && (
<div className="flex flex-col gap-4">
{message.toolInvocations.map((toolInvocation) => {
const { toolName, toolCallId, state, args } = toolInvocation;
if (state === 'result') {
const { result } = toolInvocation;
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>
)}
<Markdown>{part.text}</Markdown>
</div>
);
}
</div>
);
}
if (mode === 'edit') {
return (
<div key={key} className="flex flex-row gap-2 items-start">
<div className="size-8" />
<MessageEditor
key={message.id}
message={message}
setMode={setMode}
setMessages={setMessages}
reload={reload}
/>
</div>
);
}
}
if (type === 'tool-invocation') {
const { toolInvocation } = part;
const { toolName, toolCallId, state } = toolInvocation;
if (state === 'call') {
const { args } = toolInvocation;
return (
<div
key={toolCallId}
@ -199,9 +179,40 @@ const PurePreviewMessage = ({
) : null}
</div>
);
})}
</div>
)}
}
if (state === 'result') {
const { result } = toolInvocation;
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>
)}
</div>
);
}
}
})}
{!isReadonly && (
<MessageActions
@ -223,16 +234,8 @@ export const PreviewMessage = memo(
PurePreviewMessage,
(prevProps, nextProps) => {
if (prevProps.isLoading !== nextProps.isLoading) return false;
if (prevProps.message.reasoning !== nextProps.message.reasoning)
return false;
if (prevProps.message.content !== nextProps.message.content) return false;
if (
!equal(
prevProps.message.toolInvocations,
nextProps.message.toolInvocations,
)
)
return false;
if (prevProps.message.id !== nextProps.message.id) return false;
if (!equal(prevProps.message.parts, nextProps.message.parts)) return false;
if (!equal(prevProps.vote, nextProps.vote)) return false;
return true;
@ -264,7 +267,7 @@ export const ThinkingMessage = () => {
<div className="flex flex-col gap-2 w-full">
<div className="flex flex-col gap-4 text-muted-foreground">
Thinking...
Hmm...
</div>
</div>
</div>