omnidev / components /new /scaffold.tsx
kalhdrawi's picture
Reupload OmniDev clean version
c586a80
raw
history blame
7.07 kB
"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>
);
}