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 {
|
import {
|
||||||
MessageSquare, Send, Code2, Palette, Search,
|
MessageSquare, Send, Code2, Palette, Search,
|
||||||
Trash2, Copy, Monitor, StopCircle, X, Zap, Ghost,
|
Trash2, Copy, Monitor, StopCircle, X, Zap, Ghost,
|
||||||
Wand2, LayoutPanelLeft, Play, Orbit
|
Wand2, LayoutPanelLeft, Play, Orbit, Plus
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
@@ -34,11 +34,62 @@ interface PreviewData {
|
|||||||
* A ultra-stable iframe wrapper that avoids hydration issues
|
* A ultra-stable iframe wrapper that avoids hydration issues
|
||||||
* and provides a WOW visual experience.
|
* and provides a WOW visual experience.
|
||||||
*/
|
*/
|
||||||
const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: string, isStreaming: boolean }) => {
|
const BuildingArtifact = ({ type }: { type: string }) => {
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
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(() => {
|
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
|
// Decode HTML entities if present
|
||||||
const isEncodedHtml = data.includes("<") && data.includes(">");
|
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);
|
const isReactLike = normalized.includes("import React") || normalized.includes("useState") || normalized.includes("useEffect") || /<[A-Z][\s\S]*>/.test(normalized);
|
||||||
|
|
||||||
let doc: string;
|
let doc: string;
|
||||||
if (isFullDocument) {
|
try {
|
||||||
// ... same as before but add React support if needed ...
|
if (isFullDocument) {
|
||||||
const reactScripts = isReactLike ? `
|
// If it's a full document, inject Tailwind CSS but keep the structure
|
||||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
const reactScripts = isReactLike ? `
|
||||||
<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>
|
|
||||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></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/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/@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 ? `
|
if (hasHeadTag) {
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
doc = normalized.replace(/<head>/i, `<head>
|
||||||
root.render(React.createElement(${mainComponent}));
|
${reactScripts}
|
||||||
` : `
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
// No clear component found to mount, executing raw code
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap">
|
||||||
`}
|
`);
|
||||||
</script>
|
} else {
|
||||||
</body>
|
doc = normalized.replace(/<html[^>]*>/i, (match) => `${match}
|
||||||
</html>
|
<head>
|
||||||
`;
|
${reactScripts}
|
||||||
} else {
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
// Wrap fragments in a styled container
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap">
|
||||||
doc = `
|
</head>
|
||||||
<!DOCTYPE html>
|
`);
|
||||||
<html class="dark">
|
}
|
||||||
<head>
|
} else if (isReactLike) {
|
||||||
<meta charset="utf-8">
|
// Specialized React Runner for fragments/components
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
const cleanedCode = normalized
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap">
|
.replace(/import\s+(?:React\s*,\s*)?{?([\s\S]*?)}?\s+from\s+['"]react['"];?/g, "const { $1 } = React;")
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
.replace(/import\s+React\s+from\s+['"]react['"];?/g, "/* React already global */")
|
||||||
<script>
|
.replace(/import\s+[\s\S]*?from\s+['"]lucide-react['"];?/g, "const { ...lucide } = window.lucide || {};")
|
||||||
tailwind.config = {
|
.replace(/export\s+default\s+/g, "const MainComponent = ");
|
||||||
darkMode: 'class',
|
|
||||||
theme: {
|
// Try to find the component name to render
|
||||||
extend: {
|
const componentMatch = cleanedCode.match(/const\s+([A-Z]\w+)\s*=\s*\(\)\s*=>/);
|
||||||
colors: {
|
const mainComponent = componentMatch ? componentMatch[1] : (cleanedCode.includes("MainComponent") ? "MainComponent" : null);
|
||||||
primary: { 50: '#ecfdf3', 100: '#d1fae5', 200: '#a7f3d0', 300: '#6ee7b7', 400: '#34d399', 500: '#10b981', 600: '#059669', 700: '#047857', 800: '#065f46', 900: '#064e3b', 950: '#022c22' }
|
|
||||||
|
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>
|
||||||
</script>
|
::-webkit-scrollbar { width: 8px; }
|
||||||
<style>
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
::-webkit-scrollbar { width: 8px; }
|
::-webkit-scrollbar-thumb { background: #115e59; border-radius: 4px; }
|
||||||
::-webkit-scrollbar-track { background: transparent; }
|
::-webkit-scrollbar-thumb:hover { background: #0f766e; }
|
||||||
::-webkit-scrollbar-thumb { background: #115e59; border-radius: 4px; }
|
body {
|
||||||
::-webkit-scrollbar-thumb:hover { background: #0f766e; }
|
margin: 0;
|
||||||
body {
|
padding: 24px;
|
||||||
margin: 0;
|
font-family: "Space Grotesk", "IBM Plex Sans", system-ui, sans-serif;
|
||||||
padding: 24px;
|
background: #f8fafc;
|
||||||
font-family: "Space Grotesk", "IBM Plex Sans", system-ui, sans-serif;
|
color: #1e293b;
|
||||||
background: #f8fafc;
|
min-height: 100vh;
|
||||||
color: #1e293b;
|
}
|
||||||
min-height: 100vh;
|
</style>
|
||||||
}
|
</head>
|
||||||
</style>
|
<body>
|
||||||
</head>
|
${normalized}
|
||||||
<body>
|
</body>
|
||||||
${normalized}
|
</html>
|
||||||
</body>
|
`;
|
||||||
</html>
|
}
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
iframeRef.current.srcdoc = doc;
|
if (iframeRef.current) {
|
||||||
}, [data, type]);
|
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 (
|
return (
|
||||||
<div className="w-full h-full relative group">
|
<div className="w-full h-full relative group bg-[#0b1414] overflow-hidden rounded-b-2xl">
|
||||||
<iframe
|
{isStreaming && <BuildingArtifact type={type} />}
|
||||||
ref={iframeRef}
|
|
||||||
title="Canvas Preview"
|
{renderError ? (
|
||||||
className="w-full h-full border-none rounded-b-2xl bg-[#0b1414] shadow-inner"
|
<div className="absolute inset-0 flex flex-col items-center justify-center p-12 text-center animate-in zoom-in-95 duration-300">
|
||||||
sandbox="allow-scripts"
|
<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 && (
|
{isStreaming && (
|
||||||
<div className="absolute inset-x-0 bottom-0 h-1 bg-blue-500/20 overflow-hidden">
|
<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 className="h-full bg-blue-500 animate-[loading_1.5s_infinite]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<style jsx>{`
|
|
||||||
@keyframes loading {
|
|
||||||
0% { transform: translateX(-100%); }
|
|
||||||
100% { transform: translateX(200%); }
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -308,18 +381,33 @@ function parseStreamingContent(text: string, currentAgent: string) {
|
|||||||
export default function AIAssist() {
|
export default function AIAssist() {
|
||||||
const {
|
const {
|
||||||
language,
|
language,
|
||||||
aiAssistHistory,
|
aiAssistTabs,
|
||||||
setAIAssistHistory,
|
activeTabId,
|
||||||
|
setActiveTabId,
|
||||||
|
addAIAssistTab,
|
||||||
|
removeAIAssistTab,
|
||||||
|
updateActiveTab,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
selectedModels,
|
selectedModels,
|
||||||
setSelectedModel
|
setSelectedModel
|
||||||
} = useStore();
|
} = useStore();
|
||||||
const t = translations[language].aiAssist;
|
const t = translations[language].aiAssist;
|
||||||
|
|
||||||
|
const activeTab = aiAssistTabs.find(t => t.id === activeTabId) || aiAssistTabs[0];
|
||||||
|
const aiAssistHistory = activeTab?.history || [];
|
||||||
|
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [currentAgent, setCurrentAgent] = useState("general");
|
const [currentAgent, setCurrentAgent] = useState(activeTab?.currentAgent || "general");
|
||||||
const [previewData, setPreviewData] = useState<PreviewData | null>(null);
|
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 [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||||
const [showCanvas, setShowCanvas] = useState(false);
|
const [showCanvas, setShowCanvas] = useState(false);
|
||||||
const [viewMode, setViewMode] = useState<"preview" | "code">("preview");
|
const [viewMode, setViewMode] = useState<"preview" | "code">("preview");
|
||||||
@@ -334,10 +422,22 @@ export default function AIAssist() {
|
|||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const isPreviewRenderable = (preview?: PreviewData | null) => {
|
const isPreviewRenderable = (preview?: PreviewData | null) => {
|
||||||
if (!preview) return false;
|
if (!preview) return false;
|
||||||
return ["web", "app", "design", "html", "ui"].includes(preview.type)
|
|
||||||
|| preview.language === "html"
|
// Detect and block backend code from rendering in iframe
|
||||||
|| preview.data.includes("<")
|
const lowerData = preview.data.toLowerCase();
|
||||||
|| (preview.data.includes("<") && preview.data.includes(">"));
|
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);
|
const canRenderPreview = isPreviewRenderable(previewData);
|
||||||
|
|
||||||
@@ -389,7 +489,7 @@ export default function AIAssist() {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
const newHistory = [...aiAssistHistory, userMsg];
|
const newHistory = [...aiAssistHistory, userMsg];
|
||||||
setAIAssistHistory(newHistory);
|
updateActiveTab({ history: newHistory });
|
||||||
setInput("");
|
setInput("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,13 +502,16 @@ export default function AIAssist() {
|
|||||||
agent: currentAgent,
|
agent: currentAgent,
|
||||||
timestamp: new Date()
|
timestamp: new Date()
|
||||||
};
|
};
|
||||||
setAIAssistHistory(prev => [...prev, assistantMsg]);
|
|
||||||
|
// Update history in active tab
|
||||||
|
const updatedHistory = [...aiAssistHistory, assistantMsg];
|
||||||
|
updateActiveTab({ history: updatedHistory });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let accumulated = "";
|
let accumulated = "";
|
||||||
let lastParsedPreview: PreviewData | null = null;
|
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 => {
|
const formattedHistory = aiAssistHistory.map(m => {
|
||||||
if (m.role === "assistant") {
|
if (m.role === "assistant") {
|
||||||
const { chatDisplay, preview } = parseStreamingContent(m.content, m.agent || "general");
|
const { chatDisplay, preview } = parseStreamingContent(m.content, m.agent || "general");
|
||||||
@@ -431,37 +534,32 @@ export default function AIAssist() {
|
|||||||
|
|
||||||
if (streamStatus) setStatus(streamStatus);
|
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)) {
|
if (preview && JSON.stringify(preview) !== JSON.stringify(lastParsedPreview)) {
|
||||||
setPreviewData(preview);
|
setPreviewData(preview);
|
||||||
lastParsedPreview = preview;
|
lastParsedPreview = preview;
|
||||||
setShowCanvas(true);
|
setShowCanvas(true);
|
||||||
if (isPreviewRenderable(preview)) setViewMode("preview");
|
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 => {
|
// Stream updates to current tab history
|
||||||
const last = prev[prev.length - 1];
|
const lastMsg = {
|
||||||
if (last && last.role === "assistant") {
|
role: "assistant" as const,
|
||||||
return [...prev.slice(0, -1), {
|
content: accumulated,
|
||||||
...last,
|
agent,
|
||||||
content: accumulated, // Keep raw for AI context
|
preview: preview ? { type: preview.type, data: preview.data, language: preview.language } : undefined,
|
||||||
agent,
|
timestamp: new Date()
|
||||||
preview: preview ? { type: preview.type, data: preview.data, language: preview.language } : undefined
|
};
|
||||||
} as AIAssistMessage];
|
|
||||||
}
|
updateActiveTab({
|
||||||
return prev;
|
history: [...updatedHistory.slice(0, -1), lastMsg]
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
signal: controller.signal
|
signal: controller.signal
|
||||||
@@ -480,14 +578,9 @@ export default function AIAssist() {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Assist error:", error);
|
console.error("Assist error:", error);
|
||||||
setAIAssistHistory(prev => {
|
const message = error instanceof Error ? error.message : "AI Assist failed";
|
||||||
const last = prev[prev.length - 1];
|
const errorMsg: AIAssistMessage = { role: "assistant", content: message, timestamp: new Date() };
|
||||||
const message = error instanceof Error ? error.message : "AI Assist failed";
|
updateActiveTab({ history: [...aiAssistHistory, errorMsg] });
|
||||||
if (last && last.role === "assistant") {
|
|
||||||
return [...prev.slice(0, -1), { ...last, content: message }];
|
|
||||||
}
|
|
||||||
return [...prev, { role: "assistant", content: message, timestamp: new Date() }];
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
setAbortController(null);
|
setAbortController(null);
|
||||||
@@ -509,7 +602,11 @@ export default function AIAssist() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clearHistory = () => {
|
const clearHistory = () => {
|
||||||
setAIAssistHistory([]);
|
updateActiveTab({
|
||||||
|
history: [],
|
||||||
|
previewData: null,
|
||||||
|
currentAgent: "general"
|
||||||
|
});
|
||||||
setPreviewData(null);
|
setPreviewData(null);
|
||||||
setShowCanvas(false);
|
setShowCanvas(false);
|
||||||
setAssistStep("idle");
|
setAssistStep("idle");
|
||||||
@@ -569,7 +666,44 @@ export default function AIAssist() {
|
|||||||
</div>
|
</div>
|
||||||
</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="px-6 pt-6">
|
||||||
<div className="flex flex-wrap gap-2 pb-4">
|
<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:
|
AGENTS & CAPABILITIES:
|
||||||
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
- 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.
|
- smm: Social Media Manager. Create multi-platform content plans and calendars.
|
||||||
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||||
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
- 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].
|
- 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].
|
- 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):
|
ITERATIVE MODIFICATIONS (CRITICAL):
|
||||||
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
|
- 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.
|
- 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:
|
AGENTS & CAPABILITIES:
|
||||||
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
- 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.
|
- smm: Social Media Manager. Create multi-platform content plans and calendars.
|
||||||
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||||
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
- 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].
|
- 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].
|
- 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):
|
ITERATIVE MODIFICATIONS (CRITICAL):
|
||||||
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
|
- 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.
|
- 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:
|
AGENTS & CAPABILITIES:
|
||||||
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
- 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.
|
- smm: Social Media Manager. Create multi-platform content plans and calendars.
|
||||||
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||||
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
- 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].
|
- 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].
|
- 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):
|
ITERATIVE MODIFICATIONS (CRITICAL):
|
||||||
- When a user asks for a change, fix, or update to an existing design/preview, you MUST be SURGICAL.
|
- 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.
|
- 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 { create } from "zustand";
|
||||||
import type { ModelProvider, PromptEnhancement, PRD, ActionPlan, SlidesPresentation, GoogleAdsResult, MagicWandResult, MarketResearchResult, AppView, AIAssistMessage } from "@/types";
|
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 {
|
interface AppState {
|
||||||
currentPrompt: string;
|
currentPrompt: string;
|
||||||
enhancedPrompt: string | null;
|
enhancedPrompt: string | null;
|
||||||
@@ -10,7 +18,11 @@ interface AppState {
|
|||||||
googleAdsResult: GoogleAdsResult | null;
|
googleAdsResult: GoogleAdsResult | null;
|
||||||
magicWandResult: MagicWandResult | null;
|
magicWandResult: MagicWandResult | null;
|
||||||
marketResearchResult: MarketResearchResult | null;
|
marketResearchResult: MarketResearchResult | null;
|
||||||
aiAssistHistory: AIAssistMessage[];
|
|
||||||
|
// AI Assist Tabs
|
||||||
|
aiAssistTabs: AIAssistTab[];
|
||||||
|
activeTabId: string | null;
|
||||||
|
|
||||||
language: "en" | "ru" | "he";
|
language: "en" | "ru" | "he";
|
||||||
selectedProvider: ModelProvider;
|
selectedProvider: ModelProvider;
|
||||||
selectedModels: Record<ModelProvider, string>;
|
selectedModels: Record<ModelProvider, string>;
|
||||||
@@ -37,7 +49,14 @@ interface AppState {
|
|||||||
setGoogleAdsResult: (result: GoogleAdsResult | null) => void;
|
setGoogleAdsResult: (result: GoogleAdsResult | null) => void;
|
||||||
setMagicWandResult: (result: MagicWandResult | null) => void;
|
setMagicWandResult: (result: MagicWandResult | null) => void;
|
||||||
setMarketResearchResult: (result: MarketResearchResult | 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;
|
setLanguage: (lang: "en" | "ru" | "he") => void;
|
||||||
setSelectedProvider: (provider: ModelProvider) => void;
|
setSelectedProvider: (provider: ModelProvider) => void;
|
||||||
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
setSelectedModel: (provider: ModelProvider, model: string) => void;
|
||||||
@@ -60,7 +79,15 @@ const useStore = create<AppState>((set) => ({
|
|||||||
googleAdsResult: null,
|
googleAdsResult: null,
|
||||||
magicWandResult: null,
|
magicWandResult: null,
|
||||||
marketResearchResult: null,
|
marketResearchResult: null,
|
||||||
aiAssistHistory: [],
|
|
||||||
|
aiAssistTabs: [{
|
||||||
|
id: "default",
|
||||||
|
title: "New Chat",
|
||||||
|
history: [],
|
||||||
|
currentAgent: "general"
|
||||||
|
}],
|
||||||
|
activeTabId: "default",
|
||||||
|
|
||||||
language: "en",
|
language: "en",
|
||||||
selectedProvider: "qwen",
|
selectedProvider: "qwen",
|
||||||
selectedModels: {
|
selectedModels: {
|
||||||
@@ -90,9 +117,39 @@ const useStore = create<AppState>((set) => ({
|
|||||||
setGoogleAdsResult: (result) => set({ googleAdsResult: result }),
|
setGoogleAdsResult: (result) => set({ googleAdsResult: result }),
|
||||||
setMagicWandResult: (result) => set({ magicWandResult: result }),
|
setMagicWandResult: (result) => set({ magicWandResult: result }),
|
||||||
setMarketResearchResult: (result) => set({ marketResearchResult: 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 }),
|
setLanguage: (lang) => set({ language: lang }),
|
||||||
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
setSelectedProvider: (provider) => set({ selectedProvider: provider }),
|
||||||
setSelectedModel: (provider, model) =>
|
setSelectedModel: (provider, model) =>
|
||||||
@@ -132,7 +189,13 @@ const useStore = create<AppState>((set) => ({
|
|||||||
googleAdsResult: null,
|
googleAdsResult: null,
|
||||||
magicWandResult: null,
|
magicWandResult: null,
|
||||||
marketResearchResult: null,
|
marketResearchResult: null,
|
||||||
aiAssistHistory: [],
|
aiAssistTabs: [{
|
||||||
|
id: "default",
|
||||||
|
title: "New Chat",
|
||||||
|
history: [],
|
||||||
|
currentAgent: "general"
|
||||||
|
}],
|
||||||
|
activeTabId: "default",
|
||||||
error: null,
|
error: null,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user