From 6f3c2949bb8eae5dcf9c1a6e5e9648a38b54d6a7 Mon Sep 17 00:00:00 2001 From: Gemini AI Date: Mon, 29 Dec 2025 02:49:13 +0400 Subject: [PATCH] feat: multi-tab AI Assist sessions + canvas UX enhancements - Implemented multi-tab support for AI Assist (like Gemini/ChatGPT) - Added Tab Bar UI with New Chat (+) and Close Tab (x) buttons - Synced chat history, agent, and preview data per tab - Added BuildingArtifact loading animation during code generation - Prevented broken code display during streaming with high-fidelity checklist - Updated isPreviewRenderable to detect and skip backend code (Node.js/Express) - Enhanced all AI provider prompts for React support and backend simulation - Updated SEO Agent to generate ADHD-friendly, modern HTML5 dashboard reports - Fixed LiveCanvas try/catch error handling for graceful error displays --- components/AIAssist.tsx | 468 ++++++++++++++++++++++------------- lib/services/ollama-cloud.ts | 7 +- lib/services/qwen-oauth.ts | 7 +- lib/services/zai-plan.ts | 7 +- lib/store.ts | 75 +++++- 5 files changed, 388 insertions(+), 176 deletions(-) diff --git a/components/AIAssist.tsx b/components/AIAssist.tsx index 402cb21..c1137aa 100644 --- a/components/AIAssist.tsx +++ b/components/AIAssist.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect, useRef, memo } from "react"; import { MessageSquare, Send, Code2, Palette, Search, Trash2, Copy, Monitor, StopCircle, X, Zap, Ghost, - Wand2, LayoutPanelLeft, Play, Orbit + Wand2, LayoutPanelLeft, Play, Orbit, Plus } from "lucide-react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; @@ -34,11 +34,62 @@ interface PreviewData { * A ultra-stable iframe wrapper that avoids hydration issues * and provides a WOW visual experience. */ -const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: string, isStreaming: boolean }) => { - const iframeRef = useRef(null); +const BuildingArtifact = ({ type }: { type: string }) => { + const [progress, setProgress] = useState(0); + const steps = [ + "Initializing neural links...", + "Scaffolding architecture...", + "Writing logic blocks...", + "Injecting dynamic modules...", + "Finalizing interactive layers..." + ]; + const [currentStep, setCurrentStep] = useState(0); useEffect(() => { - if (!iframeRef.current || !data) return; + const interval = setInterval(() => { + setProgress(p => (p < 95 ? p + (100 - p) * 0.1 : p)); + setCurrentStep(s => (s < steps.length - 1 ? s + 1 : s)); + }, 2000); + return () => clearInterval(interval); + }, []); + + return ( +
+
+
+
+ +
+ +

+ Building {type} +

+ +
+
+
+ +
+ {steps.map((step, idx) => ( +
+
+ + {step} + + {idx < currentStep && } +
+ ))} +
+
+ ); +}; +const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: string, isStreaming: boolean }) => { + const iframeRef = useRef(null); + const [renderError, setRenderError] = useState(null); + + useEffect(() => { + if (!iframeRef.current || !data || isStreaming) return; + setRenderError(null); // Decode HTML entities if present const isEncodedHtml = data.includes("<") && data.includes(">"); @@ -58,138 +109,160 @@ const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: stri const isReactLike = normalized.includes("import React") || normalized.includes("useState") || normalized.includes("useEffect") || /<[A-Z][\s\S]*>/.test(normalized); let doc: string; - if (isFullDocument) { - // ... same as before but add React support if needed ... - const reactScripts = isReactLike ? ` - - - - ` : ""; - - if (hasHeadTag) { - doc = normalized.replace(//i, ` - ${reactScripts} - - - `); - } else { - doc = normalized.replace(/]*>/i, (match) => `${match} - - ${reactScripts} - - - - `); - } - } else if (isReactLike) { - // Specialized React Runner for fragments/components - const cleanedCode = normalized - .replace(/import\s+(?:React\s*,\s*)?{?([\s\S]*?)}?\s+from\s+['"]react['"];?/g, "const { $1 } = React;") - .replace(/import\s+React\s+from\s+['"]react['"];?/g, "/* React already global */") - .replace(/import\s+[\s\S]*?from\s+['"]lucide-react['"];?/g, "const { ...lucide } = window.lucide || {};") - .replace(/export\s+default\s+/g, "const MainComponent = "); - - // Try to find the component name to render - const componentMatch = cleanedCode.match(/const\s+([A-Z]\w+)\s*=\s*\(\)\s*=>/); - const mainComponent = componentMatch ? componentMatch[1] : (cleanedCode.includes("MainComponent") ? "MainComponent" : null); - - doc = ` - - - - - - + try { + if (isFullDocument) { + // If it's a full document, inject Tailwind CSS but keep the structure + const reactScripts = isReactLike ? ` - - - - -
- - - - `; - } else { - // Wrap fragments in a styled container - doc = ` - - - - - - - - + + `); + } else { + doc = normalized.replace(/]*>/i, (match) => `${match} + + ${reactScripts} + + + + `); + } + } else if (isReactLike) { + // Specialized React Runner for fragments/components + const cleanedCode = normalized + .replace(/import\s+(?:React\s*,\s*)?{?([\s\S]*?)}?\s+from\s+['"]react['"];?/g, "const { $1 } = React;") + .replace(/import\s+React\s+from\s+['"]react['"];?/g, "/* React already global */") + .replace(/import\s+[\s\S]*?from\s+['"]lucide-react['"];?/g, "const { ...lucide } = window.lucide || {};") + .replace(/export\s+default\s+/g, "const MainComponent = "); + + // Try to find the component name to render + const componentMatch = cleanedCode.match(/const\s+([A-Z]\w+)\s*=\s*\(\)\s*=>/); + const mainComponent = componentMatch ? componentMatch[1] : (cleanedCode.includes("MainComponent") ? "MainComponent" : null); + + doc = ` + + + + + + + + + + + + + +
+ + + + `; + } else { + // Wrap fragments in a styled container + doc = ` + + + + + + + + - - - - ${normalized} - - - `; - } + + + + + ${normalized} + + + `; + } - iframeRef.current.srcdoc = doc; - }, [data, type]); + if (iframeRef.current) { + iframeRef.current.srcdoc = doc; + } + } catch (e) { + console.error("Canvas Render Error:", e); + setRenderError(e instanceof Error ? e.message : "Internal rendering failure"); + } + }, [data, type, isStreaming]); return ( -
-