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,68 +63,74 @@ 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="flex"> <div className="artifact border border-bolt-elements-borderColor flex flex-col overflow-hidden rounded-lg w-full transition-border duration-150">
<button <div className="flex">
className="flex items-stretch bg-bolt-elements-artifacts-background hover:bg-bolt-elements-artifacts-backgroundHover w-full overflow-hidden" <button
onClick={() => { className="flex items-stretch bg-bolt-elements-artifacts-background hover:bg-bolt-elements-artifacts-backgroundHover w-full overflow-hidden"
const showWorkbench = workbenchStore.showWorkbench.get(); onClick={() => {
workbenchStore.showWorkbench.set(!showWorkbench); const showWorkbench = workbenchStore.showWorkbench.get();
}} workbenchStore.showWorkbench.set(!showWorkbench);
> }}
{artifact.type == 'bundled' && ( >
<> <div className="px-5 p-3.5 w-full text-left">
<div className="p-4"> <div className="w-full text-bolt-elements-textPrimary font-medium leading-5 text-sm">
{allActionFinished ? ( {artifact.type === 'bundled' ? 'Setup Project' : artifact?.title}
<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>
<div className="bg-bolt-elements-artifacts-borderColor w-[1px]" /> <div className="w-full w-full text-bolt-elements-textSecondary text-xs mt-0.5">
</> Click to open Workbench
)} </div>
<div className="px-5 p-3.5 w-full text-left"> </div>
<div className="w-full text-bolt-elements-textPrimary font-medium leading-5 text-sm">{artifact?.title}</div> </button>
<div className="w-full w-full text-bolt-elements-textSecondary text-xs mt-0.5">Click to open Workbench</div> {artifact.type !== 'bundled' && <div className="bg-bolt-elements-artifacts-borderColor w-[1px]" />}
<AnimatePresence>
{actions.length && artifact.type !== 'bundled' && (
<motion.button
initial={{ width: 0 }}
animate={{ width: 'auto' }}
exit={{ width: 0 }}
transition={{ duration: 0.15, ease: cubicEasingFn }}
className="bg-bolt-elements-artifacts-background hover:bg-bolt-elements-artifacts-backgroundHover"
onClick={toggleActions}
>
<div className="p-4">
<div className={showActions ? 'i-ph:caret-up-bold' : 'i-ph:caret-down-bold'}></div>
</div>
</motion.button>
)}
</AnimatePresence>
</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> </div>
</button> )}
<div className="bg-bolt-elements-artifacts-borderColor w-[1px]" />
<AnimatePresence> <AnimatePresence>
{actions.length && artifact.type !== 'bundled' && ( {artifact.type !== 'bundled' && showActions && actions.length > 0 && (
<motion.button <motion.div
initial={{ width: 0 }} className="actions"
animate={{ width: 'auto' }} initial={{ height: 0 }}
exit={{ width: 0 }} animate={{ height: 'auto' }}
transition={{ duration: 0.15, ease: cubicEasingFn }} exit={{ height: '0px' }}
className="bg-bolt-elements-artifacts-background hover:bg-bolt-elements-artifacts-backgroundHover" transition={{ duration: 0.15 }}
onClick={toggleActions}
> >
<div className="p-4"> <div className="bg-bolt-elements-artifacts-borderColor h-[1px]" />
<div className={showActions ? 'i-ph:caret-up-bold' : 'i-ph:caret-down-bold'}></div>
<div className="p-5 text-left bg-bolt-elements-actions-background">
<ActionList actions={actions} />
</div> </div>
</motion.button> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
<AnimatePresence> </>
{artifact.type !== 'bundled' && showActions && actions.length > 0 && (
<motion.div
className="actions"
initial={{ height: 0 }}
animate={{ height: 'auto' }}
exit={{ height: '0px' }}
transition={{ duration: 0.15 }}
>
<div className="bg-bolt-elements-artifacts-borderColor h-[1px]" />
<div className="p-5 text-left bg-bolt-elements-actions-background">
<ActionList actions={actions} />
</div>
</motion.div>
)}
</AnimatePresence>
</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>
)} )}
{usage && ( <div className="flex w-full items-center justify-between mb-2">
<div> {usage && (
Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens}) <div>
</div> Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens})
)} </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));