refactor(chat): improve UI layout, artifact handling, and template naming

- Restructured alert components in BaseChat for better layout organization
- Updated artifact component to display dynamic titles based on state
- Simplified template names in constants for better readability
- Enhanced snapshot restoration process by consolidating command actions into a single artifact
This commit is contained in:
KevIsDev 2025-04-28 14:03:58 +01:00
parent bf03b6f0fe
commit 42eaa2f5e1
8 changed files with 94 additions and 79 deletions

View File

@ -75,7 +75,7 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
> >
<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"> <div className="w-full text-bolt-elements-textPrimary font-medium leading-5 text-sm">
{artifact.type === 'bundled' ? 'Setup Project' : artifact?.title} {artifact?.title}
</div> </div>
<div className="w-full w-full text-bolt-elements-textSecondary text-xs mt-0.5"> <div className="w-full w-full text-bolt-elements-textSecondary text-xs mt-0.5">
Click to open Workbench Click to open Workbench
@ -109,7 +109,13 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
<div className="i-svg-spinners:90-ring-with-bg"></div> <div className="i-svg-spinners:90-ring-with-bg"></div>
)} )}
</div> </div>
<div className="text-bolt-elements-textPrimary font-medium leading-5 text-sm">Create initial files</div> <div className="text-bolt-elements-textPrimary font-medium leading-5 text-sm">
{allActionFinished
? artifact.id === 'imported-files'
? 'Restore files from snapshot'
: 'Initial files created'
: 'Creating initial files'}
</div>
</div> </div>
)} )}
<AnimatePresence> <AnimatePresence>

View File

