File size: 7,067 Bytes
a51c5c9
 
 
 
 
 
 
b0e750c
a51c5c9
e3c1646
b0e750c
 
 
 
 
 
 
 
a51c5c9
 
b0e750c
 
e3c1646
b0e750c
a51c5c9
b0e750c
 
a51c5c9
 
b0e750c
 
 
 
 
 
 
 
e3c1646
b0e750c
 
 
 
 
 
 
 
 
 
 
e3c1646
b0e750c
 
 
 
a51c5c9
 
 
 
 
 
 
 
b0e750c
 
 
a51c5c9
 
 
 
 
 
 
 
 
 
 
e3c1646
a51c5c9
 
 
 
 
b0e750c
 
 
a51c5c9
e3c1646
 
 
a51c5c9
 
 
 
 
 
 
 
 
 
 
b0e750c
a51c5c9
 
 
 
 
 
 
 
 
 
 
 
 
 
b0e750c
a51c5c9
 
b0e750c
 
c586a80
b0e750c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3c1646
b0e750c
 
 
a51c5c9
 
b0e750c
 
a51c5c9
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"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>
  );
}