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:
75
app/api/ai-assist/route.ts
Normal file
75
app/api/ai-assist/route.ts
Normal 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
37
app/api/slides/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user