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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user