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

@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.4.0] - 2026-03-18 19:57 UTC
### Added
- **Review Code Button** — Post-coding action to send generated code back to AI for review
- `reviewCode()` function sends code with review-focused prompt
- Emerald-green "Review" button alongside Preview and Modify
- 3-column post-coding action grid: Preview / Review / Modify
- **Web Search Grounding** — Enrich AI prompts with live web search results
- `lib/services/search-api.ts` — SearXNG public API wrapper with 4 instance fallback
- `/api/search` route — server-side search proxy endpoint
- Toggle button in toolbar to enable/disable (amber highlight when active)
- Shows "Searching the web..." status while fetching
- Appends top 5 results as `[WEB SEARCH CONTEXT]` to user prompt
- **Responsive Preview** — Device size selector in canvas panel
- Full / Desktop (1280px) / Tablet (768px) / Mobile (375px) buttons
- Centered device frame with border, shadow, and scroll on overflow
- Only visible in preview mode, below Live Render / Inspect Code tabs
### Fixed
- **Model selector text color** — Option text now white on dark theme (`bg-[#0b1414] text-white`)
- **Button text overflow** — Shortened labels (Preview/Review/Modify), added `min-w-0` for proper truncation
- **Duplicate activateArtifact button** — Original button now hides when post-coding action row is shown
### Technical Details
- Files modified: 1 (AIAssist.tsx: +85/-11 lines)
- Files added: 2 (`lib/services/search-api.ts`, `app/api/search/route.ts`)
- New state: `deviceSize`, `webSearchEnabled`
- New function: `reviewCode()`
## [1.3.0] - 2026-03-18 18:51 UTC
### Added

View File

@@ -47,6 +47,11 @@ Transform vague ideas into production-ready prompts and PRDs. PromptArch is an A
- Live code rendering with `[PREVIEW]` tags
- HTML, React, Python, and more — rendered in-browser
- Auto-detect renderable vs. code-only previews
- Responsive preview with device size selector (Full / Desktop / Tablet / Mobile)
### Code Review & Web Search
- **Review Code** — Send generated code back to AI for bug/security/performance review
- **Web Search Grounding** — Toggle to enrich prompts with live web search results via SearXNG
### Enhanced Prompt Engine
- 9 strategies: clarify, add-context, add-constraints, structure, add-examples, set-tone, expand, simplify, chain-of-thought
@@ -136,6 +141,7 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
| Version | Date | Highlights |
|---------|------|------------|
| [1.4.0](CHANGELOG.md#140---2026-03-18) | 2026-03-18 19:57 | Review Code button, web search grounding, responsive preview, model selector fix |
| [1.3.0](CHANGELOG.md#130---2026-03-18) | 2026-03-18 18:51 | Plan-first workflow, OpenRouter, post-coding UX, enhanced prompt engine |
| [1.2.0](CHANGELOG.md#120---2026-01-19) | 2026-01-19 19:16 | SEO agent fixes, Z.AI API validation |
| [1.1.0](CHANGELOG.md#110---2025-12-29) | 2025-12-29 17:55 | GitHub push, XLSX/HTML export, OAuth management |

27
app/api/search/route.ts Normal file
View File

@@ -0,0 +1,27 @@
/**
* Next.js API route: Web search proxy.
* Calls SearXNG public instances and returns top results.
* Endpoint: GET /api/search?q=your+query
*/
import { NextRequest, NextResponse } from "next/server";
import { searchWeb } from "@/lib/services/search-api";
export async function GET(request: NextRequest) {
const query = request.nextUrl.searchParams.get("q");
if (!query || query.trim().length < 3) {
return NextResponse.json({ results: [], error: "Query too short" });
}
try {
const results = await searchWeb(query);
return NextResponse.json({ results });
} catch (error) {
console.error("Search API error:", error);
return NextResponse.json(
{ results: [], error: "Search failed" },
{ status: 500 }
);
}
}

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">

View File

@@ -0,0 +1,66 @@
/**
* Web search API wrapper using SearXNG public instances.
* No API key required — free for server-side use.
*/
export interface SearchResult {
title: string;
url: string;
snippet: string;
}
const SEARXNG_INSTANCES = [
"https://searx.be",
"https://search.sapti.me",
"https://searx.tiekoetter.com",
"https://search.bus-hit.me",
];
async function searchSearXNG(query: string): Promise<SearchResult[]> {
for (const instance of SEARXNG_INSTANCES) {
try {
const url = `${instance}/search?q=${encodeURIComponent(query)}&format=json&categories=general&language=en`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
const res = await fetch(url, {
signal: controller.signal,
headers: {
"User-Agent": "PromptArch/1.4 (https://rommark.dev)",
Accept: "application/json",
},
});
clearTimeout(timeout);
if (!res.ok) continue;
const data = await res.json();
const results: SearchResult[] = (data.results || [])
.slice(0, 8)
.map((r: Record<string, string>) => ({
title: r.title || "",
url: r.url || "",
snippet: r.content || "",
}))
.filter((r: SearchResult) => r.title && r.url);
if (results.length > 0) return results;
} catch {
// Try next instance
}
}
return [];
}
export async function searchWeb(query: string): Promise<SearchResult[]> {
// Clean the query — take first meaningful line, max 200 chars
const cleanQuery = query
.split("\n")[0]
.replace(/\[.*?\]/g, "")
.trim()
.substring(0, 200);
if (!cleanQuery || cleanQuery.length < 3) return [];
return searchSearXNG(cleanQuery);
}

View File

@@ -1,6 +1,6 @@
{
"name": "promptarch",
"version": "1.3.0",
"version": "1.4.0",
"description": "Transform vague ideas into production-ready prompts and PRDs",
"scripts": {
"dev": "next dev",