@ -367,33 +367,32 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
}} }}
</ClientOnly> </ClientOnly>
</StickToBottom.Content> </StickToBottom.Content>
{deployAlert && (
<DeployChatAlert
alert={deployAlert}
clearAlert={() => clearDeployAlert?.()}
postMessage={(message: string | undefined) => {
sendMessage?.({} as any, message);
clearSupabaseAlert?.();
}}
/>
)}
{supabaseAlert && (
<SupabaseChatAlert
alert={supabaseAlert}
clearAlert={() => clearSupabaseAlert?.()}
postMessage={(message) => {
sendMessage?.({} as any, message);
clearSupabaseAlert?.();
}}
/>
)}
<ScrollToBottom />
<div <div
className={classNames('my-auto flex flex-col gap-2 w-full max-w-chat mx-auto z-prompt mb-6', { className={classNames('my-auto flex flex-col gap-2 w-full max-w-chat mx-auto z-prompt mb-6', {
'sticky bottom-2': chatStarted, 'sticky bottom-2': chatStarted,
})} })}
> >
<div className="bg-bolt-elements-background-depth-2"> <div className="flex flex-col gap-2">
{deployAlert && (
<DeployChatAlert
alert={deployAlert}
clearAlert={() => clearDeployAlert?.()}
postMessage={(message: string | undefined) => {
sendMessage?.({} as any, message);
clearSupabaseAlert?.();
}}
/>
)}
{supabaseAlert && (
<SupabaseChatAlert
alert={supabaseAlert}
clearAlert={() => clearSupabaseAlert?.()}
postMessage={(message) => {
sendMessage?.({} as any, message);
clearSupabaseAlert?.();
}}
/>
)}
{actionAlert && ( {actionAlert && (
<ChatAlert <ChatAlert
alert={actionAlert} alert={actionAlert}
@ -405,10 +404,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
/> />
)} )}
</div> </div>
<ScrollToBottom />
{progressAnnotations && <ProgressCompilation data={progressAnnotations} />} {progressAnnotations && <ProgressCompilation data={progressAnnotations} />}
<div <div
className={classNames( className={classNames(
'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt', 'relative bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
/* /*
* { * {
@ -686,7 +686,7 @@ function ScrollToBottom() {
return ( return (
!isAtBottom && ( !isAtBottom && (
<button <button
className="absolute z-50 top-[50%] translate-y-[-60%] text-4xl rounded-lg left-[50%] translate-x-[-50%] px-1.5 py-0.5 flex items-center gap-2 bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor text-bolt-elements-textPrimary text-sm" className="absolute z-50 top-[0%] translate-y-[-100%] text-4xl rounded-lg left-[50%] translate-x-[-50%] px-1.5 py-0.5 flex items-center gap-2 bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor text-bolt-elements-textPrimary text-sm"
onClick={() => scrollToBottom()} onClick={() => scrollToBottom()}
> >
Go to last message Go to last message

View File

@ -316,14 +316,14 @@ export const ChatImpl = memo(
setFakeLoading(true); setFakeLoading(true);
if (autoSelectTemplate) { if (autoSelectTemplate) {
const { template, title } = await selectStarterTemplate({ const { template } = await selectStarterTemplate({
message: messageContent, message: messageContent,
model, model,
provider, provider,
}); });
if (template !== 'blank') { if (template !== 'blank') {
const temResp = await getTemplates(template, title).catch((e) => { const temResp = await getTemplates(template).catch((e) => {
if (e.message.includes('rate limit')) { if (e.message.includes('rate limit')) {
toast.warning('Rate limit exceeded. Skipping starter template\n Continuing with blank template'); toast.warning('Rate limit exceeded. Skipping starter template\n Continuing with blank template');
} else { } else {

View File

@ -99,7 +99,7 @@ export function SupabaseChatAlert({ alert, clearAlert, postMessage }: Props) {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }} exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className="max-w-chat rounded-lg border-l-2 border-l-[#098F5F] border-bolt-elements-borderColor bg-bolt-elements-background-depth-2" className="max-w-chat rounded-lg border-l-2 border-l-[#098F5F] border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2"
> >
{/* Header */} {/* Header */}
<div className="p-4 pb-2"> <div className="p-4 pb-2">

View File

@ -20,7 +20,7 @@ import {
import type { FileMap } from '~/lib/stores/files'; import type { FileMap } from '~/lib/stores/files';
import type { Snapshot } from './types'; import type { Snapshot } from './types';
import { webcontainer } from '~/lib/webcontainer'; import { webcontainer } from '~/lib/webcontainer';
import { createCommandsMessage, detectProjectCommands } from '~/utils/projectCommands'; import { detectProjectCommands, createCommandActionsString } from '~/utils/projectCommands';
import type { ContextAnnotation } from '~/types/context'; import type { ContextAnnotation } from '~/types/context';
export interface ChatHistoryItem { export interface ChatHistoryItem {
@ -112,23 +112,26 @@ export function useChatHistory() {
path: key, path: key,
}; };
}) })
.filter((x) => !!x); .filter((x): x is { content: string; path: string } => !!x); // Type assertion
const projectCommands = await detectProjectCommands(files); const projectCommands = await detectProjectCommands(files);
const commands = createCommandsMessage(projectCommands);
// Call the modified function to get only the command actions string
const commandActionsString = createCommandActionsString(projectCommands);
filteredMessages = [ filteredMessages = [
{ {
id: generateId(), id: generateId(),
role: 'user', role: 'user',
content: `Restore project from snapshot content: `Restore project from snapshot`, // Removed newline
`,
annotations: ['no-store', 'hidden'], annotations: ['no-store', 'hidden'],
}, },
{ {
id: storedMessages.messages[snapshotIndex].id, id: storedMessages.messages[snapshotIndex].id,
role: 'assistant', role: 'assistant',
content: ` 📦 Chat Restored from snapshot, You can revert this message to load the full chat history
<boltArtifact id="imported-files" title="Project Files Snapshot" type="bundled"> // Combine followup message and the artifact with files and command actions
content: `Bolt Restored your chat from a snapshot. You can revert this message to load the full chat history.
<boltArtifact id="restored-project-setup" title="Restored Project & Setup">
${Object.entries(snapshot?.files || {}) ${Object.entries(snapshot?.files || {})
.map(([key, value]) => { .map(([key, value]) => {
if (value?.type === 'file') { if (value?.type === 'file') {
@ -142,8 +145,9 @@ ${value.content}
} }
}) })
.join('\n')} .join('\n')}
${commandActionsString}
</boltArtifact> </boltArtifact>
`, `, // Added commandActionsString, followupMessage, updated id and title
annotations: [ annotations: [
'no-store', 'no-store',
...(summary ...(summary
@ -157,33 +161,13 @@ ${value.content}
: []), : []),
], ],
}, },
...(commands !== null
? [ // Remove the separate user and assistant messages for commands
{ /*
id: `${storedMessages.messages[snapshotIndex].id}-2`, *...(commands !== null // This block is no longer needed
role: 'user' as const, * ? [ ... ]
content: `setup project`, * : []),
annotations: ['no-store', 'hidden'], */
},
{
...commands,
id: `${storedMessages.messages[snapshotIndex].id}-3`,
annotations: [
'no-store',
...(commands.annotations || []),
...(summary
? [
{
chatId: `${storedMessages.messages[snapshotIndex].id}-3`,
type: 'chatSummary',
summary,
} satisfies ContextAnnotation,
]
: []),
],
},
]
: []),
...filteredMessages, ...filteredMessages,
]; ];
restoreSnapshot(mixedId); restoreSnapshot(mixedId);

View File

@ -26,7 +26,7 @@ PROVIDER_LIST.forEach((provider) => {
export const STARTER_TEMPLATES: Template[] = [ export const STARTER_TEMPLATES: Template[] = [
{ {
name: 'bolt-expo-app', name: 'Expo App',
label: 'Expo App', label: 'Expo App',
description: 'Expo starter template for building cross-platform mobile apps', description: 'Expo starter template for building cross-platform mobile apps',
githubRepo: 'xKevIsDev/bolt-expo-template', githubRepo: 'xKevIsDev/bolt-expo-template',
@ -34,7 +34,7 @@ export const STARTER_TEMPLATES: Template[] = [
icon: 'i-bolt:expo', icon: 'i-bolt:expo',
}, },
{ {
name: 'bolt-astro-basic', name: 'Basic Astro',
label: 'Astro Basic', label: 'Astro Basic',
description: 'Lightweight Astro starter template for building fast static websites', description: 'Lightweight Astro starter template for building fast static websites',
githubRepo: 'xKevIsDev/bolt-astro-basic-template', githubRepo: 'xKevIsDev/bolt-astro-basic-template',
@ -42,7 +42,7 @@ export const STARTER_TEMPLATES: Template[] = [
icon: 'i-bolt:astro', icon: 'i-bolt:astro',
}, },
{ {
name: 'bolt-nextjs-shadcn', name: 'NextJS Shadcn',
label: 'Next.js with shadcn/ui', label: 'Next.js with shadcn/ui',
description: 'Next.js starter fullstack template integrated with shadcn/ui components and styling system', description: 'Next.js starter fullstack template integrated with shadcn/ui components and styling system',
githubRepo: 'xKevIsDev/bolt-nextjs-shadcn-template', githubRepo: 'xKevIsDev/bolt-nextjs-shadcn-template',
@ -50,7 +50,7 @@ export const STARTER_TEMPLATES: Template[] = [
icon: 'i-bolt:nextjs', icon: 'i-bolt:nextjs',
}, },
{ {
name: 'bolt-qwik-ts', name: 'Qwik Typescript',
label: 'Qwik TypeScript', label: 'Qwik TypeScript',
description: 'Qwik framework starter with TypeScript for building resumable applications', description: 'Qwik framework starter with TypeScript for building resumable applications',
githubRepo: 'xKevIsDev/bolt-qwik-ts-template', githubRepo: 'xKevIsDev/bolt-qwik-ts-template',
@ -58,7 +58,7 @@ export const STARTER_TEMPLATES: Template[] = [
icon: 'i-bolt:qwik', icon: 'i-bolt:qwik',
}, },
{ {
name: 'bolt-remix-ts', name: 'Remix Typescript',
label: 'Remix TypeScript', label: 'Remix TypeScript',
description: 'Remix framework starter with TypeScript for full-stack web applications', description: 'Remix framework starter with TypeScript for full-stack web applications',
githubRepo: 'xKevIsDev/bolt-remix-ts-template', githubRepo: 'xKevIsDev/bolt-remix-ts-template',
@ -66,7 +66,7 @@ export const STARTER_TEMPLATES: Template[] = [
icon: 'i-bolt:remix', icon: 'i-bolt:remix',
}, },
{ {
name: 'bolt-slidev', name: 'Slidev',
label: 'Slidev Presentation', label: 'Slidev Presentation',
description: 'Slidev starter template for creating developer-friendly presentations using Markdown', description: 'Slidev starter template for creating developer-friendly presentations using Markdown',
githubRepo: 'xKevIsDev/bolt-slidev-template', githubRepo: 'xKevIsDev/bolt-slidev-template',
@ -74,7 +74,7 @@ export const STARTER_TEMPLATES: Template[] = [
icon: 'i-bolt:slidev', icon: 'i-bolt:slidev',
}, },
{ {
name: 'bolt-sveltekit', name: 'Sveltekit',
label: 'SvelteKit', label: 'SvelteKit',
description: 'SvelteKit starter template for building fast, efficient web applications', description: 'SvelteKit starter template for building fast, efficient web applications',
githubRepo: 'bolt-sveltekit-template', githubRepo: 'bolt-sveltekit-template',
@ -82,7 +82,7 @@ export const STARTER_TEMPLATES: Template[] = [
icon: 'i-bolt:svelte', icon: 'i-bolt:svelte',
}, },
{ {
name: 'vanilla-vite', name: 'Vanilla Vite',
label: 'Vanilla + Vite', label: 'Vanilla + Vite',
description: 'Minimal Vite starter template for vanilla JavaScript projects', description: 'Minimal Vite starter template for vanilla JavaScript projects',
githubRepo: 'xKevIsDev/vanilla-vite-template', githubRepo: 'xKevIsDev/vanilla-vite-template',
@ -90,7 +90,7 @@ export const STARTER_TEMPLATES: Template[] = [
icon: 'i-bolt:vite', icon: 'i-bolt:vite',
}, },
{ {
name: 'bolt-vite-react', name: 'Vite React',
label: 'React + Vite + typescript', label: 'React + Vite + typescript',
description: 'React starter template powered by Vite for fast development experience', description: 'React starter template powered by Vite for fast development experience',
githubRepo: 'xKevIsDev/bolt-vite-react-ts-template', githubRepo: 'xKevIsDev/bolt-vite-react-ts-template',
@ -98,7 +98,7 @@ export const STARTER_TEMPLATES: Template[] = [
icon: 'i-bolt:react', icon: 'i-bolt:react',
}, },
{ {
name: 'bolt-vite-ts', name: 'Vite Typescript',
label: 'Vite + TypeScript', label: 'Vite + TypeScript',
description: 'Vite starter template with TypeScript configuration for type-safe development', description: 'Vite starter template with TypeScript configuration for type-safe development',
githubRepo: 'xKevIsDev/bolt-vite-ts-template', githubRepo: 'xKevIsDev/bolt-vite-ts-template',
@ -106,7 +106,7 @@ export const STARTER_TEMPLATES: Template[] = [
icon: 'i-bolt:typescript', icon: 'i-bolt:typescript',
}, },
{ {
name: 'bolt-vue', name: 'Vue',
label: 'Vue.js', label: 'Vue.js',
description: 'Vue.js starter template with modern tooling and best practices', description: 'Vue.js starter template with modern tooling and best practices',
githubRepo: 'xKevIsDev/bolt-vue-template', githubRepo: 'xKevIsDev/bolt-vue-template',
@ -114,7 +114,7 @@ export const STARTER_TEMPLATES: Template[] = [
icon: 'i-bolt:vue', icon: 'i-bolt:vue',
}, },
{ {
name: 'bolt-angular', name: 'Angular',
label: 'Angular Starter', label: 'Angular Starter',
description: 'A modern Angular starter template with TypeScript support and best practices configuration', description: 'A modern Angular starter template with TypeScript support and best practices configuration',
githubRepo: 'xKevIsDev/bolt-angular-template', githubRepo: 'xKevIsDev/bolt-angular-template',

View File

@ -84,9 +84,10 @@ export function createCommandsMessage(commands: ProjectCommands): Message | null
return { return {
role: 'assistant', role: 'assistant',
content: ` content: `
${commands.followupMessage ? `\n\n${commands.followupMessage}` : ''}
<boltArtifact id="project-setup" title="Project Setup"> <boltArtifact id="project-setup" title="Project Setup">
${commandString} ${commandString}
</boltArtifact>${commands.followupMessage ? `\n\n${commands.followupMessage}` : ''}`, </boltArtifact>`,
id: generateId(), id: generateId(),
createdAt: new Date(), createdAt: new Date(),
}; };
@ -127,3 +128,26 @@ export function escapeBoltAActionTags(input: string) {
export function escapeBoltTags(input: string) { export function escapeBoltTags(input: string) {
return escapeBoltArtifactTags(escapeBoltAActionTags(input)); return escapeBoltArtifactTags(escapeBoltAActionTags(input));
} }
// We have this seperate function to simplify the restore snapshot process in to one single artifact.
export function createCommandActionsString(commands: ProjectCommands): string {
if (!commands.setupCommand && !commands.startCommand) {
// Return empty string if no commands
return '';
}
let commandString = '';
if (commands.setupCommand) {
commandString += `
<boltAction type="shell">${commands.setupCommand}</boltAction>`;
}
if (commands.startCommand) {
commandString += `
<boltAction type="start">${commands.startCommand}</boltAction>
`;
}
return commandString;
}

View File

@ -129,7 +129,7 @@ const getGitHubRepoContent = async (repoName: string): Promise<{ name: string; p
} }
}; };
export async function getTemplates(templateName: string, title?: string) { export async function getTemplates(templateName: string) {
const template = STARTER_TEMPLATES.find((t) => t.name == templateName); const template = STARTER_TEMPLATES.find((t) => t.name == templateName);
if (!template) { if (!template) {
@ -182,7 +182,8 @@ export async function getTemplates(templateName: string, title?: string) {
} }
const assistantMessage = ` const assistantMessage = `
<boltArtifact id="imported-files" title="${title || 'Importing Starter Files'}" type="bundled"> Bolt is initializing your project with the required files using the ${template.name} template.
<boltArtifact id="imported-files" title="Create initial files" type="bundled">
${filesToImport.files ${filesToImport.files
.map( .map(
(file) => (file) =>