feat(design): add design scheme support and UI improvements

- Implement design scheme system with palette, typography, and feature customization
- Add color scheme dialog for user customization
- Update chat UI components to use design scheme values
- Improve header actions with consolidated deploy and export buttons
- Adjust layout spacing and styling across multiple components (chat, workbench etc...)
- Add model and provider info to chat messages
- Refactor workbench and sidebar components for better responsiveness
This commit is contained in:
KevIsDev
2025-05-28 23:49:51 +01:00
parent 12f9f4dcdc
commit cd37599f3b
21 changed files with 701 additions and 255 deletions

View File

@@ -6,6 +6,7 @@ import { workbenchStore } from '~/lib/stores/workbench';
import { WORK_DIR } from '~/utils/constants';
import WithTooltip from '~/components/ui/Tooltip';
import type { Message } from 'ai';
import type { ProviderInfo } from '~/types/model';
interface AssistantMessageProps {
content: string;
@@ -16,6 +17,8 @@ interface AssistantMessageProps {
append?: (message: Message) => void;
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
model?: string;
provider?: ProviderInfo;
}
function openArtifactInWorkbench(filePath: string) {
@@ -43,7 +46,18 @@ function normalizedFilePath(path: string) {
}
export const AssistantMessage = memo(
({ content, annotations, messageId, onRewind, onFork, append, chatMode, setChatMode }: AssistantMessageProps) => {
({
content,
annotations,
messageId,
onRewind,
onFork,
append,
chatMode,
setChatMode,
model,
provider,
}: AssistantMessageProps) => {
const filteredAnnotations = (annotations?.filter(
(annotation: JSONValue) =>
annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
@@ -141,7 +155,7 @@ export const AssistantMessage = memo(
</div>
</div>
</>
<Markdown append={append} chatMode={chatMode} setChatMode={setChatMode} html>
<Markdown append={append} chatMode={chatMode} setChatMode={setChatMode} model={model} provider={provider} html>
{content}
</Markdown>
</div>

View File

@@ -31,6 +31,7 @@ import { expoUrlAtom } from '~/lib/stores/qrCodeStore';
import { useStore } from '@nanostores/react';
import { StickToBottom, useStickToBottomContext } from '~/lib/hooks';
import { ChatBox } from './ChatBox';
import type { DesignScheme } from '~/types/design-scheme';
const TEXTAREA_MIN_HEIGHT = 76;
@@ -73,6 +74,8 @@ interface BaseChatProps {
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
append?: (message: Message) => void;
designScheme?: DesignScheme;
setDesignScheme?: (scheme: DesignScheme) => void;
}
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
@@ -114,6 +117,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
chatMode,
setChatMode,
append,
designScheme,
setDesignScheme,
},
ref,
) => {
@@ -332,7 +337,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
<div className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
<div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
{!chatStarted && (
<div id="intro" className="mt-[16vh] max-w-chat mx-auto text-center px-4 lg:px-0">
<div id="intro" className="mt-[16vh] max-w-2xl mx-auto text-center px-4 lg:px-0">
<h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
Where ideas begin
</h1>
@@ -353,12 +358,14 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
{() => {
return chatStarted ? (
<Messages
className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"
className="flex flex-col w-full flex-1 max-w-chat pb-4 mx-auto z-1"
messages={messages}
isStreaming={isStreaming}
append={append}
chatMode={chatMode}
setChatMode={setChatMode}
model={model}
provider={provider}
/>
) : null;
}}
@@ -440,6 +447,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
handleFileUpload={handleFileUpload}
chatMode={chatMode}
setChatMode={setChatMode}
designScheme={designScheme}
setDesignScheme={setDesignScheme}
/>
</div>
</StickToBottom>

View File

@@ -27,6 +27,7 @@ import { logStore } from '~/lib/stores/logs';
import { streamingState } from '~/lib/stores/streaming';
import { filesToArtifacts } from '~/utils/fileUtils';
import { supabaseConnection } from '~/lib/stores/supabase';
import { defaultDesignScheme, type DesignScheme } from '~/types/design-scheme';
const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
@@ -124,6 +125,10 @@ export const ChatImpl = memo(
const [searchParams, setSearchParams] = useSearchParams();
const [fakeLoading, setFakeLoading] = useState(false);
const files = useStore(workbenchStore.files);
const [designScheme, setDesignScheme] = useState<DesignScheme>(defaultDesignScheme);
console.log(designScheme);
const actionAlert = useStore(workbenchStore.alert);
const deployAlert = useStore(workbenchStore.deployAlert);
const supabaseConn = useStore(supabaseConnection); // Add this line to get Supabase connection
@@ -170,6 +175,7 @@ export const ChatImpl = memo(
promptId,
contextOptimization: contextOptimizationEnabled,
chatMode,
designScheme,
supabase: {
isConnected: supabaseConn.isConnected,
hasSelectedProject: !!selectedProject,
@@ -569,6 +575,8 @@ export const ChatImpl = memo(
chatMode={chatMode}
setChatMode={setChatMode}
append={append}
designScheme={designScheme}
setDesignScheme={setDesignScheme}
/>
);
},

View File

@@ -11,11 +11,12 @@ import { SendButton } from './SendButton.client';
import { IconButton } from '~/components/ui/IconButton';
import { toast } from 'react-toastify';
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
import { SupabaseConnection } from './SupabaseConnection';
import { ExpoQrModal } from '~/components/workbench/ExpoQrModal';
import styles from './BaseChat.module.scss';
import type { ProviderInfo } from '~/types/model';
import { ColorSchemeDialog } from '~/components/ui/ColorSchemeDialog';
import type { DesignScheme } from '~/types/design-scheme';
interface ChatBoxProps {
isModelSettingsCollapsed: boolean;
@@ -54,13 +55,15 @@ interface ChatBoxProps {
enhancePrompt?: (() => void) | undefined;
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
designScheme?: DesignScheme;
setDesignScheme?: (scheme: DesignScheme) => void;
}
export const ChatBox: React.FC<ChatBoxProps> = (props) => {
return (
<div
className={classNames(
'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',
'relative bg-bolt-elements-background-depth-2 backdrop-blur p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
/*
* {
@@ -237,6 +240,7 @@ export const ChatBox: React.FC<ChatBoxProps> = (props) => {
</ClientOnly>
<div className="flex justify-between items-center text-sm p-4 pt-2">
<div className="flex gap-1 items-center">
<ColorSchemeDialog designScheme={props.designScheme} setDesignScheme={props.setDesignScheme} />
<IconButton title="Upload file" className="transition-all" onClick={() => props.handleFileUpload()}>
<div className="i-ph:paperclip text-xl"></div>
</IconButton>
@@ -279,7 +283,6 @@ export const ChatBox: React.FC<ChatBoxProps> = (props) => {
{props.chatMode === 'discuss' ? <span>Discuss</span> : <span />}
</IconButton>
)}
{props.chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={props.exportChat} />}</ClientOnly>}
<IconButton
title="Model Settings"
className={classNames('transition-all flex items-center gap-1', {

View File

@@ -8,6 +8,7 @@ import { CodeBlock } from './CodeBlock';
import type { Message } from 'ai';
import styles from './Markdown.module.scss';
import ThoughtBox from './ThoughtBox';
import type { ProviderInfo } from '~/types/model';
const logger = createScopedLogger('MarkdownComponent');
@@ -18,10 +19,12 @@ interface MarkdownProps {
append?: (message: Message) => void;
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
model?: string;
provider?: ProviderInfo;
}
export const Markdown = memo(
({ children, html = false, limitedMarkdown = false, append, setChatMode }: MarkdownProps) => {
({ children, html = false, limitedMarkdown = false, append, setChatMode, model, provider }: MarkdownProps) => {
logger.trace('Render');
const components = useMemo(() => {
@@ -106,17 +109,17 @@ export const Markdown = memo(
openArtifactInWorkbench(path);
} else if (type === 'message' && append) {
append({
id: 'random-message', // Replace with your ID generation logic
content: message as string, // The message content from the action
role: 'user', // Or another role as appropriate
id: `quick-action-message-${Date.now()}`,
content: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`,
role: 'user',
});
console.log('Message appended:', message); // Log the appended message
console.log('Message appended:', message);
} else if (type === 'implement' && append && setChatMode) {
setChatMode('build');
append({
id: 'implement-message', // Replace with your ID generation logic
content: message as string, // The message content from the action
role: 'user', // Or another role as appropriate
id: `quick-action-implement-${Date.now()}`,
content: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`,
role: 'user',
});
} else if (type === 'link' && typeof href === 'string') {
try {

View File

@@ -11,6 +11,7 @@ import { useStore } from '@nanostores/react';
import { profileStore } from '~/lib/stores/profile';
import { forwardRef } from 'react';
import type { ForwardedRef } from 'react';
import type { ProviderInfo } from '~/types/model';
interface MessagesProps {
id?: string;
@@ -20,6 +21,8 @@ interface MessagesProps {
append?: (message: Message) => void;
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
model?: string;
provider?: ProviderInfo;
}
export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
@@ -65,7 +68,7 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
return (
<div
key={index}
className={classNames('flex gap-4 p-6 py-5 w-full rounded-[calc(0.75rem-1px)]', {
className={classNames('flex gap-4 p-3 py-3 w-full rounded-lg', {
'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
isStreaming && isLast,
@@ -100,6 +103,8 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
append={props.append}
chatMode={props.chatMode}
setChatMode={props.setChatMode}
model={props.model}
provider={props.provider}
/>
)}
</div>

View File

@@ -1,13 +1,49 @@
import WithTooltip from '~/components/ui/Tooltip';
import { IconButton } from '~/components/ui/IconButton';
import React from 'react';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { workbenchStore } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames';
export const ExportChatButton = ({ exportChat }: { exportChat?: () => void }) => {
return (
<WithTooltip tooltip="Export Chat">
<IconButton title="Export Chat" onClick={() => exportChat?.()}>
<div className="i-ph:download-simple text-xl"></div>
</IconButton>
</WithTooltip>
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
<DropdownMenu.Root>
<DropdownMenu.Trigger className="rounded-md items-center justify-center [&:is(:disabled,.disabled)]:cursor-not-allowed [&:is(:disabled,.disabled)]:opacity-60 px-3 py-1.5 text-xs bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary [&:not(:disabled,.disabled)]:hover:bg-bolt-elements-button-primary-backgroundHover outline-accent-500 flex gap-1.7">
Export
<span className={classNames('i-ph:caret-down transition-transform')} />
</DropdownMenu.Trigger>
<DropdownMenu.Content
className={classNames(
'z-[250]',
'bg-bolt-elements-background-depth-2',
'rounded-lg shadow-lg',
'border border-bolt-elements-borderColor',
'animate-in fade-in-0 zoom-in-95',
'py-1',
)}
sideOffset={5}
align="end"
>
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-auto px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
)}
onClick={() => {
workbenchStore.downloadZip();
}}
>
<div className="i-ph:code size-4.5"></div>
<span>Download Code</span>
</DropdownMenu.Item>
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
)}
onClick={() => exportChat?.()}
>
<div className="i-ph:chat size-4.5"></div>
<span>Export Chat</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
);
};