feat: PromptArch v2.1 - Agentic AI Assist 3-Step Flow + Slides Enhancements

- Implemented Plan -> Approve -> Generate workflow in AIAssist
- Added Plan Review Card with architecture/tech stack preview
- Added Reveal.js Markdown export to SlidesGenerator
- Created /api/ai-assist and /api/slides orchestration routes
- Fixed all syntax errors and build issues
- Maintained premium blue/teal glassmorphism design
This commit is contained in:
Gemini AI
2025-12-28 23:48:03 +04:00
Unverified
parent 37612bbac2
commit ba1fee3fc7
4 changed files with 306 additions and 118 deletions

View File

@@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
// We'll use the environment variables for provider routing
const schema = z.object({
request: z.string().min(10),
step: z.enum(["plan", "generate", "preview"]).default("plan"),
plan: z.any().optional(),
code: z.string().optional(),
provider: z.string().optional(),
model: z.string().optional()
});
const STEPS = {
plan: `You are an expert software architect. Create a DETAILED DEVELOPMENT PLAN for the following request: "{request}"
Output ONLY a JSON object:
{
"summary": "One sentence overview",
"architecture": "High-level components + data flow",
"techStack": ["Next.js", "Tailwind", "Lucide Icons"],
"files": [
{"path": "app/page.tsx", "purpose": "Main UI"},
{"path": "components/Preview.tsx", "purpose": "Core logic"}
],
"timeline": "Estimate",
"risks": ["Potential blockers"]
}`,
generate: `You are a Senior Vibe Coder. Execute the following approved plan:
Plan: {plan}
Generate COMPLETE, PRODUCTION-READY code for all files.
Focus on the request: "{request}"
Output ONLY a JSON object:
{
"files": {
"app/page.tsx": "// code here",
"components/UI.tsx": "// more code"
},
"explanation": "How it works"
}`,
preview: `Convert the following code into a single-file interactive HTML preview (Standalone).
Use Tailwind CDN.
Code: {code}
Output ONLY valid HTML.`
};
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { request, step, plan, code } = schema.parse(body);
let prompt = STEPS[step];
prompt = prompt.replace("{request}", request);
if (plan) prompt = prompt.replace("{plan}", JSON.stringify(plan));
if (code) prompt = prompt.replace("{code}", code);
// In a real scenario, this would call the ModelAdapter/Service
// For now, we'll return a structure that the frontend can handle,
// instructing it to use the existing streaming adapter for the heavy lifting.
return NextResponse.json({
prompt,
step,
success: true
});
} catch (error: any) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
}
}

37
app/api/slides/route.ts Normal file
View File

@@ -0,0 +1,37 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
const schema = z.object({
topic: z.string().min(3),
slideCount: z.number().min(3).max(15).default(8),
style: z.enum(["professional", "creative", "technical", "pitch"]).default("professional"),
});
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { topic, slideCount, style } = schema.parse(body);
const systemPrompt = `You are an elite presentation designer. Create a visually stunning presentation with ${slideCount} slides about "${topic}".
Style: ${style}
Output ONLY a sequence of slides separated by "---".
Format each slide as:
## [Slide Title]
- [Bullet Point 1]
- [Bullet Point 2]
VISUAL: [Detailed description of image/chart/icon]
---
`;
// The frontend will handle the actual generation call to keep use of the ModelAdapter,
// this route serves as the prompt orchestrator.
return NextResponse.json({
prompt: systemPrompt,
success: true
});
} catch (error: any) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
}
}

View File

