style: emerald theme redesign and ai-assist streaming enhancements
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef, useCallback, memo } from "react";
|
import React, { useState, useEffect, useRef, memo } from "react";
|
||||||
import {
|
import {
|
||||||
MessageSquare, Send, Sparkles, Brain, Cpu, Code2, Palette, Search,
|
MessageSquare, Send, Code2, Palette, Search,
|
||||||
Terminal, Eye, Trash2, Loader2, Bot, User, X, RotateCcw,
|
Trash2, Copy, Monitor, StopCircle, X, Zap, Ghost,
|
||||||
CheckCircle2, Copy, Monitor, StopCircle, Maximize2, Minimize2,
|
Wand2, LayoutPanelLeft, Play, Orbit
|
||||||
ChevronRight, Layout, Zap, Ghost
|
|
||||||
} 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";
|
||||||
@@ -41,14 +40,16 @@ const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: stri
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!iframeRef.current) return;
|
if (!iframeRef.current) return;
|
||||||
|
|
||||||
const isHtml = data.includes("<div") || data.includes("<section") || data.includes("class=");
|
const isHtml = data.includes("<") && data.includes(">");
|
||||||
if (isHtml || type === "web" || type === "app" || type === "design") {
|
const shouldRender = isHtml || ["web", "app", "design", "html", "ui"].includes(type);
|
||||||
|
if (shouldRender) {
|
||||||
const doc = `
|
const doc = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html class="dark">
|
<html class="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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 src="https://cdn.tailwindcss.com"></script>
|
||||||
<script>
|
<script>
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
@@ -56,7 +57,7 @@ const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: stri
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
primary: { 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc', 400: '#38bdf8', 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1', 800: '#075985', 900: '#0c4a6e', 950: '#082f49' }
|
primary: { 50: '#ecfdf3', 100: '#d1fae5', 200: '#a7f3d0', 300: '#6ee7b7', 400: '#34d399', 500: '#10b981', 600: '#059669', 700: '#047857', 800: '#065f46', 900: '#064e3b', 950: '#022c22' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,19 +66,19 @@ const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: stri
|
|||||||
<style>
|
<style>
|
||||||
::-webkit-scrollbar { width: 8px; }
|
::-webkit-scrollbar { width: 8px; }
|
||||||
::-webkit-scrollbar-track { background: transparent; }
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
|
::-webkit-scrollbar-thumb { background: #115e59; border-radius: 4px; }
|
||||||
::-webkit-scrollbar-thumb:hover { background: #475569; }
|
::-webkit-scrollbar-thumb:hover { background: #0f766e; }
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
font-family: 'Inter', system-ui, sans-serif;
|
font-family: "Space Grotesk", "IBM Plex Sans", system-ui, sans-serif;
|
||||||
background: #0f172a;
|
background: #0b1414;
|
||||||
color: #f1f5f9;
|
color: #ecfdf3;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-slate-950 text-slate-100">
|
<body class="bg-[#0b1414] text-emerald-50">
|
||||||
${data}
|
${data}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -91,12 +92,12 @@ const LiveCanvas = memo(({ data, type, isStreaming }: { data: string, type: stri
|
|||||||
<iframe
|
<iframe
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
title="Canvas Preview"
|
title="Canvas Preview"
|
||||||
className="w-full h-full border-none rounded-b-2xl bg-slate-950 shadow-inner"
|
className="w-full h-full border-none rounded-b-2xl bg-[#0b1414] shadow-inner"
|
||||||
sandbox="allow-scripts"
|
sandbox="allow-scripts"
|
||||||
/>
|
/>
|
||||||
{isStreaming && (
|
{isStreaming && (
|
||||||
<div className="absolute inset-x-0 bottom-0 h-1 bg-indigo-500/20 overflow-hidden">
|
<div className="absolute inset-x-0 bottom-0 h-1 bg-emerald-500/20 overflow-hidden">
|
||||||
<div className="h-full bg-indigo-500 animate-[loading_1.5s_infinite]" />
|
<div className="h-full bg-emerald-500 animate-[loading_1.5s_infinite]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
@@ -115,11 +116,34 @@ LiveCanvas.displayName = "LiveCanvas";
|
|||||||
|
|
||||||
function parseStreamingContent(text: string) {
|
function parseStreamingContent(text: string) {
|
||||||
let agent = "general";
|
let agent = "general";
|
||||||
const agentMatch = text.match(/\[AGENT:(\w+)\]/);
|
let preview: PreviewData | null = null;
|
||||||
|
let chatDisplay = text.trim();
|
||||||
|
|
||||||
|
const jsonCandidate = text.trim();
|
||||||
|
if (jsonCandidate.startsWith("{") && jsonCandidate.endsWith("}")) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(jsonCandidate);
|
||||||
|
if (parsed?.agent) agent = parsed.agent;
|
||||||
|
if (parsed?.preview?.data) {
|
||||||
|
preview = {
|
||||||
|
type: parsed.preview.type || "web",
|
||||||
|
language: parsed.preview.language || "text",
|
||||||
|
data: parsed.preview.data,
|
||||||
|
isStreaming: !text.includes("[/PREVIEW]")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (typeof parsed?.content === "string") {
|
||||||
|
chatDisplay = parsed.content.trim();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore malformed JSON during stream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentMatch = text.match(/\[AGENT:([\w-]+)\]/);
|
||||||
if (agentMatch) agent = agentMatch[1];
|
if (agentMatch) agent = agentMatch[1];
|
||||||
|
|
||||||
let preview: PreviewData | null = null;
|
const previewMatch = text.match(/\[PREVIEW:([\w-]+):?([\w-]+)?\]([\s\S]*?)(?:\[\/PREVIEW\]|$)/);
|
||||||
const previewMatch = text.match(/\[PREVIEW:(\w+):?(\w+)?\]([\s\S]*?)(?:\[\/PREVIEW\]|$)/);
|
|
||||||
if (previewMatch) {
|
if (previewMatch) {
|
||||||
preview = {
|
preview = {
|
||||||
type: previewMatch[1],
|
type: previewMatch[1],
|
||||||
@@ -129,13 +153,28 @@ function parseStreamingContent(text: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let chatDisplay = text
|
if (/\[AGENT:|\[PREVIEW:/.test(text)) {
|
||||||
.replace(/\[AGENT:\w+\]/g, "")
|
chatDisplay = text
|
||||||
.replace(/\[PREVIEW:\w+:?\w+?\][\s\S]*?(?:\[\/PREVIEW\]|$)/g, "")
|
.replace(/\[AGENT:[\w-]+\]/g, "")
|
||||||
.trim();
|
.replace(/\[PREVIEW:[\w-]+:?[\w-]+?\][\s\S]*?(?:\[\/PREVIEW\]|$)/g, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preview) {
|
||||||
|
const fenced = text.match(/```(html|css|javascript|tsx|jsx|md|markdown)\s*([\s\S]*?)```/i);
|
||||||
|
if (fenced) {
|
||||||
|
const language = fenced[1].toLowerCase();
|
||||||
|
preview = {
|
||||||
|
type: language === "html" ? "web" : "code",
|
||||||
|
language,
|
||||||
|
data: fenced[2].trim(),
|
||||||
|
isStreaming: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!chatDisplay && preview) {
|
if (!chatDisplay && preview) {
|
||||||
chatDisplay = `Building visual artifact...`;
|
chatDisplay = `Rendering live artifact...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { chatDisplay, preview, agent };
|
return { chatDisplay, preview, agent };
|
||||||
@@ -164,6 +203,9 @@ export default function AIAssist() {
|
|||||||
const [abortController, setAbortController] = useState<AbortController | null>(null);
|
const [abortController, setAbortController] = useState<AbortController | null>(null);
|
||||||
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const canRenderPreview = previewData
|
||||||
|
? ["web", "app", "design", "html", "ui"].includes(previewData.type) || previewData.data.includes("<")
|
||||||
|
: false;
|
||||||
|
|
||||||
// Auto-scroll logic
|
// Auto-scroll logic
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -175,6 +217,12 @@ export default function AIAssist() {
|
|||||||
}
|
}
|
||||||
}, [aiAssistHistory, isProcessing]);
|
}, [aiAssistHistory, isProcessing]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (previewData?.data) {
|
||||||
|
setViewMode("preview");
|
||||||
|
}
|
||||||
|
}, [previewData?.data]);
|
||||||
|
|
||||||
// Load available models
|
// Load available models
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadModels = async () => {
|
const loadModels = async () => {
|
||||||
@@ -220,7 +268,7 @@ export default function AIAssist() {
|
|||||||
let accumulated = "";
|
let accumulated = "";
|
||||||
let lastParsedPreview: PreviewData | null = null;
|
let lastParsedPreview: PreviewData | null = null;
|
||||||
|
|
||||||
await modelAdapter.generateAIAssistStream(
|
const response = await modelAdapter.generateAIAssistStream(
|
||||||
{
|
{
|
||||||
messages: newHistory,
|
messages: newHistory,
|
||||||
currentAgent,
|
currentAgent,
|
||||||
@@ -249,13 +297,25 @@ export default function AIAssist() {
|
|||||||
}
|
}
|
||||||
return prev;
|
return prev;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
signal: controller.signal
|
||||||
},
|
},
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
selectedModels[selectedProvider]
|
selectedModels[selectedProvider]
|
||||||
);
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || "Streaming failed");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Assist error:", 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() }];
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
setAbortController(null);
|
setAbortController(null);
|
||||||
@@ -276,40 +336,28 @@ export default function AIAssist() {
|
|||||||
setShowCanvas(false);
|
setShowCanvas(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAgentIcon = (agent: string) => {
|
|
||||||
switch (agent.toLowerCase()) {
|
|
||||||
case 'code': return <Code2 className="h-4 w-4" />;
|
|
||||||
case 'design': return <Palette className="h-4 w-4" />;
|
|
||||||
case 'seo': return <TargetIcon className="h-4 w-4" />;
|
|
||||||
case 'research': return <Search className="h-4 w-4" />;
|
|
||||||
default: return <Sparkles className="h-4 w-4" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[calc(100vh-140px)] flex gap-4 lg:gap-8 overflow-hidden animate-in fade-in duration-700">
|
<div className="ai-assist h-[calc(100vh-140px)] flex flex-col lg:flex-row gap-4 lg:gap-8 overflow-hidden animate-in fade-in duration-700">
|
||||||
{/* --- Chat Panel --- */}
|
{/* --- Chat Panel --- */}
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"flex flex-col h-full transition-all duration-700 cubic-bezier(0.4, 0, 0.2, 1)",
|
"flex flex-col h-full transition-all duration-700 cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
showCanvas ? "w-2/5 min-w-[400px]" : "w-full max-w-4xl mx-auto"
|
showCanvas ? "w-full lg:w-2/5 lg:min-w-[400px]" : "w-full max-w-4xl mx-auto"
|
||||||
)}>
|
)}>
|
||||||
<Card className="flex-1 flex flex-col border border-slate-200/40 dark:border-slate-800/60 shadow-[0_8px_32px_rgba(0,0,0,0.12)] bg-white/70 dark:bg-slate-900/40 backdrop-blur-2xl rounded-[2rem] overflow-hidden">
|
<Card className="flex-1 flex flex-col border border-emerald-100/60 dark:border-emerald-950/60 shadow-[0_18px_50px_rgba(15,23,42,0.15)] bg-[#f8f5ef]/80 dark:bg-[#0b1414]/80 backdrop-blur-2xl rounded-[2rem] overflow-hidden">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="px-6 py-5 border-b border-slate-200/60 dark:border-slate-800/40 flex items-center justify-between shrink-0 bg-slate-50/30 dark:bg-slate-900/20 backdrop-blur-md">
|
<div className="px-6 py-5 border-b border-emerald-100/60 dark:border-emerald-950/40 flex items-center justify-between shrink-0 bg-white/60 dark:bg-[#0b1414]/60 backdrop-blur-md">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="p-2.5 bg-gradient-to-tr from-indigo-500 to-violet-600 rounded-2xl text-white shadow-lg shadow-indigo-500/20">
|
<div className="p-2.5 bg-gradient-to-tr from-emerald-500 to-teal-600 rounded-2xl text-white shadow-lg shadow-emerald-500/20">
|
||||||
<MessageSquare className="h-5 w-5" />
|
<MessageSquare className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute -bottom-1 -right-1 w-4 h-4 rounded-full bg-emerald-500 border-2 border-white dark:border-slate-900 animate-pulse" />
|
<div className="absolute -bottom-1 -right-1 w-4 h-4 rounded-full bg-amber-400 border-2 border-white dark:border-[#0b1414] animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-black text-slate-800 dark:text-slate-100 tracking-tight">{t.title}</h2>
|
<h2 className="text-xl font-black text-slate-900 dark:text-emerald-50 tracking-tight">{t.title}</h2>
|
||||||
<div className="flex items-center gap-1.5 overflow-hidden">
|
<p className="text-[11px] font-bold uppercase tracking-[0.25em] text-emerald-700/70 dark:text-emerald-200/70">
|
||||||
<span className="text-[10px] font-bold text-indigo-500 uppercase tracking-widest animate-in slide-in-from-left-4">
|
Agent {currentAgent}
|
||||||
Active: {currentAgent}
|
</p>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -317,10 +365,19 @@ export default function AIAssist() {
|
|||||||
<select
|
<select
|
||||||
value={selectedModels[selectedProvider]}
|
value={selectedModels[selectedProvider]}
|
||||||
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
|
||||||
className="text-[11px] font-black h-9 px-3 rounded-xl border-slate-200 dark:border-slate-800 bg-white/50 dark:bg-slate-950/40 focus:ring-2 focus:ring-indigo-500/40 transition-all outline-none"
|
className="text-[11px] font-black h-9 px-3 rounded-xl border-emerald-100 dark:border-emerald-900 bg-white/80 dark:bg-[#0b1414]/80 focus:ring-2 focus:ring-emerald-400/40 transition-all outline-none"
|
||||||
>
|
>
|
||||||
{availableModels.map(m => <option key={m} value={m}>{m}</option>)}
|
{availableModels.map(m => <option key={m} value={m}>{m}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setShowCanvas((prev) => !prev)}
|
||||||
|
className="h-9 w-9 text-emerald-700 hover:text-emerald-950 hover:bg-emerald-100 dark:text-emerald-200 dark:hover:text-white dark:hover:bg-emerald-900/40 rounded-xl transition-colors"
|
||||||
|
disabled={!previewData}
|
||||||
|
>
|
||||||
|
<LayoutPanelLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -333,21 +390,59 @@ export default function AIAssist() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div ref={scrollRef} className="flex-1 overflow-y-auto px-6 py-8 space-y-8 scrollbar-thin scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-800">
|
<div className="px-6 pt-6">
|
||||||
|
<div className="flex flex-wrap gap-2 pb-4">
|
||||||
|
{[
|
||||||
|
{ label: "General", agent: "general", icon: <Orbit className="h-3.5 w-3.5" /> },
|
||||||
|
{ label: "Code", agent: "code", icon: <Code2 className="h-3.5 w-3.5" /> },
|
||||||
|
{ label: "Design", agent: "design", icon: <Palette className="h-3.5 w-3.5" /> },
|
||||||
|
{ label: "SEO", agent: "seo", icon: <Search className="h-3.5 w-3.5" /> },
|
||||||
|
{ label: "Web", agent: "web", icon: <LayoutPanelLeft className="h-3.5 w-3.5" /> },
|
||||||
|
{ label: "App", agent: "app", icon: <Play className="h-3.5 w-3.5" /> },
|
||||||
|
].map(({ label, agent, icon }) => (
|
||||||
|
<button
|
||||||
|
key={agent}
|
||||||
|
onClick={() => setCurrentAgent(agent)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 px-3 py-2 rounded-full text-[11px] font-black uppercase tracking-widest border transition-all",
|
||||||
|
currentAgent === agent
|
||||||
|
? "bg-emerald-600 text-white border-emerald-600 shadow-lg shadow-emerald-600/30"
|
||||||
|
: "bg-white/70 text-emerald-700 border-emerald-100 hover:border-emerald-300 dark:bg-[#0f1a1a] dark:text-emerald-200 dark:border-emerald-900"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref={scrollRef} className="flex-1 overflow-y-auto px-6 py-6 space-y-8 scrollbar-thin scrollbar-thumb-emerald-200/60 dark:scrollbar-thumb-emerald-900">
|
||||||
{aiAssistHistory.length === 0 && (
|
{aiAssistHistory.length === 0 && (
|
||||||
<div className="h-full flex flex-col items-center justify-center text-center py-20 animate-in zoom-in-95 duration-500">
|
<div className="h-full flex flex-col items-center justify-center text-center py-20 animate-in zoom-in-95 duration-500">
|
||||||
<div className="p-8 bg-indigo-500/5 dark:bg-indigo-500/10 rounded-full mb-8 relative">
|
<div className="p-8 bg-emerald-500/5 dark:bg-emerald-500/10 rounded-full mb-8 relative">
|
||||||
<Ghost className="h-20 w-20 text-indigo-400/40 animate-bounce duration-[3s]" />
|
<Ghost className="h-20 w-20 text-emerald-400/40 animate-bounce duration-[3s]" />
|
||||||
<div className="absolute inset-0 bg-indigo-500/10 blur-3xl rounded-full" />
|
<div className="absolute inset-0 bg-emerald-500/10 blur-3xl rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-3xl font-black text-slate-800 dark:text-slate-100 mb-3 tracking-tighter">Your AI Workspace</h3>
|
<h3 className="text-3xl font-black text-slate-900 dark:text-emerald-50 mb-3 tracking-tighter">Studio-grade AI Assist</h3>
|
||||||
<p className="max-w-xs text-sm font-medium text-slate-500 dark:text-slate-400 leading-relaxed">
|
<p className="max-w-xs text-sm font-medium text-slate-600 dark:text-emerald-100/70 leading-relaxed">
|
||||||
I switch agents and render visual artifacts automatically. How can I help today?
|
Switch agents, stream answers, and light up the canvas with live artifacts.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-10 flex flex-wrap justify-center gap-3">
|
<div className="mt-10 flex flex-wrap justify-center gap-3">
|
||||||
{['Build a UI', 'SEO Audit', 'App Design'].map(chip => (
|
{[
|
||||||
<Badge key={chip} variant="secondary" className="px-4 py-2 rounded-full cursor-pointer hover:bg-indigo-500 hover:text-white transition-all text-[11px] font-black border-transparent shadow-sm">
|
{ label: "Build a landing UI", agent: "web" },
|
||||||
{chip}
|
{ label: "SEO diagnostic", agent: "seo" },
|
||||||
|
{ label: "Mobile onboarding", agent: "app" },
|
||||||
|
].map((chip) => (
|
||||||
|
<Badge
|
||||||
|
key={chip.label}
|
||||||
|
variant="secondary"
|
||||||
|
className="px-4 py-2 rounded-full cursor-pointer hover:bg-emerald-600 hover:text-white transition-all text-[11px] font-black border-transparent shadow-sm"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentAgent(chip.agent);
|
||||||
|
setInput(chip.label);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{chip.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -362,8 +457,8 @@ export default function AIAssist() {
|
|||||||
<div className={cn(
|
<div className={cn(
|
||||||
"max-w-[90%] p-5 rounded-3xl relative transition-all duration-300",
|
"max-w-[90%] p-5 rounded-3xl relative transition-all duration-300",
|
||||||
msg.role === "user"
|
msg.role === "user"
|
||||||
? "bg-gradient-to-br from-indigo-600 to-violet-700 text-white rounded-tr-none shadow-[0_8px_20px_rgba(99,102,241,0.25)]"
|
? "bg-gradient-to-br from-emerald-600 to-teal-600 text-white rounded-tr-none shadow-[0_10px_24px_rgba(16,185,129,0.25)]"
|
||||||
: "bg-white dark:bg-slate-800/80 border border-slate-200/60 dark:border-slate-700/50 text-slate-700 dark:text-slate-200 rounded-tl-none shadow-sm backdrop-blur-xl"
|
: "bg-white dark:bg-[#0f1a1a]/80 border border-emerald-100/70 dark:border-emerald-900/50 text-slate-700 dark:text-emerald-50 rounded-tl-none shadow-sm backdrop-blur-xl"
|
||||||
)}>
|
)}>
|
||||||
<div className="absolute top-0 right-0 p-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="absolute top-0 right-0 p-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<button onClick={() => navigator.clipboard.writeText(msg.content)} className="text-inherit opacity-40 hover:opacity-100">
|
<button onClick={() => navigator.clipboard.writeText(msg.content)} className="text-inherit opacity-40 hover:opacity-100">
|
||||||
@@ -381,7 +476,7 @@ export default function AIAssist() {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="mt-5 w-full bg-slate-50 dark:bg-slate-900/50 border border-slate-200/50 dark:border-white/5 text-indigo-500 dark:text-indigo-400 font-black uppercase tracking-[0.1em] text-[10px] rounded-2xl h-11 hover:scale-[1.02] active:scale-[0.98] transition-all"
|
className="mt-5 w-full bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200/60 dark:border-emerald-800 text-emerald-700 dark:text-emerald-200 font-black uppercase tracking-[0.1em] text-[10px] rounded-2xl h-11 hover:scale-[1.02] active:scale-[0.98] transition-all"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPreviewData({ ...msg.preview!, isStreaming: false });
|
setPreviewData({ ...msg.preview!, isStreaming: false });
|
||||||
setShowCanvas(true);
|
setShowCanvas(true);
|
||||||
@@ -393,7 +488,7 @@ export default function AIAssist() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 px-2">
|
<div className="flex items-center gap-2 px-2">
|
||||||
<span className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">
|
<span className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">
|
||||||
{msg.role === "assistant" ? `Neural • ${msg.agent || 'core'}` : 'Explorer'}
|
{msg.role === "assistant" ? `Agent ${msg.agent || 'core'}` : 'Explorer'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -401,15 +496,15 @@ export default function AIAssist() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input Area */}
|
{/* Input Area */}
|
||||||
<div className="p-6 bg-slate-50/40 dark:bg-slate-900/20 border-t border-slate-200/60 dark:border-slate-800/40 shrink-0">
|
<div className="p-6 bg-white/70 dark:bg-[#0b1414]/60 border-t border-emerald-100/60 dark:border-emerald-950/40 shrink-0">
|
||||||
<form onSubmit={handleSendMessage} className="relative group">
|
<form onSubmit={handleSendMessage} className="relative group">
|
||||||
<div className="absolute inset-0 bg-indigo-500/5 rounded-[1.5rem] blur-xl group-focus-within:bg-indigo-500/10 transition-all" />
|
<div className="absolute inset-0 bg-emerald-500/5 rounded-[1.5rem] blur-xl group-focus-within:bg-emerald-500/10 transition-all" />
|
||||||
<Input
|
<Input
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
placeholder={t.placeholder}
|
placeholder={t.placeholder}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
className="relative pr-24 py-7 rounded-[1.5rem] bg-white/80 dark:bg-slate-950/60 border-slate-200/80 dark:border-slate-800/80 shadow-lg shadow-indigo-500/5 focus:ring-4 focus:ring-indigo-500/10 transition-all font-medium text-base h-16 outline-none"
|
className="relative pr-24 py-7 rounded-[1.5rem] bg-white/90 dark:bg-[#0f1a1a]/70 border-emerald-200/80 dark:border-emerald-900/80 shadow-lg shadow-emerald-500/5 focus:ring-4 focus:ring-emerald-500/10 transition-all font-medium text-base h-16 outline-none"
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
@@ -425,13 +520,23 @@ export default function AIAssist() {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!input.trim()}
|
disabled={!input.trim()}
|
||||||
className="h-11 w-11 rounded-2xl bg-indigo-600 shadow-lg shadow-indigo-600/30 hover:scale-105 active:scale-95 transition-all p-0"
|
className="h-11 w-11 rounded-2xl bg-emerald-600 shadow-lg shadow-emerald-600/30 hover:scale-105 active:scale-95 transition-all p-0"
|
||||||
>
|
>
|
||||||
<Send className="h-5 w-5" />
|
<Send className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<div className="flex items-center justify-between mt-4 text-[11px] font-semibold text-emerald-700/70 dark:text-emerald-100/70">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Wand2 className="h-3.5 w-3.5" />
|
||||||
|
Ask for a design, code, or research artifact.
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<LayoutPanelLeft className="h-3.5 w-3.5" />
|
||||||
|
Canvas {previewData ? "ready" : "idle"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -439,24 +544,24 @@ export default function AIAssist() {
|
|||||||
{/* --- Canvas Panel --- */}
|
{/* --- Canvas Panel --- */}
|
||||||
{showCanvas && (
|
{showCanvas && (
|
||||||
<div className="flex-1 h-full min-w-0 animate-in slide-in-from-right-12 duration-700 cubic-bezier(0,0,0.2,1)">
|
<div className="flex-1 h-full min-w-0 animate-in slide-in-from-right-12 duration-700 cubic-bezier(0,0,0.2,1)">
|
||||||
<Card className="h-full flex flex-col bg-slate-950 rounded-[2.5rem] overflow-hidden border border-slate-800 shadow-[0_20px_80px_rgba(0,0,0,0.6)]">
|
<Card className="h-full flex flex-col bg-[#081010] rounded-[2.5rem] overflow-hidden border border-emerald-900/60 shadow-[0_20px_80px_rgba(0,0,0,0.6)]">
|
||||||
<div className="px-6 py-5 border-b border-slate-800/60 bg-slate-900/50 backdrop-blur-2xl flex items-center justify-between shrink-0">
|
<div className="px-6 py-5 border-b border-emerald-900/60 bg-[#0b1414]/70 backdrop-blur-2xl flex items-center justify-between shrink-0">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="p-2.5 bg-indigo-500/10 rounded-2xl border border-indigo-500/20">
|
<div className="p-2.5 bg-emerald-500/10 rounded-2xl border border-emerald-500/20">
|
||||||
{viewMode === "preview" ? <Monitor className="h-5 w-5 text-indigo-400" /> : <Code2 className="h-5 w-5 text-emerald-400" />}
|
{viewMode === "preview" ? <Monitor className="h-5 w-5 text-emerald-400" /> : <Code2 className="h-5 w-5 text-amber-300" />}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xs font-black text-white uppercase tracking-[0.2em]">{previewData?.type} Canvas</h3>
|
<h3 className="text-xs font-black text-emerald-50 uppercase tracking-[0.2em]">{previewData?.type || "Live"} Canvas</h3>
|
||||||
<div className="flex bg-slate-800/60 rounded-xl p-1 mt-2">
|
<div className="flex bg-emerald-900/60 rounded-xl p-1 mt-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode("preview")}
|
onClick={() => setViewMode("preview")}
|
||||||
className={cn("px-4 py-1.5 text-[10px] uppercase font-black rounded-lg transition-all", viewMode === "preview" ? "bg-indigo-500 text-white shadow-lg" : "text-slate-500 hover:text-slate-300")}
|
className={cn("px-4 py-1.5 text-[10px] uppercase font-black rounded-lg transition-all", viewMode === "preview" ? "bg-emerald-500 text-white shadow-lg" : "text-emerald-300/60 hover:text-emerald-100")}
|
||||||
>
|
>
|
||||||
Live Render
|
Live Render
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode("code")}
|
onClick={() => setViewMode("code")}
|
||||||
className={cn("px-4 py-1.5 text-[10px] uppercase font-black rounded-lg transition-all", viewMode === "code" ? "bg-indigo-500 text-white shadow-lg" : "text-slate-500 hover:text-slate-300")}
|
className={cn("px-4 py-1.5 text-[10px] uppercase font-black rounded-lg transition-all", viewMode === "code" ? "bg-emerald-500 text-white shadow-lg" : "text-emerald-300/60 hover:text-emerald-100")}
|
||||||
>
|
>
|
||||||
Inspect Code
|
Inspect Code
|
||||||
</button>
|
</button>
|
||||||
@@ -467,7 +572,7 @@ export default function AIAssist() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-10 w-10 text-slate-400 hover:text-white hover:bg-slate-800 rounded-2xl"
|
className="h-10 w-10 text-emerald-200/70 hover:text-white hover:bg-emerald-900 rounded-2xl"
|
||||||
onClick={() => navigator.clipboard.writeText(previewData?.data || "")}
|
onClick={() => navigator.clipboard.writeText(previewData?.data || "")}
|
||||||
>
|
>
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
@@ -475,7 +580,7 @@ export default function AIAssist() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-10 w-10 text-slate-400 hover:text-rose-500 hover:bg-rose-500/10 rounded-2xl"
|
className="h-10 w-10 text-emerald-200/70 hover:text-rose-400 hover:bg-rose-500/10 rounded-2xl"
|
||||||
onClick={() => setShowCanvas(false)}
|
onClick={() => setShowCanvas(false)}
|
||||||
>
|
>
|
||||||
<X className="h-5 w-5" />
|
<X className="h-5 w-5" />
|
||||||
@@ -484,57 +589,45 @@ export default function AIAssist() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden relative">
|
<div className="flex-1 overflow-hidden relative">
|
||||||
{viewMode === "preview" && previewData ? (
|
{viewMode === "preview" && previewData && canRenderPreview ? (
|
||||||
<LiveCanvas
|
<LiveCanvas
|
||||||
data={previewData.data}
|
data={previewData.data}
|
||||||
type={previewData.type}
|
type={previewData.type}
|
||||||
isStreaming={!!previewData.isStreaming}
|
isStreaming={!!previewData.isStreaming}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full bg-[#050505] p-8 font-mono text-sm overflow-auto scrollbar-thin scrollbar-thumb-slate-800">
|
<div className="h-full bg-[#050505] p-8 font-mono text-sm overflow-auto scrollbar-thin scrollbar-thumb-emerald-900">
|
||||||
<pre className="text-emerald-400/90 leading-relaxed selection:bg-emerald-500/20 whitespace-pre-wrap">
|
<pre className="text-emerald-300/90 leading-relaxed selection:bg-emerald-500/20 whitespace-pre-wrap">
|
||||||
<code>{previewData?.data}</code>
|
<code>{previewData?.data}</code>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-6 py-3 border-t border-slate-800/40 bg-slate-900/30 flex items-center justify-between">
|
<div className="px-6 py-3 border-t border-emerald-900/40 bg-[#0b1414]/70 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className={cn("w-2 h-2 rounded-full", previewData?.isStreaming ? "bg-amber-500 animate-pulse" : "bg-emerald-500")} />
|
<div className={cn("w-2 h-2 rounded-full", previewData?.isStreaming ? "bg-amber-500 animate-pulse" : "bg-emerald-500")} />
|
||||||
<span className="text-[10px] text-slate-500 font-bold uppercase tracking-widest leading-none">
|
<span className="text-[10px] text-emerald-200/60 font-bold uppercase tracking-widest leading-none">
|
||||||
{previewData?.isStreaming ? "Neural Link Active" : "Sync Complete"}
|
{previewData?.isStreaming ? "Neural Link Active" : "Sync Complete"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" className="text-[9px] border-slate-800 text-slate-500 font-black">
|
<Badge variant="outline" className="text-[9px] border-emerald-900 text-emerald-200/50 font-black">
|
||||||
{previewData?.language?.toUpperCase()} • UTF-8
|
{previewData?.language?.toUpperCase()} UTF-8
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<style jsx global>{`
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap');
|
||||||
|
.ai-assist {
|
||||||
|
font-family: "Space Grotesk", "IBM Plex Sans", system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
.ai-assist .prose :where(code):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom simple icon for Target/SEO
|
|
||||||
function TargetIcon(props: any) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
<circle cx="12" cy="12" r="6" />
|
|
||||||
<circle cx="12" cy="12" r="2" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -98,6 +98,19 @@ export class ModelAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getService(provider: ModelProvider): any {
|
||||||
|
switch (provider) {
|
||||||
|
case "qwen":
|
||||||
|
return this.qwenService;
|
||||||
|
case "ollama":
|
||||||
|
return this.ollamaService;
|
||||||
|
case "zai":
|
||||||
|
return this.zaiService;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async callWithFallback<T>(
|
private async callWithFallback<T>(
|
||||||
operation: (service: any) => Promise<APIResponse<T>>,
|
operation: (service: any) => Promise<APIResponse<T>>,
|
||||||
providers: ModelProvider[]
|
providers: ModelProvider[]
|
||||||
@@ -282,15 +295,16 @@ export class ModelAdapter {
|
|||||||
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
const fallback = this.buildFallbackProviders(this.preferredProvider, "qwen", "ollama", "zai");
|
||||||
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
const providers: ModelProvider[] = provider ? [provider] : fallback;
|
||||||
|
|
||||||
// For now we don't handle fallback for streaming strictly, just use first available
|
const activeProvider = providers.find((candidate) => {
|
||||||
const activeProvider = providers[0];
|
const service = this.getService(candidate);
|
||||||
let service: any;
|
return this.isProviderAuthenticated(candidate) && !!service?.generateAIAssistStream;
|
||||||
switch (activeProvider) {
|
});
|
||||||
case "qwen": service = this.qwenService; break;
|
if (!activeProvider) {
|
||||||
case "ollama": service = this.ollamaService; break;
|
return { success: false, error: "No authenticated providers available for streaming" };
|
||||||
case "zai": service = this.zaiService; break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const service = this.getService(activeProvider);
|
||||||
|
|
||||||
if (!service || !service.generateAIAssistStream) {
|
if (!service || !service.generateAIAssistStream) {
|
||||||
return { success: false, error: "Streaming not supported for this provider" };
|
return { success: false, error: "Streaming not supported for this provider" };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user