diff --git a/app/components/chat/AssistantMessage.tsx b/app/components/chat/AssistantMessage.tsx index 2d013b86..022b4fbe 100644 --- a/app/components/chat/AssistantMessage.tsx +++ b/app/components/chat/AssistantMessage.tsx @@ -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( - + {content} diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index e315fdcc..3040a8b8 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -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( @@ -114,6 +117,8 @@ export const BaseChat = React.forwardRef( chatMode, setChatMode, append, + designScheme, + setDesignScheme, }, ref, ) => { @@ -332,7 +337,7 @@ export const BaseChat = React.forwardRef(
{!chatStarted && ( -
+

Where ideas begin

@@ -353,12 +358,14 @@ export const BaseChat = React.forwardRef( {() => { return chatStarted ? ( ) : null; }} @@ -440,6 +447,8 @@ export const BaseChat = React.forwardRef( handleFileUpload={handleFileUpload} chatMode={chatMode} setChatMode={setChatMode} + designScheme={designScheme} + setDesignScheme={setDesignScheme} />
diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index 8a9dc700..3a1eab87 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -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(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} /> ); }, diff --git a/app/components/chat/ChatBox.tsx b/app/components/chat/ChatBox.tsx index 29fa7c53..c8a3ead9 100644 --- a/app/components/chat/ChatBox.tsx +++ b/app/components/chat/ChatBox.tsx @@ -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 = (props) => { return (
= (props) => {
+ props.handleFileUpload()}>
@@ -279,7 +283,6 @@ export const ChatBox: React.FC = (props) => { {props.chatMode === 'discuss' ? Discuss : } )} - {props.chatStarted && {() => }} 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 { diff --git a/app/components/chat/Messages.client.tsx b/app/components/chat/Messages.client.tsx index 6dc8d7f3..effe4c30 100644 --- a/app/components/chat/Messages.client.tsx +++ b/app/components/chat/Messages.client.tsx @@ -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( @@ -65,7 +68,7 @@ export const Messages = forwardRef( return (
( append={props.append} chatMode={props.chatMode} setChatMode={props.setChatMode} + model={props.model} + provider={props.provider} /> )}
diff --git a/app/components/chat/chatExportAndImport/ExportChatButton.tsx b/app/components/chat/chatExportAndImport/ExportChatButton.tsx index 6ab294bc..53665d98 100644 --- a/app/components/chat/chatExportAndImport/ExportChatButton.tsx +++ b/app/components/chat/chatExportAndImport/ExportChatButton.tsx @@ -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 ( - - exportChat?.()}> -
-
-
+
+ + + Export + + + + { + workbenchStore.downloadZip(); + }} + > +
+ Download Code +
+ exportChat?.()} + > +
+ Export Chat +
+
+
+
); }; diff --git a/app/components/deploy/DeployButton.tsx b/app/components/deploy/DeployButton.tsx new file mode 100644 index 00000000..9e55df14 --- /dev/null +++ b/app/components/deploy/DeployButton.tsx @@ -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; + onNetlifyDeploy?: () => Promise; +} + +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 ( +
+ + + {isDeploying ? `Deploying to ${deployingTo}...` : 'Deploy'} + + + + + + {!netlifyConn.user ? 'No Netlify Account Connected' : 'Deploy to Netlify'} + {netlifyConn.user && } + + + + vercel + {!vercelConn.user ? 'No Vercel Account Connected' : 'Deploy to Vercel'} + {vercelConn.user && } + + + + cloudflare + Deploy to Cloudflare (Coming Soon) + + + +
+ ); +}; diff --git a/app/components/header/Header.tsx b/app/components/header/Header.tsx index ce46702a..1d509ce8 100644 --- a/app/components/header/Header.tsx +++ b/app/components/header/Header.tsx @@ -10,7 +10,7 @@ export function Header() { return (
{() => ( -
- +
+
)} diff --git a/app/components/header/HeaderActionButtons.client.tsx b/app/components/header/HeaderActionButtons.client.tsx index ff211f30..5fe19a51 100644 --- a/app/components/header/HeaderActionButtons.client.tsx +++ b/app/components/header/HeaderActionButtons.client.tsx @@ -1,206 +1,28 @@ 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 { classNames } from '~/utils/classNames'; -import { useEffect, useRef, useState } from 'react'; +import { useState } from 'react'; import { streamingState } from '~/lib/stores/streaming'; -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'; +import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton'; +import { useChatHistory } from '~/lib/persistence'; +import { DeployButton } from '~/components/deploy/DeployButton'; -interface HeaderActionButtonsProps {} +interface HeaderActionButtonsProps { + chatStarted: boolean; +} -export function HeaderActionButtons({}: HeaderActionButtonsProps) { - const showWorkbench = useStore(workbenchStore.showWorkbench); - const { showChat } = useStore(chatStore); - const netlifyConn = useStore(netlifyConnection); - const vercelConn = useStore(vercelConnection); +export function HeaderActionButtons({ chatStarted }: HeaderActionButtonsProps) { 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 isSmallViewport = useViewport(1024); - const canHideChat = showWorkbench || !showChat; - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const dropdownRef = useRef(null); const isStreaming = useStore(streamingState); - const { handleVercelDeploy } = useVercelDeploy(); - const { handleNetlifyDeploy } = useNetlifyDeploy(); + const { exportChat } = useChatHistory(); - useEffect(() => { - 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); - } - }; + const shouldShowButtons = !isStreaming && activePreview; return ( -
-
-
- -
- - {isDropdownOpen && ( -
- - - -
- )} -
-
- -
- -
+
+ {chatStarted && shouldShowButtons && } + {shouldShowButtons && }
); } - -interface ButtonProps { - active?: boolean; - disabled?: boolean; - children?: any; - onClick?: VoidFunction; - className?: string; -} - -function Button({ active = false, disabled = false, children, onClick, className }: ButtonProps) { - return ( - - ); -} diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index 953dfbdd..f0e975eb 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -279,8 +279,8 @@ export const Menu = () => { }, [open, selectionMode]); useEffect(() => { - const enterThreshold = 40; - const exitThreshold = 40; + const enterThreshold = 20; + const exitThreshold = 20; function onMouseMove(event: MouseEvent) { if (isSettingsOpen) { @@ -331,13 +331,13 @@ export const Menu = () => { variants={menuVariants} style={{ width: '340px' }} className={classNames( - 'flex selection-accent flex-col side-menu fixed top-0 h-full', - 'bg-white dark:bg-gray-950 border-r border-gray-100 dark:border-gray-800/50', + 'flex selection-accent flex-col side-menu fixed top-0 h-full rounded-r-2xl', + 'bg-white dark:bg-gray-950 border-r border-bolt-elements-borderColor', 'shadow-sm text-sm', isSettingsOpen ? 'z-40' : 'z-sidebar', )} > -
+
diff --git a/app/components/ui/ColorSchemeDialog.tsx b/app/components/ui/ColorSchemeDialog.tsx new file mode 100644 index 00000000..ee1d5e16 --- /dev/null +++ b/app/components/ui/ColorSchemeDialog.tsx @@ -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 = ({ setDesignScheme, designScheme }) => { + const [palette, setPalette] = useState<{ [key: string]: string }>(() => { + if (designScheme?.palette) { + return { ...defaultDesignScheme.palette, ...designScheme.palette }; + } + + return defaultDesignScheme.palette; + }); + + const [features, setFeatures] = useState(designScheme?.features || defaultDesignScheme.features); + + const [font, setFont] = useState(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 ( +
+ setIsDialogOpen(!isDialogOpen)}> +
+
+ + +
+ Design Palette & Features + + Choose your color palette, typography, and key design features. These will be used as design instructions + for the LLM. + + +
+
+ Color Palette + +
+
+ {paletteRoles.map((role) => ( +
+
+
document.getElementById(`color-input-${role.key}`)?.click()} + /> + handleColorChange(role.key, e.target.value)} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + tabIndex={-1} + /> +
+
+
{role.label}
+
{role.description}
+
+ {palette[role.key]} +
+
+
+ ))} +
+
+ +
+
+ Typography + + + Scroll for more + +
+
+ {designFonts.map((f) => ( + + ))} +
+
+ +
+
+ Design Features + + + Scroll for more + +
+
+ {designFeatures.map((f) => { + const isSelected = features.includes(f.key); + + return ( + + ); + })} +
+
+ +
+ + +
+
+
+
+
+ ); +}; diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx index 7b7d8ce2..950b4193 100644 --- a/app/components/workbench/Workbench.client.tsx +++ b/app/components/workbench/Workbench.client.tsx @@ -26,6 +26,7 @@ import useViewport from '~/lib/hooks'; import { PushToGitHubDialog } from '~/components/@settings/tabs/connections/components/PushToGitHubDialog'; import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import { usePreviewStore } from '~/lib/stores/previews'; +import { chatStore } from '~/lib/stores/chat'; interface WorkspaceProps { chatStarted?: boolean; @@ -294,6 +295,8 @@ export const Workbench = memo( const unsavedFiles = useStore(workbenchStore.unsavedFiles); const files = useStore(workbenchStore.files); const selectedView = useStore(workbenchStore.currentView); + const { showChat } = useStore(chatStore); + const canHideChat = showWorkbench || !showChat; const isSmallViewport = useViewport(1024); @@ -370,7 +373,7 @@ export const Workbench = memo( >
-
+
-
+
+
+