diff --git a/app/api/ai-assist/route.ts b/app/api/ai-assist/route.ts new file mode 100644 index 0000000..0e97e96 --- /dev/null +++ b/app/api/ai-assist/route.ts @@ -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 }); + } +} diff --git a/app/api/slides/route.ts b/app/api/slides/route.ts new file mode 100644 index 0000000..64e944d --- /dev/null +++ b/app/api/slides/route.ts @@ -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 }); + } +} diff --git a/components/AIAssist.tsx b/components/AIAssist.tsx index 690c00d..60c037c 100644 --- a/components/AIAssist.tsx +++ b/components/AIAssist.tsx @@ -267,6 +267,10 @@ export default function AIAssist() { const [viewMode, setViewMode] = useState<"preview" | "code">("preview"); const [abortController, setAbortController] = useState(null); + // Agentic States + const [assistStep, setAssistStep] = useState<"idle" | "plan" | "generating" | "preview">("idle"); + const [aiPlan, setAiPlan] = useState(null); + const scrollRef = useRef(null); const isPreviewRenderable = (preview?: PreviewData | null) => { if (!preview) return false; @@ -309,23 +313,28 @@ export default function AIAssist() { loadModels(); }, [selectedProvider, selectedModels, setSelectedModel]); - const handleSendMessage = async (e?: React.FormEvent) => { + const handleSendMessage = async (e?: React.FormEvent, forcedPrompt?: string) => { if (e) e.preventDefault(); - if (!input.trim() || isProcessing) return; + const finalInput = forcedPrompt || input; + if (!finalInput.trim() || isProcessing) return; const controller = new AbortController(); setAbortController(controller); - const userMsg: AIAssistMessage = { - role: "user", - content: input, - timestamp: new Date(), - }; + // UI Update for user message + if (!forcedPrompt) { + const userMsg: AIAssistMessage = { + role: "user", + content: finalInput, + timestamp: new Date(), + }; + const newHistory = [...aiAssistHistory, userMsg]; + setAIAssistHistory(newHistory); + setInput(""); + } - const newHistory = [...aiAssistHistory, userMsg]; - setAIAssistHistory(newHistory); - setInput(""); setIsProcessing(true); + if (assistStep === "idle") setAssistStep("plan"); const assistantMsg: AIAssistMessage = { role: "assistant", @@ -333,21 +342,42 @@ export default function AIAssist() { agent: currentAgent, timestamp: new Date() }; - setAIAssistHistory([...newHistory, assistantMsg]); + setAIAssistHistory(prev => [...prev, assistantMsg]); 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 lastParsedPreview: PreviewData | null = null; const response = await modelAdapter.generateAIAssistStream( { - messages: newHistory, + messages: [...aiAssistHistory, { role: "system", content: prompt } as any], currentAgent, onChunk: (chunk) => { accumulated += chunk; 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)) { setPreviewData(preview); lastParsedPreview = preview; @@ -374,40 +404,17 @@ export default function AIAssist() { 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) { 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 => { const last = prev[prev.length - 1]; 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 = () => { if (abortController) { abortController.abort(); @@ -434,6 +446,8 @@ export default function AIAssist() { setAIAssistHistory([]); setPreviewData(null); setShowCanvas(false); + setAssistStep("idle"); + setAiPlan(null); }; return ( @@ -572,6 +586,42 @@ export default function AIAssist() { + {/* Agentic Plan Review Card */} + {msg.role === "assistant" && aiPlan && i === aiAssistHistory.length - 1 && assistStep === "plan" && ( +
+

+ Proposed Solution Plan +

+
+
+

Architecture

+

{aiPlan.architecture}

+
+
+
+

Tech Stack

+
+ {aiPlan.techStack?.map((t: string) => ( + {t} + ))} +
+
+
+

Files

+

{aiPlan.files?.length} modules planned

+
+
+ +
+
+ )} + {msg.role === "assistant" && msg.preview && ( - + { + showCanvas && ( +
+ +
+
+
+ {viewMode === "preview" ? : } +
+
+

{previewData?.type || "Live"} Canvas

+
+ + +
-
-
- - -
-
- -
- {viewMode === "preview" && previewData ? ( - - ) : ( -
-
-                                        {previewData?.data}
-                                    
+
+ +
- )} -
- -
-
-
- - {previewData?.isStreaming ? "Neural Link Active" : "Sync Complete"} -
- - {previewData?.language?.toUpperCase()} UTF-8 - -
- -
- )} + +
+ {viewMode === "preview" && previewData ? ( + + ) : ( +
+
+                                            {previewData?.data}
+                                        
+
+ )} +
+ +
+
+
+ + {previewData?.isStreaming ? "Neural Link Active" : "Sync Complete"} + +
+ + {previewData?.language?.toUpperCase()} UTF-8 + +
+ +
+ ) + } -
+ ); } diff --git a/components/SlidesGenerator.tsx b/components/SlidesGenerator.tsx index c553247..8c82609 100644 --- a/components/SlidesGenerator.tsx +++ b/components/SlidesGenerator.tsx @@ -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 = () => { if (!slidesPresentation) return; @@ -1205,9 +1226,12 @@ export default function SlidesGenerator() { - +