diff --git a/app/components/chat/AssistantMessage.tsx b/app/components/chat/AssistantMessage.tsx index 022b4fb..7ed2840 100644 --- a/app/components/chat/AssistantMessage.tsx +++ b/app/components/chat/AssistantMessage.tsx @@ -83,78 +83,76 @@ export const AssistantMessage = memo( return (
- <> -
- {(codeContext || chatSummary) && ( - }> - {chatSummary && ( -
-
-

Summary

-
- {chatSummary} +
+ {(codeContext || chatSummary) && ( + }> + {chatSummary && ( +
+
+

Summary

+
+ {chatSummary} +
+
+ {codeContext && ( +
+

Context

+
+ {codeContext.map((x) => { + const normalized = normalizedFilePath(x); + return ( + + { + e.preventDefault(); + e.stopPropagation(); + openArtifactInWorkbench(normalized); + }} + > + {normalized} + + + ); + })}
- {codeContext && ( -
-

Context

-
- {codeContext.map((x) => { - const normalized = normalizedFilePath(x); - return ( - - { - e.preventDefault(); - e.stopPropagation(); - openArtifactInWorkbench(normalized); - }} - > - {normalized} - - - ); - })} -
-
- )} -
- )} -
-
+ )} +
+ )} +
+ + )} +
+ {usage && ( +
+ Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens}) +
+ )} + {(onRewind || onFork) && messageId && ( +
+ {onRewind && ( + +
)} -
- {usage && ( -
- Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens}) -
- )} - {(onRewind || onFork) && messageId && ( -
- {onRewind && ( - -
- )} -
- +
{content} diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 3040a8b..97c9314 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -32,6 +32,7 @@ import { useStore } from '@nanostores/react'; import { StickToBottom, useStickToBottomContext } from '~/lib/hooks'; import { ChatBox } from './ChatBox'; import type { DesignScheme } from '~/types/design-scheme'; +import type { ElementInfo } from '~/components/workbench/Inspector'; const TEXTAREA_MIN_HEIGHT = 76; @@ -76,6 +77,8 @@ interface BaseChatProps { append?: (message: Message) => void; designScheme?: DesignScheme; setDesignScheme?: (scheme: DesignScheme) => void; + selectedElement?: ElementInfo | null; + setSelectedElement?: (element: ElementInfo | null) => void; } export const BaseChat = React.forwardRef( @@ -119,6 +122,8 @@ export const BaseChat = React.forwardRef( append, designScheme, setDesignScheme, + selectedElement, + setSelectedElement, }, ref, ) => { @@ -258,6 +263,7 @@ export const BaseChat = React.forwardRef( const handleSendMessage = (event: React.UIEvent, messageInput?: string) => { if (sendMessage) { sendMessage(event, messageInput); + setSelectedElement?.(null); if (recognition) { recognition.abort(); // Stop current recognition @@ -353,7 +359,7 @@ export const BaseChat = React.forwardRef( resize="smooth" initial="smooth" > - + {() => { return chatStarted ? ( @@ -370,6 +376,7 @@ export const BaseChat = React.forwardRef( ) : null; }} +
( /> )}
- {progressAnnotations && } ( setChatMode={setChatMode} designScheme={designScheme} setDesignScheme={setDesignScheme} + selectedElement={selectedElement} + setSelectedElement={setSelectedElement} />
@@ -479,6 +487,7 @@ export const BaseChat = React.forwardRef( actionRunner={actionRunner ?? ({} as ActionRunner)} chatStarted={chatStarted} isStreaming={isStreaming} + setSelectedElement={setSelectedElement} /> )} @@ -495,13 +504,16 @@ function ScrollToBottom() { return ( !isAtBottom && ( - + <> +
+ + ) ); } diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index 3a1eab8..be5ff42 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -28,6 +28,7 @@ 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'; +import type { ElementInfo } from '~/components/workbench/Inspector'; const toastAnimation = cssTransition({ enter: 'animated fadeInRight', @@ -126,9 +127,6 @@ export const ChatImpl = memo( 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 @@ -137,7 +135,6 @@ export const ChatImpl = memo( ); const supabaseAlert = useStore(workbenchStore.supabaseAlert); const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings(); - const [model, setModel] = useState(() => { const savedModel = Cookies.get('selectedModel'); return savedModel || DEFAULT_MODEL; @@ -146,14 +143,11 @@ export const ChatImpl = memo( const savedProvider = Cookies.get('selectedProvider'); return (PROVIDER_LIST.find((p) => p.name === savedProvider) || DEFAULT_PROVIDER) as ProviderInfo; }); - const { showChat } = useStore(chatStore); - const [animationScope, animate] = useAnimate(); - const [apiKeys, setApiKeys] = useState>({}); - const [chatMode, setChatMode] = useState<'discuss' | 'build'>('build'); + const [selectedElement, setSelectedElement] = useState(null); const { messages, isLoading, @@ -318,8 +312,14 @@ export const ChatImpl = memo( return; } - // If no locked items, proceed normally with the original message - const finalMessageContent = messageContent; + let finalMessageContent = messageContent; + + if (selectedElement) { + console.log('Selected Element:', selectedElement); + + const elementInfo = `
${JSON.stringify(`${selectedElement.displayText}`)}
`; + finalMessageContent = messageContent + elementInfo; + } runAnimation(); @@ -577,6 +577,8 @@ export const ChatImpl = memo( append={append} designScheme={designScheme} setDesignScheme={setDesignScheme} + selectedElement={selectedElement} + setSelectedElement={setSelectedElement} /> ); }, diff --git a/app/components/chat/ChatBox.tsx b/app/components/chat/ChatBox.tsx index c8a3ead..99aa835 100644 --- a/app/components/chat/ChatBox.tsx +++ b/app/components/chat/ChatBox.tsx @@ -17,6 +17,7 @@ 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'; +import type { ElementInfo } from '~/components/workbench/Inspector'; interface ChatBoxProps { isModelSettingsCollapsed: boolean; @@ -57,6 +58,8 @@ interface ChatBoxProps { setChatMode?: (mode: 'discuss' | 'build') => void; designScheme?: DesignScheme; setDesignScheme?: (scheme: DesignScheme) => void; + selectedElement?: ElementInfo | null; + setSelectedElement?: ((element: ElementInfo | null) => void) | undefined; } export const ChatBox: React.FC = (props) => { @@ -146,6 +149,22 @@ export const ChatBox: React.FC = (props) => { /> )} + {props.selectedElement && ( +
+
+ + {props?.selectedElement?.tagName} + + selected for inspection +
+ +
+ )}
diff --git a/app/components/chat/Markdown.tsx b/app/components/chat/Markdown.tsx index 89ecf8a..8211845 100644 --- a/app/components/chat/Markdown.tsx +++ b/app/components/chat/Markdown.tsx @@ -42,6 +42,42 @@ export const Markdown = memo( return ; } + if (className?.includes('__boltSelectedElement__')) { + const messageId = node?.properties.dataMessageId as string; + const elementDataAttr = node?.properties.dataElement as string; + + // Parse the element data if it exists + let elementData: any = null; + + if (elementDataAttr) { + try { + elementData = JSON.parse(elementDataAttr); + } catch (e) { + console.error('Failed to parse element data:', e); + } + } + + if (!messageId) { + logger.error(`Invalid message id ${messageId}`); + } + + return ( +
+
+ + {elementData?.tagName} + + {elementData?.className && ( + .{elementData.className} + )} +
+ + {elementData?.displayText} + +
+ ); + } + if (className?.includes('__boltThought__')) { return {children}; } @@ -110,7 +146,12 @@ export const Markdown = memo( } else if (type === 'message' && append) { append({ id: `quick-action-message-${Date.now()}`, - content: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`, + content: [ + { + type: 'text', + text: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`, + }, + ] as any, // Type assertion to bypass compiler check role: 'user', }); console.log('Message appended:', message); @@ -118,7 +159,12 @@ export const Markdown = memo( setChatMode('build'); append({ id: `quick-action-implement-${Date.now()}`, - content: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`, + content: [ + { + type: 'text', + text: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`, + }, + ] as any, // Type assertion to bypass compiler check role: 'user', }); } else if (type === 'link' && typeof href === 'string') { diff --git a/app/components/chat/Messages.client.tsx b/app/components/chat/Messages.client.tsx index effe4c3..c98aa4d 100644 --- a/app/components/chat/Messages.client.tsx +++ b/app/components/chat/Messages.client.tsx @@ -7,8 +7,6 @@ import { useLocation } from '@remix-run/react'; import { db, chatId } from '~/lib/persistence/useChatHistory'; import { forkChat } from '~/lib/persistence/db'; import { toast } from 'react-toastify'; -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'; @@ -29,7 +27,6 @@ export const Messages = forwardRef( (props: MessagesProps, ref: ForwardedRef | undefined) => { const { id, isStreaming = false, messages = [] } = props; const location = useLocation(); - const profile = useStore(profileStore); const handleRewind = (messageId: string) => { const searchParams = new URLSearchParams(location.search); @@ -58,7 +55,6 @@ export const Messages = forwardRef( const { role, content, id: messageId, annotations } = message; const isUserMessage = role === 'user'; const isFirst = index === 0; - const isLast = index === messages.length - 1; const isHidden = annotations?.includes('hidden'); if (isHidden) { @@ -68,28 +64,10 @@ export const Messages = forwardRef( return (
- {isUserMessage && ( -
- {profile?.avatar ? ( - {profile?.username - ) : ( -
- )} -
- )}
{isUserMessage ? ( diff --git a/app/components/chat/UserMessage.tsx b/app/components/chat/UserMessage.tsx index 4e3d4ad..3d6da43 100644 --- a/app/components/chat/UserMessage.tsx +++ b/app/components/chat/UserMessage.tsx @@ -4,6 +4,8 @@ */ import { MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants'; import { Markdown } from './Markdown'; +import { useStore } from '@nanostores/react'; +import { profileStore } from '~/lib/stores/profile'; interface UserMessageProps { content: string | Array<{ type: string; text?: string; image?: string }>; @@ -14,10 +16,29 @@ export function UserMessage({ content }: UserMessageProps) { const textItem = content.find((item) => item.type === 'text'); const textContent = stripMetadata(textItem?.text || ''); const images = content.filter((item) => item.type === 'image' && item.image); + const profile = useStore(profileStore); return ( -
-
+
+
+ {profile?.avatar || profile?.username ? ( +
+ {profile?.username + + {profile?.username ? profile.username : ''} + +
+ ) : ( +
+ )} +
+
{textContent && {textContent}} {images.map((item, index) => ( {isDeploying ? `Deploying to ${deployingTo}...` : 'Deploy'} diff --git a/app/components/ui/ColorSchemeDialog.tsx b/app/components/ui/ColorSchemeDialog.tsx index ee1d5e1..58d181d 100644 --- a/app/components/ui/ColorSchemeDialog.tsx +++ b/app/components/ui/ColorSchemeDialog.tsx @@ -20,8 +20,9 @@ export const ColorSchemeDialog: React.FC = ({ setDesignS }); const [features, setFeatures] = useState(designScheme?.features || defaultDesignScheme.features); - const [font, setFont] = useState(designScheme?.font || defaultDesignScheme.font); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [activeSection, setActiveSection] = useState<'colors' | 'typography' | 'features'>('colors'); useEffect(() => { if (designScheme) { @@ -29,7 +30,6 @@ export const ColorSchemeDialog: React.FC = ({ setDesignS 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); @@ -37,10 +37,7 @@ export const ColorSchemeDialog: React.FC = ({ setDesignS }, [designScheme]); const handleColorChange = (role: string, value: string) => { - setPalette((prev) => ({ - ...prev, - [role]: value, - })); + setPalette((prev) => ({ ...prev, [role]: value })); }; const handleFeatureToggle = (key: string) => { @@ -51,8 +48,6 @@ export const ColorSchemeDialog: React.FC = ({ setDesignS 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); @@ -64,210 +59,306 @@ export const ColorSchemeDialog: React.FC = ({ setDesignS setFont(defaultDesignScheme.font); }; + const renderColorSection = () => ( +
+
+

+
+ Color Palette +

+ +
+ +
+ {paletteRoles.map((role) => ( +
+
+
document.getElementById(`color-input-${role.key}`)?.click()} + role="button" + tabIndex={0} + aria-label={`Change ${role.label} color`} + /> + 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]} +
+
+
+ ))} +
+
+ ); + + const renderTypographySection = () => ( +
+

+
+ Typography +

+ +
+ {designFonts.map((f) => ( + + ))} +
+
+ ); + + const renderFeaturesSection = () => ( +
+

+
+ Design Features +

+ +
+ {designFeatures.map((f) => { + const isSelected = features.includes(f.key); + + return ( +
+ +
+ ); + })} +
+
+ ); + return (
- setIsDialogOpen(!isDialogOpen)}> + setIsDialogOpen(!isDialogOpen)}>
+ -
- Design Palette & Features - - Choose your color palette, typography, and key design features. These will be used as design instructions - for the LLM. - +
+
+ + Design Palette & Features + + + Customize your color palette, typography, and design features. These preferences will guide the AI in + creating designs that match your style. + +
-
-
- Color Palette + {/* Navigation Tabs */} +
+ {[ + { key: 'colors', label: 'Colors', icon: 'i-ph:palette' }, + { key: 'typography', label: 'Typography', icon: 'i-ph:text-aa' }, + { key: 'features', label: 'Features', icon: 'i-ph:magic-wand' }, + ].map((tab) => ( -
-
- {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) => ( - - ))} -
+ {/* Content Area */} +
+ {activeSection === 'colors' && renderColorSection()} + {activeSection === 'typography' && renderTypographySection()} + {activeSection === 'features' && renderFeaturesSection()}
-
-
- Design Features - - - Scroll for more - + {/* Action Buttons */} +
+
+ {Object.keys(palette).length} colors • {font.length} fonts • {features.length} features
-
- {designFeatures.map((f) => { - const isSelected = features.includes(f.key); - - return ( - - ); - })} +
+ +
- -
- - -
+ +
); }; diff --git a/app/components/workbench/Inspector.tsx b/app/components/workbench/Inspector.tsx new file mode 100644 index 0000000..198380e --- /dev/null +++ b/app/components/workbench/Inspector.tsx @@ -0,0 +1,126 @@ +import { useEffect, useRef, useState } from 'react'; + +interface InspectorProps { + isActive: boolean; + iframeRef: React.RefObject; + onElementSelect: (elementInfo: ElementInfo) => void; +} + +export interface ElementInfo { + displayText: string; + tagName: string; + className: string; + id: string; + textContent: string; + styles: Record; // Changed from CSSStyleDeclaration + rect: { + x: number; + y: number; + width: number; + height: number; + top: number; + left: number; + }; +} + +export const Inspector = ({ isActive, iframeRef, onElementSelect }: InspectorProps) => { + const [hoveredElement, setHoveredElement] = useState(null); + const overlayRef = useRef(null); + + useEffect(() => { + if (!isActive || !iframeRef.current) { + return undefined; + } + + const iframe = iframeRef.current; + + // Listen for messages from the iframe + const handleMessage = (event: MessageEvent) => { + if (event.data.type === 'INSPECTOR_HOVER') { + const elementInfo = event.data.elementInfo; + + // Adjust coordinates relative to iframe position + const iframeRect = iframe.getBoundingClientRect(); + elementInfo.rect.x += iframeRect.x; + elementInfo.rect.y += iframeRect.y; + elementInfo.rect.top += iframeRect.y; + elementInfo.rect.left += iframeRect.x; + + setHoveredElement(elementInfo); + } else if (event.data.type === 'INSPECTOR_CLICK') { + const elementInfo = event.data.elementInfo; + + // Adjust coordinates relative to iframe position + const iframeRect = iframe.getBoundingClientRect(); + elementInfo.rect.x += iframeRect.x; + elementInfo.rect.y += iframeRect.y; + elementInfo.rect.top += iframeRect.y; + elementInfo.rect.left += iframeRect.x; + + onElementSelect(elementInfo); + } else if (event.data.type === 'INSPECTOR_LEAVE') { + setHoveredElement(null); + } + }; + + window.addEventListener('message', handleMessage); + + // Send activation message to iframe + const sendActivationMessage = () => { + if (iframe.contentWindow) { + iframe.contentWindow.postMessage( + { + type: 'INSPECTOR_ACTIVATE', + active: isActive, + }, + '*', + ); + } + }; + + // Try to send activation message immediately and on load + sendActivationMessage(); + iframe.addEventListener('load', sendActivationMessage); + + return () => { + window.removeEventListener('message', handleMessage); + iframe.removeEventListener('load', sendActivationMessage); + + // Deactivate inspector in iframe + if (iframe.contentWindow) { + iframe.contentWindow.postMessage( + { + type: 'INSPECTOR_ACTIVATE', + active: false, + }, + '*', + ); + } + }; + }, [isActive, iframeRef, onElementSelect]); + + // Render overlay for hovered element + return ( + <> + {isActive && hoveredElement && ( +
+ {/* Element info tooltip */} +
+ {hoveredElement.tagName.toLowerCase()} + {hoveredElement.id && `#${hoveredElement.id}`} + {hoveredElement.className && `.${hoveredElement.className.split(' ')[0]}`} +
+
+ )} + + ); +}; diff --git a/app/components/workbench/InspectorPanel.tsx b/app/components/workbench/InspectorPanel.tsx new file mode 100644 index 0000000..e2de986 --- /dev/null +++ b/app/components/workbench/InspectorPanel.tsx @@ -0,0 +1,146 @@ +import { useState } from 'react'; + +interface ElementInfo { + tagName: string; + className: string; + id: string; + textContent: string; + styles: Record; // Changed from CSSStyleDeclaration + rect: { + x: number; + y: number; + width: number; + height: number; + top: number; + left: number; + }; +} + +interface InspectorPanelProps { + selectedElement: ElementInfo | null; + isVisible: boolean; + onClose: () => void; +} + +export const InspectorPanel = ({ selectedElement, isVisible, onClose }: InspectorPanelProps) => { + const [activeTab, setActiveTab] = useState<'styles' | 'computed' | 'box'>('styles'); + + if (!isVisible || !selectedElement) { + return null; + } + + const getRelevantStyles = (styles: Record) => { + const relevantProps = [ + 'display', + 'position', + 'width', + 'height', + 'margin', + 'padding', + 'border', + 'background', + 'color', + 'font-size', + 'font-family', + 'text-align', + 'flex-direction', + 'justify-content', + 'align-items', + ]; + + return relevantProps.reduce( + (acc, prop) => { + const value = styles[prop]; + + if (value) { + acc[prop] = value; + } + + return acc; + }, + {} as Record, + ); + }; + + return ( +
+ {/* Header */} +
+

Element Inspector

+ +
+ + {/* Element Info */} +
+
+
+ {selectedElement.tagName.toLowerCase()} + {selectedElement.id && #{selectedElement.id}} + {selectedElement.className && ( + .{selectedElement.className.split(' ')[0]} + )} +
+ {selectedElement.textContent && ( +
+ "{selectedElement.textContent}" +
+ )} +
+
+ + {/* Tabs */} +
+ {(['styles', 'computed', 'box'] as const).map((tab) => ( + + ))} +
+ + {/* Content */} +
+ {activeTab === 'styles' && ( +
+ {Object.entries(getRelevantStyles(selectedElement.styles)).map(([prop, value]) => ( +
+ {prop}: + {value} +
+ ))} +
+ )} + + {activeTab === 'box' && ( +
+
+ Width: + {Math.round(selectedElement.rect.width)}px +
+
+ Height: + {Math.round(selectedElement.rect.height)}px +
+
+ Top: + {Math.round(selectedElement.rect.top)}px +
+
+ Left: + {Math.round(selectedElement.rect.left)}px +
+
+ )} +
+
+ ); +}; diff --git a/app/components/workbench/Preview.tsx b/app/components/workbench/Preview.tsx index 0d41571..a9917de 100644 --- a/app/components/workbench/Preview.tsx +++ b/app/components/workbench/Preview.tsx @@ -6,9 +6,14 @@ import { PortDropdown } from './PortDropdown'; import { ScreenshotSelector } from './ScreenshotSelector'; import { expoUrlAtom } from '~/lib/stores/qrCodeStore'; import { ExpoQrModal } from '~/components/workbench/ExpoQrModal'; +import type { ElementInfo } from './Inspector'; type ResizeSide = 'left' | 'right' | null; +interface PreviewProps { + setSelectedElement?: (element: ElementInfo | null) => void; +} + interface WindowSize { name: string; width: number; @@ -47,11 +52,10 @@ const WINDOW_SIZES: WindowSize[] = [ { name: '4K Display', width: 3840, height: 2160, icon: 'i-ph:monitor', hasFrame: true, frameType: 'desktop' }, ]; -export const Preview = memo(() => { +export const Preview = memo(({ setSelectedElement }: PreviewProps) => { const iframeRef = useRef(null); const containerRef = useRef(null); const inputRef = useRef(null); - const [activePreviewIndex, setActivePreviewIndex] = useState(0); const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); @@ -61,11 +65,8 @@ export const Preview = memo(() => { const [displayPath, setDisplayPath] = useState('/'); const [iframeUrl, setIframeUrl] = useState(); const [isSelectionMode, setIsSelectionMode] = useState(false); - - // Toggle between responsive mode and device mode + const [isInspectorMode, setIsInspectorMode] = useState(false); const [isDeviceModeOn, setIsDeviceModeOn] = useState(false); - - // Use percentage for width const [widthPercent, setWidthPercent] = useState(37.5); const [currentWidth, setCurrentWidth] = useState(0); @@ -618,6 +619,47 @@ export const Preview = memo(() => { }; }, [showDeviceFrameInPreview]); + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.data.type === 'INSPECTOR_READY') { + if (iframeRef.current?.contentWindow) { + iframeRef.current.contentWindow.postMessage( + { + type: 'INSPECTOR_ACTIVATE', + active: isInspectorMode, + }, + '*', + ); + } + } else if (event.data.type === 'INSPECTOR_CLICK') { + const element = event.data.elementInfo; + + navigator.clipboard.writeText(element.displayText).then(() => { + setSelectedElement?.(element); + }); + } + }; + + window.addEventListener('message', handleMessage); + + return () => window.removeEventListener('message', handleMessage); + }, [isInspectorMode]); + + const toggleInspectorMode = () => { + const newInspectorMode = !isInspectorMode; + setIsInspectorMode(newInspectorMode); + + if (iframeRef.current?.contentWindow) { + iframeRef.current.contentWindow.postMessage( + { + type: 'INSPECTOR_ACTIVATE', + active: newInspectorMode, + }, + '*', + ); + } + }; + return (
{isPortDropdownOpen && ( @@ -697,7 +739,14 @@ export const Preview = memo(() => { /> )} - + void; + setSelectedElement?: (element: ElementInfo | null) => void; } const viewTransition = { ease: cubicEasingFn }; @@ -279,7 +281,7 @@ const FileModifiedDropdown = memo( ); export const Workbench = memo( - ({ chatStarted, isStreaming, actionRunner, metadata, updateChatMestaData }: WorkspaceProps) => { + ({ chatStarted, isStreaming, actionRunner, metadata, updateChatMestaData, setSelectedElement }: WorkspaceProps) => { renderLogger.trace('Workbench'); const [isSyncing, setIsSyncing] = useState(false); @@ -487,7 +489,7 @@ export const Workbench = memo( - +
diff --git a/app/lib/common/prompts/new-prompt.ts b/app/lib/common/prompts/new-prompt.ts index ee1bd24..f86c1ea 100644 --- a/app/lib/common/prompts/new-prompt.ts +++ b/app/lib/common/prompts/new-prompt.ts @@ -326,14 +326,6 @@ The year is 2025. When creating designs or UIs for applications, follow these guidelines indefinitely this is non-negotiable: - - 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)} - - CRITICAL: - Always strive for professional, beautiful, and unique designs - All designs should be fully featured and worthy of production use @@ -438,6 +430,14 @@ The year is 2025. - Use CSS Grid and Flexbox for layouts - Implement appropriate container queries when needed - Structure mobile-first designs that progressively enhance for larger screens + + + USER PROVIDED DESIGN SCHEME: + - ALWAYS use the user provided design scheme when creating designs ensuring it complies with the professionalism of design instructions below, unless the user specifically requests otherwise. + FONT: ${JSON.stringify(designScheme?.font)} + COLOR PALETTE: ${JSON.stringify(designScheme?.palette)} + FEATURES: ${JSON.stringify(designScheme?.features)} + diff --git a/app/lib/hooks/StickToBottom.tsx b/app/lib/hooks/StickToBottom.tsx index c07b739..69a0b27 100644 --- a/app/lib/hooks/StickToBottom.tsx +++ b/app/lib/hooks/StickToBottom.tsx @@ -133,6 +133,7 @@ function Content({ children, ...props }: StickToBottomContentProps) {
{typeof children === 'function' ? children(context) : children}
+ {/* Blur effect overlay */}
); } diff --git a/app/lib/webcontainer/index.ts b/app/lib/webcontainer/index.ts index f64b59f..0de9401 100644 --- a/app/lib/webcontainer/index.ts +++ b/app/lib/webcontainer/index.ts @@ -34,6 +34,10 @@ if (!import.meta.env.SSR) { const { workbenchStore } = await import('~/lib/stores/workbench'); + const response = await fetch('/inspector-script.js'); + const inspectorScript = await response.text(); + await webcontainer.setPreviewScript(inspectorScript); + // Listen for preview errors webcontainer.on('preview-message', (message) => { console.log('WebContainer preview message:', message); diff --git a/app/styles/index.scss b/app/styles/index.scss index 85f168f..60bff4f 100644 --- a/app/styles/index.scss +++ b/app/styles/index.scss @@ -11,6 +11,7 @@ html, body { height: 100%; width: 100%; + background-color: var(--bolt-elements-bg-depth-1); } :root { diff --git a/app/types/design-scheme.ts b/app/types/design-scheme.ts index 0ef1ff6..a37ba97 100644 --- a/app/types/design-scheme.ts +++ b/app/types/design-scheme.ts @@ -22,7 +22,6 @@ export const defaultDesignScheme: DesignScheme = { font: ['sans-serif'], }; -// Define the semantic color roles for the UI export const paletteRoles = [ { key: 'primary', @@ -84,17 +83,10 @@ export const designFeatures = [ { 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' }, - */ ]; diff --git a/app/utils/markdown.ts b/app/utils/markdown.ts index accc755..5daf701 100644 --- a/app/utils/markdown.ts +++ b/app/utils/markdown.ts @@ -56,6 +56,7 @@ export const allowedHTMLElements = [ 'ul', 'var', 'think', + 'header', ]; // Add custom rehype plugin @@ -85,7 +86,7 @@ const rehypeSanitizeOptions: RehypeSanitizeOptions = { div: [ ...(defaultSchema.attributes?.div ?? []), 'data*', - ['className', '__boltArtifact__', '__boltThought__', '__boltQuickAction'], + ['className', '__boltArtifact__', '__boltThought__', '__boltQuickAction', '__boltSelectedElement__'], // ['className', '__boltThought__'] ], diff --git a/public/inspector-script.js b/public/inspector-script.js new file mode 100644 index 0000000..4b5a8c2 --- /dev/null +++ b/public/inspector-script.js @@ -0,0 +1,292 @@ +(function() { + let isInspectorActive = false; + let inspectorStyle = null; + let currentHighlight = null; + + // Function to get relevant styles + function getRelevantStyles(element) { + const computedStyles = window.getComputedStyle(element); + const relevantProps = [ + 'display', 'position', 'width', 'height', 'margin', 'padding', + 'border', 'background', 'color', 'font-size', 'font-family', + 'text-align', 'flex-direction', 'justify-content', 'align-items' + ]; + + const styles = {}; + relevantProps.forEach(prop => { + const value = computedStyles.getPropertyValue(prop); + if (value) styles[prop] = value; + }); + + return styles; + } + + // Function to create a readable element selector + function createReadableSelector(element) { + let selector = element.tagName.toLowerCase(); + + // Add ID if present + if (element.id) { + selector += `#${element.id}`; + } + + // Add classes if present + let className = ''; + if (element.className) { + if (typeof element.className === 'string') { + className = element.className; + } else if (element.className.baseVal !== undefined) { + className = element.className.baseVal; + } else { + className = element.className.toString(); + } + + if (className.trim()) { + const classes = className.trim().split(/\s+/).slice(0, 3); // Limit to first 3 classes + selector += `.${classes.join('.')}`; + } + } + + return selector; + } + + // Function to create element display text + function createElementDisplayText(element) { + const tagName = element.tagName.toLowerCase(); + let displayText = `<${tagName}`; + + // Add ID attribute + if (element.id) { + displayText += ` id="${element.id}"`; + } + + // Add class attribute (limit to first 3 classes for readability) + let className = ''; + if (element.className) { + if (typeof element.className === 'string') { + className = element.className; + } else if (element.className.baseVal !== undefined) { + className = element.className.baseVal; + } else { + className = element.className.toString(); + } + + if (className.trim()) { + const classes = className.trim().split(/\s+/); + const displayClasses = classes.length > 3 ? + classes.slice(0, 3).join(' ') + '...' : + classes.join(' '); + displayText += ` class="${displayClasses}"`; + } + } + + // Add other important attributes + const importantAttrs = ['type', 'name', 'href', 'src', 'alt', 'title']; + importantAttrs.forEach(attr => { + const value = element.getAttribute(attr); + if (value) { + const truncatedValue = value.length > 30 ? value.substring(0, 30) + '...' : value; + displayText += ` ${attr}="${truncatedValue}"`; + } + }); + + displayText += '>'; + + // Add text content preview for certain elements + const textElements = ['span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'button', 'a', 'label']; + if (textElements.includes(tagName) && element.textContent) { + const textPreview = element.textContent.trim().substring(0, 50); + if (textPreview) { + displayText += textPreview.length < element.textContent.trim().length ? + textPreview + '...' : textPreview; + } + } + + displayText += ``; + + return displayText; + } + + // Function to create element info + function createElementInfo(element) { + const rect = element.getBoundingClientRect(); + + return { + tagName: element.tagName, + className: getElementClassName(element), + id: element.id || '', + textContent: element.textContent?.slice(0, 100) || '', + styles: getRelevantStyles(element), + rect: { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + top: rect.top, + left: rect.left + }, + // Add new readable formats + selector: createReadableSelector(element), + displayText: createElementDisplayText(element), + elementPath: getElementPath(element) + }; + } + + // Helper function to get element class name consistently + function getElementClassName(element) { + if (!element.className) return ''; + + if (typeof element.className === 'string') { + return element.className; + } else if (element.className.baseVal !== undefined) { + return element.className.baseVal; + } else { + return element.className.toString(); + } + } + + // Function to get element path (breadcrumb) + function getElementPath(element) { + const path = []; + let current = element; + + while (current && current !== document.body && current !== document.documentElement) { + let pathSegment = current.tagName.toLowerCase(); + + if (current.id) { + pathSegment += `#${current.id}`; + } else if (current.className) { + const className = getElementClassName(current); + if (className.trim()) { + const firstClass = className.trim().split(/\s+/)[0]; + pathSegment += `.${firstClass}`; + } + } + + path.unshift(pathSegment); + current = current.parentElement; + + // Limit path length + if (path.length >= 5) break; + } + + return path.join(' > '); + } + + // Event handlers + function handleMouseMove(e) { + if (!isInspectorActive) return; + + const target = e.target; + if (!target || target === document.body || target === document.documentElement) return; + + // Remove previous highlight + if (currentHighlight) { + currentHighlight.classList.remove('inspector-highlight'); + } + + // Add highlight to current element + target.classList.add('inspector-highlight'); + currentHighlight = target; + + const elementInfo = createElementInfo(target); + + // Send message to parent + window.parent.postMessage({ + type: 'INSPECTOR_HOVER', + elementInfo: elementInfo + }, '*'); + } + + function handleClick(e) { + if (!isInspectorActive) return; + + e.preventDefault(); + e.stopPropagation(); + + const target = e.target; + if (!target || target === document.body || target === document.documentElement) return; + + const elementInfo = createElementInfo(target); + + // Send message to parent + window.parent.postMessage({ + type: 'INSPECTOR_CLICK', + elementInfo: elementInfo + }, '*'); + } + + function handleMouseLeave() { + if (!isInspectorActive) return; + + // Remove highlight + if (currentHighlight) { + currentHighlight.classList.remove('inspector-highlight'); + currentHighlight = null; + } + + // Send message to parent + window.parent.postMessage({ + type: 'INSPECTOR_LEAVE' + }, '*'); + } + + // Function to activate/deactivate inspector + function setInspectorActive(active) { + isInspectorActive = active; + + if (active) { + // Add inspector styles + if (!inspectorStyle) { + inspectorStyle = document.createElement('style'); + inspectorStyle.textContent = ` + .inspector-active * { + cursor: crosshair !important; + } + .inspector-highlight { + outline: 2px solid #3b82f6 !important; + outline-offset: -2px !important; + background-color: rgba(59, 130, 246, 0.1) !important; + } + `; + document.head.appendChild(inspectorStyle); + } + + document.body.classList.add('inspector-active'); + + // Add event listeners + document.addEventListener('mousemove', handleMouseMove, true); + document.addEventListener('click', handleClick, true); + document.addEventListener('mouseleave', handleMouseLeave, true); + } else { + document.body.classList.remove('inspector-active'); + + // Remove highlight + if (currentHighlight) { + currentHighlight.classList.remove('inspector-highlight'); + currentHighlight = null; + } + + // Remove event listeners + document.removeEventListener('mousemove', handleMouseMove, true); + document.removeEventListener('click', handleClick, true); + document.removeEventListener('mouseleave', handleMouseLeave, true); + + // Remove styles + if (inspectorStyle) { + inspectorStyle.remove(); + inspectorStyle = null; + } + } + } + + // Listen for messages from parent + window.addEventListener('message', function(event) { + if (event.data.type === 'INSPECTOR_ACTIVATE') { + setInspectorActive(event.data.active); + } + }); + + // Auto-inject if inspector is already active + window.parent.postMessage({ type: 'INSPECTOR_READY' }, '*'); +})(); \ No newline at end of file