Spaces:
Running
Running
| "use client"; | |
| import { useEffect, useState } from "react"; | |
| import { useUser } from "@/hooks/useUser"; | |
| import { useAi } from "@/hooks/useAi"; | |
| import { useRouter } from "next/navigation"; | |
| import { Button } from "@/components/ui/button"; | |
| import { toast } from "sonner"; | |
| import { NEW_STACKS, NewStackId } from "@/lib/new-stacks"; | |
| function buildInstruction(stack: NewStackId, lang: "js" | "ts", title?: string) { | |
| const jsOrTs = lang === 'ts' ? 'TypeScript' : 'JavaScript'; | |
| const reactEntry = lang === 'ts' ? "/frontend/src/main.tsx and /frontend/src/App.tsx" : "/frontend/src/main.jsx and /frontend/src/App.jsx"; | |
| switch (stack) { | |
| case 'express-react': | |
| return `Initialize a complete full-stack web project with the following structure and runnable code. | |
| Title: ${title || 'OmniDev Full-Stack App'} | |
| Requirements: | |
| - Frontend under /frontend using React + Vite (${jsOrTs}), TailwindCSS preconfigured. | |
| - Backend under /backend using Express (${jsOrTs}, ESM), with basic routes (GET /, GET /health) and CORS enabled. | |
| - Create a visually striking Hero section (animated) appropriate to the app, optimized and accessible. | |
| - Add a minimal README.md at root with start instructions. | |
| - Provide package.json in both /frontend and /backend with scripts to start dev/prod. | |
| - Provide /frontend/index.html and ${reactEntry}. | |
| - Provide /backend/server.${lang === 'ts' ? 'ts' : 'js'}. | |
| - Use ports 5173 for frontend and 3000 for backend. | |
| - Keep everything simple and runnable. | |
| - Return STRICT JSON ONLY as file updates (no markdown), paths rooted from repo (e.g., /frontend/..., /backend/...).`; | |
| case 'nextjs': | |
| return `Scaffold a full-stack Next.js 15 App Router project. | |
| Title: ${title || 'OmniDev Next App'} | |
| Requirements: | |
| - Next.js (${jsOrTs}), App Router, TailwindCSS. | |
| - Implement a landing page with an animated Hero section suitable for the domain. | |
| - Add /api/health route that returns { ok: true }. | |
| - Provide package.json with dev/build scripts. | |
| - Keep it simple and runnable with \'next dev\'. | |
| - Return STRICT JSON ONLY as file updates (no markdown), paths rooted from repo (e.g., /app/page.${lang === 'ts' ? 'tsx' : 'jsx'}, /app/api/health/route.${lang === 'ts' ? 'ts' : 'js'}).`; | |
| case 'nestjs-react': | |
| return `Initialize a NestJS backend and React (Vite) frontend. | |
| Title: ${title || 'OmniDev Nest + React'} | |
| Requirements: | |
| - Backend under /backend using NestJS (${jsOrTs}). Generate AppModule, AppController with GET / and GET /health. Enable CORS. | |
| - Frontend under /frontend using React + Vite (${jsOrTs}), TailwindCSS with a modern animated Hero section appropriate to the app. | |
| - Provide package.json in both apps with start/build scripts. | |
| - Return STRICT JSON ONLY as file updates (no markdown), paths rooted from repo.`; | |
| } | |
| } | |
| export default function ScaffoldNew() { | |
| const { user, openLoginWindow } = useUser(); | |
| const { model, provider } = useAi(); | |
| const router = useRouter(); | |
| const [loading, setLoading] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [logs, setLogs] = useState<string[]>([]); | |
| const [stack, setStack] = useState<NewStackId>("express-react"); | |
| const [lang, setLang] = useState<"js" | "ts">("js"); | |
| const [title, setTitle] = useState<string>(""); | |
| async function runScaffold() { | |
| if (!user) { | |
| await openLoginWindow(); | |
| return; | |
| } | |
| setLoading(true); | |
| setError(null); | |
| setLogs(["Starting scaffold via Augment..."]); | |
| try { | |
| // 1) Call augment to generate full-stack files | |
| const instruction = buildInstruction(stack, lang, title); | |
| const aug = await fetch('/api/augment', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| context: '/ (empty project)', | |
| instruction, | |
| language: lang === 'ts' ? 'typescript' : 'javascript', | |
| framework: stack, | |
| response_type: 'file_updates', | |
| // Force Gemini for scaffold stage | |
| model: 'gemini-2.5-flash', | |
| provider: 'google', | |
| }), | |
| }).then(r => r.json()); | |
| if (!aug?.ok || !Array.isArray(aug.files) || aug.files.length === 0) { | |
| throw new Error(aug?.message || 'Augment did not return files'); | |
| } | |
| setLogs(prev => [...prev, `Augment produced ${aug.files.length} files`]); | |
| // 2) Create space with initial files | |
| const created = await fetch('/api/me/projects', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ title: title || 'OmniDev Project', initialFiles: aug.files.map((f: any) => ({ path: f.path, content: f.content })) }) | |
| }).then(r => r.json()); | |
| if (!created?.space?.project?.space_id) { | |
| throw new Error(created?.error || 'Failed to create project'); | |
| } | |
| setLogs(prev => [...prev, 'Project created, redirecting...']); | |
| router.push(`/${created.space.project.space_id}`); | |
| } catch (e: any) { | |
| setError(e?.message || 'Scaffold failed'); | |
| toast.error(e?.message || 'Scaffold failed'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| // Removed auto-run; wait for user selection | |
| return ( | |
| <section className="max-w-3xl mx-auto p-6 text-neutral-200"> | |
| <h1 className="text-2xl font-semibold mb-1">Create New Project</h1> | |
| <p className="text-sm text-neutral-400 mb-4">Choose your stack, then OmniDev will scaffold a complete project using AI.</p> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label className="text-sm block mb-1">Project Title</label> | |
| <input className="w-full bg-neutral-800 rounded p-2 text-sm" placeholder="OmniDev Project" value={title} onChange={(e) => setTitle(e.target.value)} /> | |
| </div> | |
| <div> | |
| <label className="text-sm block mb-1">Language</label> | |
| <select className="w-full bg-neutral-800 rounded p-2 text-sm" value={lang} onChange={(e) => setLang(e.target.value as any)}> | |
| <option value="js">JavaScript</option> | |
| <option value="ts">TypeScript</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label className="text-sm block mb-1">Stack</label> | |
| <select className="w-full bg-neutral-800 rounded p-2 text-sm" value={stack} onChange={(e) => setStack(e.target.value as NewStackId)}> | |
| {NEW_STACKS.map(s => ( | |
| <option key={s.id} value={s.id}>{s.label}</option> | |
| ))} | |
| </select> | |
| <p className="text-xs text-neutral-500 mt-1">{NEW_STACKS.find(s => s.id === stack)?.description}</p> | |
| </div> | |
| {/* Hero selection removed: AI decides the best hero automatically */} | |
| </div> | |
| {error && <p className="text-red-400 text-sm mt-3">{error}</p>} | |
| <ul className="text-sm text-neutral-400 space-y-1 mt-3"> | |
| {logs.map((l, i) => (<li key={i}>• {l}</li>))} | |
| </ul> | |
| <div className="mt-5"> | |
| <Button size="sm" onClick={runScaffold} disabled={loading}>{loading ? 'Working...' : 'Create Project'}</Button> | |
| </div> | |
| </section> | |
| ); | |
| } | |