mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
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:
parent
682ed764a9
commit
3ca85875f1
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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) => (
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user