mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 10:16:01 +00:00
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:
parent
12f9f4dcdc
commit
cd37599f3b
@ -6,6 +6,7 @@ import { workbenchStore } from '~/lib/stores/workbench';
|
|||||||
import { WORK_DIR } from '~/utils/constants';
|
import { WORK_DIR } from '~/utils/constants';
|
||||||
import WithTooltip from '~/components/ui/Tooltip';
|
import WithTooltip from '~/components/ui/Tooltip';
|
||||||
import type { Message } from 'ai';
|
import type { Message } from 'ai';
|
||||||
|
import type { ProviderInfo } from '~/types/model';
|
||||||
|
|
||||||
interface AssistantMessageProps {
|
interface AssistantMessageProps {
|
||||||
content: string;
|
content: string;
|
||||||
@ -16,6 +17,8 @@ interface AssistantMessageProps {
|
|||||||
append?: (message: Message) => void;
|
append?: (message: Message) => void;
|
||||||
chatMode?: 'discuss' | 'build';
|
chatMode?: 'discuss' | 'build';
|
||||||
setChatMode?: (mode: 'discuss' | 'build') => void;
|
setChatMode?: (mode: 'discuss' | 'build') => void;
|
||||||
|
model?: string;
|
||||||
|
provider?: ProviderInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openArtifactInWorkbench(filePath: string) {
|
function openArtifactInWorkbench(filePath: string) {
|
||||||
@ -43,7 +46,18 @@ function normalizedFilePath(path: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AssistantMessage = memo(
|
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(
|
const filteredAnnotations = (annotations?.filter(
|
||||||
(annotation: JSONValue) =>
|
(annotation: JSONValue) =>
|
||||||
annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
|
annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
|
||||||
@ -141,7 +155,7 @@ export const AssistantMessage = memo(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
<Markdown append={append} chatMode={chatMode} setChatMode={setChatMode} html>
|
<Markdown append={append} chatMode={chatMode} setChatMode={setChatMode} model={model} provider={provider} html>
|
||||||
{content}
|
{content}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,6 +31,7 @@ import { expoUrlAtom } from '~/lib/stores/qrCodeStore';
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { StickToBottom, useStickToBottomContext } from '~/lib/hooks';
|
import { StickToBottom, useStickToBottomContext } from '~/lib/hooks';
|
||||||
import { ChatBox } from './ChatBox';
|
import { ChatBox } from './ChatBox';
|
||||||
|
import type { DesignScheme } from '~/types/design-scheme';
|
||||||
|
|
||||||
const TEXTAREA_MIN_HEIGHT = 76;
|
const TEXTAREA_MIN_HEIGHT = 76;
|
||||||
|
|
||||||
@ -73,6 +74,8 @@ interface BaseChatProps {
|
|||||||
chatMode?: 'discuss' | 'build';
|
chatMode?: 'discuss' | 'build';
|
||||||
setChatMode?: (mode: 'discuss' | 'build') => void;
|
setChatMode?: (mode: 'discuss' | 'build') => void;
|
||||||
append?: (message: Message) => void;
|
append?: (message: Message) => void;
|
||||||
|
designScheme?: DesignScheme;
|
||||||
|
setDesignScheme?: (scheme: DesignScheme) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||||
@ -114,6 +117,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
chatMode,
|
chatMode,
|
||||||
setChatMode,
|
setChatMode,
|
||||||
append,
|
append,
|
||||||
|
designScheme,
|
||||||
|
setDesignScheme,
|
||||||
},
|
},
|
||||||
ref,
|
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="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')}>
|
<div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
|
||||||
{!chatStarted && (
|
{!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">
|
<h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
|
||||||
Where ideas begin
|
Where ideas begin
|
||||||
</h1>
|
</h1>
|
||||||
@ -353,12 +358,14 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
{() => {
|
{() => {
|
||||||
return chatStarted ? (
|
return chatStarted ? (
|
||||||
<Messages
|
<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}
|
messages={messages}
|
||||||
isStreaming={isStreaming}
|
isStreaming={isStreaming}
|
||||||
append={append}
|
append={append}
|
||||||
chatMode={chatMode}
|
chatMode={chatMode}
|
||||||
setChatMode={setChatMode}
|
setChatMode={setChatMode}
|
||||||
|
model={model}
|
||||||
|
provider={provider}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
}}
|
}}
|
||||||
@ -440,6 +447,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
handleFileUpload={handleFileUpload}
|
handleFileUpload={handleFileUpload}
|
||||||
chatMode={chatMode}
|
chatMode={chatMode}
|
||||||
setChatMode={setChatMode}
|
setChatMode={setChatMode}
|
||||||
|
designScheme={designScheme}
|
||||||
|
setDesignScheme={setDesignScheme}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</StickToBottom>
|
</StickToBottom>
|
||||||
|
@ -27,6 +27,7 @@ import { logStore } from '~/lib/stores/logs';
|
|||||||
import { streamingState } from '~/lib/stores/streaming';
|
import { streamingState } from '~/lib/stores/streaming';
|
||||||
import { filesToArtifacts } from '~/utils/fileUtils';
|
import { filesToArtifacts } from '~/utils/fileUtils';
|
||||||
import { supabaseConnection } from '~/lib/stores/supabase';
|
import { supabaseConnection } from '~/lib/stores/supabase';
|
||||||
|
import { defaultDesignScheme, type DesignScheme } from '~/types/design-scheme';
|
||||||
|
|
||||||
const toastAnimation = cssTransition({
|
const toastAnimation = cssTransition({
|
||||||
enter: 'animated fadeInRight',
|
enter: 'animated fadeInRight',
|
||||||
@ -124,6 +125,10 @@ export const ChatImpl = memo(
|
|||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [fakeLoading, setFakeLoading] = useState(false);
|
const [fakeLoading, setFakeLoading] = useState(false);
|
||||||
const files = useStore(workbenchStore.files);
|
const files = useStore(workbenchStore.files);
|
||||||
|
const [designScheme, setDesignScheme] = useState<DesignScheme>(defaultDesignScheme);
|
||||||
|
|
||||||
|
console.log(designScheme);
|
||||||
|
|
||||||
const actionAlert = useStore(workbenchStore.alert);
|
const actionAlert = useStore(workbenchStore.alert);
|
||||||
const deployAlert = useStore(workbenchStore.deployAlert);
|
const deployAlert = useStore(workbenchStore.deployAlert);
|
||||||
const supabaseConn = useStore(supabaseConnection); // Add this line to get Supabase connection
|
const supabaseConn = useStore(supabaseConnection); // Add this line to get Supabase connection
|
||||||
@ -170,6 +175,7 @@ export const ChatImpl = memo(
|
|||||||
promptId,
|
promptId,
|
||||||
contextOptimization: contextOptimizationEnabled,
|
contextOptimization: contextOptimizationEnabled,
|
||||||
chatMode,
|
chatMode,
|
||||||
|
designScheme,
|
||||||
supabase: {
|
supabase: {
|
||||||
isConnected: supabaseConn.isConnected,
|
isConnected: supabaseConn.isConnected,
|
||||||
hasSelectedProject: !!selectedProject,
|
hasSelectedProject: !!selectedProject,
|
||||||
@ -569,6 +575,8 @@ export const ChatImpl = memo(
|
|||||||
chatMode={chatMode}
|
chatMode={chatMode}
|
||||||
setChatMode={setChatMode}
|
setChatMode={setChatMode}
|
||||||
append={append}
|
append={append}
|
||||||
|
designScheme={designScheme}
|
||||||
|
setDesignScheme={setDesignScheme}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -11,11 +11,12 @@ import { SendButton } from './SendButton.client';
|
|||||||
import { IconButton } from '~/components/ui/IconButton';
|
import { IconButton } from '~/components/ui/IconButton';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
||||||
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
|
|
||||||
import { SupabaseConnection } from './SupabaseConnection';
|
import { SupabaseConnection } from './SupabaseConnection';
|
||||||
import { ExpoQrModal } from '~/components/workbench/ExpoQrModal';
|
import { ExpoQrModal } from '~/components/workbench/ExpoQrModal';
|
||||||
import styles from './BaseChat.module.scss';
|
import styles from './BaseChat.module.scss';
|
||||||
import type { ProviderInfo } from '~/types/model';
|
import type { ProviderInfo } from '~/types/model';
|
||||||
|
import { ColorSchemeDialog } from '~/components/ui/ColorSchemeDialog';
|
||||||
|
import type { DesignScheme } from '~/types/design-scheme';
|
||||||
|
|
||||||
interface ChatBoxProps {
|
interface ChatBoxProps {
|
||||||
isModelSettingsCollapsed: boolean;
|
isModelSettingsCollapsed: boolean;
|
||||||
@ -54,13 +55,15 @@ interface ChatBoxProps {
|
|||||||
enhancePrompt?: (() => void) | undefined;
|
enhancePrompt?: (() => void) | undefined;
|
||||||
chatMode?: 'discuss' | 'build';
|
chatMode?: 'discuss' | 'build';
|
||||||
setChatMode?: (mode: 'discuss' | 'build') => void;
|
setChatMode?: (mode: 'discuss' | 'build') => void;
|
||||||
|
designScheme?: DesignScheme;
|
||||||
|
setDesignScheme?: (scheme: DesignScheme) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatBox: React.FC<ChatBoxProps> = (props) => {
|
export const ChatBox: React.FC<ChatBoxProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
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>
|
</ClientOnly>
|
||||||
<div className="flex justify-between items-center text-sm p-4 pt-2">
|
<div className="flex justify-between items-center text-sm p-4 pt-2">
|
||||||
<div className="flex gap-1 items-center">
|
<div className="flex gap-1 items-center">
|
||||||
|
<ColorSchemeDialog designScheme={props.designScheme} setDesignScheme={props.setDesignScheme} />
|
||||||
<IconButton title="Upload file" className="transition-all" onClick={() => props.handleFileUpload()}>
|
<IconButton title="Upload file" className="transition-all" onClick={() => props.handleFileUpload()}>
|
||||||
<div className="i-ph:paperclip text-xl"></div>
|
<div className="i-ph:paperclip text-xl"></div>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -279,7 +283,6 @@ export const ChatBox: React.FC<ChatBoxProps> = (props) => {
|
|||||||
{props.chatMode === 'discuss' ? <span>Discuss</span> : <span />}
|
{props.chatMode === 'discuss' ? <span>Discuss</span> : <span />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
{props.chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={props.exportChat} />}</ClientOnly>}
|
|
||||||
<IconButton
|
<IconButton
|
||||||
title="Model Settings"
|
title="Model Settings"
|
||||||
className={classNames('transition-all flex items-center gap-1', {
|
className={classNames('transition-all flex items-center gap-1', {
|
||||||
|
@ -8,6 +8,7 @@ import { CodeBlock } from './CodeBlock';
|
|||||||
import type { Message } from 'ai';
|
import type { Message } from 'ai';
|
||||||
import styles from './Markdown.module.scss';
|
import styles from './Markdown.module.scss';
|
||||||
import ThoughtBox from './ThoughtBox';
|
import ThoughtBox from './ThoughtBox';
|
||||||
|
import type { ProviderInfo } from '~/types/model';
|
||||||
|
|
||||||
const logger = createScopedLogger('MarkdownComponent');
|
const logger = createScopedLogger('MarkdownComponent');
|
||||||
|
|
||||||
@ -18,10 +19,12 @@ interface MarkdownProps {
|
|||||||
append?: (message: Message) => void;
|
append?: (message: Message) => void;
|
||||||
chatMode?: 'discuss' | 'build';
|
chatMode?: 'discuss' | 'build';
|
||||||
setChatMode?: (mode: 'discuss' | 'build') => void;
|
setChatMode?: (mode: 'discuss' | 'build') => void;
|
||||||
|
model?: string;
|
||||||
|
provider?: ProviderInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Markdown = memo(
|
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');
|
logger.trace('Render');
|
||||||
|
|
||||||
const components = useMemo(() => {
|
const components = useMemo(() => {
|
||||||
@ -106,17 +109,17 @@ export const Markdown = memo(
|
|||||||
openArtifactInWorkbench(path);
|
openArtifactInWorkbench(path);
|
||||||
} else if (type === 'message' && append) {
|
} else if (type === 'message' && append) {
|
||||||
append({
|
append({
|
||||||
id: 'random-message', // Replace with your ID generation logic
|
id: `quick-action-message-${Date.now()}`,
|
||||||
content: message as string, // The message content from the action
|
content: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`,
|
||||||
role: 'user', // Or another role as appropriate
|
role: 'user',
|
||||||
});
|
});
|
||||||
console.log('Message appended:', message); // Log the appended message
|
console.log('Message appended:', message);
|
||||||
} else if (type === 'implement' && append && setChatMode) {
|
} else if (type === 'implement' && append && setChatMode) {
|
||||||
setChatMode('build');
|
setChatMode('build');
|
||||||
append({
|
append({
|
||||||
id: 'implement-message', // Replace with your ID generation logic
|
id: `quick-action-implement-${Date.now()}`,
|
||||||
content: message as string, // The message content from the action
|
content: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`,
|
||||||
role: 'user', // Or another role as appropriate
|
role: 'user',
|
||||||
});
|
});
|
||||||
} else if (type === 'link' && typeof href === 'string') {
|
} else if (type === 'link' && typeof href === 'string') {
|
||||||
try {
|
try {
|
||||||
|
@ -11,6 +11,7 @@ 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';
|
||||||
import type { ForwardedRef } from 'react';
|
import type { ForwardedRef } from 'react';
|
||||||
|
import type { ProviderInfo } from '~/types/model';
|
||||||
|
|
||||||
interface MessagesProps {
|
interface MessagesProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -20,6 +21,8 @@ interface MessagesProps {
|
|||||||
append?: (message: Message) => void;
|
append?: (message: Message) => void;
|
||||||
chatMode?: 'discuss' | 'build';
|
chatMode?: 'discuss' | 'build';
|
||||||
setChatMode?: (mode: 'discuss' | 'build') => void;
|
setChatMode?: (mode: 'discuss' | 'build') => void;
|
||||||
|
model?: string;
|
||||||
|
provider?: ProviderInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
|
export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
|
||||||
@ -65,7 +68,7 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
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-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,
|
||||||
@ -100,6 +103,8 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
|
|||||||
append={props.append}
|
append={props.append}
|
||||||
chatMode={props.chatMode}
|
chatMode={props.chatMode}
|
||||||
setChatMode={props.setChatMode}
|
setChatMode={props.setChatMode}
|
||||||
|
model={props.model}
|
||||||
|
provider={props.provider}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,49 @@
|
|||||||
import WithTooltip from '~/components/ui/Tooltip';
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
import { IconButton } from '~/components/ui/IconButton';
|
import { workbenchStore } from '~/lib/stores/workbench';
|
||||||
import React from 'react';
|
import { classNames } from '~/utils/classNames';
|
||||||
|
|
||||||
export const ExportChatButton = ({ exportChat }: { exportChat?: () => void }) => {
|
export const ExportChatButton = ({ exportChat }: { exportChat?: () => void }) => {
|
||||||
return (
|
return (
|
||||||
<WithTooltip tooltip="Export Chat">
|
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
|
||||||
<IconButton title="Export Chat" onClick={() => exportChat?.()}>
|
<DropdownMenu.Root>
|
||||||
<div className="i-ph:download-simple text-xl"></div>
|
<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">
|
||||||
</IconButton>
|
Export
|
||||||
</WithTooltip>
|
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
146
app/components/deploy/DeployButton.tsx
Normal file
146
app/components/deploy/DeployButton.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { netlifyConnection } from '~/lib/stores/netlify';
|
||||||
|
import { vercelConnection } from '~/lib/stores/vercel';
|
||||||
|
import { workbenchStore } from '~/lib/stores/workbench';
|
||||||
|
import { streamingState } from '~/lib/stores/streaming';
|
||||||
|
import { classNames } from '~/utils/classNames';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client';
|
||||||
|
import { VercelDeploymentLink } from '~/components/chat/VercelDeploymentLink.client';
|
||||||
|
import { useVercelDeploy } from '~/components/deploy/VercelDeploy.client';
|
||||||
|
import { useNetlifyDeploy } from '~/components/deploy/NetlifyDeploy.client';
|
||||||
|
|
||||||
|
interface DeployButtonProps {
|
||||||
|
onVercelDeploy?: () => Promise<void>;
|
||||||
|
onNetlifyDeploy?: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeployButton = ({ onVercelDeploy, onNetlifyDeploy }: DeployButtonProps) => {
|
||||||
|
const netlifyConn = useStore(netlifyConnection);
|
||||||
|
const vercelConn = useStore(vercelConnection);
|
||||||
|
const [activePreviewIndex] = useState(0);
|
||||||
|
const previews = useStore(workbenchStore.previews);
|
||||||
|
const activePreview = previews[activePreviewIndex];
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
|
const [deployingTo, setDeployingTo] = useState<'netlify' | 'vercel' | null>(null);
|
||||||
|
const isStreaming = useStore(streamingState);
|
||||||
|
const { handleVercelDeploy } = useVercelDeploy();
|
||||||
|
const { handleNetlifyDeploy } = useNetlifyDeploy();
|
||||||
|
|
||||||
|
const handleVercelDeployClick = async () => {
|
||||||
|
setIsDeploying(true);
|
||||||
|
setDeployingTo('vercel');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (onVercelDeploy) {
|
||||||
|
await onVercelDeploy();
|
||||||
|
} else {
|
||||||
|
await handleVercelDeploy();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsDeploying(false);
|
||||||
|
setDeployingTo(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNetlifyDeployClick = async () => {
|
||||||
|
setIsDeploying(true);
|
||||||
|
setDeployingTo('netlify');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (onNetlifyDeploy) {
|
||||||
|
await onNetlifyDeploy();
|
||||||
|
} else {
|
||||||
|
await handleNetlifyDeploy();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsDeploying(false);
|
||||||
|
setDeployingTo(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden text-sm">
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
disabled={isDeploying || !activePreview || isStreaming}
|
||||||
|
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-item-contentAccent text-white hover:text-bolt-elements-item-contentAccent [&:not(:disabled,.disabled)]:hover:bg-bolt-elements-button-primary-backgroundHover outline-accent-500 flex gap-1.7"
|
||||||
|
>
|
||||||
|
{isDeploying ? `Deploying to ${deployingTo}...` : 'Deploy'}
|
||||||
|
<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-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
|
||||||
|
{
|
||||||
|
'opacity-60 cursor-not-allowed': isDeploying || !activePreview || !netlifyConn.user,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
disabled={isDeploying || !activePreview || !netlifyConn.user}
|
||||||
|
onClick={handleNetlifyDeployClick}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="w-5 h-5"
|
||||||
|
height="24"
|
||||||
|
width="24"
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src="https://cdn.simpleicons.org/netlify"
|
||||||
|
/>
|
||||||
|
<span className="mx-auto">{!netlifyConn.user ? 'No Netlify Account Connected' : 'Deploy to Netlify'}</span>
|
||||||
|
{netlifyConn.user && <NetlifyDeploymentLink />}
|
||||||
|
</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',
|
||||||
|
{
|
||||||
|
'opacity-60 cursor-not-allowed': isDeploying || !activePreview || !vercelConn.user,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
disabled={isDeploying || !activePreview || !vercelConn.user}
|
||||||
|
onClick={handleVercelDeployClick}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="w-5 h-5 bg-black p-1 rounded"
|
||||||
|
height="24"
|
||||||
|
width="24"
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src="https://cdn.simpleicons.org/vercel/white"
|
||||||
|
alt="vercel"
|
||||||
|
/>
|
||||||
|
<span className="mx-auto">{!vercelConn.user ? 'No Vercel Account Connected' : 'Deploy to Vercel'}</span>
|
||||||
|
{vercelConn.user && <VercelDeploymentLink />}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
<DropdownMenu.Item
|
||||||
|
disabled
|
||||||
|
className="flex items-center w-full rounded-md px-4 py-2 text-sm text-bolt-elements-textTertiary gap-2 opacity-60 cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="w-5 h-5"
|
||||||
|
height="24"
|
||||||
|
width="24"
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src="https://cdn.simpleicons.org/cloudflare"
|
||||||
|
alt="cloudflare"
|
||||||
|
/>
|
||||||
|
<span className="mx-auto">Deploy to Cloudflare (Coming Soon)</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -10,7 +10,7 @@ export function Header() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={classNames('flex items-center p-5 border-b h-[var(--header-height)]', {
|
className={classNames('flex items-center px-4 border-b h-[var(--header-height)]', {
|
||||||
'border-transparent': !chat.started,
|
'border-transparent': !chat.started,
|
||||||
'border-bolt-elements-borderColor': chat.started,
|
'border-bolt-elements-borderColor': chat.started,
|
||||||
})}
|
})}
|
||||||
@ -30,8 +30,8 @@ export function Header() {
|
|||||||
</span>
|
</span>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
{() => (
|
{() => (
|
||||||
<div className="mr-1">
|
<div className="">
|
||||||
<HeaderActionButtons />
|
<HeaderActionButtons chatStarted={chat.started} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
|
@ -1,206 +1,28 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import useViewport from '~/lib/hooks';
|
|
||||||
import { chatStore } from '~/lib/stores/chat';
|
|
||||||
import { netlifyConnection } from '~/lib/stores/netlify';
|
|
||||||
import { vercelConnection } from '~/lib/stores/vercel';
|
|
||||||
import { workbenchStore } from '~/lib/stores/workbench';
|
import { workbenchStore } from '~/lib/stores/workbench';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { useState } from 'react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { streamingState } from '~/lib/stores/streaming';
|
import { streamingState } from '~/lib/stores/streaming';
|
||||||
import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client';
|
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
|
||||||
import { VercelDeploymentLink } from '~/components/chat/VercelDeploymentLink.client';
|
import { useChatHistory } from '~/lib/persistence';
|
||||||
import { useVercelDeploy } from '~/components/deploy/VercelDeploy.client';
|
import { DeployButton } from '~/components/deploy/DeployButton';
|
||||||
import { useNetlifyDeploy } from '~/components/deploy/NetlifyDeploy.client';
|
|
||||||
|
|
||||||
interface HeaderActionButtonsProps {}
|
interface HeaderActionButtonsProps {
|
||||||
|
chatStarted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
export function HeaderActionButtons({ chatStarted }: HeaderActionButtonsProps) {
|
||||||
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
|
||||||
const { showChat } = useStore(chatStore);
|
|
||||||
const netlifyConn = useStore(netlifyConnection);
|
|
||||||
const vercelConn = useStore(vercelConnection);
|
|
||||||
const [activePreviewIndex] = useState(0);
|
const [activePreviewIndex] = useState(0);
|
||||||
const previews = useStore(workbenchStore.previews);
|
const previews = useStore(workbenchStore.previews);
|
||||||
const activePreview = previews[activePreviewIndex];
|
const activePreview = previews[activePreviewIndex];
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
|
||||||
const [deployingTo, setDeployingTo] = useState<'netlify' | 'vercel' | null>(null);
|
|
||||||
const isSmallViewport = useViewport(1024);
|
|
||||||
const canHideChat = showWorkbench || !showChat;
|
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
const isStreaming = useStore(streamingState);
|
const isStreaming = useStore(streamingState);
|
||||||
const { handleVercelDeploy } = useVercelDeploy();
|
const { exportChat } = useChatHistory();
|
||||||
const { handleNetlifyDeploy } = useNetlifyDeploy();
|
|
||||||
|
|
||||||
useEffect(() => {
|
const shouldShowButtons = !isStreaming && activePreview;
|
||||||
function handleClickOutside(event: MouseEvent) {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
||||||
setIsDropdownOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
|
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onVercelDeploy = async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
setDeployingTo('vercel');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await handleVercelDeploy();
|
|
||||||
} finally {
|
|
||||||
setIsDeploying(false);
|
|
||||||
setDeployingTo(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onNetlifyDeploy = async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
setDeployingTo('netlify');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await handleNetlifyDeploy();
|
|
||||||
} finally {
|
|
||||||
setIsDeploying(false);
|
|
||||||
setDeployingTo(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex">
|
<div className="flex items-center">
|
||||||
<div className="relative" ref={dropdownRef}>
|
{chatStarted && shouldShowButtons && <ExportChatButton exportChat={exportChat} />}
|
||||||
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
|
{shouldShowButtons && <DeployButton />}
|
||||||
<Button
|
|
||||||
active
|
|
||||||
disabled={isDeploying || !activePreview || isStreaming}
|
|
||||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
|
||||||
className="px-4 hover:bg-bolt-elements-item-backgroundActive flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{isDeploying ? `Deploying to ${deployingTo}...` : 'Deploy'}
|
|
||||||
<div
|
|
||||||
className={classNames('i-ph:caret-down w-4 h-4 transition-transform', isDropdownOpen ? 'rotate-180' : '')}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isDropdownOpen && (
|
|
||||||
<div className="absolute right-2 flex flex-col gap-1 z-50 p-1 mt-1 min-w-[13.5rem] bg-bolt-elements-background-depth-2 rounded-md shadow-lg bg-bolt-elements-backgroundDefault border border-bolt-elements-borderColor">
|
|
||||||
<Button
|
|
||||||
active
|
|
||||||
onClick={() => {
|
|
||||||
onNetlifyDeploy();
|
|
||||||
setIsDropdownOpen(false);
|
|
||||||
}}
|
|
||||||
disabled={isDeploying || !activePreview || !netlifyConn.user}
|
|
||||||
className="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"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className="w-5 h-5"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
src="https://cdn.simpleicons.org/netlify"
|
|
||||||
/>
|
|
||||||
<span className="mx-auto">
|
|
||||||
{!netlifyConn.user ? 'No Netlify Account Connected' : 'Deploy to Netlify'}
|
|
||||||
</span>
|
|
||||||
{netlifyConn.user && <NetlifyDeploymentLink />}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
active
|
|
||||||
onClick={() => {
|
|
||||||
onVercelDeploy();
|
|
||||||
setIsDropdownOpen(false);
|
|
||||||
}}
|
|
||||||
disabled={isDeploying || !activePreview || !vercelConn.user}
|
|
||||||
className="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"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className="w-5 h-5 bg-black p-1 rounded"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
src="https://cdn.simpleicons.org/vercel/white"
|
|
||||||
alt="vercel"
|
|
||||||
/>
|
|
||||||
<span className="mx-auto">{!vercelConn.user ? 'No Vercel Account Connected' : 'Deploy to Vercel'}</span>
|
|
||||||
{vercelConn.user && <VercelDeploymentLink />}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
active={false}
|
|
||||||
disabled
|
|
||||||
className="flex items-center w-full rounded-md px-4 py-2 text-sm text-bolt-elements-textTertiary gap-2"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Coming Soon</span>
|
|
||||||
<img
|
|
||||||
className="w-5 h-5"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
src="https://cdn.simpleicons.org/cloudflare"
|
|
||||||
alt="cloudflare"
|
|
||||||
/>
|
|
||||||
<span className="mx-auto">Deploy to Cloudflare (Coming Soon)</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
|
|
||||||
<Button
|
|
||||||
active={showChat}
|
|
||||||
disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's not needed
|
|
||||||
onClick={() => {
|
|
||||||
if (canHideChat) {
|
|
||||||
chatStore.setKey('showChat', !showChat);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="i-bolt:chat text-sm" />
|
|
||||||
</Button>
|
|
||||||
<div className="w-[1px] bg-bolt-elements-borderColor" />
|
|
||||||
<Button
|
|
||||||
active={showWorkbench}
|
|
||||||
onClick={() => {
|
|
||||||
if (showWorkbench && !showChat) {
|
|
||||||
chatStore.setKey('showChat', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
workbenchStore.showWorkbench.set(!showWorkbench);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="i-ph:code-bold" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ButtonProps {
|
|
||||||
active?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
children?: any;
|
|
||||||
onClick?: VoidFunction;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Button({ active = false, disabled = false, children, onClick, className }: ButtonProps) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center p-1.5',
|
|
||||||
{
|
|
||||||
'bg-bolt-elements-item-backgroundDefault hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary':
|
|
||||||
!active,
|
|
||||||
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': active && !disabled,
|
|
||||||
'bg-bolt-elements-item-backgroundDefault text-alpha-gray-20 dark:text-alpha-white-20 cursor-not-allowed':
|
|
||||||
disabled,
|
|
||||||
},
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -279,8 +279,8 @@ export const Menu = () => {
|
|||||||
}, [open, selectionMode]);
|
}, [open, selectionMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const enterThreshold = 40;
|
const enterThreshold = 20;
|
||||||
const exitThreshold = 40;
|
const exitThreshold = 20;
|
||||||
|
|
||||||
function onMouseMove(event: MouseEvent) {
|
function onMouseMove(event: MouseEvent) {
|
||||||
if (isSettingsOpen) {
|
if (isSettingsOpen) {
|
||||||
@ -331,13 +331,13 @@ export const Menu = () => {
|
|||||||
variants={menuVariants}
|
variants={menuVariants}
|
||||||
style={{ width: '340px' }}
|
style={{ width: '340px' }}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex selection-accent flex-col side-menu fixed top-0 h-full',
|
'flex selection-accent flex-col side-menu fixed top-0 h-full rounded-r-2xl',
|
||||||
'bg-white dark:bg-gray-950 border-r border-gray-100 dark:border-gray-800/50',
|
'bg-white dark:bg-gray-950 border-r border-bolt-elements-borderColor',
|
||||||
'shadow-sm text-sm',
|
'shadow-sm text-sm',
|
||||||
isSettingsOpen ? 'z-40' : 'z-sidebar',
|
isSettingsOpen ? 'z-40' : 'z-sidebar',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="h-12 flex items-center justify-between px-4 border-b border-gray-100 dark:border-gray-800/50 bg-gray-50/50 dark:bg-gray-900/50">
|
<div className="h-12 flex items-center justify-between px-4 border-b border-gray-100 dark:border-gray-800/50 bg-gray-50/50 dark:bg-gray-900/50 rounded-tr-2xl">
|
||||||
<div className="text-gray-900 dark:text-white font-medium"></div>
|
<div className="text-gray-900 dark:text-white font-medium"></div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="font-medium text-sm text-gray-900 dark:text-white truncate">
|
<span className="font-medium text-sm text-gray-900 dark:text-white truncate">
|
||||||
|
273
app/components/ui/ColorSchemeDialog.tsx
Normal file
273
app/components/ui/ColorSchemeDialog.tsx
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Dialog, DialogTitle, DialogDescription, DialogRoot } from './Dialog';
|
||||||
|
import { Button } from './Button';
|
||||||
|
import { IconButton } from './IconButton';
|
||||||
|
import type { DesignScheme } from '~/types/design-scheme';
|
||||||
|
import { defaultDesignScheme, designFeatures, designFonts, paletteRoles } from '~/types/design-scheme';
|
||||||
|
|
||||||
|
export interface ColorSchemeDialogProps {
|
||||||
|
designScheme?: DesignScheme;
|
||||||
|
setDesignScheme?: (scheme: DesignScheme) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ColorSchemeDialog: React.FC<ColorSchemeDialogProps> = ({ setDesignScheme, designScheme }) => {
|
||||||
|
const [palette, setPalette] = useState<{ [key: string]: string }>(() => {
|
||||||
|
if (designScheme?.palette) {
|
||||||
|
return { ...defaultDesignScheme.palette, ...designScheme.palette };
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultDesignScheme.palette;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [features, setFeatures] = useState<string[]>(designScheme?.features || defaultDesignScheme.features);
|
||||||
|
|
||||||
|
const [font, setFont] = useState<string[]>(designScheme?.font || defaultDesignScheme.font);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (designScheme) {
|
||||||
|
setPalette(() => ({ ...defaultDesignScheme.palette, ...designScheme.palette }));
|
||||||
|
setFeatures(designScheme.features || defaultDesignScheme.features);
|
||||||
|
setFont(designScheme.font || defaultDesignScheme.font);
|
||||||
|
} else {
|
||||||
|
// Reset to defaults if no designScheme provided
|
||||||
|
setPalette(defaultDesignScheme.palette);
|
||||||
|
setFeatures(defaultDesignScheme.features);
|
||||||
|
setFont(defaultDesignScheme.font);
|
||||||
|
}
|
||||||
|
}, [designScheme]);
|
||||||
|
|
||||||
|
const handleColorChange = (role: string, value: string) => {
|
||||||
|
setPalette((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[role]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFeatureToggle = (key: string) => {
|
||||||
|
setFeatures((prev) => (prev.includes(key) ? prev.filter((f) => f !== key) : [...prev, key]));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFontToggle = (key: string) => {
|
||||||
|
setFont((prev) => (prev.includes(key) ? prev.filter((f) => f !== key) : [...prev, key]));
|
||||||
|
};
|
||||||
|
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
setDesignScheme?.({ palette, features, font });
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setPalette(defaultDesignScheme.palette);
|
||||||
|
setFeatures(defaultDesignScheme.features);
|
||||||
|
setFont(defaultDesignScheme.font);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<IconButton title="Upload file" className="transition-all" onClick={() => setIsDialogOpen(!isDialogOpen)}>
|
||||||
|
<div className="i-ph:palette text-xl"></div>
|
||||||
|
</IconButton>
|
||||||
|
<DialogRoot open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
|
<Dialog>
|
||||||
|
<div className="p-8 min-w-[380px] max-w-[95vw]">
|
||||||
|
<DialogTitle className="mb-2 text-lg font-bold">Design Palette & Features</DialogTitle>
|
||||||
|
<DialogDescription className="mb-6 text-sm text-bolt-elements-textPrimary">
|
||||||
|
Choose your color palette, typography, and key design features. These will be used as design instructions
|
||||||
|
for the LLM.
|
||||||
|
</DialogDescription>
|
||||||
|
|
||||||
|
<div className="mb-5">
|
||||||
|
<div className="w-full flex justify-between items-center mb-3">
|
||||||
|
<span className="font-semibold text-sm text-bolt-elements-textPrimary">Color Palette</span>
|
||||||
|
<button
|
||||||
|
onClick={handleReset}
|
||||||
|
className="text-xs bg-transparent text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary flex items-center gap-1 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="i-ph:arrow-clockwise" />
|
||||||
|
Reset to defaults
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 max-h-48 overflow-y-auto">
|
||||||
|
{paletteRoles.map((role) => (
|
||||||
|
<div
|
||||||
|
key={role.key}
|
||||||
|
className="flex items-center gap-3 p-2 rounded-lg bg-bolt-elements-background-depth-3 hover:bg-bolt-elements-background-depth-2"
|
||||||
|
>
|
||||||
|
<div className="relative flex-shrink-0">
|
||||||
|
<div
|
||||||
|
className="w-10 h-10 rounded-lg shadow-sm cursor-pointer hover:scale-105 transition-transform"
|
||||||
|
style={{ backgroundColor: palette[role.key] }}
|
||||||
|
onClick={() => document.getElementById(`color-input-${role.key}`)?.click()}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
id={`color-input-${role.key}`}
|
||||||
|
type="color"
|
||||||
|
value={palette[role.key]}
|
||||||
|
onChange={(e) => handleColorChange(role.key, e.target.value)}
|
||||||
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||||
|
tabIndex={-1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-medium text-sm text-bolt-elements-textPrimary">{role.label}</div>
|
||||||
|
<div className="text-xs text-bolt-elements-textSecondary truncate">{role.description}</div>
|
||||||
|
<div className="text-xs text-bolt-elements-textSecondary opacity-50 font-mono">
|
||||||
|
{palette[role.key]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-5">
|
||||||
|
<div className="w-full flex justify-between items-center mb-3">
|
||||||
|
<span className="font-semibold text-sm text-bolt-elements-textPrimary">Typography</span>
|
||||||
|
<span className="text-xs text-bolt-elements-textSecondary flex items-center gap-1">
|
||||||
|
<span className="i-ph:arrow-right" />
|
||||||
|
Scroll for more
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 overflow-x-auto pb-2 px-0.5">
|
||||||
|
{designFonts.map((f) => (
|
||||||
|
<button
|
||||||
|
key={f.key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleFontToggle(f.key)}
|
||||||
|
className={`flex-shrink-0 px-4 py-2 rounded-lg border text-xs font-medium transition-colors shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-300 flex items-center gap-2 min-w-[120px] ${font.includes(f.key) ? 'bg-blue-100 border-blue-400 text-blue-700' : 'bg-gray-50 border-gray-200 text-gray-500 hover:bg-blue-50 hover:border-blue-300'}`}
|
||||||
|
style={{ fontFamily: f.key }}
|
||||||
|
>
|
||||||
|
<span className="text-lg" style={{ fontFamily: f.key }}>
|
||||||
|
{f.preview}
|
||||||
|
</span>
|
||||||
|
<span>{f.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="w-full flex justify-between items-center mb-3">
|
||||||
|
<span className="font-semibold text-sm text-bolt-elements-textPrimary">Design Features</span>
|
||||||
|
<span className="text-xs text-bolt-elements-textSecondary flex items-center gap-1">
|
||||||
|
<span className="i-ph:arrow-right" />
|
||||||
|
Scroll for more
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 overflow-x-auto pb-2 px-0.5">
|
||||||
|
{designFeatures.map((f) => {
|
||||||
|
const isSelected = features.includes(f.key);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={f.key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleFeatureToggle(f.key)}
|
||||||
|
className={`
|
||||||
|
group relative px-4 py-2 text-xs font-medium transition-all duration-300
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-purple-300 cursor-pointer
|
||||||
|
transform hover:scale-105 active:scale-95 flex-shrink-0 min-w-[140px]
|
||||||
|
${
|
||||||
|
f.key === 'rounded'
|
||||||
|
? isSelected
|
||||||
|
? 'rounded-2xl'
|
||||||
|
: 'rounded-lg hover:rounded-xl'
|
||||||
|
: f.key === 'border'
|
||||||
|
? 'rounded-md'
|
||||||
|
: 'rounded-lg'
|
||||||
|
}
|
||||||
|
${
|
||||||
|
f.key === 'border'
|
||||||
|
? isSelected
|
||||||
|
? 'border-2 border-purple-400 bg-purple-50'
|
||||||
|
: 'border-2 border-gray-200 hover:border-purple-300 bg-white'
|
||||||
|
: f.key === 'gradient'
|
||||||
|
? ''
|
||||||
|
: isSelected
|
||||||
|
? 'bg-purple-100 text-purple-700'
|
||||||
|
: 'bg-gray-50 hover:bg-purple-50 text-gray-600 hover:text-purple-600'
|
||||||
|
}
|
||||||
|
${
|
||||||
|
f.key === 'shadow'
|
||||||
|
? isSelected
|
||||||
|
? 'shadow-md shadow-purple-200'
|
||||||
|
: 'shadow-md hover:shadow-lg'
|
||||||
|
: 'shadow-sm hover:shadow-md'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
...(f.key === 'gradient' && {
|
||||||
|
background: isSelected
|
||||||
|
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
||||||
|
: 'linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%)',
|
||||||
|
color: isSelected ? 'white' : '#6b7280',
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Feature preview area */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Visual preview */}
|
||||||
|
<div className="flex items-center justify-center w-6 h-6">
|
||||||
|
{f.key === 'rounded' && (
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 bg-current transition-all duration-300 ${
|
||||||
|
isSelected ? 'rounded-full' : 'rounded-sm group-hover:rounded-lg'
|
||||||
|
} opacity-70`}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
{f.key === 'border' && (
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded transition-all duration-300 ${
|
||||||
|
isSelected
|
||||||
|
? 'border-2 border-current opacity-80'
|
||||||
|
: 'border border-current opacity-60 group-hover:border-2'
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
{f.key === 'gradient' && (
|
||||||
|
<div className="w-4 h-4 rounded-sm bg-gradient-to-br from-purple-400 via-pink-400 to-indigo-400 opacity-90 transition-all duration-300 group-hover:scale-110"></div>
|
||||||
|
)}
|
||||||
|
{f.key === 'shadow' && (
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 bg-current rounded transition-all duration-300 ${
|
||||||
|
isSelected ? 'opacity-80' : 'opacity-60'
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={`absolute top-0.5 left-0.5 w-4 h-4 bg-current rounded transition-all duration-300 ${
|
||||||
|
isSelected ? 'opacity-30' : 'opacity-20'
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Label */}
|
||||||
|
<span className="transition-all duration-300">{f.label}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hover effect overlay (might replace this) */}
|
||||||
|
<div className="absolute inset-0 bg-purple-400 opacity-0 group-hover:opacity-5 transition-opacity duration-300 rounded-inherit"></div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="secondary" onClick={() => setIsDialogOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" onClick={handleSave}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</DialogRoot>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -26,6 +26,7 @@ import useViewport from '~/lib/hooks';
|
|||||||
import { PushToGitHubDialog } from '~/components/@settings/tabs/connections/components/PushToGitHubDialog';
|
import { PushToGitHubDialog } from '~/components/@settings/tabs/connections/components/PushToGitHubDialog';
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
import { usePreviewStore } from '~/lib/stores/previews';
|
import { usePreviewStore } from '~/lib/stores/previews';
|
||||||
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
|
|
||||||
interface WorkspaceProps {
|
interface WorkspaceProps {
|
||||||
chatStarted?: boolean;
|
chatStarted?: boolean;
|
||||||
@ -294,6 +295,8 @@ export const Workbench = memo(
|
|||||||
const unsavedFiles = useStore(workbenchStore.unsavedFiles);
|
const unsavedFiles = useStore(workbenchStore.unsavedFiles);
|
||||||
const files = useStore(workbenchStore.files);
|
const files = useStore(workbenchStore.files);
|
||||||
const selectedView = useStore(workbenchStore.currentView);
|
const selectedView = useStore(workbenchStore.currentView);
|
||||||
|
const { showChat } = useStore(chatStore);
|
||||||
|
const canHideChat = showWorkbench || !showChat;
|
||||||
|
|
||||||
const isSmallViewport = useViewport(1024);
|
const isSmallViewport = useViewport(1024);
|
||||||
|
|
||||||
@ -370,7 +373,7 @@ export const Workbench = memo(
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[var(--workbench-inner-width)] mr-4 z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
|
'fixed top-[calc(var(--header-height)+1.2rem)] bottom-6 w-[var(--workbench-inner-width)] z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
|
||||||
{
|
{
|
||||||
'w-full': isSmallViewport,
|
'w-full': isSmallViewport,
|
||||||
'left-0': showWorkbench && isSmallViewport,
|
'left-0': showWorkbench && isSmallViewport,
|
||||||
@ -379,9 +382,18 @@ export const Workbench = memo(
|
|||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 px-2 lg:px-6">
|
<div className="absolute inset-0 px-2 lg:px-4">
|
||||||
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
|
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
|
||||||
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor gap-1">
|
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor gap-1.5">
|
||||||
|
<button
|
||||||
|
className={`${showChat ? 'i-ph:sidebar-simple-fill' : 'i-ph:sidebar-simple'} text-lg text-bolt-elements-textSecondary`}
|
||||||
|
disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's not needed
|
||||||
|
onClick={() => {
|
||||||
|
if (canHideChat) {
|
||||||
|
chatStore.setKey('showChat', !showChat);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
||||||
<div className="ml-auto" />
|
<div className="ml-auto" />
|
||||||
{selectedView === 'code' && (
|
{selectedView === 'code' && (
|
||||||
@ -398,7 +410,7 @@ export const Workbench = memo(
|
|||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger className="text-sm flex items-center gap-1 text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive rounded-md p-1 enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed">
|
<DropdownMenu.Trigger className="text-sm flex items-center gap-1 text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive rounded-md p-1 enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed">
|
||||||
<div className="i-ph:box-arrow-up" />
|
<div className="i-ph:box-arrow-up" />
|
||||||
Sync & Export
|
Sync
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@ -412,19 +424,6 @@ export const Workbench = memo(
|
|||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
align="end"
|
align="end"
|
||||||
>
|
>
|
||||||
<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={() => {
|
|
||||||
workbenchStore.downloadZip();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="i-ph:download-simple"></div>
|
|
||||||
<span>Download Code</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
className={classNames(
|
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',
|
'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',
|
||||||
|
@ -9,6 +9,7 @@ import { LLMManager } from '~/lib/modules/llm/manager';
|
|||||||
import { createScopedLogger } from '~/utils/logger';
|
import { createScopedLogger } from '~/utils/logger';
|
||||||
import { createFilesContext, extractPropertiesFromMessage } from './utils';
|
import { createFilesContext, extractPropertiesFromMessage } from './utils';
|
||||||
import { discussPrompt } from '~/lib/common/prompts/discuss-prompt';
|
import { discussPrompt } from '~/lib/common/prompts/discuss-prompt';
|
||||||
|
import type { DesignScheme } from '~/types/design-scheme';
|
||||||
|
|
||||||
export type Messages = Message[];
|
export type Messages = Message[];
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ export async function streamText(props: {
|
|||||||
summary?: string;
|
summary?: string;
|
||||||
messageSliceId?: number;
|
messageSliceId?: number;
|
||||||
chatMode?: 'discuss' | 'build';
|
chatMode?: 'discuss' | 'build';
|
||||||
|
designScheme?: DesignScheme;
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
@ -51,6 +53,7 @@ export async function streamText(props: {
|
|||||||
contextFiles,
|
contextFiles,
|
||||||
summary,
|
summary,
|
||||||
chatMode,
|
chatMode,
|
||||||
|
designScheme,
|
||||||
} = props;
|
} = props;
|
||||||
let currentModel = DEFAULT_MODEL;
|
let currentModel = DEFAULT_MODEL;
|
||||||
let currentProvider = DEFAULT_PROVIDER.name;
|
let currentProvider = DEFAULT_PROVIDER.name;
|
||||||
@ -120,6 +123,7 @@ export async function streamText(props: {
|
|||||||
cwd: WORK_DIR,
|
cwd: WORK_DIR,
|
||||||
allowedHtmlElements: allowedHTMLElements,
|
allowedHtmlElements: allowedHTMLElements,
|
||||||
modificationTagName: MODIFICATIONS_TAG_NAME,
|
modificationTagName: MODIFICATIONS_TAG_NAME,
|
||||||
|
designScheme,
|
||||||
supabase: {
|
supabase: {
|
||||||
isConnected: options?.supabaseConnection?.isConnected || false,
|
isConnected: options?.supabaseConnection?.isConnected || false,
|
||||||
hasSelectedProject: options?.supabaseConnection?.hasSelectedProject || false,
|
hasSelectedProject: options?.supabaseConnection?.hasSelectedProject || false,
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { getSystemPrompt } from './prompts/prompts';
|
import { getSystemPrompt } from './prompts/prompts';
|
||||||
import optimized from './prompts/optimized';
|
import optimized from './prompts/optimized';
|
||||||
import { getFineTunedPrompt } from './prompts/new-prompt';
|
import { getFineTunedPrompt } from './prompts/new-prompt';
|
||||||
|
import type { DesignScheme } from '~/types/design-scheme';
|
||||||
|
|
||||||
export interface PromptOptions {
|
export interface PromptOptions {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
allowedHtmlElements: string[];
|
allowedHtmlElements: string[];
|
||||||
modificationTagName: string;
|
modificationTagName: string;
|
||||||
|
designScheme?: DesignScheme;
|
||||||
supabase?: {
|
supabase?: {
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
hasSelectedProject: boolean;
|
hasSelectedProject: boolean;
|
||||||
@ -28,12 +30,12 @@ export class PromptLibrary {
|
|||||||
default: {
|
default: {
|
||||||
label: 'Default Prompt',
|
label: 'Default Prompt',
|
||||||
description: 'This is the battle tested default system Prompt',
|
description: 'This is the battle tested default system Prompt',
|
||||||
get: (options) => getSystemPrompt(options.cwd, options.supabase),
|
get: (options) => getSystemPrompt(options.cwd, options.supabase, options.designScheme),
|
||||||
},
|
},
|
||||||
enhanced: {
|
enhanced: {
|
||||||
label: 'Fine Tuned Prompt',
|
label: 'Fine Tuned Prompt',
|
||||||
description: 'An fine tuned prompt for better results',
|
description: 'An fine tuned prompt for better results',
|
||||||
get: (options) => getFineTunedPrompt(options.cwd, options.supabase),
|
get: (options) => getFineTunedPrompt(options.cwd, options.supabase, options.designScheme),
|
||||||
},
|
},
|
||||||
optimized: {
|
optimized: {
|
||||||
label: 'Optimized Prompt (experimental)',
|
label: 'Optimized Prompt (experimental)',
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { DesignScheme } from '~/types/design-scheme';
|
||||||
import { WORK_DIR } from '~/utils/constants';
|
import { WORK_DIR } from '~/utils/constants';
|
||||||
import { allowedHTMLElements } from '~/utils/markdown';
|
import { allowedHTMLElements } from '~/utils/markdown';
|
||||||
import { stripIndents } from '~/utils/stripIndent';
|
import { stripIndents } from '~/utils/stripIndent';
|
||||||
@ -9,6 +10,7 @@ export const getFineTunedPrompt = (
|
|||||||
hasSelectedProject: boolean;
|
hasSelectedProject: boolean;
|
||||||
credentials?: { anonKey?: string; supabaseUrl?: string };
|
credentials?: { anonKey?: string; supabaseUrl?: string };
|
||||||
},
|
},
|
||||||
|
designScheme?: DesignScheme,
|
||||||
) => `
|
) => `
|
||||||
You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices, created by StackBlitz.
|
You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices, created by StackBlitz.
|
||||||
|
|
||||||
@ -324,6 +326,14 @@ The year is 2025.
|
|||||||
<design_instructions>
|
<design_instructions>
|
||||||
When creating designs or UIs for applications, follow these guidelines indefinitely this is non-negotiable:
|
When creating designs or UIs for applications, follow these guidelines indefinitely this is non-negotiable:
|
||||||
|
|
||||||
|
<user_provided_design>
|
||||||
|
USER PROVIDED DESIGN SCHEME:
|
||||||
|
- ALWAYS use the user provided design scheme when creating designs unless the user specifically requests otherwise.
|
||||||
|
FONT: ${JSON.stringify(designScheme?.font)}
|
||||||
|
COLOR PALETTE: ${JSON.stringify(designScheme?.palette)}
|
||||||
|
FEATURES: ${JSON.stringify(designScheme?.features)}
|
||||||
|
</user_provided_design>
|
||||||
|
|
||||||
CRITICAL:
|
CRITICAL:
|
||||||
- Always strive for professional, beautiful, and unique designs
|
- Always strive for professional, beautiful, and unique designs
|
||||||
- All designs should be fully featured and worthy of production use
|
- All designs should be fully featured and worthy of production use
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { DesignScheme } from '~/types/design-scheme';
|
||||||
import { WORK_DIR } from '~/utils/constants';
|
import { WORK_DIR } from '~/utils/constants';
|
||||||
import { allowedHTMLElements } from '~/utils/markdown';
|
import { allowedHTMLElements } from '~/utils/markdown';
|
||||||
import { stripIndents } from '~/utils/stripIndent';
|
import { stripIndents } from '~/utils/stripIndent';
|
||||||
@ -9,6 +10,7 @@ export const getSystemPrompt = (
|
|||||||
hasSelectedProject: boolean;
|
hasSelectedProject: boolean;
|
||||||
credentials?: { anonKey?: string; supabaseUrl?: string };
|
credentials?: { anonKey?: string; supabaseUrl?: string };
|
||||||
},
|
},
|
||||||
|
designScheme?: DesignScheme,
|
||||||
) => `
|
) => `
|
||||||
You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices.
|
You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices.
|
||||||
|
|
||||||
@ -392,6 +394,14 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
|||||||
<design_instructions>
|
<design_instructions>
|
||||||
Overall Goal: Create visually stunning, unique, highly interactive, content-rich, and production-ready applications. Avoid generic templates.
|
Overall Goal: Create visually stunning, unique, highly interactive, content-rich, and production-ready applications. Avoid generic templates.
|
||||||
|
|
||||||
|
<user_provided_design>
|
||||||
|
USER PROVIDED DESIGN SCHEME:
|
||||||
|
- ALWAYS use the user provided design scheme when creating designs unless the user specifically requests otherwise.
|
||||||
|
FONT: ${JSON.stringify(designScheme?.font)}
|
||||||
|
COLOR PALETTE: ${JSON.stringify(designScheme?.palette)}
|
||||||
|
FEATURES: ${JSON.stringify(designScheme?.features)}
|
||||||
|
</user_provided_design>
|
||||||
|
|
||||||
Visual Identity & Branding:
|
Visual Identity & Branding:
|
||||||
- Establish a distinctive art direction (unique shapes, grids, illustrations).
|
- Establish a distinctive art direction (unique shapes, grids, illustrations).
|
||||||
- Use premium typography with refined hierarchy and spacing.
|
- Use premium typography with refined hierarchy and spacing.
|
||||||
|
@ -49,16 +49,14 @@ export function ChatDescription() {
|
|||||||
{currentDescription}
|
{currentDescription}
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<WithTooltip tooltip="Rename chat">
|
<WithTooltip tooltip="Rename chat">
|
||||||
<div className="flex justify-between items-center p-2 rounded-md bg-bolt-elements-item-backgroundAccent ml-2">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="ml-2 i-ph:pencil-fill scale-110 hover:text-bolt-elements-item-contentAccent"
|
||||||
className="i-ph:pencil-fill scale-110 hover:text-bolt-elements-item-contentAccent"
|
onClick={(event) => {
|
||||||
onClick={(event) => {
|
event.preventDefault();
|
||||||
event.preventDefault();
|
toggleEditMode();
|
||||||
toggleEditMode();
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</WithTooltip>
|
</WithTooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</>
|
</>
|
||||||
|
@ -11,6 +11,7 @@ import type { ContextAnnotation, ProgressAnnotation } from '~/types/context';
|
|||||||
import { WORK_DIR } from '~/utils/constants';
|
import { WORK_DIR } from '~/utils/constants';
|
||||||
import { createSummary } from '~/lib/.server/llm/create-summary';
|
import { createSummary } from '~/lib/.server/llm/create-summary';
|
||||||
import { extractPropertiesFromMessage } from '~/lib/.server/llm/utils';
|
import { extractPropertiesFromMessage } from '~/lib/.server/llm/utils';
|
||||||
|
import type { DesignScheme } from '~/types/design-scheme';
|
||||||
|
|
||||||
export async function action(args: ActionFunctionArgs) {
|
export async function action(args: ActionFunctionArgs) {
|
||||||
return chatAction(args);
|
return chatAction(args);
|
||||||
@ -37,12 +38,13 @@ function parseCookies(cookieHeader: string): Record<string, string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function chatAction({ context, request }: ActionFunctionArgs) {
|
async function chatAction({ context, request }: ActionFunctionArgs) {
|
||||||
const { messages, files, promptId, contextOptimization, supabase, chatMode } = await request.json<{
|
const { messages, files, promptId, contextOptimization, supabase, chatMode, designScheme } = await request.json<{
|
||||||
messages: Messages;
|
messages: Messages;
|
||||||
files: any;
|
files: any;
|
||||||
promptId?: string;
|
promptId?: string;
|
||||||
contextOptimization: boolean;
|
contextOptimization: boolean;
|
||||||
chatMode: 'discuss' | 'build';
|
chatMode: 'discuss' | 'build';
|
||||||
|
designScheme?: DesignScheme;
|
||||||
supabase?: {
|
supabase?: {
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
hasSelectedProject: boolean;
|
hasSelectedProject: boolean;
|
||||||
@ -250,6 +252,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
contextOptimization,
|
contextOptimization,
|
||||||
contextFiles: filteredFiles,
|
contextFiles: filteredFiles,
|
||||||
chatMode,
|
chatMode,
|
||||||
|
designScheme,
|
||||||
summary,
|
summary,
|
||||||
messageSliceId,
|
messageSliceId,
|
||||||
});
|
});
|
||||||
@ -290,6 +293,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
contextOptimization,
|
contextOptimization,
|
||||||
contextFiles: filteredFiles,
|
contextFiles: filteredFiles,
|
||||||
chatMode,
|
chatMode,
|
||||||
|
designScheme,
|
||||||
summary,
|
summary,
|
||||||
messageSliceId,
|
messageSliceId,
|
||||||
});
|
});
|
||||||
|
@ -221,8 +221,8 @@
|
|||||||
*/
|
*/
|
||||||
:root {
|
:root {
|
||||||
--header-height: 54px;
|
--header-height: 54px;
|
||||||
--chat-max-width: 35rem;
|
--chat-max-width: 33rem;
|
||||||
--chat-min-width: 575px;
|
--chat-min-width: 533px;
|
||||||
--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));
|
||||||
|
100
app/types/design-scheme.ts
Normal file
100
app/types/design-scheme.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
export interface DesignScheme {
|
||||||
|
palette: { [key: string]: string }; // Changed from string[] to object
|
||||||
|
features: string[];
|
||||||
|
font: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultDesignScheme: DesignScheme = {
|
||||||
|
palette: {
|
||||||
|
primary: '#7c3aed',
|
||||||
|
secondary: '#38bdf8',
|
||||||
|
accent: '#f472b6',
|
||||||
|
background: '#f9fafb',
|
||||||
|
surface: '#ffffff',
|
||||||
|
text: '#18181b',
|
||||||
|
textSecondary: '#64748b',
|
||||||
|
border: '#e2e8f0',
|
||||||
|
success: '#10b981',
|
||||||
|
warning: '#f59e0b',
|
||||||
|
error: '#ef4444',
|
||||||
|
},
|
||||||
|
features: ['rounded', 'shadow'],
|
||||||
|
font: ['sans-serif'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define the semantic color roles for the UI
|
||||||
|
export const paletteRoles = [
|
||||||
|
{
|
||||||
|
key: 'primary',
|
||||||
|
label: 'Primary',
|
||||||
|
description: 'Main brand color - use for primary buttons, active links, and key interactive elements',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'secondary',
|
||||||
|
label: 'Secondary',
|
||||||
|
description: 'Supporting brand color - use for secondary buttons, inactive states, and complementary elements',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'accent',
|
||||||
|
label: 'Accent',
|
||||||
|
description: 'Highlight color - use for badges, notifications, focus states, and call-to-action elements',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'background',
|
||||||
|
label: 'Background',
|
||||||
|
description: 'Page backdrop - use for the main application/website background behind all content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'surface',
|
||||||
|
label: 'Surface',
|
||||||
|
description: 'Elevated content areas - use for cards, modals, dropdowns, and panels that sit above the background',
|
||||||
|
},
|
||||||
|
{ key: 'text', label: 'Text', description: 'Primary text - use for headings, body text, and main readable content' },
|
||||||
|
{
|
||||||
|
key: 'textSecondary',
|
||||||
|
label: 'Text Secondary',
|
||||||
|
description: 'Muted text - use for captions, placeholders, timestamps, and less important information',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'border',
|
||||||
|
label: 'Border',
|
||||||
|
description: 'Separators - use for input borders, dividers, table lines, and element outlines',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'success',
|
||||||
|
label: 'Success',
|
||||||
|
description: 'Positive feedback - use for success messages, completed states, and positive indicators',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'warning',
|
||||||
|
label: 'Warning',
|
||||||
|
description: 'Caution alerts - use for warning messages, pending states, and attention-needed indicators',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'error',
|
||||||
|
label: 'Error',
|
||||||
|
description: 'Error states - use for error messages, failed states, and destructive action indicators',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const designFeatures = [
|
||||||
|
{ key: 'rounded', label: 'Rounded Corners' },
|
||||||
|
{ key: 'border', label: 'Subtle Border' },
|
||||||
|
{ key: 'gradient', label: 'Gradient Accent' },
|
||||||
|
{ key: 'shadow', label: 'Soft Shadow' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add font options for easy reference
|
||||||
|
export const designFonts = [
|
||||||
|
{ key: 'sans-serif', label: 'Sans Serif', preview: 'Aa' },
|
||||||
|
{ key: 'serif', label: 'Serif', preview: 'Aa' },
|
||||||
|
{ key: 'monospace', label: 'Monospace', preview: 'Aa' },
|
||||||
|
{ key: 'cursive', label: 'Cursive', preview: 'Aa' },
|
||||||
|
{ key: 'fantasy', label: 'Fantasy', preview: 'Aa' },
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add custom fonts here if needed
|
||||||
|
* { key: 'Inter', label: 'Inter', preview: 'Aa' },
|
||||||
|
* { key: 'Roboto', label: 'Roboto', preview: 'Aa' },
|
||||||
|
*/
|
||||||
|
];
|
Loading…
Reference in New Issue
Block a user