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
This commit is contained in:
@@ -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<HTMLIFrameElement>(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 (
|
||||
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-[#0b1414] text-white p-8 animate-in fade-in duration-500 rounded-b-2xl">
|
||||
<div className="relative w-24 h-24 mb-10">
|
||||
<div className="absolute inset-0 border-4 border-blue-500/10 rounded-full" />
|
||||
<div className="absolute inset-0 border-4 border-blue-500 rounded-full animate-spin border-t-transparent" />
|
||||
<Orbit className="absolute inset-0 m-auto h-12 w-12 text-blue-400 animate-pulse" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-black uppercase tracking-[0.3em] mb-4 text-white drop-shadow-lg">
|
||||
Building <span className="text-blue-500">{type}</span>
|
||||
</h3>
|
||||
|
||||
<div className="w-full max-w-sm h-1.5 bg-slate-800/50 rounded-full overflow-hidden mb-10 backdrop-blur-sm border border-white/5">
|
||||
<div className="h-full bg-blue-500 shadow-[0_0_15px_rgba(59,130,246,0.5)] transition-all duration-700 ease-out" style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 w-full max-w-xs">
|
||||
{steps.map((step, idx) => (
|
||||
<div key={idx} className={`flex items-center gap-4 transition-all duration-500 ${idx <= currentStep ? 'opacity-100' : 'opacity-20'}`}>
|
||||
<div className={`h-2 w-2 rounded-full ${idx <= currentStep ? 'bg-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.5)]' : 'bg-slate-700'}`} />
|
||||
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-300">
|
||||
{step}
|
||||
</span>
|
||||
{idx < currentStep && <Zap className="h-3.5 w-3.5 text-blue-400 animate-pulse ml-auto" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: string, isStreaming: boolean }) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const [renderError, setRenderError] = useState<string | null>(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 ? `
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
` : "";
|
||||
|
||||
if (hasHeadTag) {
|
||||
doc = normalized.replace(/<head>/i, `<head>
|
||||
${reactScripts}
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap">
|
||||
`);
|
||||
} else {
|
||||
doc = normalized.replace(/<html[^>]*>/i, (match) => `${match}
|
||||
<head>
|
||||
${reactScripts}
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap">
|
||||
</head>
|
||||
`);
|
||||
}
|
||||
} 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 = `
|
||||
<!DOCTYPE html>
|
||||
<html class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
try {
|
||||
if (isFullDocument) {
|
||||
// If it's a full document, inject Tailwind CSS but keep the structure
|
||||
const reactScripts = isReactLike ? `
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<style>
|
||||
body { margin: 0; padding: 20px; font-family: sans-serif; background: #0b1414; color: white; }
|
||||
#root { min-height: 100vh; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
${cleanedCode}
|
||||
|
||||
${mainComponent ? `
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(React.createElement(${mainComponent}));
|
||||
` : `
|
||||
// No clear component found to mount, executing raw code
|
||||
`}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
} else {
|
||||
// Wrap fragments in a styled container
|
||||
doc = `
|
||||
<!DOCTYPE html>
|
||||
<html class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: { 50: '#ecfdf3', 100: '#d1fae5', 200: '#a7f3d0', 300: '#6ee7b7', 400: '#34d399', 500: '#10b981', 600: '#059669', 700: '#047857', 800: '#065f46', 900: '#064e3b', 950: '#022c22' }
|
||||
` : "";
|
||||
|
||||
if (hasHeadTag) {
|
||||
doc = normalized.replace(/<head>/i, `<head>
|
||||
${reactScripts}
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap">
|
||||
`);
|
||||
} else {
|
||||
doc = normalized.replace(/<html[^>]*>/i, (match) => `${match}
|
||||
<head>
|
||||
${reactScripts}
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap">
|
||||
</head>
|
||||
`);
|
||||
}
|
||||
} 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 = `
|
||||
<!DOCTYPE html>
|
||||
<html class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<style>
|
||||
body { margin: 0; padding: 20px; font-family: sans-serif; background: #0b1414; color: white; }
|
||||
#root { min-height: 100vh; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
${cleanedCode}
|
||||
|
||||
${mainComponent ? `
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(React.createElement(${mainComponent}));
|
||||
` : `
|
||||
// No clear component found to mount, executing raw code
|
||||
`}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
} else {
|
||||
// Wrap fragments in a styled container
|
||||
doc = `
|
||||
<!DOCTYPE html>
|
||||
<html class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: { 50: '#ecfdf3', 100: '#d1fae5', 200: '#a7f3d0', 300: '#6ee7b7', 400: '#34d399', 500: '#10b981', 600: '#059669', 700: '#047857', 800: '#065f46', 900: '#064e3b', 950: '#022c22' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
::-webkit-scrollbar { width: 8px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #115e59; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #0f766e; }
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
font-family: "Space Grotesk", "IBM Plex Sans", system-ui, sans-serif;
|
||||
background: #f8fafc;
|
||||
color: #1e293b;
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${normalized}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
::-webkit-scrollbar { width: 8px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #115e59; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #0f766e; }
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
font-family: "Space Grotesk", "IBM Plex Sans", system-ui, sans-serif;
|
||||
background: #f8fafc;
|
||||
color: #1e293b;
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${normalized}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="w-full h-full relative group">
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="Canvas Preview"
|
||||
className="w-full h-full border-none rounded-b-2xl bg-[#0b1414] shadow-inner"
|
||||
sandbox="allow-scripts"
|
||||
/>
|
||||
<div className="w-full h-full relative group bg-[#0b1414] overflow-hidden rounded-b-2xl">
|
||||
{isStreaming && <BuildingArtifact type={type} />}
|
||||
|
||||
{renderError ? (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center p-12 text-center animate-in zoom-in-95 duration-300">
|
||||
<StopCircle className="h-10 w-10 text-red-500/40 mb-5" />
|
||||
<h4 className="text-xs font-black uppercase tracking-[0.2em] text-red-400 mb-3">Runtime Execution Error</h4>
|
||||
<p className="text-[9px] font-mono text-slate-500 max-w-sm border border-red-500/10 bg-red-500/5 p-4 rounded-xl leading-relaxed">
|
||||
{renderError}
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="mt-6 text-[9px] font-black uppercase tracking-widest text-slate-400 hover:text-white"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
Try Refreshing Page
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="Canvas Preview"
|
||||
className={`w-full h-full border-none bg-[#0b1414] shadow-inner transition-all duration-1000 ${isStreaming ? 'opacity-0 scale-95 blur-sm' : 'opacity-100 scale-100 blur-0'}`}
|
||||
sandbox="allow-scripts"
|
||||
/>
|
||||
)}
|
||||
|
||||
{isStreaming && (
|
||||
<div className="absolute inset-x-0 bottom-0 h-1 bg-blue-500/20 overflow-hidden">
|
||||
<div className="h-full bg-blue-500 animate-[loading_1.5s_infinite]" />
|
||||
</div>
|
||||
)}
|
||||
<style jsx>{`
|
||||
@keyframes loading {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(200%); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -308,18 +381,33 @@ function parseStreamingContent(text: string, currentAgent: string) {
|
||||
export default function AIAssist() {
|
||||
const {
|
||||
language,
|
||||
aiAssistHistory,
|
||||
setAIAssistHistory,
|
||||
aiAssistTabs,
|
||||
activeTabId,
|
||||
setActiveTabId,
|
||||
addAIAssistTab,
|
||||
removeAIAssistTab,
|
||||
updateActiveTab,
|
||||
selectedProvider,
|
||||
selectedModels,
|
||||
setSelectedModel
|
||||
} = useStore();
|
||||
const t = translations[language].aiAssist;
|
||||
|
||||
const activeTab = aiAssistTabs.find(t => t.id === activeTabId) || aiAssistTabs[0];
|
||||
const aiAssistHistory = activeTab?.history || [];
|
||||
|
||||
const [input, setInput] = useState("");
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [currentAgent, setCurrentAgent] = useState("general");
|
||||
const [previewData, setPreviewData] = useState<PreviewData | null>(null);
|
||||
const [currentAgent, setCurrentAgent] = useState(activeTab?.currentAgent || "general");
|
||||
const [previewData, setPreviewData] = useState<PreviewData | null>(activeTab?.previewData || null);
|
||||
|
||||
// Sync local state when tab changes
|
||||
useEffect(() => {
|
||||
if (activeTab) {
|
||||
setCurrentAgent(activeTab.currentAgent);
|
||||
setPreviewData(activeTab.previewData);
|
||||
}
|
||||
}, [activeTabId]);
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
const [showCanvas, setShowCanvas] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<"preview" | "code">("preview");
|
||||
@@ -334,10 +422,22 @@ export default function AIAssist() {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const isPreviewRenderable = (preview?: PreviewData | null) => {
|
||||
if (!preview) return false;
|
||||
return ["web", "app", "design", "html", "ui"].includes(preview.type)
|
||||
|| preview.language === "html"
|
||||
|| preview.data.includes("<")
|
||||
|| (preview.data.includes("<") && preview.data.includes(">"));
|
||||
|
||||
// Detect and block backend code from rendering in iframe
|
||||
const lowerData = preview.data.toLowerCase();
|
||||
const isBackend =
|
||||
lowerData.includes("require('express')") ||
|
||||
lowerData.includes("import express") ||
|
||||
lowerData.includes("app.listen(") ||
|
||||
lowerData.includes("mongoose.connect") ||
|
||||
lowerData.includes("process.env.") ||
|
||||
/module\.exports/i.test(preview.data);
|
||||
|
||||
// Client-side detection
|
||||
const isUI = ["web", "app", "design", "html", "ui"].includes(preview.type);
|
||||
const hasTags = /<[a-z][\s\S]*>/i.test(preview.data);
|
||||
|
||||
return (isUI || hasTags || preview.language === "html") && !isBackend;
|
||||
};
|
||||
const canRenderPreview = isPreviewRenderable(previewData);
|
||||
|
||||
@@ -389,7 +489,7 @@ export default function AIAssist() {
|
||||
timestamp: new Date(),
|
||||
};
|
||||
const newHistory = [...aiAssistHistory, userMsg];
|
||||
setAIAssistHistory(newHistory);
|
||||
updateActiveTab({ history: newHistory });
|
||||
setInput("");
|
||||
}
|
||||
|
||||
@@ -402,13 +502,16 @@ export default function AIAssist() {
|
||||
agent: currentAgent,
|
||||
timestamp: new Date()
|
||||
};
|
||||
setAIAssistHistory(prev => [...prev, assistantMsg]);
|
||||
|
||||
// Update history in active tab
|
||||
const updatedHistory = [...aiAssistHistory, assistantMsg];
|
||||
updateActiveTab({ history: updatedHistory });
|
||||
|
||||
try {
|
||||
let accumulated = "";
|
||||
let lastParsedPreview: PreviewData | null = null;
|
||||
|
||||
// Format history to remove internal tags and provide clear context for surgical edits
|
||||
// Format history for context
|
||||
const formattedHistory = aiAssistHistory.map(m => {
|
||||
if (m.role === "assistant") {
|
||||
const { chatDisplay, preview } = parseStreamingContent(m.content, m.agent || "general");
|
||||
@@ -431,37 +534,32 @@ export default function AIAssist() {
|
||||
|
||||
if (streamStatus) setStatus(streamStatus);
|
||||
|
||||
// If we're in planning mode and see JSON, try to parse the plan
|
||||
if (assistStep === "plan" || assistStep === "idle") {
|
||||
const jsonMatch = accumulated.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
if (parsed.summary && parsed.files) setAiPlan(parsed);
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
if (preview && JSON.stringify(preview) !== JSON.stringify(lastParsedPreview)) {
|
||||
setPreviewData(preview);
|
||||
lastParsedPreview = preview;
|
||||
setShowCanvas(true);
|
||||
if (isPreviewRenderable(preview)) setViewMode("preview");
|
||||
|
||||
// Save preview data to tab
|
||||
updateActiveTab({ previewData: preview });
|
||||
}
|
||||
|
||||
if (agent !== currentAgent) setCurrentAgent(agent);
|
||||
if (agent !== currentAgent) {
|
||||
setCurrentAgent(agent);
|
||||
updateActiveTab({ currentAgent: agent });
|
||||
}
|
||||
|
||||
setAIAssistHistory(prev => {
|
||||
const last = prev[prev.length - 1];
|
||||
if (last && last.role === "assistant") {
|
||||
return [...prev.slice(0, -1), {
|
||||
...last,
|
||||
content: accumulated, // Keep raw for AI context
|
||||
agent,
|
||||
preview: preview ? { type: preview.type, data: preview.data, language: preview.language } : undefined
|
||||
} as AIAssistMessage];
|
||||
}
|
||||
return prev;
|
||||
// Stream updates to current tab history
|
||||
const lastMsg = {
|
||||
role: "assistant" as const,
|
||||
content: accumulated,
|
||||
agent,
|
||||
preview: preview ? { type: preview.type, data: preview.data, language: preview.language } : undefined,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
updateActiveTab({
|
||||
history: [...updatedHistory.slice(0, -1), lastMsg]
|
||||
});
|
||||
},
|
||||
signal: controller.signal
|
||||
@@ -480,14 +578,9 @@ export default function AIAssist() {
|
||||
|
||||
} catch (error) {
|
||||
console.error("Assist error:", error);
|
||||
setAIAssistHistory(prev => {
|
||||
const last = prev[prev.length - 1];
|
||||
const message = error instanceof Error ? error.message : "AI Assist failed";
|
||||
if (last && last.role === "assistant") {
|
||||
return [...prev.slice(0, -1), { ...last, content: message }];
|
||||
}
|
||||
return [...prev, { role: "assistant", content: message, timestamp: new Date() }];
|
||||
});
|
||||
const message = error instanceof Error ? error.message : "AI Assist failed";
|
||||
const errorMsg: AIAssistMessage = { role: "assistant", content: message, timestamp: new Date() };
|
||||
updateActiveTab({ history: [...aiAssistHistory, errorMsg] });
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
setAbortController(null);
|
||||
@@ -509,7 +602,11 @@ export default function AIAssist() {
|
||||
};
|
||||
|
||||
const clearHistory = () => {
|
||||
setAIAssistHistory([]);
|
||||
updateActiveTab({
|
||||
history: [],
|
||||
previewData: null,
|
||||
currentAgent: "general"
|
||||
});
|
||||
setPreviewData(null);
|
||||
setShowCanvas(false);
|
||||
setAssistStep("idle");
|
||||
@@ -569,7 +666,44 @@ export default function AIAssist() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
{/* Tab Bar */}
|
||||
<div className="flex items-center gap-1 px-4 py-2 bg-slate-800/20 border-b border-white/5 backdrop-blur-md overflow-x-auto no-scrollbar">
|
||||
{aiAssistTabs.map(tab => (
|
||||
<div
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTabId(tab.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-1.5 rounded-xl cursor-pointer transition-all duration-300 whitespace-nowrap group",
|
||||
activeTabId === tab.id
|
||||
? "bg-blue-500/20 text-blue-400 border border-blue-500/30"
|
||||
: "hover:bg-white/5 text-slate-500 opacity-70 hover:opacity-100"
|
||||
)}
|
||||
>
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
<span className="text-[10px] font-black uppercase tracking-wider">{tab.title}</span>
|
||||
{aiAssistTabs.length > 1 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeAIAssistTab(tab.id);
|
||||
}}
|
||||
className="ml-1 opacity-0 group-hover:opacity-100 hover:text-red-400 transition-opacity"
|
||||
>
|
||||
<X className="h-2.5 w-2.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={() => addAIAssistTab()}
|
||||
className="p-1.5 rounded-xl hover:bg-white/10 text-slate-400 hover:text-white transition-all ml-1"
|
||||
title="New Chat"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Agent Selector */}
|
||||
<div className="px-6 pt-6">
|
||||
<div className="flex flex-wrap gap-2 pb-4">
|
||||
{[
|
||||
|
||||
@@ -746,7 +746,7 @@ Perform a DEEP 360° competitive intelligence analysis and generate 5-7 strategi
|
||||
|
||||
AGENTS & CAPABILITIES:
|
||||
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
||||
- seo: SEO Specialist. Provide deep audits, keyword research, and strategy reports. Even if you cannot crawl a live site, provide an expert simulation/analysis.
|
||||
- seo: SEO Specialist. Provide deep audits, keyword research, and strategy reports. **ALL REPORTS MUST be generated as visually stunning, ADHD-friendly, modern, and intuitive HTML5 dashboards.** Use [PREVIEW:seo:html]. Reports should feature progress rings, clear hierarchy, and interactive insights.
|
||||
- smm: Social Media Manager. Create multi-platform content plans and calendars.
|
||||
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
||||
@@ -754,6 +754,11 @@ AGENTS & CAPABILITIES:
|
||||
- web: Frontend Developer. Build responsive sites using HTML/Tailwind or React. Use [PREVIEW:web:html] or [PREVIEW:web:javascript].
|
||||
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. React components are supported and rendered live. Use [PREVIEW:app:javascript].
|
||||
|
||||
BACKEND LOGIC & SIMULATION:
|
||||
- If a user asks for backend logic (Node.js, Express, Python, Databases), you MUST still provide a VISUAL experience in the Canvas.
|
||||
- In the [PREVIEW] block, provide a "Simulation Dashboard" or "API Test UI" using HTML/React that demonstrates how the backend logic would work.
|
||||
- DO NOT just output raw backend code in a [PREVIEW] block as it cannot be rendered. Put raw backend code in standard Markdown blocks AFTER the preview.
|
||||
|
||||
ITERATIVE MODIFICATIONS (CRITICAL):
|
||||
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
|
||||
- Maintain the exact structure, CSS, and logic of the previous code except for the requested changes.
|
||||
|
||||
@@ -1020,7 +1020,7 @@ Perform analysis based on provided instructions.`,
|
||||
|
||||
AGENTS & CAPABILITIES:
|
||||
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
||||
- seo: SEO Specialist. Provide deep audits, keyword research, and strategy reports. Even if you cannot crawl a live site, provide an expert simulation/analysis.
|
||||
- seo: SEO Specialist. Provide deep audits, keyword research, and strategy reports. **ALL REPORTS MUST be generated as visually stunning, ADHD-friendly, modern, and intuitive HTML5 dashboards.** Use [PREVIEW:seo:html]. Reports should feature progress rings, clear hierarchy, and interactive insights.
|
||||
- smm: Social Media Manager. Create multi-platform content plans and calendars.
|
||||
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
||||
@@ -1028,6 +1028,11 @@ AGENTS & CAPABILITIES:
|
||||
- web: Frontend Developer. Build responsive sites using HTML/Tailwind or React. Use [PREVIEW:web:html] or [PREVIEW:web:javascript].
|
||||
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. React components are supported and rendered live. Use [PREVIEW:app:javascript].
|
||||
|
||||
BACKEND LOGIC & SIMULATION:
|
||||
- If a user asks for backend logic (Node.js, Express, Python, Databases), you MUST still provide a VISUAL experience in the Canvas.
|
||||
- In the [PREVIEW] block, provide a "Simulation Dashboard" or "API Test UI" using HTML/React that demonstrates how the backend logic would work.
|
||||
- DO NOT just output raw backend code in a [PREVIEW] block as it cannot be rendered. Put raw backend code in standard Markdown blocks AFTER the preview.
|
||||
|
||||
ITERATIVE MODIFICATIONS (CRITICAL):
|
||||
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
|
||||
- Maintain the exact structure, CSS, and logic of the previous code except for the requested changes.
|
||||
|
||||
@@ -819,7 +819,7 @@ MISSION: Perform a DEEP 360° competitive intelligence analysis and generate 5-7
|
||||
|
||||
AGENTS & CAPABILITIES:
|
||||
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
||||
- seo: SEO Specialist. Provide deep audits, keyword research, and strategy reports. Even if you cannot crawl a live site, provide an expert simulation/analysis.
|
||||
- seo: SEO Specialist. Provide deep audits, keyword research, and strategy reports. **ALL REPORTS MUST be generated as visually stunning, ADHD-friendly, modern, and intuitive HTML5 dashboards.** Use [PREVIEW:seo:html]. Reports should feature progress rings, clear hierarchy, and interactive insights.
|
||||
- smm: Social Media Manager. Create multi-platform content plans and calendars.
|
||||
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
||||
@@ -827,6 +827,11 @@ AGENTS & CAPABILITIES:
|
||||
- web: Frontend Developer. Build responsive sites using HTML/Tailwind or React. Use [PREVIEW:web:html] or [PREVIEW:web:javascript].
|
||||
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. React components are supported and rendered live. Use [PREVIEW:app:javascript].
|
||||
|
||||
BACKEND LOGIC & SIMULATION:
|
||||
- If a user asks for backend logic (Node.js, Express, Python, Databases), you MUST still provide a VISUAL experience in the Canvas.
|
||||
- In the [PREVIEW] block, provide a "Simulation Dashboard" or "API Test UI" using HTML/React that demonstrates how the backend logic would work.
|
||||
- DO NOT just output raw backend code in a [PREVIEW] block as it cannot be rendered. Put raw backend code in standard Markdown blocks AFTER the preview.
|
||||
|
||||
ITERATIVE MODIFICATIONS (CRITICAL):
|
||||
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
|
||||
- Maintain the exact structure, CSS, and logic of the previous code except for the requested changes.
|
||||
|
||||
75
lib/store.ts
75
lib/store.ts
@@ -1,6 +1,14 @@
|
||||
import { create } from "zustand";
|
||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult, MagicWandResult, MarketResearchResult, AppView, AIAssistMessage } from "@/types";
|
||||
|
||||
interface AIAssistTab {
|
||||
id: string;
|
||||
title: string;
|
||||
history: AIAssistMessage[];
|
||||
currentAgent: string;
|
||||
previewData?: any | null; // PreviewData type from AIAssist
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
currentPrompt: string;
|
||||
enhancedPrompt: string | null;
|
||||
@@ -10,7 +18,11 @@ interface AppState {
|
||||
googleAdsResult: GoogleAdsResult | null;
|
||||
magicWandResult: MagicWandResult | null;
|
||||
marketResearchResult: MarketResearchResult | null;
|
||||
aiAssistHistory: AIAssistMessage[];
|
||||
|
||||
// AI Assist Tabs
|
||||
aiAssistTabs: AIAssistTab[];
|
||||
activeTabId: string | null;
|
||||
|
||||
language: "en" | "ru" | "he";
|
||||
selectedProvider: ModelProvider;
|
||||
selectedModels: Record<ModelProvider, string>;
|
||||
@@ -37,7 +49,14 @@ interface AppState {
|
||||
setGoogleAdsResult: (result: GoogleAdsResult | null) => void;
|
||||
setMagicWandResult: (result: MagicWandResult | null) => void;
|
||||
setMarketResearchResult: (result: MarketResearchResult | null) => void;
|
||||
setAIAssistHistory: (history: AIAssistMessage[] | ((prev: AIAssistMessage[]) => AIAssistMessage[])) => void;
|
||||
|
||||
// Tab Management
|
||||
setAIAssistTabs: (tabs: AIAssistTab[]) => void;
|
||||
setActiveTabId: (id: string | null) => void;
|
||||
addAIAssistTab: (agent?: string) => void;
|
||||
removeAIAssistTab: (id: string) => void;
|
||||
updateActiveTab: (updates: Partial<AIAssistTab>) => void;
|
||||
|
||||
setLanguage: (lang: "en" | "ru" | "he") => void;
|
||||
setSelectedProvider: (provider: ModelProvider) => void;
|
||||
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
||||
@@ -60,7 +79,15 @@ const useStore = create<AppState>((set) => ({
|
||||
googleAdsResult: null,
|
||||
magicWandResult: null,
|
||||
marketResearchResult: null,
|
||||
aiAssistHistory: [],
|
||||
|
||||
aiAssistTabs: [{
|
||||
id: "default",
|
||||
title: "New Chat",
|
||||
history: [],
|
||||
currentAgent: "general"
|
||||
}],
|
||||
activeTabId: "default",
|
||||
|
||||
language: "en",
|
||||
selectedProvider: "qwen",
|
||||
selectedModels: {
|
||||
@@ -90,9 +117,39 @@ const useStore = create<AppState>((set) => ({
|
||||
setGoogleAdsResult: (result) => set({ googleAdsResult: result }),
|
||||
setMagicWandResult: (result) => set({ magicWandResult: result }),
|
||||
setMarketResearchResult: (result) => set({ marketResearchResult: result }),
|
||||
setAIAssistHistory: (update) => set((state) => ({
|
||||
aiAssistHistory: typeof update === 'function' ? update(state.aiAssistHistory) : update
|
||||
|
||||
setAIAssistTabs: (tabs) => set({ aiAssistTabs: tabs }),
|
||||
setActiveTabId: (id) => set({ activeTabId: id }),
|
||||
addAIAssistTab: (agent = "general") => set((state) => {
|
||||
const newId = Math.random().toString(36).substr(2, 9);
|
||||
const newTab = {
|
||||
id: newId,
|
||||
title: `Chat ${state.aiAssistTabs.length + 1}`,
|
||||
history: [],
|
||||
currentAgent: agent
|
||||
};
|
||||
return {
|
||||
aiAssistTabs: [...state.aiAssistTabs, newTab],
|
||||
activeTabId: newId
|
||||
};
|
||||
}),
|
||||
removeAIAssistTab: (id) => set((state) => {
|
||||
const newTabs = state.aiAssistTabs.filter(t => t.id !== id);
|
||||
let nextActiveId = state.activeTabId;
|
||||
if (state.activeTabId === id) {
|
||||
nextActiveId = newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null;
|
||||
}
|
||||
return {
|
||||
aiAssistTabs: newTabs,
|
||||
activeTabId: nextActiveId
|
||||
};
|
||||
}),
|
||||
updateActiveTab: (updates) => set((state) => ({
|
||||
aiAssistTabs: state.aiAssistTabs.map(t =>
|
||||
t.id === state.activeTabId ? { ...t, ...updates } : t
|
||||
)
|
||||
})),
|
||||
|
||||
setLanguage: (lang) => set({ language: lang }),
|
||||
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
||||
setSelectedModel: (provider, model) =>
|
||||
@@ -132,7 +189,13 @@ const useStore = create<AppState>((set) => ({
|
||||
googleAdsResult: null,
|
||||
magicWandResult: null,
|
||||
marketResearchResult: null,
|
||||
aiAssistHistory: [],
|
||||
aiAssistTabs: [{
|
||||
id: "default",
|
||||
title: "New Chat",
|
||||
history: [],
|
||||
currentAgent: "general"
|
||||
}],
|
||||
activeTabId: "default",
|
||||
error: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user