feat: v1.4.0 — review code, web search grounding, responsive preview

New features:
- Review Code button sends generated code back to AI for review
- Web search grounding via SearXNG (toggle in toolbar, enriches prompts)
- Responsive preview with device size selector (Full/Desktop/Tablet/Mobile)
- /api/search proxy route

Fixes:
- Model selector text color now white on dark theme
- Post-coding button text overflow (shorter labels + min-w-0)
- Duplicate activateArtifact button suppressed when action row shows
This commit is contained in:
admin
2026-03-18 19:58:52 +00:00
Unverified
parent d2a99e6f44
commit 7266920593
6 changed files with 214 additions and 12 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, Plus, Key
Wand2, LayoutPanelLeft, Play, Orbit, Plus, Key, ShieldCheck
} from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
@@ -509,11 +509,14 @@ export default function AIAssist() {
const [input, setInput] = useState("");
const [isProcessing, setIsProcessing] = useState(false);
const [webSearchEnabled, setWebSearchEnabled] = useState(false);
const [currentAgent, setCurrentAgent] = useState(activeTab?.currentAgent || "general");
const [previewData, setPreviewData] = useState<PreviewData | null>(activeTab?.previewData || null);
const [availableModels, setAvailableModels] = useState<string[]>([]);
const [showCanvas, setShowCanvas] = useState(activeTab?.showCanvas === true);
const [viewMode, setViewMode] = useState<"preview" | "code">("preview");
const [deviceSize, setDeviceSize] = useState<"full" | "desktop" | "tablet" | "mobile">("full");
const deviceWidths: Record<string, string> = { full: "100%", desktop: "1280px", tablet: "768px", mobile: "375px" };
const [abortController, setAbortController] = useState<AbortController | null>(null);
// Agent suggestion state
@@ -668,9 +671,30 @@ export default function AIAssist() {
return m;
});
// Web search grounding: enrich prompt with search results if enabled
let enrichedInput = finalInput;
if (webSearchEnabled) {
setStatus("Searching the web...");
try {
const searchRes = await fetch("/api/search?q=" + encodeURIComponent(finalInput.split("\n")[0].substring(0, 200)));
if (searchRes.ok) {
const searchData = await searchRes.json();
if (searchData.results && searchData.results.length > 0) {
const searchContext = searchData.results.slice(0, 5).map((r: { title: string; url: string; snippet: string }, i: number) =>
`${i + 1}. **${r.title}** (${r.url}) - ${r.snippet}`
).join("\n");
enrichedInput = `[WEB SEARCH CONTEXT - Top 5 relevant results]\n${searchContext}\n\n---\nUsing the above search results as reference context, answer the user query. Cite sources when relevant.\n\nUser query: ${finalInput}`;
}
}
} catch (e) {
console.warn("Web search failed:", e);
}
setStatus(null);
}
const response = await modelAdapter.generateAIAssistStream(
{
messages: [...formattedHistory, { role: "user" as const, content: finalInput, timestamp: new Date() }],
messages: [...formattedHistory, { role: "user" as const, content: enrichedInput, timestamp: new Date() }],
currentAgent,
onChunk: (chunk) => {
accumulated += chunk;
@@ -761,6 +785,12 @@ export default function AIAssist() {
handleSendMessage(undefined, "Approved. Please generate the code according to the plan.", true);
};
const reviewCode = () => {
if (!previewData?.data) return;
setAssistStep("generating");
const reviewPrompt = "Please review this generated code for bugs, security issues, performance problems, and best practices. Provide specific improvements:" + "\n" + "\n" + "```" + (previewData.language || "code") + "\n" + previewData.data + "\n" + "```";
};
const stopGeneration = () => {
if (abortController) {
abortController.abort();
@@ -868,10 +898,20 @@ export default function AIAssist() {
<select
value={selectedModels[selectedProvider]}
onChange={(e) => setSelectedModel(selectedProvider, e.target.value)}
className="text-[10px] font-bold h-8 px-2 rounded-lg border border-blue-100 dark:border-blue-900 bg-white/80 dark:bg-[#0b1414]/80 focus:ring-2 focus:ring-blue-400/30 transition-all outline-none min-w-[120px]"
className="text-[10px] font-bold h-8 px-2 rounded-lg border border-blue-100 dark:border-blue-900 bg-white/80 dark:bg-[#0b1414]/80 focus:ring-2 focus:ring-blue-400/30 transition-all outline-none min-w-[120px] text-slate-900 dark:text-white"
>
{availableModels.map(m => <option key={m} value={m}>{m}</option>)}
{availableModels.map(m => <option key={m} value={m} className="bg-[#0b1414] text-white">{m}</option>)}
</select>
<button
onClick={() => setWebSearchEnabled(prev => !prev)}
className={`flex items-center gap-1 h-8 px-2 rounded-lg text-[10px] font-bold transition-all border ${
webSearchEnabled
? "bg-amber-500/20 border-amber-500/40 text-amber-300"
: "bg-transparent border-transparent text-slate-400/70 hover:text-slate-300 hover:bg-slate-500/10"
}`}
>
<Search className="h-3.5 w-3.5" /> {webSearchEnabled ? "ON" : "Search"}
</button>
<div className="flex items-center gap-1 ml-1 border-l border-blue-100 dark:border-blue-900 pl-2">
<Button
variant="ghost"
@@ -1143,14 +1183,14 @@ export default function AIAssist() {
onClick={() => { setAiPlan(null); setAssistStep("idle"); setInput("Modify this plan: "); setTimeout(() => { const el = document.querySelector<HTMLInputElement>(`[data-ai-input]`); if (el) el.focus(); }, 100); }}
disabled={isProcessing}
variant="outline"
className="bg-slate-500/10 hover:bg-slate-500/20 border-slate-500/20 text-slate-300 font-black uppercase text-[10px] tracking-widest py-4 rounded-xl"
className="bg-slate-500/10 hover:bg-slate-500/20 border-slate-500/20 text-slate-300 font-black uppercase text-[9px] tracking-wider py-4 rounded-xl min-w-0"
>
<LayoutPanelLeft className="h-3.5 w-3.5 mr-1.5" /> {t.modifyPlan}
</Button>
<Button
onClick={approveAndGenerate}
disabled={isProcessing}
className="bg-blue-600 hover:bg-blue-500 text-white font-black uppercase text-[10px] tracking-widest py-4 rounded-xl shadow-lg shadow-blue-500/20"
className="bg-blue-600 hover:bg-blue-500 text-white font-black uppercase text-[9px] tracking-wider py-4 rounded-xl shadow-lg shadow-blue-500/20 min-w-0"
>
{isProcessing ? t.startingEngine : t.startCoding}
</Button>
@@ -1167,7 +1207,7 @@ export default function AIAssist() {
</div>
)}
{msg.role === "assistant" && msg.preview && (
{msg.role === "assistant" && msg.preview && !(assistStep === "preview" && i === aiAssistHistory.length - 1 && !isProcessing) && (
<Button
variant="secondary"
size="sm"
@@ -1185,19 +1225,27 @@ export default function AIAssist() {
{/* Post-coding action buttons */}
{msg.role === "assistant" && assistStep === "preview" && i === aiAssistHistory.length - 1 && !isProcessing && (
<div className="mt-4 grid grid-cols-2 gap-2 animate-in zoom-in-95 duration-300">
<div className="mt-4 grid grid-cols-3 gap-2 animate-in zoom-in-95 duration-300">
<Button
onClick={() => { setShowCanvas(true); setViewMode(isPreviewRenderable(previewData as PreviewData) ? "preview" : "code"); }}
className="bg-blue-600 hover:bg-blue-500 text-white font-black uppercase text-[10px] tracking-widest py-4 rounded-xl shadow-lg shadow-blue-500/20"
>
<Zap className="h-3.5 w-3.5 mr-1.5" /> {t.activateArtifact}
<Zap className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Preview</span>
</Button>
<Button
onClick={reviewCode}
disabled={!previewData?.data}
variant="outline"
className="bg-emerald-500/10 hover:bg-emerald-500/20 border-emerald-500/20 text-emerald-300 font-black uppercase text-[9px] tracking-wider py-4 rounded-xl min-w-0"
>
<ShieldCheck className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Review</span>
</Button>
<Button
onClick={() => { setAssistStep("idle"); setInput("Modify this: "); setTimeout(() => { const el = document.querySelector<HTMLInputElement>(`[data-ai-input]`); if (el) el.focus(); }, 100); }}
variant="outline"
className="bg-slate-500/10 hover:bg-slate-500/20 border-slate-500/20 text-slate-300 font-black uppercase text-[10px] tracking-widest py-4 rounded-xl"
>
<LayoutPanelLeft className="h-3.5 w-3.5 mr-1.5" /> Request Modifications
<LayoutPanelLeft className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Modify</span>
</Button>
</div>
)}
@@ -1302,6 +1350,24 @@ export default function AIAssist() {
{t.inspectCode}
</button>
</div>
{viewMode === "preview" && (
<div className="flex items-center gap-1 mt-1.5">
{([["full", "Full"], ["desktop", "Desktop"], ["tablet", "Tablet"], ["mobile", "Mobile"]] as const).map(([size, label]) => (
<button
key={size}
onClick={() => setDeviceSize(size)}
className={cn(
"px-2 py-1 text-[9px] font-black uppercase rounded-md transition-all border",
deviceSize === size
? "bg-blue-500 text-white border-blue-500 shadow-md"
: "text-blue-300/50 border-transparent hover:text-blue-200 hover:bg-blue-900/30"
)}
>
{label}
</button>
))}
</div>
)}
</div>
</div>
<div className="flex items-center gap-3">
@@ -1345,14 +1411,22 @@ export default function AIAssist() {
</div>
</div>
<div className="flex-1 overflow-hidden relative">
<div className="flex-1 overflow-auto relative bg-[#050505]">
{viewMode === "preview" && currentPreviewData ? (
<CanvasErrorBoundary>
<div className="mx-auto transition-all duration-300 h-full"
style={deviceSize !== "full"
? { width: deviceWidths[deviceSize], maxWidth: "100%",
border: "1px solid rgba(30,58,138,0.3)",
borderRadius: "12px", overflow: "hidden",
boxShadow: "0 20px 60px rgba(0,0,0,0.5)" }
: undefined}>
<LiveCanvas
data={currentPreviewData.data || ""}
type={currentPreviewData.type || "preview"}
isStreaming={!!currentPreviewData.isStreaming}
/>
</div>
</CanvasErrorBoundary>
) : (
<div className="h-full bg-[#050505] p-8 font-mono text-sm overflow-auto scrollbar-thin scrollbar-thumb-blue-900">