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:
29
CHANGELOG.md
29
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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
27
app/api/search/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
66
lib/services/search-api.ts
Normal file
66
lib/services/search-api.ts
Normal 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);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user