feat(chat): adjust chat layout and add rewind/fork functionality

- Modify chat max/min width for better responsiveness
- Update UserMessage and AssistantMessage components for improved alignment
- Add rewind and fork functionality to AssistantMessage
- Refactor Artifact component to handle bundled artifacts more clearly
This commit is contained in:
KevIsDev 2025-04-15 22:54:00 +01:00
parent 682ed764a9
commit 3ca85875f1
5 changed files with 105 additions and 93 deletions

View File

@ -63,6 +63,7 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
}, [actions]); }, [actions]);
return ( return (
<>
<div className="artifact border border-bolt-elements-borderColor flex flex-col overflow-hidden rounded-lg w-full transition-border duration-150"> <div className="artifact border border-bolt-elements-borderColor flex flex-col overflow-hidden rounded-lg w-full transition-border duration-150">
<div className="flex"> <div className="flex">
<button <button
@ -72,24 +73,16 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
workbenchStore.showWorkbench.set(!showWorkbench); workbenchStore.showWorkbench.set(!showWorkbench);
}} }}
> >
{artifact.type == 'bundled' && (
<>
<div className="p-4">
{allActionFinished ? (
<div className={'i-ph:files-light'} style={{ fontSize: '2rem' }}></div>
) : (
<div className={'i-svg-spinners:90-ring-with-bg'} style={{ fontSize: '2rem' }}></div>
)}
</div>
<div className="bg-bolt-elements-artifacts-borderColor w-[1px]" />
</>
)}
<div className="px-5 p-3.5 w-full text-left"> <div className="px-5 p-3.5 w-full text-left">
<div className="w-full text-bolt-elements-textPrimary font-medium leading-5 text-sm">{artifact?.title}</div> <div className="w-full text-bolt-elements-textPrimary font-medium leading-5 text-sm">
<div className="w-full w-full text-bolt-elements-textSecondary text-xs mt-0.5">Click to open Workbench</div> {artifact.type === 'bundled' ? 'Setup Project' : artifact?.title}
</div>
<div className="w-full w-full text-bolt-elements-textSecondary text-xs mt-0.5">
Click to open Workbench
</div>
</div> </div>
</button> </button>
<div className="bg-bolt-elements-artifacts-borderColor w-[1px]" /> {artifact.type !== 'bundled' && <div className="bg-bolt-elements-artifacts-borderColor w-[1px]" />}
<AnimatePresence> <AnimatePresence>
{actions.length && artifact.type !== 'bundled' && ( {actions.length && artifact.type !== 'bundled' && (
<motion.button <motion.button
@ -107,6 +100,18 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
{artifact.type === 'bundled' && (
<div className="flex items-center gap-1.5 p-5 bg-bolt-elements-actions-background border-t border-bolt-elements-artifacts-borderColor">
<div className={classNames('text-lg', getIconColor(allActionFinished ? 'complete' : 'running'))}>
{allActionFinished ? (
<div className="i-ph:check"></div>
) : (
<div className="i-svg-spinners:90-ring-with-bg"></div>
)}
</div>
<div className="text-bolt-elements-textPrimary font-medium leading-5 text-sm">Create initial files</div>
</div>
)}
<AnimatePresence> <AnimatePresence>
{artifact.type !== 'bundled' && showActions && actions.length > 0 && ( {artifact.type !== 'bundled' && showActions && actions.length > 0 && (
<motion.div <motion.div
@ -125,6 +130,7 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
</>
); );
}); });

View File

@ -4,10 +4,14 @@ import type { JSONValue } from 'ai';
import Popover from '~/components/ui/Popover'; import Popover from '~/components/ui/Popover';
import { workbenchStore } from '~/lib/stores/workbench'; import { workbenchStore } from '~/lib/stores/workbench';
import { WORK_DIR } from '~/utils/constants'; import { WORK_DIR } from '~/utils/constants';
import WithTooltip from '~/components/ui/Tooltip';
interface AssistantMessageProps { interface AssistantMessageProps {
content: string; content: string;
annotations?: JSONValue[]; annotations?: JSONValue[];
messageId?: string;
onRewind?: (messageId: string) => void;
onFork?: (messageId: string) => void;
} }
function openArtifactInWorkbench(filePath: string) { function openArtifactInWorkbench(filePath: string) {
@ -34,7 +38,7 @@ function normalizedFilePath(path: string) {
return normalizedPath; return normalizedPath;
} }
export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => { export const AssistantMessage = memo(({ content, annotations, messageId, onRewind, onFork }: AssistantMessageProps) => {
const filteredAnnotations = (annotations?.filter( const filteredAnnotations = (annotations?.filter(
(annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'), (annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
) || []) as { type: string; value: any } & { [key: string]: any }[]; ) || []) as { type: string; value: any } & { [key: string]: any }[];
@ -100,11 +104,35 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage
<div className="context"></div> <div className="context"></div>
</Popover> </Popover>
)} )}
<div className="flex w-full items-center justify-between mb-2">
{usage && ( {usage && (
<div> <div>
Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens}) Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens})
</div> </div>
)} )}
{(onRewind || onFork) && messageId && (
<div className="flex gap-2 flex-col lg:flex-row ml-auto">
{onRewind && (
<WithTooltip tooltip="Revert to this message">
<button
onClick={() => onRewind(messageId)}
key="i-ph:arrow-u-up-left"
className="i-ph:arrow-u-up-left text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors"
/>
</WithTooltip>
)}
{onFork && (
<WithTooltip tooltip="Fork chat from this message">
<button
onClick={() => onFork(messageId)}
key="i-ph:git-fork"
className="i-ph:git-fork text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors"
/>
</WithTooltip>
)}
</div>
)}
</div>
</div> </div>
</> </>
<Markdown html>{content}</Markdown> <Markdown html>{content}</Markdown>

