The isStreaming prop was passed through multiple chat components but wasn't being strict enough in the Markdown component where it was ultimately passed causing the quick actions to be disabled.
117 lines
4.4 KiB
TypeScript
117 lines
4.4 KiB
TypeScript
import type { Message } from 'ai';
|
|
import { Fragment } from 'react';
|
|
import { classNames } from '~/utils/classNames';
|
|
import { AssistantMessage } from './AssistantMessage';
|
|
import { UserMessage } from './UserMessage';
|
|
import { useLocation } from '@remix-run/react';
|
|
import { db, chatId } from '~/lib/persistence/useChatHistory';
|
|
import { forkChat } from '~/lib/persistence/db';
|
|
import { toast } from 'react-toastify';
|
|
import { useStore } from '@nanostores/react';
|
|
import { profileStore } from '~/lib/stores/profile';
|
|
import { forwardRef } from 'react';
|
|
import type { ForwardedRef } from 'react';
|
|
|
|
interface MessagesProps {
|
|
id?: string;
|
|
className?: string;
|
|
isStreaming?: boolean;
|
|
messages?: Message[];
|
|
append?: (message: Message) => void;
|
|
chatMode?: 'discuss' | 'build';
|
|
setChatMode?: (mode: 'discuss' | 'build') => void;
|
|
}
|
|
|
|
export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
|
|
(props: MessagesProps, ref: ForwardedRef<HTMLDivElement> | undefined) => {
|
|
const { id, isStreaming = false, messages = [] } = props;
|
|
const location = useLocation();
|
|
const profile = useStore(profileStore);
|
|
|
|
const handleRewind = (messageId: string) => {
|
|
const searchParams = new URLSearchParams(location.search);
|
|
searchParams.set('rewindTo', messageId);
|
|
window.location.search = searchParams.toString();
|
|
};
|
|
|
|
const handleFork = async (messageId: string) => {
|
|
try {
|
|
if (!db || !chatId.get()) {
|
|
toast.error('Chat persistence is not available');
|
|
return;
|
|
}
|
|
|
|
const urlId = await forkChat(db, chatId.get()!, messageId);
|
|
window.location.href = `/chat/${urlId}`;
|
|
} catch (error) {
|
|
toast.error('Failed to fork chat: ' + (error as Error).message);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div id={id} className={props.className} ref={ref}>
|
|
{messages.length > 0
|
|
? messages.map((message, index) => {
|
|
const { role, content, id: messageId, annotations } = message;
|
|
const isUserMessage = role === 'user';
|
|
const isFirst = index === 0;
|
|
const isLast = index === messages.length - 1;
|
|
const isHidden = annotations?.includes('hidden');
|
|
|
|
if (isHidden) {
|
|
return <Fragment key={index} />;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
className={classNames('flex gap-4 p-6 py-5 w-full rounded-[calc(0.75rem-1px)]', {
|
|
'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
|
|
'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
|
|
isStreaming && isLast,
|
|
'mt-4': !isFirst,
|
|
})}
|
|
>
|
|
{isUserMessage && (
|
|
<div className="flex items-center justify-center w-[40px] h-[40px] overflow-hidden bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-500 rounded-full shrink-0 self-start">
|
|
{profile?.avatar ? (
|
|
<img
|
|
src={profile.avatar}
|
|
alt={profile?.username || 'User'}
|
|
className="w-full h-full object-cover"
|
|
loading="eager"
|
|
decoding="sync"
|
|
/>
|
|
) : (
|
|
<div className="i-ph:user-fill text-2xl" />
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className="grid grid-col-1 w-full">
|
|
{isUserMessage ? (
|
|
<UserMessage content={content} />
|
|
) : (
|
|
<AssistantMessage
|
|
content={content}
|
|
annotations={message.annotations}
|
|
messageId={messageId}
|
|
onRewind={handleRewind}
|
|
onFork={handleFork}
|
|
append={props.append}
|
|
chatMode={props.chatMode}
|
|
setChatMode={props.setChatMode}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
: null}
|
|
{isStreaming && (
|
|
<div className="text-center w-full text-bolt-elements-item-contentAccent i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
);
|