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="w-full text-bolt-elements-textPrimary font-medium leading-5 text-sm">
{artifact.type === 'bundled' ? 'Setup Project' : artifact?.title}
{artifact?.title}
</div>
<div className="w-full w-full text-bolt-elements-textSecondary text-xs mt-0.5">
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>
<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>
)}
<AnimatePresence>

View File

@ -367,33 +367,32 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
}}
</ClientOnly>
</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
className={classNames('my-auto flex flex-col gap-2 w-full max-w-chat mx-auto z-prompt mb-6', {
'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 && (
<ChatAlert
alert={actionAlert}
@ -405,10 +404,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
/>
)}
</div>
<ScrollToBottom />
{progressAnnotations && <ProgressCompilation data={progressAnnotations} />}
<div
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 (
!isAtBottom && (
<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()}
>
Go to last message

View File

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

View File

@ -99,7 +99,7 @@ export function SupabaseChatAlert({ alert, clearAlert, postMessage }: Props) {
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
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 */}
<div className="p-4 pb-2">

View File

@ -20,7 +20,7 @@ import {
import type { FileMap } from '~/lib/stores/files';
import type { Snapshot } from './types';
import { webcontainer } from '~/lib/webcontainer';
import { createCommandsMessage, detectProjectCommands } from '~/utils/projectCommands';
import { detectProjectCommands, createCommandActionsString } from '~/utils/projectCommands';
import type { ContextAnnotation } from '~/types/context';
export interface ChatHistoryItem {
@ -112,23 +112,26 @@ export function useChatHistory() {
path: key,
};
})
.filter((x) => !!x);
.filter((x): x is { content: string; path: string } => !!x); // Type assertion
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 = [
{
id: generateId(),
role: 'user',
content: `Restore project from snapshot
`,
content: `Restore project from snapshot`, // Removed newline
annotations: ['no-store', 'hidden'],
},
{
id: storedMessages.messages[snapshotIndex].id,
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 || {})
.map(([key, value]) => {
if (value?.type === 'file') {
@ -142,8 +145,9 @@ ${value.content}
}
})
.join('\n')}
${commandActionsString}
</boltArtifact>
`,
`, // Added commandActionsString, followupMessage, updated id and title
annotations: [
'no-store',
...(summary
@ -157,33 +161,13 @@ ${value.content}
: []),
],
},
...(commands !== null
? [
{
id: `${storedMessages.messages[snapshotIndex].id}-2`,
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,
]
: []),
],
},
]
: []),
// Remove the separate user and assistant messages for commands
/*
*...(commands !== null // This block is no longer needed
* ? [ ... ]
* : []),
*/
...filteredMessages,
];
restoreSnapshot(mixedId);

View File

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

View File

@ -84,9 +84,10 @@ export function createCommandsMessage(commands: ProjectCommands): Message | null
return {
role: 'assistant',
content: `
${commands.followupMessage ? `\n\n${commands.followupMessage}` : ''}
<boltArtifact id="project-setup" title="Project Setup">
${commandString}
</boltArtifact>${commands.followupMessage ? `\n\n${commands.followupMessage}` : ''}`,
</boltArtifact>`,
id: generateId(),
createdAt: new Date(),
};
@ -127,3 +128,26 @@ export function escapeBoltAActionTags(input: string) {
export function escapeBoltTags(input: string) {
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);
if (!template) {
@ -182,7 +182,8 @@ export async function getTemplates(templateName: string, title?: string) {
}
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
.map(
(file) =>