View File

@ -7,7 +7,6 @@ import { useLocation } from '@remix-run/react';
import { db, chatId } from '~/lib/persistence/useChatHistory'; import { db, chatId } from '~/lib/persistence/useChatHistory';
import { forkChat } from '~/lib/persistence/db'; import { forkChat } from '~/lib/persistence/db';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import WithTooltip from '~/components/ui/Tooltip';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { profileStore } from '~/lib/stores/profile'; import { profileStore } from '~/lib/stores/profile';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
@ -63,7 +62,7 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
return ( return (
<div <div
key={index} key={index}
className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)]', { 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-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent': 'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
isStreaming && isLast, isStreaming && isLast,
@ -89,36 +88,15 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
{isUserMessage ? ( {isUserMessage ? (
<UserMessage content={content} /> <UserMessage content={content} />
) : ( ) : (
<AssistantMessage content={content} annotations={message.annotations} /> <AssistantMessage
content={content}
annotations={message.annotations}
messageId={messageId}
onRewind={handleRewind}
onFork={handleFork}
/>
)} )}
</div> </div>
{!isUserMessage && (
<div className="flex gap-2 flex-col lg:flex-row">
{messageId && (
<WithTooltip tooltip="Revert to this message">
<button
onClick={() => handleRewind(messageId)}
key="i-ph:arrow-u-up-left"
className={classNames(
'i-ph:arrow-u-up-left',
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
)}
/>
</WithTooltip>
)}
<WithTooltip tooltip="Fork chat from this message">
<button
onClick={() => handleFork(messageId)}
key="i-ph:git-fork"
className={classNames(
'i-ph:git-fork',
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
)}
/>
</WithTooltip>
</div>
)}
</div> </div>
); );
}) })

View File

@ -16,7 +16,7 @@ export function UserMessage({ content }: UserMessageProps) {
const images = content.filter((item) => item.type === 'image' && item.image); const images = content.filter((item) => item.type === 'image' && item.image);
return ( return (
<div className="overflow-hidden pt-[4px]"> <div className="overflow-hidden flex items-center">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{textContent && <Markdown html>{textContent}</Markdown>} {textContent && <Markdown html>{textContent}</Markdown>}
{images.map((item, index) => ( {images.map((item, index) => (

View File

@ -217,8 +217,8 @@
*/ */
:root { :root {
--header-height: 54px; --header-height: 54px;
--chat-max-width: 37rem; --chat-max-width: 35rem;
--chat-min-width: 640px; --chat-min-width: 575px;
--workbench-width: min(calc(100% - var(--chat-min-width)), 2536px); --workbench-width: min(calc(100% - var(--chat-min-width)), 2536px);
--workbench-inner-width: var(--workbench-width); --workbench-inner-width: var(--workbench-width);
--workbench-left: calc(100% - var(--workbench-width)); --workbench-left: calc(100% - var(--workbench-width));