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:
Gemini AI
2025-12-29 02:49:13 +04:00
Unverified
parent e8904c6b6c
commit 6f3c2949bb
5 changed files with 388 additions and 176 deletions

View File

@@ -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("&lt;") && data.includes("&gt;");
@@ -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("&lt;") && preview.data.includes("&gt;"));
// 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">
{[

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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,
}),
}));