@@ -267,6 +267,10 @@ export default function AIAssist() {
const [viewMode, setViewMode] = useState<"preview" | "code">("preview"); const [viewMode, setViewMode] = useState<"preview" | "code">("preview");
const [abortController, setAbortController] = useState<AbortController | null>(null); const [abortController, setAbortController] = useState<AbortController | null>(null);
// Agentic States
const [assistStep, setAssistStep] = useState<"idle" | "plan" | "generating" | "preview">("idle");
const [aiPlan, setAiPlan] = useState<any>(null);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const isPreviewRenderable = (preview?: PreviewData | null) => { const isPreviewRenderable = (preview?: PreviewData | null) => {
if (!preview) return false; if (!preview) return false;
@@ -309,23 +313,28 @@ export default function AIAssist() {
loadModels(); loadModels();
}, [selectedProvider, selectedModels, setSelectedModel]); }, [selectedProvider, selectedModels, setSelectedModel]);
const handleSendMessage = async (e?: React.FormEvent) => { const handleSendMessage = async (e?: React.FormEvent, forcedPrompt?: string) => {
if (e) e.preventDefault(); if (e) e.preventDefault();
if (!input.trim() || isProcessing) return; const finalInput = forcedPrompt || input;
if (!finalInput.trim() || isProcessing) return;
const controller = new AbortController(); const controller = new AbortController();
setAbortController(controller); setAbortController(controller);
// UI Update for user message
if (!forcedPrompt) {
const userMsg: AIAssistMessage = { const userMsg: AIAssistMessage = {
role: "user", role: "user",
content: input, content: finalInput,
timestamp: new Date(), timestamp: new Date(),
}; };
const newHistory = [...aiAssistHistory, userMsg]; const newHistory = [...aiAssistHistory, userMsg];
setAIAssistHistory(newHistory); setAIAssistHistory(newHistory);
setInput(""); setInput("");
}
setIsProcessing(true); setIsProcessing(true);
if (assistStep === "idle") setAssistStep("plan");
const assistantMsg: AIAssistMessage = { const assistantMsg: AIAssistMessage = {
role: "assistant", role: "assistant",
@@ -333,21 +342,42 @@ export default function AIAssist() {
agent: currentAgent, agent: currentAgent,
timestamp: new Date() timestamp: new Date()
}; };
setAIAssistHistory([...newHistory, assistantMsg]); setAIAssistHistory(prev => [...prev, assistantMsg]);
try { try {
// First, get the plan orchestrator prompt from our new API
const apiRes = await fetch("/api/ai-assist", {
method: "POST",
body: JSON.stringify({
request: finalInput,
step: assistStep === "plan" ? "generate" : "plan",
plan: aiPlan
}),
});
const { prompt } = await apiRes.json();
let accumulated = ""; let accumulated = "";
let lastParsedPreview: PreviewData | null = null; let lastParsedPreview: PreviewData | null = null;
const response = await modelAdapter.generateAIAssistStream( const response = await modelAdapter.generateAIAssistStream(
{ {
messages: newHistory, messages: [...aiAssistHistory, { role: "system", content: prompt } as any],
currentAgent, currentAgent,
onChunk: (chunk) => { onChunk: (chunk) => {
accumulated += chunk; accumulated += chunk;
const { chatDisplay, preview, agent } = parseStreamingContent(accumulated); const { chatDisplay, preview, agent } = parseStreamingContent(accumulated);
// Only update preview state if it actually changed to avoid iframe jitters // If we're in planning mode and see JSON, try to parse the plan
if (assistStep === "plan" || assistStep === "idle") {
const jsonMatch = accumulated.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
const parsed = JSON.parse(jsonMatch[0]);
if (parsed.summary && parsed.files) setAiPlan(parsed);
} catch (e) { }
}
}
if (preview && JSON.stringify(preview) !== JSON.stringify(lastParsedPreview)) { if (preview && JSON.stringify(preview) !== JSON.stringify(lastParsedPreview)) {
setPreviewData(preview); setPreviewData(preview);
lastParsedPreview = preview; lastParsedPreview = preview;
@@ -374,40 +404,17 @@ export default function AIAssist() {
selectedProvider, selectedProvider,
selectedModels[selectedProvider] selectedModels[selectedProvider]
); );
if (!response.success) {
throw new Error(response.error || "Streaming failed"); if (!response.success) throw new Error(response.error);
if (assistStep === "plan" || assistStep === "idle") {
setAssistStep("plan");
} else {
setAssistStep("preview");
} }
} catch (error) { } catch (error) {
console.error("Assist error:", error); console.error("Assist error:", error);
try {
const fallback = await modelAdapter.generateAIAssist(
{ messages: newHistory, currentAgent }
);
if (fallback.success && fallback.data) {
const { chatDisplay, preview, agent } = parseStreamingContent(fallback.data);
if (preview) {
setPreviewData(preview);
setShowCanvas(true);
setViewMode(isPreviewRenderable(preview) ? "preview" : "code");
}
setAIAssistHistory(prev => {
const last = prev[prev.length - 1];
const nextAgent = agent || currentAgent;
if (last && last.role === "assistant") {
return [...prev.slice(0, -1), {
...last,
content: chatDisplay || fallback.data,
agent: nextAgent,
preview: preview ? { type: preview.type, data: preview.data, language: preview.language } : undefined
} as AIAssistMessage];
}
return prev;
});
return;
}
} catch (fallbackError) {
console.error("Assist fallback error:", fallbackError);
}
setAIAssistHistory(prev => { setAIAssistHistory(prev => {
const last = prev[prev.length - 1]; const last = prev[prev.length - 1];
const message = error instanceof Error ? error.message : "AI Assist failed"; const message = error instanceof Error ? error.message : "AI Assist failed";
@@ -422,6 +429,11 @@ export default function AIAssist() {
} }
}; };
const approveAndGenerate = () => {
setAssistStep("generating");
handleSendMessage(undefined, "Approved. Please generate the code according to the plan.");
};
const stopGeneration = () => { const stopGeneration = () => {
if (abortController) { if (abortController) {
abortController.abort(); abortController.abort();
@@ -434,6 +446,8 @@ export default function AIAssist() {
setAIAssistHistory([]); setAIAssistHistory([]);
setPreviewData(null); setPreviewData(null);
setShowCanvas(false); setShowCanvas(false);
setAssistStep("idle");
setAiPlan(null);
}; };
return ( return (
@@ -572,6 +586,42 @@ export default function AIAssist() {
</ReactMarkdown> </ReactMarkdown>
</div> </div>
{/* Agentic Plan Review Card */}
{msg.role === "assistant" && aiPlan && i === aiAssistHistory.length - 1 && assistStep === "plan" && (
<div className="mt-6 p-6 rounded-2xl bg-blue-500/5 border border-blue-500/20 backdrop-blur-sm animate-in zoom-in-95 duration-300">
<h3 className="text-sm font-black text-blue-400 uppercase tracking-widest mb-4 flex items-center gap-2">
<LayoutPanelLeft className="h-4 w-4" /> Proposed Solution Plan
</h3>
<div className="space-y-4">
<div>
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">Architecture</p>
<p className="text-xs text-slate-400">{aiPlan.architecture}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">Tech Stack</p>
<div className="flex flex-wrap gap-1">
{aiPlan.techStack?.map((t: string) => (
<Badge key={t} variant="outline" className="text-[9px] border-blue-500/30 text-blue-300 px-1.5 py-0">{t}</Badge>
))}
</div>
</div>
<div>
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">Files</p>
<p className="text-[10px] text-slate-400">{aiPlan.files?.length} modules planned</p>
</div>
</div>
<Button
onClick={approveAndGenerate}
disabled={isProcessing}
className="w-full mt-4 bg-blue-600 hover:bg-blue-500 text-white font-black uppercase text-[10px] tracking-widest py-5 rounded-xl shadow-lg shadow-blue-500/20"
>
{isProcessing ? "Starting Engine..." : "Approve & Generate Development"}
</Button>
</div>
</div>
)}
{msg.role === "assistant" && msg.preview && ( {msg.role === "assistant" && msg.preview && (
<Button <Button
variant="secondary" variant="secondary"
@@ -644,7 +694,8 @@ export default function AIAssist() {
</div> </div>
{/* --- 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-[#081010] rounded-[2.5rem] overflow-hidden border border-blue-900/60 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-blue-900/60 shadow-[0_20px_80px_rgba(0,0,0,0.6)]">
<div className="px-6 py-5 border-b border-blue-900/60 bg-[#0b1414]/70 backdrop-blur-2xl flex items-center justify-between shrink-0"> <div className="px-6 py-5 border-b border-blue-900/60 bg-[#0b1414]/70 backdrop-blur-2xl flex items-center justify-between shrink-0">
@@ -719,7 +770,8 @@ export default function AIAssist() {
</div> </div>
</Card> </Card>
</div> </div>
)} )
}
<style jsx global>{` <style jsx global>{`
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700;800&display=swap');
.ai-assist { .ai-assist {

View File

@@ -623,6 +623,27 @@ export default function SlidesGenerator() {
} }
}; };
const handleDownloadMarkdown = () => {
if (!slidesPresentation) return;
// Reveal.js format
const markdown = slidesPresentation.slides.map(s => {
// Strip HTML but keep structure
const cleanContent = s.content.replace(/<[^>]*>/g, '').trim();
return `## ${s.title}\n\n${cleanContent}`;
}).join('\n\n---\n\n');
const blob = new Blob([markdown], { type: "text/markdown" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${slidesPresentation.title.replace(/\s+/g, '_')}_slides.md`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const handleDownloadHtml = () => { const handleDownloadHtml = () => {
if (!slidesPresentation) return; if (!slidesPresentation) return;
@@ -1205,9 +1226,12 @@ export default function SlidesGenerator() {
<Button variant="ghost" size="icon" onClick={toggleFullscreen} className="h-8 w-8"> <Button variant="ghost" size="icon" onClick={toggleFullscreen} className="h-8 w-8">
{isFullscreen ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />} {isFullscreen ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
</Button> </Button>
<Button variant="ghost" size="icon" onClick={handleDownloadHtml} className="h-8 w-8"> <Button variant="ghost" size="icon" onClick={handleDownloadHtml} title="Download HTML Presentation" className="h-8 w-8">
<Download className="h-3.5 w-3.5" /> <Download className="h-3.5 w-3.5" />
</Button> </Button>
<Button variant="ghost" size="icon" onClick={handleDownloadMarkdown} title="Export Reveal.js Markdown" className="h-8 w-8 text-blue-500">
<FileText className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8"> <Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8">
{copied ? <CheckCircle2 className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />} {copied ? <CheckCircle2 className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
</Button> </Button>