.env ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ OLLAMA_BASE_URL=http://127.0.0.1:11434
2
+ OLLAMA_MODEL=codellama:7b
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ app/services/__pycache__/ai_service.cpython-314.pyc filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ ENV PORT=7860
6
+
7
+ COPY requirements.txt .
8
+
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ COPY . .
12
+
13
+ EXPOSE 7860
14
+
15
+ CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
README.md CHANGED
@@ -1,12 +1,139 @@
1
- ---
2
- title: Auto Coding Agent
3
- emoji: 👀
4
- colorFrom: yellow
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: AI Project Agent that generates full-stack projects instantl
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Project Agent
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ app_port: 7860
8
+ ---
9
+
10
+ # Project Agent
11
+
12
+ Project Agent is a FastAPI web app that turns a rough project idea into a **100% runnable starter project** preview, then creates a ZIP only after the user confirms it. The deployed app is designed to stay usable for free on Hugging Face Spaces even when no Ollama service is available.
13
+
14
+ ## Features
15
+
16
+ - Suggest -> Preview -> Regenerate -> Confirm ZIP workflow
17
+ - Fast/Deep generation modes with Fast Mode as the default
18
+ - Template-first project generation with required docs, scripts, and dependency files
19
+ - Safe fallback previews when AI is unavailable, slow, or invalid
20
+ - ZIP output that contains generated source, config, scripts, and docs only
21
+ - Safe path validation and generated output constrained to `generated/`
22
+ - Docker-ready deployment for Hugging Face Spaces
23
+
24
+ ## Requirements
25
+
26
+ The app depends on:
27
+
28
+ - `fastapi`
29
+ - `uvicorn[standard]`
30
+ - `jinja2`
31
+ - `python-dotenv`
32
+ - `httpx`
33
+
34
+ ## Run Locally
35
+
36
+ Install dependencies:
37
+
38
+ ```bash
39
+ pip install -r requirements.txt
40
+ ```
41
+
42
+ Start the app with the default Hugging Face-friendly port:
43
+
44
+ ```bash
45
+ python -m app.main
46
+ ```
47
+
48
+ Or run Uvicorn directly:
49
+
50
+ ```bash
51
+ uvicorn app.main:app --host 0.0.0.0 --port 7860
52
+ ```
53
+
54
+ To use a different port:
55
+
56
+ ```bash
57
+ PORT=8000 python -m app.main
58
+ ```
59
+
60
+ Then open `http://127.0.0.1:7860` or the port you set.
61
+
62
+ ## Environment Variables
63
+
64
+ Optional app/runtime variables:
65
+
66
+ ```env
67
+ PORT=7860
68
+ OLLAMA_BASE_URL=http://localhost:11434
69
+ OLLAMA_MODEL=qwen2.5-coder
70
+ ```
71
+
72
+ Notes:
73
+
74
+ - `PORT` defaults to `7860`, which matches Hugging Face Spaces.
75
+ - If `OLLAMA_BASE_URL` is missing or unreachable, Fast Mode still works by returning a complete template-based preview.
76
+ - Deep Mode remains optional. If AI is unavailable, the app still returns a valid preview and notes that Deep AI enrichment was skipped.
77
+
78
+ ## Hugging Face Spaces Deployment
79
+
80
+ Deploy this project as a **Docker Space**.
81
+
82
+ ### 1. Create the Space
83
+
84
+ - Create a new Space on Hugging Face
85
+ - Choose **Docker** as the Space SDK
86
+ - Push this repository to the Space
87
+
88
+ ### 2. Container Runtime
89
+
90
+ The included `Dockerfile` starts the app with:
91
+
92
+ ```bash
93
+ uvicorn app.main:app --host 0.0.0.0 --port 7860
94
+ ```
95
+
96
+ The container also supports `PORT` overrides through the environment and defaults to `7860`.
97
+
98
+ ### 3. Free Deployment Behavior
99
+
100
+ For the free public version, you do **not** need cloud Ollama.
101
+
102
+ - Fast Mode works without Ollama by using template fallback
103
+ - Deep Mode attempts AI enrichment only if Ollama is reachable
104
+ - If Ollama is unavailable, the app still returns a complete preview instead of failing
105
+
106
+ ### 4. Optional AI Configuration
107
+
108
+ If you later want AI-backed planning on a deployment that can reach Ollama, set:
109
+
110
+ ```env
111
+ OLLAMA_BASE_URL=https://your-ollama-endpoint
112
+ OLLAMA_MODEL=qwen2.5-coder
113
+ ```
114
+
115
+ If those values are not set, the app remains usable in fallback mode.
116
+
117
+ ## ZIP Output Behavior
118
+
119
+ Generated ZIPs:
120
+
121
+ - do not include installed libraries
122
+ - do not include `.venv`
123
+ - do not include `node_modules`
124
+ - do include dependency manifests, setup scripts, run scripts, starter code, and generated docs
125
+
126
+ This keeps ZIP creation compatible with Hugging Face temporary storage.
127
+
128
+ ## API Endpoints
129
+
130
+ - `GET /` renders the frontend
131
+ - `POST /api/suggest` returns a normalized project preview
132
+ - `POST /api/zip` writes the confirmed project into `generated/` and returns a download URL
133
+ - `GET /downloads/{filename}` downloads the generated ZIP
134
+
135
+ ## Deployment Notes
136
+
137
+ - Generated project artifacts are written only inside `generated/`
138
+ - The app assumes ephemeral writable storage is acceptable for preview ZIP downloads
139
+ - No dependency installation happens during preview or ZIP creation
app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
app/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (159 Bytes). View file
 
app/__pycache__/main.cpython-314.pyc ADDED
Binary file (13.8 kB). View file
 
app/main.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from dotenv import load_dotenv
8
+ from fastapi import FastAPI, HTTPException, Request
9
+ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
10
+ from fastapi.staticfiles import StaticFiles
11
+ from fastapi.templating import Jinja2Templates
12
+ from pydantic import BaseModel, Field
13
+
14
+ from .services.agent_controller import agent_controller
15
+ from .services.file_service import ensure_within_directory
16
+ from .services.zip_service import create_project_zip
17
+
18
+
19
+ load_dotenv()
20
+
21
+ ROOT_DIR = Path(__file__).resolve().parent.parent
22
+ APP_DIR = ROOT_DIR / "app"
23
+ GENERATED_DIR = ROOT_DIR / "generated"
24
+ DEFAULT_PORT = 7860
25
+
26
+ app = FastAPI(title="Project Agent")
27
+ app.mount("/static", StaticFiles(directory=APP_DIR / "static"), name="static")
28
+ templates = Jinja2Templates(directory=str(APP_DIR / "templates"))
29
+
30
+
31
+ class StackSelectionPayload(BaseModel):
32
+ language: str = "Auto"
33
+ frontend: str = "Auto"
34
+ backend: str = "Auto"
35
+ database: str = "Auto"
36
+ aiTools: str = "Auto"
37
+ deployment: str = "Auto"
38
+
39
+
40
+ class EnvVariablePayload(BaseModel):
41
+ name: str
42
+ value: str = ""
43
+ description: str = ""
44
+
45
+
46
+ class RequiredInputPayload(BaseModel):
47
+ name: str
48
+ required: bool = True
49
+ example: str = ""
50
+ whereToAdd: str = ".env"
51
+ purpose: str = ""
52
+
53
+
54
+ class SuggestRequest(BaseModel):
55
+ idea: str = Field(..., min_length=1)
56
+ selectedStack: StackSelectionPayload | None = None
57
+ generationMode: str = "fast"
58
+ finalRequirements: str = ""
59
+
60
+
61
+ class AgentAnalyzeRequest(BaseModel):
62
+ idea: str = Field(..., min_length=1)
63
+
64
+
65
+ class AgentQuestionPayload(BaseModel):
66
+ id: str
67
+ question: str
68
+ type: str
69
+ options: list[str] = Field(default_factory=list)
70
+ default: str = ""
71
+ reason: str = ""
72
+
73
+
74
+ class AgentAnalyzeResponse(BaseModel):
75
+ understanding: str
76
+ assumptions: list[str] = Field(default_factory=list)
77
+ suggestedStack: StackSelectionPayload = Field(default_factory=StackSelectionPayload)
78
+ stackReasons: list[str] = Field(default_factory=list)
79
+ questions: list[AgentQuestionPayload] = Field(default_factory=list)
80
+ detectedProjectType: str = "full-stack"
81
+ confidence: int = 0
82
+
83
+
84
+ class AgentFinalizeRequest(BaseModel):
85
+ idea: str = Field(..., min_length=1)
86
+ answers: dict[str, Any] = Field(default_factory=dict)
87
+ suggestedStack: StackSelectionPayload = Field(default_factory=StackSelectionPayload)
88
+
89
+
90
+ class AgentFinalizeResponse(BaseModel):
91
+ finalRequirements: str = ""
92
+ selectedStack: StackSelectionPayload = Field(default_factory=StackSelectionPayload)
93
+ assumptions: list[str] = Field(default_factory=list)
94
+
95
+
96
+ class ModulePayload(BaseModel):
97
+ name: str
98
+ purpose: str = ""
99
+ keyFiles: list[str] = Field(default_factory=list)
100
+
101
+
102
+ class FilePayload(BaseModel):
103
+ path: str
104
+ content: str = ""
105
+
106
+
107
+ class PreviewPayload(BaseModel):
108
+ projectName: str
109
+ detectedUserChoices: list[str] = Field(default_factory=list)
110
+ selectedStack: StackSelectionPayload = Field(default_factory=StackSelectionPayload)
111
+ chosenStack: list[str] = Field(default_factory=list)
112
+ assumptions: list[str] = Field(default_factory=list)
113
+ summary: str = ""
114
+ problemStatement: str = ""
115
+ architecture: list[str] = Field(default_factory=list)
116
+ modules: list[ModulePayload] = Field(default_factory=list)
117
+ packageRequirements: list[str] = Field(default_factory=list)
118
+ installCommands: list[str] = Field(default_factory=list)
119
+ runCommands: list[str] = Field(default_factory=list)
120
+ requiredInputs: list[RequiredInputPayload] = Field(default_factory=list)
121
+ envVariables: list[EnvVariablePayload] = Field(default_factory=list)
122
+ fileTree: str = ""
123
+ files: list[FilePayload] = Field(default_factory=list)
124
+
125
+
126
+ class ZipRequest(BaseModel):
127
+ preview: PreviewPayload
128
+
129
+
130
+ @app.get("/", response_class=HTMLResponse)
131
+ async def index(request: Request) -> HTMLResponse:
132
+ return templates.TemplateResponse("index.html", {"request": request})
133
+
134
+
135
+ @app.post("/api/suggest")
136
+ async def suggest_project(payload: SuggestRequest) -> JSONResponse:
137
+ idea = payload.idea.strip()
138
+ if not idea:
139
+ raise HTTPException(status_code=400, detail="Please enter a project idea.")
140
+
141
+ try:
142
+ preview = await agent_controller.generate_files(
143
+ idea,
144
+ payload.selectedStack.model_dump() if payload.selectedStack else None,
145
+ payload.generationMode,
146
+ payload.finalRequirements,
147
+ )
148
+ except RuntimeError as exc:
149
+ raise HTTPException(status_code=502, detail=str(exc)) from exc
150
+
151
+ return JSONResponse(preview)
152
+
153
+
154
+ @app.post("/api/agent/analyze")
155
+ async def analyze_agent(payload: AgentAnalyzeRequest) -> JSONResponse:
156
+ idea = payload.idea.strip()
157
+ if not idea:
158
+ raise HTTPException(status_code=400, detail="Please enter a project idea.")
159
+
160
+ return JSONResponse(agent_controller.analyze_idea(idea))
161
+
162
+
163
+ @app.post("/api/agent/finalize")
164
+ async def finalize_agent(payload: AgentFinalizeRequest) -> JSONResponse:
165
+ idea = payload.idea.strip()
166
+ if not idea:
167
+ raise HTTPException(status_code=400, detail="Please enter a project idea.")
168
+
169
+ finalized = agent_controller.finalize_requirements(
170
+ idea,
171
+ payload.answers,
172
+ payload.suggestedStack.model_dump(),
173
+ )
174
+ return JSONResponse(finalized)
175
+
176
+
177
+ @app.post("/api/zip")
178
+ async def build_zip(payload: ZipRequest) -> JSONResponse:
179
+ try:
180
+ normalized_preview = agent_controller.validate_project(payload.preview.model_dump())
181
+ result = create_project_zip(normalized_preview, GENERATED_DIR)
182
+ except ValueError as exc:
183
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
184
+ except OSError as exc:
185
+ raise HTTPException(status_code=500, detail=f"Could not create ZIP: {exc}") from exc
186
+
187
+ return JSONResponse(result)
188
+
189
+
190
+ @app.get("/downloads/{filename}")
191
+ async def download_zip(filename: str) -> FileResponse:
192
+ download_path = GENERATED_DIR / filename
193
+ if download_path.name != filename:
194
+ raise HTTPException(status_code=404, detail="File not found.")
195
+ if download_path.suffix != ".zip":
196
+ raise HTTPException(status_code=404, detail="File not found.")
197
+ if not download_path.exists():
198
+ raise HTTPException(status_code=404, detail="File not found.")
199
+ if not ensure_within_directory(GENERATED_DIR, download_path):
200
+ raise HTTPException(status_code=404, detail="File not found.")
201
+
202
+ return FileResponse(
203
+ download_path,
204
+ media_type="application/zip",
205
+ filename=download_path.name,
206
+ )
207
+
208
+
209
+ @app.exception_handler(HTTPException)
210
+ async def http_exception_handler(_: Request, exc: HTTPException) -> JSONResponse:
211
+ if exc.status_code == 404:
212
+ return JSONResponse(status_code=404, content={"detail": exc.detail})
213
+ return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
214
+
215
+
216
+ @app.exception_handler(Exception)
217
+ async def unhandled_exception_handler(_: Request, exc: Exception) -> JSONResponse:
218
+ return JSONResponse(status_code=500, content={"detail": f"Unexpected server error: {exc}"})
219
+
220
+
221
+ if __name__ == "__main__":
222
+ import uvicorn
223
+
224
+ uvicorn.run(
225
+ "app.main:app",
226
+ host="0.0.0.0",
227
+ port=int(os.getenv("PORT", str(DEFAULT_PORT))),
228
+ )
app/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
app/services/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (168 Bytes). View file
 
app/services/__pycache__/agent_controller.cpython-314.pyc ADDED
Binary file (22.4 kB). View file
 
app/services/__pycache__/ai_service.cpython-314.pyc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:74a9427a7c4a74ab2a8f072f62122bc4c3a1a6833f369ca6ef173b2c61a9c835
3
+ size 140855
app/services/__pycache__/file_service.cpython-314.pyc ADDED
Binary file (81.7 kB). View file
 
app/services/__pycache__/zip_service.cpython-314.pyc ADDED
Binary file (5.09 kB). View file
 
app/services/agent_controller.py ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import time
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Mapping
7
+
8
+ from . import ai_service as ai
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @dataclass(slots=True)
14
+ class IdeaContext:
15
+ idea: str
16
+ requested_stack: dict[str, str]
17
+ generation_mode: str
18
+ final_requirements: str = ""
19
+ generation_context: str = ""
20
+ detected_user_choices: list[str] = field(default_factory=list)
21
+ declared_project_type: str = ""
22
+ selected_stack: dict[str, str] = field(default_factory=dict)
23
+ project_kind: dict[str, Any] = field(default_factory=dict)
24
+
25
+
26
+ @dataclass(slots=True)
27
+ class AgentAnalysisResult:
28
+ understanding: str
29
+ assumptions: list[str]
30
+ suggested_stack: dict[str, str]
31
+ stack_reasons: list[str]
32
+ questions: list[dict[str, Any]]
33
+ detected_project_type: str
34
+ confidence: int
35
+
36
+ def to_api_dict(self) -> dict[str, Any]:
37
+ return {
38
+ "understanding": self.understanding,
39
+ "assumptions": self.assumptions,
40
+ "suggestedStack": self.suggested_stack,
41
+ "stackReasons": self.stack_reasons,
42
+ "questions": self.questions,
43
+ "detectedProjectType": self.detected_project_type,
44
+ "confidence": self.confidence,
45
+ }
46
+
47
+
48
+ @dataclass(slots=True)
49
+ class FinalizedRequirementsResult:
50
+ final_requirements: str
51
+ selected_stack: dict[str, str]
52
+ assumptions: list[str]
53
+ normalized_answers: dict[str, str] = field(default_factory=dict)
54
+ project_kind: dict[str, Any] = field(default_factory=dict)
55
+
56
+ def to_api_dict(self) -> dict[str, Any]:
57
+ return {
58
+ "finalRequirements": self.final_requirements,
59
+ "selectedStack": self.selected_stack,
60
+ "assumptions": self.assumptions,
61
+ }
62
+
63
+
64
+ @dataclass(slots=True)
65
+ class ProjectStructurePlan:
66
+ project_name: str
67
+ detected_user_choices: list[str]
68
+ selected_stack: dict[str, str]
69
+ chosen_stack: list[str]
70
+ assumptions: list[str]
71
+ summary: str
72
+ problem_statement: str
73
+ architecture: list[str]
74
+ modules: list[dict[str, Any]]
75
+ package_requirements: list[str]
76
+ install_commands: list[str]
77
+ run_commands: list[str]
78
+ required_inputs: list[dict[str, Any]]
79
+ env_variables: list[dict[str, Any]]
80
+ custom_manifest: list[dict[str, str]]
81
+ files: list[dict[str, str]]
82
+ file_tree: str
83
+ project_kind: dict[str, Any] = field(default_factory=dict)
84
+
85
+ def to_preview_dict(self) -> dict[str, Any]:
86
+ return {
87
+ "projectName": self.project_name,
88
+ "detectedUserChoices": self.detected_user_choices,
89
+ "selectedStack": self.selected_stack,
90
+ "chosenStack": self.chosen_stack,
91
+ "assumptions": self.assumptions,
92
+ "summary": self.summary,
93
+ "problemStatement": self.problem_statement,
94
+ "architecture": self.architecture,
95
+ "modules": self.modules,
96
+ "packageRequirements": self.package_requirements,
97
+ "installCommands": self.install_commands,
98
+ "runCommands": self.run_commands,
99
+ "requiredInputs": self.required_inputs,
100
+ "envVariables": self.env_variables,
101
+ "fileTree": self.file_tree,
102
+ "files": self.files,
103
+ }
104
+
105
+
106
+ @dataclass(slots=True)
107
+ class GeneratedProjectResult:
108
+ preview: dict[str, Any]
109
+ fallback_used: bool = False
110
+ fallback_reason: str = ""
111
+
112
+
113
+ class AgentController:
114
+ def analyze_idea(self, idea: str) -> dict[str, Any]:
115
+ context = self._build_idea_context(idea)
116
+ questions = self.ask_questions(context)
117
+ result = AgentAnalysisResult(
118
+ understanding=ai.build_agent_understanding(
119
+ context.idea,
120
+ context.selected_stack,
121
+ context.project_kind,
122
+ ),
123
+ assumptions=ai.build_agent_analysis_assumptions(
124
+ context.selected_stack,
125
+ context.project_kind,
126
+ questions,
127
+ ),
128
+ suggested_stack=context.selected_stack,
129
+ stack_reasons=ai.build_stack_reasons(
130
+ context.selected_stack,
131
+ context.project_kind,
132
+ ),
133
+ questions=questions,
134
+ detected_project_type=context.project_kind["label"],
135
+ confidence=ai.compute_agent_confidence(
136
+ context.idea,
137
+ context.detected_user_choices,
138
+ questions,
139
+ context.project_kind,
140
+ ),
141
+ )
142
+ return result.to_api_dict()
143
+
144
+ def decide_stack(self, context: IdeaContext, model_stack: Any = None) -> dict[str, str]:
145
+ return ai.resolve_selected_stack(
146
+ context.idea,
147
+ context.requested_stack,
148
+ model_stack,
149
+ context.detected_user_choices,
150
+ )
151
+
152
+ def determine_missing_info(self, context: IdeaContext) -> list[dict[str, Any]]:
153
+ return ai.build_agent_questions(
154
+ context.idea,
155
+ context.selected_stack,
156
+ context.project_kind,
157
+ )
158
+
159
+ def ask_questions(self, context: IdeaContext) -> list[dict[str, Any]]:
160
+ return self.determine_missing_info(context)
161
+
162
+ def finalize_requirements(
163
+ self,
164
+ idea: str,
165
+ answers: Mapping[str, Any] | None,
166
+ selected_stack: Mapping[str, Any] | None,
167
+ ) -> dict[str, Any]:
168
+ normalized_answers = ai.normalize_agent_answers(answers)
169
+ resolved_stack = ai.apply_agent_answers_to_stack(
170
+ idea,
171
+ ai.normalize_stack_selection(selected_stack),
172
+ normalized_answers,
173
+ )
174
+ project_kind = ai.determine_project_kind(
175
+ resolved_stack,
176
+ normalized_answers.get("project_scope"),
177
+ )
178
+ result = FinalizedRequirementsResult(
179
+ final_requirements=ai.build_final_requirements_summary(
180
+ idea,
181
+ normalized_answers,
182
+ resolved_stack,
183
+ project_kind,
184
+ ),
185
+ selected_stack=resolved_stack,
186
+ assumptions=ai.build_agent_finalize_assumptions(
187
+ normalized_answers,
188
+ resolved_stack,
189
+ project_kind,
190
+ ),
191
+ normalized_answers=normalized_answers,
192
+ project_kind=project_kind,
193
+ )
194
+ return result.to_api_dict()
195
+
196
+ def plan_project_structure(
197
+ self,
198
+ context: IdeaContext,
199
+ raw_plan: Mapping[str, Any] | None = None,
200
+ ) -> ProjectStructurePlan:
201
+ raw = dict(raw_plan or {})
202
+ detected_choices = ai.dedupe_list(
203
+ ai.normalize_string_list(raw.get("detectedUserChoices"))
204
+ or context.detected_user_choices
205
+ or ai.detect_user_choices(context.idea)
206
+ )
207
+ selected_stack = ai.resolve_selected_stack(
208
+ context.idea,
209
+ context.requested_stack,
210
+ raw.get("selectedStack") or context.selected_stack,
211
+ detected_choices,
212
+ )
213
+ project_kind = ai.determine_project_kind(
214
+ selected_stack,
215
+ raw.get("projectType") or context.declared_project_type,
216
+ )
217
+ project_name = ai.clean_project_name(raw.get("projectName"), context.idea)
218
+
219
+ modules = ai.merge_modules(
220
+ ai.normalize_modules(raw.get("modules")),
221
+ ai.build_default_modules(selected_stack, project_kind),
222
+ )
223
+ required_inputs = ai.merge_required_inputs(
224
+ ai.normalize_required_inputs(raw.get("requiredInputs")),
225
+ ai.build_required_inputs(
226
+ context.generation_context or context.idea,
227
+ selected_stack,
228
+ project_kind,
229
+ modules,
230
+ ),
231
+ )
232
+ env_variables = ai.merge_env_variables(
233
+ ai.normalize_env_variables(raw.get("envVariables")),
234
+ ai.required_inputs_to_env_variables(required_inputs),
235
+ )
236
+ package_requirements = ai.dedupe_list(
237
+ ai.normalize_string_list(raw.get("packageRequirements"))
238
+ + ai.build_package_requirements(selected_stack, project_kind)
239
+ )
240
+ install_commands = ai.dedupe_list(
241
+ ai.normalize_string_list(raw.get("installCommands"))
242
+ + ai.build_install_commands(selected_stack, project_kind)
243
+ )
244
+ run_commands = ai.dedupe_list(
245
+ ai.normalize_string_list(raw.get("runCommands"))
246
+ + ai.build_run_commands(selected_stack, project_kind)
247
+ )
248
+ custom_manifest = ai.normalize_custom_manifest(
249
+ raw.get("customFiles"),
250
+ selected_stack,
251
+ project_kind,
252
+ )
253
+ files = ai.finalize_preview_files(
254
+ project_name=project_name,
255
+ selected_stack=selected_stack,
256
+ project_kind=project_kind,
257
+ custom_manifest=custom_manifest,
258
+ raw_files=raw.get("files"),
259
+ )
260
+ assumptions = ai.dedupe_list(
261
+ ai.normalize_string_list(raw.get("assumptions"))
262
+ + ai.build_assumptions(
263
+ selected_stack,
264
+ project_kind,
265
+ context.requested_stack,
266
+ context.generation_mode,
267
+ bool(custom_manifest),
268
+ )
269
+ )
270
+ architecture = ai.dedupe_list(
271
+ ai.normalize_string_list(raw.get("architecture"))
272
+ + ai.build_architecture(selected_stack, project_kind)
273
+ )
274
+ file_tree = ai.build_preview_file_tree(
275
+ files,
276
+ include_env_example=bool(env_variables),
277
+ )
278
+
279
+ return ProjectStructurePlan(
280
+ project_name=project_name,
281
+ detected_user_choices=detected_choices,
282
+ selected_stack=selected_stack,
283
+ chosen_stack=ai.build_chosen_stack(selected_stack),
284
+ assumptions=assumptions,
285
+ summary=str(raw.get("summary") or "").strip()
286
+ or ai.build_summary(
287
+ project_name,
288
+ project_kind,
289
+ selected_stack,
290
+ context.generation_mode,
291
+ ),
292
+ problem_statement=str(raw.get("problemStatement") or "").strip()
293
+ or context.idea.strip()
294
+ or f"Build a starter project for {project_name}.",
295
+ architecture=architecture,
296
+ modules=modules,
297
+ package_requirements=package_requirements,
298
+ install_commands=install_commands,
299
+ run_commands=run_commands,
300
+ required_inputs=required_inputs,
301
+ env_variables=env_variables,
302
+ custom_manifest=custom_manifest,
303
+ files=files,
304
+ file_tree=file_tree,
305
+ project_kind=project_kind,
306
+ )
307
+
308
+ async def generate_files(
309
+ self,
310
+ idea: str,
311
+ selected_stack: dict[str, str] | None = None,
312
+ generation_mode: str = "fast",
313
+ final_requirements: str = "",
314
+ ) -> dict[str, Any]:
315
+ context = self._build_idea_context(
316
+ idea,
317
+ selected_stack=selected_stack,
318
+ generation_mode=generation_mode,
319
+ final_requirements=final_requirements,
320
+ )
321
+ preview_started_at = time.perf_counter()
322
+ deadline = time.monotonic() + ai.preview_budget_seconds(context.generation_mode)
323
+ planner_started_at: float | None = None
324
+ planner_duration = 0.0
325
+
326
+ try:
327
+ planner_started_at = time.perf_counter()
328
+ raw_plan = await ai.generate_project_plan(
329
+ context.generation_context,
330
+ context.requested_stack,
331
+ context.generation_mode,
332
+ deadline,
333
+ )
334
+ planner_duration = time.perf_counter() - planner_started_at
335
+ structure_plan = self.plan_project_structure(context, raw_plan)
336
+ preview = structure_plan.to_preview_dict()
337
+
338
+ if context.generation_mode == "deep" and structure_plan.custom_manifest:
339
+ remaining = ai.remaining_time(deadline)
340
+ if remaining >= ai.MIN_CUSTOM_PASS_SECONDS:
341
+ try:
342
+ generated_custom_files = await ai.generate_deep_custom_files(
343
+ context.generation_context,
344
+ structure_plan.project_name,
345
+ structure_plan.selected_stack,
346
+ structure_plan.custom_manifest,
347
+ remaining,
348
+ )
349
+ preview = ai.apply_custom_file_overrides(preview, generated_custom_files)
350
+ preview["assumptions"] = ai.dedupe_list(
351
+ preview["assumptions"]
352
+ + ["Deep Mode enriched custom business logic with a second scoped AI pass."]
353
+ )
354
+ except Exception as exc:
355
+ preview["assumptions"] = ai.dedupe_list(
356
+ preview["assumptions"]
357
+ + [f"Deep Mode custom enrichment was skipped, so template custom files were kept: {exc}"]
358
+ )
359
+ else:
360
+ preview["assumptions"] = ai.dedupe_list(
361
+ preview["assumptions"]
362
+ + ["Deep Mode used the fast template custom files because the 70-second preview budget was nearly exhausted."]
363
+ )
364
+
365
+ preview = self.validate_project(preview)
366
+ total_duration = time.perf_counter() - preview_started_at
367
+ logger.info(
368
+ "project_preview_complete mode=%s planner_duration=%.2fs total_duration=%.2fs fallback_used=%s",
369
+ context.generation_mode,
370
+ planner_duration,
371
+ total_duration,
372
+ False,
373
+ )
374
+ return GeneratedProjectResult(preview=preview).preview
375
+ except Exception as exc:
376
+ if planner_started_at is not None and planner_duration == 0.0:
377
+ planner_duration = time.perf_counter() - planner_started_at
378
+ preview = self._build_fallback_preview(context, str(exc))
379
+ preview = self.validate_project(preview)
380
+ total_duration = time.perf_counter() - preview_started_at
381
+ logger.warning(
382
+ "project_preview_fallback mode=%s planner_duration=%.2fs total_duration=%.2fs fallback_used=%s reason=%s",
383
+ context.generation_mode,
384
+ planner_duration,
385
+ total_duration,
386
+ True,
387
+ str(exc),
388
+ )
389
+ return GeneratedProjectResult(
390
+ preview=preview,
391
+ fallback_used=True,
392
+ fallback_reason=str(exc),
393
+ ).preview
394
+
395
+ def validate_project(self, preview: dict[str, Any]) -> dict[str, Any]:
396
+ return ai.prepare_preview_for_output(dict(preview))
397
+
398
+ def _build_idea_context(
399
+ self,
400
+ idea: str,
401
+ *,
402
+ selected_stack: Mapping[str, Any] | None = None,
403
+ generation_mode: str = "fast",
404
+ final_requirements: str = "",
405
+ ) -> IdeaContext:
406
+ requested_stack = ai.normalize_stack_selection(selected_stack)
407
+ normalized_mode = ai.normalize_generation_mode(generation_mode)
408
+ generation_context = ai.build_generation_context(
409
+ idea,
410
+ final_requirements,
411
+ normalized_mode,
412
+ )
413
+ detected_user_choices = ai.detect_user_choices(idea)
414
+ declared_project_type = ai.infer_declared_project_type(idea)
415
+ context = IdeaContext(
416
+ idea=idea,
417
+ requested_stack=requested_stack,
418
+ generation_mode=normalized_mode,
419
+ final_requirements=final_requirements,
420
+ generation_context=generation_context,
421
+ detected_user_choices=detected_user_choices,
422
+ declared_project_type=declared_project_type,
423
+ )
424
+ context.selected_stack = self.decide_stack(context)
425
+ context.project_kind = ai.determine_project_kind(
426
+ context.selected_stack,
427
+ declared_project_type,
428
+ )
429
+ return context
430
+
431
+ def _build_fallback_preview(self, context: IdeaContext, reason: str) -> dict[str, Any]:
432
+ structure_plan = self.plan_project_structure(context, {})
433
+ preview = structure_plan.to_preview_dict()
434
+ fallback_note = (
435
+ "Deep Mode AI enrichment was unavailable, so the 100% runnable starter project uses the safe template-generated fallback."
436
+ if context.generation_mode == "deep"
437
+ else "Fast Mode AI planning was unavailable, so the 100% runnable starter project uses the safe template-generated fallback."
438
+ )
439
+ preview["assumptions"] = ai.dedupe_list(
440
+ [
441
+ fallback_note,
442
+ f"Template fallback preview was generated because the AI planner could not complete in time or returned invalid output: {reason}",
443
+ *preview.get("assumptions", []),
444
+ ]
445
+ )
446
+ return preview
447
+
448
+
449
+ agent_controller = AgentController()
app/services/ai_service.py ADDED
The diff for this file is too large to render. See raw diff
 
app/services/file_service.py ADDED
@@ -0,0 +1,1932 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path, PurePosixPath
6
+ from typing import Any, Mapping, Sequence
7
+
8
+
9
+ MAX_GENERATED_FILES = 60
10
+ MAX_FILE_SIZE_BYTES = 250 * 1024
11
+
12
+ SYSTEM_FILENAMES = {
13
+ "README.md",
14
+ "PROJECT_EXPLANATION.md",
15
+ "SETUP_INSTRUCTIONS.md",
16
+ "FILE_STRUCTURE.md",
17
+ "PACKAGE_REQUIREMENTS.md",
18
+ "REQUIRED_INPUTS.md",
19
+ ".env.example",
20
+ }
21
+
22
+
23
+ def slugify(value: str, fallback: str = "project-agent-output") -> str:
24
+ slug = re.sub(r"[^a-zA-Z0-9]+", "-", value.lower()).strip("-")
25
+ return slug or fallback
26
+
27
+
28
+ def sanitize_relative_path(raw_path: str) -> Path:
29
+ if not isinstance(raw_path, str):
30
+ raise ValueError("Generated file paths must be strings.")
31
+
32
+ normalized = raw_path.replace("\\", "/").strip()
33
+ if not normalized:
34
+ raise ValueError("Generated file paths cannot be empty.")
35
+ if normalized.startswith(("/", "~")):
36
+ raise ValueError(f"Absolute paths are not allowed: {raw_path}")
37
+ if re.match(r"^[a-zA-Z]:", normalized):
38
+ raise ValueError(f"Drive-qualified paths are not allowed: {raw_path}")
39
+
40
+ posix_path = PurePosixPath(normalized)
41
+ safe_parts: list[str] = []
42
+ for part in posix_path.parts:
43
+ if part in {"", "."}:
44
+ continue
45
+ if part == "..":
46
+ raise ValueError(f"Path traversal is not allowed: {raw_path}")
47
+ safe_parts.append(part)
48
+
49
+ if not safe_parts:
50
+ raise ValueError(f"Generated file path is invalid: {raw_path}")
51
+
52
+ return Path(*safe_parts)
53
+
54
+
55
+ def ensure_within_directory(base_dir: Path, candidate: Path) -> bool:
56
+ resolved_base = base_dir.resolve()
57
+ resolved_candidate = candidate.resolve()
58
+ return resolved_candidate.is_relative_to(resolved_base)
59
+
60
+
61
+ def validate_generated_files(files: list[dict[str, str]]) -> list[dict[str, str]]:
62
+ if len(files) > MAX_GENERATED_FILES:
63
+ raise ValueError(
64
+ f"Generated output exceeds the limit of {MAX_GENERATED_FILES} files."
65
+ )
66
+
67
+ validated: list[dict[str, str]] = []
68
+ seen_paths: set[str] = set()
69
+ for file_entry in files:
70
+ raw_path = str(file_entry.get("path", ""))
71
+ relative_path = sanitize_relative_path(raw_path)
72
+ normalized_path = relative_path.as_posix()
73
+ if normalized_path in seen_paths:
74
+ continue
75
+
76
+ content = str(file_entry.get("content", ""))
77
+ content_size = len(content.encode("utf-8"))
78
+ if content_size > MAX_FILE_SIZE_BYTES:
79
+ raise ValueError(
80
+ f"Generated file '{normalized_path}' exceeds the size limit of 250 KB."
81
+ )
82
+
83
+ seen_paths.add(normalized_path)
84
+ validated.append({"path": normalized_path, "content": content})
85
+
86
+ return validated
87
+
88
+
89
+ def build_required_docs(
90
+ preview: dict[str, Any], bundle_info: dict[str, Any] | None = None
91
+ ) -> dict[str, str]:
92
+ bundle_info = bundle_info or {}
93
+ project_name = str(preview.get("projectName") or "Generated Project").strip()
94
+ detected_choices = _listify(preview.get("detectedUserChoices"))
95
+ chosen_stack = _listify(preview.get("chosenStack"))
96
+ assumptions = _listify(preview.get("assumptions"))
97
+ architecture = _listify(preview.get("architecture"))
98
+ package_requirements = _listify(preview.get("packageRequirements"))
99
+ install_commands = _listify(preview.get("installCommands"))
100
+ run_commands = _listify(preview.get("runCommands"))
101
+ modules = preview.get("modules") or []
102
+ required_inputs = _required_input_list(preview.get("requiredInputs"))
103
+ env_variables = _env_list(preview.get("envVariables")) or _required_inputs_to_env_list(
104
+ required_inputs
105
+ )
106
+ file_tree = str(
107
+ bundle_info.get("actualFileTree") or preview.get("fileTree") or ""
108
+ ).strip()
109
+ summary = str(preview.get("summary") or "No summary was provided by the model.").strip()
110
+ problem_statement = str(
111
+ preview.get("problemStatement") or "No problem statement was provided by the model."
112
+ ).strip()
113
+ selected_stack = preview.get("selectedStack") or {}
114
+
115
+ readme = "\n".join(
116
+ [
117
+ f"# {project_name}",
118
+ "",
119
+ summary,
120
+ "",
121
+ "## What Was Generated",
122
+ "This ZIP contains a 100% runnable starter project from the latest preview, including dependency files, setup scripts, run scripts, starter source code, and required input guidance.",
123
+ "",
124
+ "## Problem Statement",
125
+ problem_statement,
126
+ "",
127
+ "## Selected Stack",
128
+ _selected_stack_text(selected_stack),
129
+ "",
130
+ "## Chosen Stack",
131
+ _bullet_text(chosen_stack, "The generated plan did not include stack details."),
132
+ "",
133
+ "## Detected User Choices",
134
+ _bullet_text(
135
+ detected_choices,
136
+ "The user did not explicitly specify language, tooling, or framework choices.",
137
+ ),
138
+ "",
139
+ "## Architecture Highlights",
140
+ _bullet_text(architecture, "Architecture details were not provided."),
141
+ "",
142
+ "## Core Modules",
143
+ _modules_text(modules),
144
+ "",
145
+ "## Setup",
146
+ "Fill `.env` from `.env.example`, then run the setup script before starting the project.",
147
+ "- Windows: `setup.bat`",
148
+ "- Mac/Linux: `setup.sh`",
149
+ "",
150
+ "## How To Run",
151
+ _bullet_text(run_commands, "No run commands were provided."),
152
+ "",
153
+ "## Required Inputs",
154
+ "Fill these values in `.env` before running the project.",
155
+ "",
156
+ _required_inputs_summary(required_inputs),
157
+ "",
158
+ "## Notes",
159
+ _bullet_text(assumptions, "No assumptions were recorded."),
160
+ ]
161
+ ).strip() + "\n"
162
+
163
+ explanation = "\n".join(
164
+ [
165
+ f"# {project_name} Explanation",
166
+ "",
167
+ "## Summary",
168
+ summary,
169
+ "",
170
+ "## Problem Statement",
171
+ problem_statement,
172
+ "",
173
+ "## Selected Stack",
174
+ _selected_stack_text(selected_stack),
175
+ "",
176
+ "## Architecture",
177
+ _bullet_text(architecture, "Architecture details were not provided."),
178
+ "",
179
+ "## Modules",
180
+ _modules_text(modules),
181
+ "",
182
+ "## Assumptions",
183
+ _bullet_text(assumptions, "No assumptions were recorded."),
184
+ ]
185
+ ).strip() + "\n"
186
+
187
+ setup_instructions = "\n".join(
188
+ [
189
+ "# Setup Instructions",
190
+ "",
191
+ "## Normal Setup",
192
+ "1. Review `README.md`, `PROJECT_EXPLANATION.md`, and `FILE_STRUCTURE.md` first.",
193
+ "2. Review `PACKAGE_REQUIREMENTS.md` and `REQUIRED_INPUTS.md` before installing dependencies.",
194
+ "3. Copy `.env.example` to `.env` and fill the required values.",
195
+ "",
196
+ "## Windows",
197
+ "1. Fill `.env` from `.env.example`.",
198
+ "2. Run `setup.bat`.",
199
+ "3. Run `run.bat`.",
200
+ "",
201
+ "## Mac/Linux",
202
+ "1. Fill `.env` from `.env.example`.",
203
+ "2. Run `chmod +x setup.sh run.sh`.",
204
+ "3. Run `./setup.sh`.",
205
+ "4. Run `./run.sh`.",
206
+ "",
207
+ "## Setup Scripts",
208
+ "- Windows: `setup.bat`",
209
+ "- Mac/Linux: `setup.sh`",
210
+ "",
211
+ "## Selected Stack",
212
+ _selected_stack_text(selected_stack),
213
+ "",
214
+ "## Install Commands",
215
+ _bullet_text(install_commands, "No install commands were provided."),
216
+ "",
217
+ "## Run Commands",
218
+ _bullet_text(run_commands, "No run commands were provided."),
219
+ "",
220
+ "## Environment Variables",
221
+ _env_variables_text(env_variables),
222
+ "",
223
+ "## Troubleshooting",
224
+ "- If dependencies fail to install, confirm Python, Node.js, or Maven is installed for the selected stack.",
225
+ "- If the app cannot connect to a service, double-check the values in `.env` against `REQUIRED_INPUTS.md`.",
226
+ "- If frontend and backend both start locally, verify `VITE_API_BASE_URL` or related API host settings match the backend URL.",
227
+ "",
228
+ "## Notes",
229
+ _bullet_text(assumptions, "No additional assumptions were provided."),
230
+ ]
231
+ ).strip() + "\n"
232
+
233
+ structure = "\n".join(
234
+ [
235
+ "# File Structure",
236
+ "",
237
+ "## Final Generated Tree",
238
+ "```text",
239
+ file_tree or "No file tree was available.",
240
+ "```",
241
+ "",
242
+ "## Included Modules",
243
+ _modules_text(modules),
244
+ ]
245
+ ).strip() + "\n"
246
+
247
+ package_docs = "\n".join(
248
+ [
249
+ "# Package Requirements",
250
+ "",
251
+ "## Libraries And Packages",
252
+ _bullet_text(package_requirements, "No package requirements were provided."),
253
+ "",
254
+ "## Install Commands",
255
+ _bullet_text(install_commands, "No install commands were provided."),
256
+ "",
257
+ "## Run Commands",
258
+ _bullet_text(run_commands, "No run commands were provided."),
259
+ ]
260
+ ).strip() + "\n"
261
+
262
+ required_inputs_doc = "\n".join(
263
+ [
264
+ "# Required Inputs",
265
+ "",
266
+ "Fill these values in `.env` before running the project.",
267
+ "",
268
+ "| Name | Required | Example | Where To Add | Purpose |",
269
+ "|---|---|---|---|---|",
270
+ _required_inputs_table(required_inputs),
271
+ ]
272
+ ).strip() + "\n"
273
+
274
+ docs = {
275
+ "README.md": readme,
276
+ "PROJECT_EXPLANATION.md": explanation,
277
+ "SETUP_INSTRUCTIONS.md": setup_instructions,
278
+ "FILE_STRUCTURE.md": structure,
279
+ "PACKAGE_REQUIREMENTS.md": package_docs,
280
+ "REQUIRED_INPUTS.md": required_inputs_doc,
281
+ ".env.example": build_env_example(required_inputs),
282
+ }
283
+
284
+ return docs
285
+
286
+
287
+ def build_env_example(required_inputs: list[dict[str, Any]]) -> str:
288
+ if not required_inputs:
289
+ return (
290
+ "# No required inputs were detected for this starter.\n"
291
+ "# Copy this file to .env if you want to override defaults later.\n"
292
+ )
293
+
294
+ lines = [
295
+ "# Fill these values in .env before running the project.",
296
+ "# Copy this file to .env and replace the example values as needed.",
297
+ "",
298
+ ]
299
+ for variable in required_inputs:
300
+ name = str(variable.get("name") or "").strip()
301
+ example = str(variable.get("example") or "").strip()
302
+ if name:
303
+ lines.append(f"{name}={example}")
304
+ return "\n".join(lines).rstrip() + "\n"
305
+
306
+
307
+ def build_file_tree_from_paths(paths: list[str]) -> str:
308
+ if not paths:
309
+ return ""
310
+
311
+ tree: dict[str, Any] = {}
312
+ for raw_path in paths:
313
+ parts = [part for part in raw_path.replace("\\", "/").split("/") if part]
314
+ current = tree
315
+ for part in parts:
316
+ current = current.setdefault(part, {})
317
+
318
+ lines: list[str] = []
319
+
320
+ def walk(node: dict[str, Any], depth: int = 0) -> None:
321
+ for name, child in sorted(node.items()):
322
+ lines.append(f"{' ' * depth}{name}")
323
+ walk(child, depth + 1)
324
+
325
+ walk(tree)
326
+ return "\n".join(lines)
327
+
328
+
329
+ def _listify(value: Any) -> list[str]:
330
+ if value is None:
331
+ return []
332
+ if isinstance(value, str):
333
+ items = [line.strip(" -*\t") for line in value.splitlines()]
334
+ return [item for item in items if item]
335
+ if isinstance(value, dict):
336
+ return [
337
+ f"{key}: {str(item).strip()}"
338
+ for key, item in value.items()
339
+ if str(item).strip()
340
+ ]
341
+ if isinstance(value, (list, tuple, set)):
342
+ items = []
343
+ for item in value:
344
+ text = str(item).strip()
345
+ if text:
346
+ items.append(text)
347
+ return items
348
+ text = str(value).strip()
349
+ return [text] if text else []
350
+
351
+
352
+ def _env_list(value: Any) -> list[dict[str, str]]:
353
+ env_vars: list[dict[str, str]] = []
354
+ if isinstance(value, dict):
355
+ value = [
356
+ {
357
+ "name": key,
358
+ "value": str(item).strip(),
359
+ "description": "",
360
+ }
361
+ for key, item in value.items()
362
+ ]
363
+ if not isinstance(value, (list, tuple)):
364
+ return env_vars
365
+
366
+ for item in value:
367
+ if not isinstance(item, dict):
368
+ continue
369
+ name = str(item.get("name") or "").strip()
370
+ if not name:
371
+ continue
372
+ env_vars.append(
373
+ {
374
+ "name": name,
375
+ "value": str(item.get("value") or "").strip(),
376
+ "description": str(item.get("description") or "").strip(),
377
+ }
378
+ )
379
+ return env_vars
380
+
381
+
382
+ def _required_input_list(value: Any) -> list[dict[str, Any]]:
383
+ if isinstance(value, Mapping):
384
+ value = [value]
385
+ if not isinstance(value, (list, tuple)):
386
+ return []
387
+
388
+ required_inputs: list[dict[str, Any]] = []
389
+ for item in value:
390
+ if not isinstance(item, Mapping):
391
+ continue
392
+ name = str(item.get("name") or "").strip()
393
+ if not name:
394
+ continue
395
+ required_inputs.append(
396
+ {
397
+ "name": name,
398
+ "required": bool(item.get("required", True)),
399
+ "example": str(item.get("example") or item.get("value") or "").strip(),
400
+ "whereToAdd": str(item.get("whereToAdd") or ".env").strip() or ".env",
401
+ "purpose": str(item.get("purpose") or item.get("description") or "").strip(),
402
+ }
403
+ )
404
+ return required_inputs
405
+
406
+
407
+ def _bullet_text(items: list[str], fallback: str) -> str:
408
+ if not items:
409
+ return f"- {fallback}"
410
+ return "\n".join(f"- {item}" for item in items)
411
+
412
+
413
+ def _selected_stack_text(selected_stack: dict[str, Any]) -> str:
414
+ if not isinstance(selected_stack, dict) or not selected_stack:
415
+ return "- No explicit stack selection was recorded."
416
+
417
+ ordered_labels = [
418
+ ("language", "Language"),
419
+ ("frontend", "Frontend"),
420
+ ("backend", "Backend"),
421
+ ("database", "Database"),
422
+ ("aiTools", "AI / Tools"),
423
+ ("deployment", "Deployment"),
424
+ ]
425
+ lines = []
426
+ for key, label in ordered_labels:
427
+ value = str(selected_stack.get(key) or "Auto").strip()
428
+ lines.append(f"- {label}: {value}")
429
+ return "\n".join(lines)
430
+
431
+
432
+ def _env_variables_text(env_variables: list[dict[str, str]]) -> str:
433
+ if not env_variables:
434
+ return "- No environment variables are required."
435
+
436
+ lines = []
437
+ for variable in env_variables:
438
+ description = variable.get("description", "").strip()
439
+ value = variable.get("value", "").strip()
440
+ suffix = f" ({description})" if description else ""
441
+ default_text = f" default `{value}`" if value else ""
442
+ lines.append(f"- `{variable['name']}`{suffix}{default_text}")
443
+ return "\n".join(lines)
444
+
445
+
446
+ def _required_inputs_to_env_list(required_inputs: list[dict[str, Any]]) -> list[dict[str, str]]:
447
+ env_vars: list[dict[str, str]] = []
448
+ for item in required_inputs:
449
+ env_vars.append(
450
+ {
451
+ "name": str(item.get("name") or "").strip(),
452
+ "value": str(item.get("example") or "").strip(),
453
+ "description": str(item.get("purpose") or "").strip(),
454
+ }
455
+ )
456
+ return [item for item in env_vars if item["name"]]
457
+
458
+
459
+ def _required_inputs_summary(required_inputs: list[dict[str, Any]]) -> str:
460
+ if not required_inputs:
461
+ return "- No required inputs were detected. `.env.example` is still included for future overrides."
462
+ return "\n".join(
463
+ f"- `{item['name']}` ({'required' if item.get('required', True) else 'optional'}): {item.get('purpose') or 'No description provided.'}"
464
+ for item in required_inputs
465
+ )
466
+
467
+
468
+ def _required_inputs_table(required_inputs: list[dict[str, Any]]) -> str:
469
+ if not required_inputs:
470
+ return "| None | No | n/a | `.env` | No required external values were detected for this starter. |"
471
+
472
+ rows = []
473
+ for item in required_inputs:
474
+ rows.append(
475
+ "| {name} | {required} | {example} | {where_to_add} | {purpose} |".format(
476
+ name=item.get("name") or "",
477
+ required="Yes" if item.get("required", True) else "No",
478
+ example=(item.get("example") or "").replace("|", "\\|"),
479
+ where_to_add=(item.get("whereToAdd") or ".env").replace("|", "\\|"),
480
+ purpose=(item.get("purpose") or "").replace("|", "\\|"),
481
+ )
482
+ )
483
+ return "\n".join(rows)
484
+
485
+
486
+ def _modules_text(modules: list[dict[str, Any]]) -> str:
487
+ if not modules:
488
+ return "- No modules were provided."
489
+
490
+ lines: list[str] = []
491
+ for module in modules:
492
+ name = str(module.get("name") or "Unnamed module").strip()
493
+ purpose = str(module.get("purpose") or "No purpose provided.").strip()
494
+ key_files = _listify(module.get("keyFiles"))
495
+ lines.append(f"- {name}: {purpose}")
496
+ if key_files:
497
+ lines.append(f" Key files: {', '.join(key_files)}")
498
+ return "\n".join(lines)
499
+
500
+
501
+ MAX_CUSTOM_TEMPLATE_FILES = 8
502
+ MAX_CUSTOM_FILE_LINES = 300
503
+
504
+
505
+ def finalize_preview_files(
506
+ *,
507
+ project_name: str,
508
+ selected_stack: Mapping[str, Any],
509
+ project_kind: Mapping[str, Any],
510
+ custom_manifest: Sequence[Mapping[str, Any]] | None = None,
511
+ raw_files: Any = None,
512
+ ) -> list[dict[str, str]]:
513
+ standard_files = _build_standard_files(project_name, selected_stack, project_kind)
514
+ custom_template_files = _build_custom_template_files(
515
+ custom_manifest or [],
516
+ project_name,
517
+ selected_stack,
518
+ project_kind,
519
+ )
520
+ existing_files = _normalize_preview_files(raw_files)
521
+
522
+ merged_files = _merge_file_entries(standard_files, custom_template_files)
523
+ merged_files = _merge_file_entries(merged_files, existing_files)
524
+ completed_files = _ensure_minimum_project_files(
525
+ merged_files,
526
+ project_name,
527
+ selected_stack,
528
+ project_kind,
529
+ )
530
+ repaired_files = _repair_runtime_contract(
531
+ completed_files,
532
+ project_name,
533
+ selected_stack,
534
+ project_kind,
535
+ )
536
+ return validate_generated_files(repaired_files)
537
+
538
+
539
+ def build_preview_file_tree(
540
+ files: Sequence[Mapping[str, Any]],
541
+ *,
542
+ include_env_example: bool,
543
+ ) -> str:
544
+ doc_paths = [
545
+ "README.md",
546
+ "PROJECT_EXPLANATION.md",
547
+ "SETUP_INSTRUCTIONS.md",
548
+ "FILE_STRUCTURE.md",
549
+ "PACKAGE_REQUIREMENTS.md",
550
+ "REQUIRED_INPUTS.md",
551
+ ".env.example",
552
+ ]
553
+ file_paths = [str(file_entry.get("path") or "").strip() for file_entry in files]
554
+ return build_file_tree_from_paths([path for path in file_paths if path] + doc_paths)
555
+
556
+
557
+ def _build_standard_files(
558
+ project_name: str,
559
+ selected_stack: Mapping[str, Any],
560
+ project_kind: Mapping[str, Any],
561
+ ) -> list[dict[str, str]]:
562
+ files: dict[str, str] = {}
563
+ if project_kind["isFullStack"]:
564
+ if project_kind["hasFrontend"]:
565
+ files.update(
566
+ _build_frontend_files(
567
+ str(selected_stack.get("frontend") or "React"),
568
+ project_name,
569
+ "frontend",
570
+ )
571
+ )
572
+ if project_kind["hasBackend"]:
573
+ files.update(_build_backend_files(selected_stack, project_name, "backend"))
574
+ files.update(_build_root_scripts(selected_stack, project_kind))
575
+ elif project_kind["hasBackend"]:
576
+ files.update(_build_backend_files(selected_stack, project_name, ""))
577
+ files.update(_build_root_scripts(selected_stack, project_kind))
578
+ else:
579
+ files.update(
580
+ _build_frontend_files(
581
+ str(selected_stack.get("frontend") or "React"),
582
+ project_name,
583
+ "",
584
+ )
585
+ )
586
+ files.update(_build_root_scripts(selected_stack, project_kind))
587
+ return [{"path": path, "content": content} for path, content in files.items()]
588
+
589
+
590
+ def _build_custom_template_files(
591
+ manifest: Sequence[Mapping[str, Any]],
592
+ project_name: str,
593
+ selected_stack: Mapping[str, Any],
594
+ project_kind: Mapping[str, Any],
595
+ ) -> list[dict[str, str]]:
596
+ files: list[dict[str, str]] = []
597
+ for item in list(manifest)[:MAX_CUSTOM_TEMPLATE_FILES]:
598
+ path = _clean_relative_path(item.get("path"))
599
+ purpose = str(item.get("purpose") or "").strip()
600
+ if not path or not purpose:
601
+ continue
602
+ content = _build_custom_template_content(
603
+ path,
604
+ purpose,
605
+ project_name,
606
+ selected_stack,
607
+ project_kind,
608
+ )
609
+ files.append({"path": path, "content": _trim_content_lines(content)})
610
+ return files
611
+
612
+
613
+ def _build_custom_template_content(
614
+ path: str,
615
+ purpose: str,
616
+ project_name: str,
617
+ selected_stack: Mapping[str, Any],
618
+ project_kind: Mapping[str, Any],
619
+ ) -> str:
620
+ del selected_stack, project_kind
621
+ stem = Path(path).stem
622
+ pretty_name = stem.replace("_", " ").replace("-", " ").title()
623
+ extension = Path(path).suffix.lower()
624
+
625
+ if extension in {".jsx", ".tsx"}:
626
+ if "page" in stem.lower() or "/pages/" in path:
627
+ return f"""const cards = [
628
+ {{
629
+ title: "{pretty_name} Overview",
630
+ detail: "{purpose}"
631
+ }},
632
+ {{
633
+ title: "Starter Workflow",
634
+ detail: "Use this page to connect forms, API requests, and user-facing business actions."
635
+ }}
636
+ ];
637
+
638
+ export default function {_safe_component_name(stem)}() {{
639
+ return (
640
+ <section className="card">
641
+ <h2>{pretty_name}</h2>
642
+ <p>{purpose}</p>
643
+ <ul>
644
+ {{cards.map((card) => (
645
+ <li key={{card.title}}>
646
+ <strong>{{card.title}}</strong>: {{card.detail}}
647
+ </li>
648
+ ))}}
649
+ </ul>
650
+ </section>
651
+ );
652
+ }}
653
+ """
654
+ return f"""export default function {_safe_component_name(stem)}() {{
655
+ return (
656
+ <section className="card">
657
+ <h3>{pretty_name}</h3>
658
+ <p>{purpose}</p>
659
+ </section>
660
+ );
661
+ }}
662
+ """
663
+
664
+ if extension == ".py":
665
+ if "router" in path or "/routers/" in path:
666
+ return f"""from fastapi import APIRouter
667
+
668
+ router = APIRouter()
669
+
670
+
671
+ @router.get("/")
672
+ def read_{_safe_python_name(stem)}() -> dict[str, str]:
673
+ return {{
674
+ "message": "{purpose}",
675
+ "project": "{project_name}",
676
+ }}
677
+ """
678
+ if "schema" in path or "/schemas/" in path:
679
+ return f"""from pydantic import BaseModel
680
+
681
+
682
+ class {_safe_component_name(stem)}(BaseModel):
683
+ name: str
684
+ description: str = "{purpose}"
685
+ """
686
+ if "model" in path or "/models/" in path:
687
+ return f"""from dataclasses import dataclass
688
+
689
+
690
+ @dataclass
691
+ class {_safe_component_name(stem)}:
692
+ name: str
693
+ status: str = "ready"
694
+ """
695
+ return f"""def {_safe_python_name(stem)}_summary() -> dict[str, str]:
696
+ return {{
697
+ "name": "{pretty_name}",
698
+ "purpose": "{purpose}",
699
+ "project": "{project_name}",
700
+ }}
701
+ """
702
+
703
+ if extension in {".js", ".mjs"}:
704
+ if "service" in path.lower():
705
+ return f"""export function get{_safe_component_name(stem)}Summary() {{
706
+ return {{
707
+ project: "{project_name}",
708
+ purpose: "{purpose}"
709
+ }};
710
+ }}
711
+ """
712
+ if "controller" in path.lower():
713
+ return f"""export function {_safe_js_name(stem)}(_req, res) {{
714
+ res.json({{
715
+ project: "{project_name}",
716
+ purpose: "{purpose}"
717
+ }});
718
+ }}
719
+ """
720
+ return f"""export const {_safe_js_name(stem)} = {{
721
+ project: "{project_name}",
722
+ purpose: "{purpose}"
723
+ }};
724
+ """
725
+
726
+ if extension == ".java":
727
+ class_name = _safe_component_name(stem)
728
+ return f"""package com.example.demo.service;
729
+
730
+ import org.springframework.stereotype.Service;
731
+
732
+ @Service
733
+ public class {class_name} {{
734
+ public String summary() {{
735
+ return "{purpose}";
736
+ }}
737
+ }}
738
+ """
739
+
740
+ return f"# {pretty_name}\n\n{purpose}\n"
741
+
742
+
743
+ def _ensure_minimum_project_files(
744
+ files: Sequence[Mapping[str, str]],
745
+ project_name: str,
746
+ selected_stack: Mapping[str, Any],
747
+ project_kind: Mapping[str, Any],
748
+ ) -> list[dict[str, str]]:
749
+ merged = {str(entry["path"]): str(entry["content"]) for entry in files}
750
+ for path, content in _build_root_scripts(selected_stack, project_kind).items():
751
+ merged.setdefault(path, content)
752
+
753
+ if project_kind["isFullStack"]:
754
+ for path, content in _build_frontend_files(
755
+ str(selected_stack.get("frontend") or "React"),
756
+ project_name,
757
+ "frontend",
758
+ ).items():
759
+ merged.setdefault(path, content)
760
+ for path, content in _build_backend_files(selected_stack, project_name, "backend").items():
761
+ merged.setdefault(path, content)
762
+ elif project_kind["hasBackend"]:
763
+ for path, content in _build_backend_files(selected_stack, project_name, "").items():
764
+ merged.setdefault(path, content)
765
+ else:
766
+ for path, content in _build_frontend_files(
767
+ str(selected_stack.get("frontend") or "React"),
768
+ project_name,
769
+ "",
770
+ ).items():
771
+ merged.setdefault(path, content)
772
+
773
+ filler_index = 1
774
+ minimum_files = int(project_kind.get("minimumFiles") or 0)
775
+ while len(merged) < minimum_files:
776
+ filler_path = f"notes/starter-note-{filler_index}.md"
777
+ merged.setdefault(
778
+ filler_path,
779
+ f"# Starter Note {filler_index}\n\nThis file preserves the complete minimum project structure while you continue iterating.\n",
780
+ )
781
+ filler_index += 1
782
+
783
+ return [{"path": path, "content": content} for path, content in merged.items()]
784
+
785
+
786
+ def _repair_runtime_contract(
787
+ files: Sequence[Mapping[str, str]],
788
+ project_name: str,
789
+ selected_stack: Mapping[str, Any],
790
+ project_kind: Mapping[str, Any],
791
+ ) -> list[dict[str, str]]:
792
+ standard_map = {
793
+ item["path"]: item["content"]
794
+ for item in _build_standard_files(project_name, selected_stack, project_kind)
795
+ }
796
+ merged = {str(entry["path"]): str(entry["content"]) for entry in files}
797
+
798
+ for protected_path in _protected_runtime_paths(selected_stack, project_kind):
799
+ template_content = standard_map.get(protected_path)
800
+ if template_content is not None:
801
+ merged[protected_path] = template_content
802
+
803
+ for required_path in _required_runtime_paths(selected_stack, project_kind):
804
+ template_content = standard_map.get(required_path)
805
+ if template_content is None:
806
+ continue
807
+ if not str(merged.get(required_path, "")).strip():
808
+ merged[required_path] = template_content
809
+
810
+ for path, content in list(merged.items()):
811
+ if not str(content).strip():
812
+ replacement = standard_map.get(path) or _build_safe_fallback_content(
813
+ path,
814
+ project_name,
815
+ )
816
+ merged[path] = replacement
817
+
818
+ for path, content in list(merged.items()):
819
+ if not path.endswith(".py"):
820
+ continue
821
+ if _python_compiles(content):
822
+ continue
823
+ merged[path] = standard_map.get(path) or _build_safe_fallback_content(path, project_name)
824
+
825
+ for package_json_path in _package_json_paths(selected_stack, project_kind):
826
+ if package_json_path not in merged or not _valid_package_json(
827
+ merged[package_json_path],
828
+ _expected_package_scripts(package_json_path, selected_stack, project_kind),
829
+ ):
830
+ template = standard_map.get(package_json_path)
831
+ if template is not None:
832
+ merged[package_json_path] = template
833
+
834
+ for entry_path in _entry_validation_paths(selected_stack, project_kind):
835
+ template = standard_map.get(entry_path)
836
+ content = str(merged.get(entry_path, ""))
837
+ if template is None:
838
+ continue
839
+ if not content.strip() or not _valid_entry_file(entry_path, content):
840
+ merged[entry_path] = template
841
+
842
+ if project_kind["hasBackend"]:
843
+ for endpoint_path in _backend_endpoint_paths(selected_stack, project_kind):
844
+ template = standard_map.get(endpoint_path)
845
+ if template is None:
846
+ continue
847
+ content = str(merged.get(endpoint_path, ""))
848
+ if not content.strip() or not _valid_backend_endpoint_file(endpoint_path, content):
849
+ merged[endpoint_path] = template
850
+
851
+ if project_kind["hasFrontend"]:
852
+ for page_path in _frontend_page_paths(selected_stack, project_kind):
853
+ template = standard_map.get(page_path)
854
+ if template is None:
855
+ continue
856
+ if not str(merged.get(page_path, "")).strip():
857
+ merged[page_path] = template
858
+
859
+ return [{"path": path, "content": content} for path, content in merged.items()]
860
+
861
+
862
+ def _build_backend_files(
863
+ selected_stack: Mapping[str, Any],
864
+ project_name: str,
865
+ prefix: str,
866
+ ) -> dict[str, str]:
867
+ backend = str(selected_stack.get("backend") or "FastAPI")
868
+ if backend in {"FastAPI", "Flask"}:
869
+ return _build_fastapi_backend_files(project_name, prefix)
870
+ if backend in {"Express", "NestJS"}:
871
+ return _build_express_backend_files(project_name, prefix)
872
+ if backend == "Spring Boot":
873
+ return _build_spring_backend_files(project_name, prefix)
874
+ return _build_fastapi_backend_files(project_name, prefix)
875
+
876
+
877
+ def _build_frontend_files(frontend: str, project_name: str, prefix: str) -> dict[str, str]:
878
+ if frontend in {"React", "Next.js", "Vue"}:
879
+ return _build_react_frontend_files(project_name, prefix)
880
+ return _build_vanilla_frontend_files(project_name, prefix)
881
+
882
+
883
+ def _build_fastapi_backend_files(project_name: str, prefix: str) -> dict[str, str]:
884
+ app_prefix = _prefixed(prefix, "app")
885
+ return {
886
+ _prefixed(prefix, "requirements.txt"): "\n".join(
887
+ [
888
+ "fastapi",
889
+ "uvicorn[standard]",
890
+ "pydantic",
891
+ "pydantic-settings",
892
+ "sqlalchemy",
893
+ "python-dotenv",
894
+ "aiosqlite",
895
+ "",
896
+ ]
897
+ ),
898
+ _prefixed(app_prefix, "__init__.py"): "",
899
+ _prefixed(app_prefix, "main.py"): f"""from fastapi import FastAPI
900
+
901
+ from app.routers import health, items
902
+
903
+
904
+ app = FastAPI(title="{project_name} API")
905
+ app.include_router(health.router)
906
+ app.include_router(items.router, prefix="/api/items", tags=["items"])
907
+
908
+
909
+ @app.get("/")
910
+ def read_root() -> dict[str, str]:
911
+ return {{"status": "ok", "message": "Project is running"}}
912
+ """,
913
+ _prefixed(app_prefix, "routers/__init__.py"): "",
914
+ _prefixed(app_prefix, "routers/health.py"): """from fastapi import APIRouter
915
+
916
+ from app.schemas.health import HealthResponse
917
+
918
+ router = APIRouter(tags=["health"])
919
+
920
+
921
+ @router.get("/health", response_model=HealthResponse)
922
+ def healthcheck() -> HealthResponse:
923
+ return HealthResponse(status="ok", message="Project is running")
924
+ """,
925
+ _prefixed(app_prefix, "routers/items.py"): """from fastapi import APIRouter
926
+
927
+ from app.schemas.item import Item
928
+ from app.services.item_service import list_items
929
+
930
+ router = APIRouter()
931
+
932
+
933
+ @router.get("/", response_model=list[Item])
934
+ def get_items() -> list[Item]:
935
+ return list_items()
936
+ """,
937
+ _prefixed(app_prefix, "services/__init__.py"): "",
938
+ _prefixed(app_prefix, "services/app_service.py"): f"""def get_app_summary() -> str:
939
+ return "{project_name} includes routes, services, schemas, and configuration for quick iteration."
940
+ """,
941
+ _prefixed(app_prefix, "services/item_service.py"): """from app.schemas.item import Item
942
+
943
+
944
+ def list_items() -> list[Item]:
945
+ return [
946
+ Item(id=1, name="Starter task", status="ready"),
947
+ Item(id=2, name="Next iteration", status="planned"),
948
+ ]
949
+ """,
950
+ _prefixed(app_prefix, "models/__init__.py"): "",
951
+ _prefixed(app_prefix, "models/base.py"): """from sqlalchemy.orm import DeclarativeBase
952
+
953
+
954
+ class Base(DeclarativeBase):
955
+ pass
956
+ """,
957
+ _prefixed(app_prefix, "models/item.py"): """from sqlalchemy import String
958
+ from sqlalchemy.orm import Mapped, mapped_column
959
+
960
+ from app.models.base import Base
961
+
962
+
963
+ class ItemModel(Base):
964
+ __tablename__ = "items"
965
+
966
+ id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
967
+ name: Mapped[str] = mapped_column(String(120))
968
+ status: Mapped[str] = mapped_column(String(40), default="ready")
969
+ """,
970
+ _prefixed(app_prefix, "schemas/__init__.py"): "",
971
+ _prefixed(app_prefix, "schemas/health.py"): """from pydantic import BaseModel
972
+
973
+
974
+ class HealthResponse(BaseModel):
975
+ status: str
976
+ message: str
977
+ """,
978
+ _prefixed(app_prefix, "schemas/item.py"): """from pydantic import BaseModel
979
+
980
+
981
+ class Item(BaseModel):
982
+ id: int
983
+ name: str
984
+ status: str
985
+ """,
986
+ _prefixed(app_prefix, "database.py"): """from sqlalchemy import create_engine
987
+ from sqlalchemy.orm import sessionmaker
988
+
989
+ from app.config import settings
990
+
991
+ engine = create_engine(settings.database_url, future=True)
992
+ SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
993
+ """,
994
+ _prefixed(app_prefix, "config.py"): """from pydantic_settings import BaseSettings, SettingsConfigDict
995
+
996
+
997
+ class Settings(BaseSettings):
998
+ model_config = SettingsConfigDict(env_file=".env")
999
+ app_env: str = "development"
1000
+ database_url: str = "sqlite:///./app.db"
1001
+
1002
+
1003
+ settings = Settings()
1004
+ """,
1005
+ }
1006
+
1007
+
1008
+ def _build_express_backend_files(project_name: str, prefix: str) -> dict[str, str]:
1009
+ return {
1010
+ _prefixed(prefix, "package.json"): json.dumps(
1011
+ {
1012
+ "name": project_name.lower().replace(" ", "-"),
1013
+ "version": "0.1.0",
1014
+ "private": True,
1015
+ "type": "module",
1016
+ "scripts": {"dev": "node --watch server.js", "start": "node server.js"},
1017
+ "dependencies": {"cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2"},
1018
+ },
1019
+ indent=2,
1020
+ )
1021
+ + "\n",
1022
+ _prefixed(prefix, "server.js"): f"""import cors from "cors";
1023
+ import dotenv from "dotenv";
1024
+ import express from "express";
1025
+ import indexRouter from "./src/routes/index.js";
1026
+ import itemsRouter from "./src/routes/items.js";
1027
+
1028
+ dotenv.config();
1029
+
1030
+ const app = express();
1031
+ app.use(cors());
1032
+ app.use(express.json());
1033
+ app.use("/", indexRouter);
1034
+ app.use("/api/items", itemsRouter);
1035
+
1036
+ const port = process.env.PORT || 8000;
1037
+ app.listen(port, () => {{
1038
+ console.log("{project_name} API listening on port", port);
1039
+ }});
1040
+ """,
1041
+ _prefixed(prefix, "src/routes/index.js"): """import { Router } from "express";
1042
+ import { getStatus } from "../controllers/appController.js";
1043
+
1044
+ const router = Router();
1045
+ router.get("/", getStatus);
1046
+
1047
+ export default router;
1048
+ """,
1049
+ _prefixed(prefix, "src/routes/items.js"): """import { Router } from "express";
1050
+ import { listItems } from "../controllers/itemController.js";
1051
+
1052
+ const router = Router();
1053
+ router.get("/", listItems);
1054
+
1055
+ export default router;
1056
+ """,
1057
+ _prefixed(prefix, "src/controllers/appController.js"): """export function getStatus(_req, res) {
1058
+ res.json({ status: "ok", message: "Project is running" });
1059
+ }
1060
+ """,
1061
+ _prefixed(prefix, "src/controllers/itemController.js"): """import { getItems } from "../services/itemService.js";
1062
+
1063
+ export function listItems(_req, res) {
1064
+ res.json(getItems());
1065
+ }
1066
+ """,
1067
+ _prefixed(prefix, "src/services/appService.js"): f"""export function getAppSummary() {{
1068
+ return "{project_name} includes routes, controllers, services, and starter configuration.";
1069
+ }}
1070
+ """,
1071
+ _prefixed(prefix, "src/services/itemService.js"): """export function getItems() {
1072
+ return [
1073
+ { id: 1, name: "Starter task", status: "ready" },
1074
+ { id: 2, name: "Next iteration", status: "planned" }
1075
+ ];
1076
+ }
1077
+ """,
1078
+ _prefixed(prefix, "src/models/itemModel.js"): """export const itemShape = {
1079
+ id: "number",
1080
+ name: "string",
1081
+ status: "string"
1082
+ };
1083
+ """,
1084
+ }
1085
+
1086
+
1087
+ def _build_react_frontend_files(project_name: str, prefix: str) -> dict[str, str]:
1088
+ return {
1089
+ _prefixed(prefix, "package.json"): json.dumps(
1090
+ {
1091
+ "name": project_name.lower().replace(" ", "-") + "-frontend",
1092
+ "private": True,
1093
+ "version": "0.1.0",
1094
+ "type": "module",
1095
+ "scripts": {"dev": "vite", "build": "vite build", "preview": "vite preview"},
1096
+ "dependencies": {"react": "^18.3.1", "react-dom": "^18.3.1"},
1097
+ "devDependencies": {"vite": "^5.4.0", "@vitejs/plugin-react": "^4.3.1"},
1098
+ },
1099
+ indent=2,
1100
+ )
1101
+ + "\n",
1102
+ _prefixed(prefix, "index.html"): """<!doctype html>
1103
+ <html lang="en">
1104
+ <head>
1105
+ <meta charset="UTF-8" />
1106
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1107
+ <title>Project Starter</title>
1108
+ <script type="module" src="/src/main.jsx"></script>
1109
+ </head>
1110
+ <body>
1111
+ <div id="root"></div>
1112
+ </body>
1113
+ </html>
1114
+ """,
1115
+ _prefixed(prefix, "vite.config.js"): """import { defineConfig } from "vite";
1116
+ import react from "@vitejs/plugin-react";
1117
+
1118
+ export default defineConfig({
1119
+ plugins: [react()],
1120
+ server: {
1121
+ port: 5173
1122
+ }
1123
+ });
1124
+ """,
1125
+ _prefixed(prefix, "src/main.jsx"): """import React from "react";
1126
+ import ReactDOM from "react-dom/client";
1127
+ import App from "./App";
1128
+ import "./styles.css";
1129
+
1130
+ ReactDOM.createRoot(document.getElementById("root")).render(
1131
+ <React.StrictMode>
1132
+ <App />
1133
+ </React.StrictMode>
1134
+ );
1135
+ """,
1136
+ _prefixed(prefix, "src/App.jsx"): f"""import AppShell from "./components/AppShell";
1137
+ import HomePage from "./pages/HomePage";
1138
+
1139
+ export default function App() {{
1140
+ return (
1141
+ <AppShell title="{project_name}">
1142
+ <HomePage />
1143
+ </AppShell>
1144
+ );
1145
+ }}
1146
+ """,
1147
+ _prefixed(prefix, "src/components/AppShell.jsx"): """export default function AppShell({ title, children }) {
1148
+ return (
1149
+ <div className="app-shell">
1150
+ <header className="hero">
1151
+ <p className="eyebrow">Generated by Project Agent</p>
1152
+ <h1>{title}</h1>
1153
+ </header>
1154
+ <main>{children}</main>
1155
+ </div>
1156
+ );
1157
+ }
1158
+ """,
1159
+ _prefixed(prefix, "src/pages/HomePage.jsx"): """import { getProjectHealth } from "../services/api";
1160
+
1161
+ export default function HomePage() {
1162
+ const projectHealth = getProjectHealth();
1163
+
1164
+ return (
1165
+ <section className="card">
1166
+ <h2>Starter Overview</h2>
1167
+ <p>This 100% runnable starter project is ready for your first feature slice.</p>
1168
+ <p>API health source: {projectHealth}</p>
1169
+ </section>
1170
+ );
1171
+ }
1172
+ """,
1173
+ _prefixed(prefix, "src/services/api.js"): """export function getProjectHealth() {
1174
+ return import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
1175
+ }
1176
+ """,
1177
+ _prefixed(prefix, "src/styles.css"): """:root {
1178
+ color-scheme: light;
1179
+ font-family: "Segoe UI", Arial, sans-serif;
1180
+ background: #f5f7fb;
1181
+ color: #132238;
1182
+ }
1183
+
1184
+ body {
1185
+ margin: 0;
1186
+ }
1187
+
1188
+ .app-shell {
1189
+ max-width: 960px;
1190
+ margin: 0 auto;
1191
+ padding: 32px 20px 56px;
1192
+ }
1193
+
1194
+ .hero {
1195
+ margin-bottom: 24px;
1196
+ }
1197
+
1198
+ .eyebrow {
1199
+ font-size: 0.75rem;
1200
+ letter-spacing: 0.12em;
1201
+ text-transform: uppercase;
1202
+ color: #57657d;
1203
+ }
1204
+
1205
+ .card {
1206
+ background: white;
1207
+ border-radius: 16px;
1208
+ padding: 24px;
1209
+ box-shadow: 0 18px 40px rgba(19, 34, 56, 0.08);
1210
+ }
1211
+ """,
1212
+ }
1213
+
1214
+
1215
+ def _build_vanilla_frontend_files(project_name: str, prefix: str) -> dict[str, str]:
1216
+ return {
1217
+ _prefixed(prefix, "package.json"): json.dumps(
1218
+ {
1219
+ "name": project_name.lower().replace(" ", "-") + "-frontend",
1220
+ "private": True,
1221
+ "version": "0.1.0",
1222
+ "type": "module",
1223
+ "scripts": {"dev": "vite", "build": "vite build", "preview": "vite preview"},
1224
+ "devDependencies": {"vite": "^5.4.0"},
1225
+ },
1226
+ indent=2,
1227
+ )
1228
+ + "\n",
1229
+ _prefixed(prefix, "index.html"): """<!doctype html>
1230
+ <html lang="en">
1231
+ <head>
1232
+ <meta charset="UTF-8" />
1233
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1234
+ <title>Project Starter</title>
1235
+ <script type="module" src="/src/main.js"></script>
1236
+ </head>
1237
+ <body>
1238
+ <div id="app"></div>
1239
+ </body>
1240
+ </html>
1241
+ """,
1242
+ _prefixed(prefix, "vite.config.js"): """import { defineConfig } from "vite";
1243
+
1244
+ export default defineConfig({
1245
+ server: {
1246
+ port: 5173
1247
+ }
1248
+ });
1249
+ """,
1250
+ _prefixed(prefix, "src/main.js"): f"""import {{ renderHomePage }} from "./views/home.js";
1251
+ import "./styles.css";
1252
+
1253
+ document.querySelector("#app").innerHTML = renderHomePage("{project_name}");
1254
+ """,
1255
+ _prefixed(prefix, "src/views/home.js"): """export function renderHomePage(title) {
1256
+ return `
1257
+ <main class="app-shell">
1258
+ <section class="card">
1259
+ <p class="eyebrow">Generated by Project Agent</p>
1260
+ <h1>${title}</h1>
1261
+ <p>This starter is ready for your first feature slice.</p>
1262
+ </section>
1263
+ </main>
1264
+ `;
1265
+ }
1266
+ """,
1267
+ _prefixed(prefix, "src/services/api.js"): """export function getApiBaseUrl() {
1268
+ return import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
1269
+ }
1270
+ """,
1271
+ _prefixed(prefix, "src/styles.css"): """body {
1272
+ margin: 0;
1273
+ font-family: "Segoe UI", Arial, sans-serif;
1274
+ background: #f5f7fb;
1275
+ color: #132238;
1276
+ }
1277
+
1278
+ .app-shell {
1279
+ max-width: 960px;
1280
+ margin: 0 auto;
1281
+ padding: 32px 20px 56px;
1282
+ }
1283
+
1284
+ .card {
1285
+ background: white;
1286
+ border-radius: 16px;
1287
+ padding: 24px;
1288
+ box-shadow: 0 18px 40px rgba(19, 34, 56, 0.08);
1289
+ }
1290
+
1291
+ .eyebrow {
1292
+ font-size: 0.75rem;
1293
+ text-transform: uppercase;
1294
+ color: #57657d;
1295
+ }
1296
+ """,
1297
+ }
1298
+
1299
+
1300
+ def _build_spring_backend_files(project_name: str, prefix: str) -> dict[str, str]:
1301
+ java_base = _prefixed(prefix, "src/main/java/com/example/demo")
1302
+ resources_base = _prefixed(prefix, "src/main/resources")
1303
+ return {
1304
+ _prefixed(prefix, "pom.xml"): """<project xmlns="http://maven.apache.org/POM/4.0.0"
1305
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1306
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
1307
+ <modelVersion>4.0.0</modelVersion>
1308
+ <groupId>com.example</groupId>
1309
+ <artifactId>demo</artifactId>
1310
+ <version>0.0.1-SNAPSHOT</version>
1311
+ <parent>
1312
+ <groupId>org.springframework.boot</groupId>
1313
+ <artifactId>spring-boot-starter-parent</artifactId>
1314
+ <version>3.3.2</version>
1315
+ </parent>
1316
+ <properties>
1317
+ <java.version>17</java.version>
1318
+ </properties>
1319
+ <dependencies>
1320
+ <dependency>
1321
+ <groupId>org.springframework.boot</groupId>
1322
+ <artifactId>spring-boot-starter-web</artifactId>
1323
+ </dependency>
1324
+ <dependency>
1325
+ <groupId>org.springframework.boot</groupId>
1326
+ <artifactId>spring-boot-starter-data-jpa</artifactId>
1327
+ </dependency>
1328
+ </dependencies>
1329
+ <build>
1330
+ <plugins>
1331
+ <plugin>
1332
+ <groupId>org.springframework.boot</groupId>
1333
+ <artifactId>spring-boot-maven-plugin</artifactId>
1334
+ </plugin>
1335
+ </plugins>
1336
+ </build>
1337
+ </project>
1338
+ """,
1339
+ _prefixed(java_base, "Application.java"): """package com.example.demo;
1340
+
1341
+ import org.springframework.boot.SpringApplication;
1342
+ import org.springframework.boot.autoconfigure.SpringBootApplication;
1343
+
1344
+ @SpringBootApplication
1345
+ public class Application {
1346
+ public static void main(String[] args) {
1347
+ SpringApplication.run(Application.class, args);
1348
+ }
1349
+ }
1350
+ """,
1351
+ _prefixed(java_base, "controller/AppController.java"): """package com.example.demo.controller;
1352
+
1353
+ import com.example.demo.service.AppService;
1354
+ import java.util.Map;
1355
+ import org.springframework.web.bind.annotation.GetMapping;
1356
+ import org.springframework.web.bind.annotation.RestController;
1357
+
1358
+ @RestController
1359
+ public class AppController {
1360
+ private final AppService appService;
1361
+
1362
+ public AppController(AppService appService) {
1363
+ this.appService = appService;
1364
+ }
1365
+
1366
+ @GetMapping("/")
1367
+ public Map<String, String> status() {
1368
+ return Map.of("status", "ok", "message", "Project is running");
1369
+ }
1370
+ }
1371
+ """,
1372
+ _prefixed(java_base, "service/AppService.java"): f"""package com.example.demo.service;
1373
+
1374
+ import org.springframework.stereotype.Service;
1375
+
1376
+ @Service
1377
+ public class AppService {{
1378
+ public String status() {{
1379
+ return "Project is running";
1380
+ }}
1381
+ }}
1382
+ """,
1383
+ _prefixed(java_base, "model/AppModel.java"): """package com.example.demo.model;
1384
+
1385
+ public class AppModel {
1386
+ private Long id;
1387
+ private String name;
1388
+
1389
+ public Long getId() { return id; }
1390
+ public void setId(Long id) { this.id = id; }
1391
+ public String getName() { return name; }
1392
+ public void setName(String name) { this.name = name; }
1393
+ }
1394
+ """,
1395
+ _prefixed(java_base, "repository/AppRepository.java"): """package com.example.demo.repository;
1396
+
1397
+ import com.example.demo.model.AppModel;
1398
+ import java.util.List;
1399
+ import org.springframework.stereotype.Repository;
1400
+
1401
+ @Repository
1402
+ public class AppRepository {
1403
+ public List<AppModel> findAll() {
1404
+ return List.of();
1405
+ }
1406
+ }
1407
+ """,
1408
+ _prefixed(resources_base, "application.properties"): """spring.application.name=demo
1409
+ server.port=8080
1410
+ """,
1411
+ }
1412
+
1413
+
1414
+ def _build_root_scripts(
1415
+ selected_stack: Mapping[str, Any],
1416
+ project_kind: Mapping[str, Any],
1417
+ ) -> dict[str, str]:
1418
+ if project_kind["isFullStack"]:
1419
+ return _build_fullstack_scripts(selected_stack)
1420
+ if project_kind["hasBackend"]:
1421
+ backend = str(selected_stack.get("backend") or "FastAPI")
1422
+ if backend in {"FastAPI", "Flask"}:
1423
+ return _build_python_scripts(".")
1424
+ if backend in {"Express", "NestJS"}:
1425
+ return _build_node_scripts(".", "start")
1426
+ if backend == "Spring Boot":
1427
+ return _build_java_scripts(".")
1428
+ return _build_node_scripts(".", "dev")
1429
+
1430
+
1431
+ def _build_fullstack_scripts(selected_stack: Mapping[str, Any]) -> dict[str, str]:
1432
+ backend_setup = ""
1433
+ backend_run_windows = "echo No backend runtime configured.\n"
1434
+ backend_run_unix = 'echo "No backend runtime configured."\n'
1435
+ backend = str(selected_stack.get("backend") or "")
1436
+
1437
+ if backend in {"FastAPI", "Flask"}:
1438
+ backend_setup = (
1439
+ "if exist backend\\requirements.txt (\n"
1440
+ " pushd backend\n"
1441
+ " python -m venv .venv\n"
1442
+ " call .venv\\Scripts\\python -m pip install --upgrade pip\n"
1443
+ " call .venv\\Scripts\\pip install -r requirements.txt\n"
1444
+ " popd\n"
1445
+ ")\n"
1446
+ )
1447
+ backend_run_windows = (
1448
+ 'start "Backend" cmd /k "cd backend && .venv\\Scripts\\python -m uvicorn app.main:app --reload"\n'
1449
+ )
1450
+ backend_run_unix = '(cd backend && . .venv/bin/activate && uvicorn app.main:app --reload) &\n'
1451
+ elif backend in {"Express", "NestJS"}:
1452
+ backend_setup = (
1453
+ "if exist backend\\package.json (\n"
1454
+ " pushd backend\n"
1455
+ " call npm install\n"
1456
+ " popd\n"
1457
+ ")\n"
1458
+ )
1459
+ backend_run_windows = 'start "Backend" cmd /k "cd backend && npm start"\n'
1460
+ backend_run_unix = '(cd backend && npm start) &\n'
1461
+ elif backend == "Spring Boot":
1462
+ backend_setup = (
1463
+ "where mvn >nul 2>nul && (\n"
1464
+ " pushd backend\n"
1465
+ " call mvn install\n"
1466
+ " popd\n"
1467
+ ") || echo Maven not found. Skipping backend install.\n"
1468
+ )
1469
+ backend_run_windows = 'start "Backend" cmd /k "cd backend && mvn spring-boot:run"\n'
1470
+ backend_run_unix = '(cd backend && mvn spring-boot:run) &\n'
1471
+
1472
+ return {
1473
+ "setup.bat": f"""@echo off
1474
+ setlocal
1475
+ {backend_setup}if exist frontend\\package.json (
1476
+ pushd frontend
1477
+ call npm install
1478
+ popd
1479
+ )
1480
+ echo Setup complete.
1481
+ """,
1482
+ "setup.sh": """#!/usr/bin/env bash
1483
+ set -e
1484
+ if [ -f backend/requirements.txt ]; then
1485
+ (
1486
+ cd backend
1487
+ python3 -m venv .venv
1488
+ . .venv/bin/activate
1489
+ python -m pip install --upgrade pip
1490
+ pip install -r requirements.txt
1491
+ )
1492
+ fi
1493
+ if [ -f backend/package.json ]; then
1494
+ (cd backend && npm install)
1495
+ fi
1496
+ if [ -f backend/pom.xml ]; then
1497
+ if command -v mvn >/dev/null 2>&1; then
1498
+ (cd backend && mvn install)
1499
+ else
1500
+ echo "Maven not found. Skipping backend install."
1501
+ fi
1502
+ fi
1503
+ if [ -f frontend/package.json ]; then
1504
+ (cd frontend && npm install)
1505
+ fi
1506
+ echo "Setup complete."
1507
+ """,
1508
+ "run.bat": f"""@echo off
1509
+ setlocal
1510
+ {backend_run_windows}start "Frontend" cmd /k "cd frontend && npm run dev"
1511
+ """,
1512
+ "run.sh": f"""#!/usr/bin/env bash
1513
+ set -e
1514
+ {backend_run_unix}(cd frontend && npm run dev) &
1515
+ wait
1516
+ """,
1517
+ }
1518
+
1519
+
1520
+ def _build_python_scripts(target_dir: str) -> dict[str, str]:
1521
+ del target_dir
1522
+ return {
1523
+ "setup.bat": """@echo off
1524
+ setlocal
1525
+ python -m venv .venv
1526
+ call .venv\\Scripts\\python -m pip install --upgrade pip
1527
+ call .venv\\Scripts\\pip install -r requirements.txt
1528
+ echo Setup complete.
1529
+ """,
1530
+ "setup.sh": """#!/usr/bin/env bash
1531
+ set -e
1532
+ python3 -m venv .venv
1533
+ . .venv/bin/activate
1534
+ python -m pip install --upgrade pip
1535
+ pip install -r requirements.txt
1536
+ echo "Setup complete."
1537
+ """,
1538
+ "run.bat": """@echo off
1539
+ setlocal
1540
+ call .venv\\Scripts\\python -m uvicorn app.main:app --reload
1541
+ """,
1542
+ "run.sh": """#!/usr/bin/env bash
1543
+ set -e
1544
+ . .venv/bin/activate
1545
+ uvicorn app.main:app --reload
1546
+ """,
1547
+ }
1548
+
1549
+
1550
+ def _build_node_scripts(target_dir: str, script_name: str) -> dict[str, str]:
1551
+ directory_prefix = "" if target_dir in {"", "."} else f"{target_dir}/"
1552
+ windows_prefix = "" if target_dir in {"", "."} else f"{target_dir}\\"
1553
+ run_script = "npm start" if script_name == "start" else "npm run dev"
1554
+ return {
1555
+ "setup.bat": f"""@echo off
1556
+ setlocal
1557
+ pushd {windows_prefix or "."}
1558
+ call npm install
1559
+ popd
1560
+ echo Setup complete.
1561
+ """,
1562
+ "setup.sh": f"""#!/usr/bin/env bash
1563
+ set -e
1564
+ (cd {directory_prefix or "."} && npm install)
1565
+ echo "Setup complete."
1566
+ """,
1567
+ "run.bat": f"""@echo off
1568
+ setlocal
1569
+ pushd {windows_prefix or "."}
1570
+ call {run_script}
1571
+ popd
1572
+ """,
1573
+ "run.sh": f"""#!/usr/bin/env bash
1574
+ set -e
1575
+ (cd {directory_prefix or "."} && {run_script})
1576
+ """,
1577
+ }
1578
+
1579
+
1580
+ def _protected_runtime_paths(
1581
+ selected_stack: Mapping[str, Any],
1582
+ project_kind: Mapping[str, Any],
1583
+ ) -> set[str]:
1584
+ paths: set[str] = {"setup.bat", "setup.sh", "run.bat", "run.sh"}
1585
+ backend_prefix = "backend/" if project_kind["isFullStack"] else ""
1586
+ frontend_prefix = "frontend/" if project_kind["isFullStack"] else ""
1587
+
1588
+ if project_kind["hasBackend"]:
1589
+ backend = str(selected_stack.get("backend") or "FastAPI")
1590
+ if backend in {"FastAPI", "Flask"}:
1591
+ paths.update(
1592
+ {
1593
+ f"{backend_prefix}requirements.txt",
1594
+ f"{backend_prefix}app/main.py",
1595
+ f"{backend_prefix}app/routers/health.py",
1596
+ f"{backend_prefix}app/schemas/health.py",
1597
+ f"{backend_prefix}app/config.py",
1598
+ f"{backend_prefix}app/database.py",
1599
+ }
1600
+ )
1601
+ elif backend in {"Express", "NestJS"}:
1602
+ paths.update(
1603
+ {
1604
+ f"{backend_prefix}package.json",
1605
+ f"{backend_prefix}server.js",
1606
+ f"{backend_prefix}src/routes/index.js",
1607
+ f"{backend_prefix}src/controllers/appController.js",
1608
+ }
1609
+ )
1610
+ elif backend == "Spring Boot":
1611
+ paths.update(
1612
+ {
1613
+ f"{backend_prefix}pom.xml",
1614
+ f"{backend_prefix}src/main/java/com/example/demo/Application.java",
1615
+ f"{backend_prefix}src/main/java/com/example/demo/controller/AppController.java",
1616
+ f"{backend_prefix}src/main/resources/application.properties",
1617
+ }
1618
+ )
1619
+
1620
+ if project_kind["hasFrontend"]:
1621
+ frontend = str(selected_stack.get("frontend") or "React")
1622
+ if frontend in {"React", "Next.js", "Vue"}:
1623
+ paths.update(
1624
+ {
1625
+ f"{frontend_prefix}package.json",
1626
+ f"{frontend_prefix}index.html",
1627
+ f"{frontend_prefix}vite.config.js",
1628
+ f"{frontend_prefix}src/main.jsx",
1629
+ f"{frontend_prefix}src/App.jsx",
1630
+ f"{frontend_prefix}src/pages/HomePage.jsx",
1631
+ }
1632
+ )
1633
+ else:
1634
+ paths.update(
1635
+ {
1636
+ f"{frontend_prefix}package.json",
1637
+ f"{frontend_prefix}index.html",
1638
+ f"{frontend_prefix}vite.config.js",
1639
+ f"{frontend_prefix}src/main.js",
1640
+ f"{frontend_prefix}src/views/home.js",
1641
+ }
1642
+ )
1643
+
1644
+ return paths
1645
+
1646
+
1647
+ def _required_runtime_paths(
1648
+ selected_stack: Mapping[str, Any],
1649
+ project_kind: Mapping[str, Any],
1650
+ ) -> set[str]:
1651
+ return set(_protected_runtime_paths(selected_stack, project_kind))
1652
+
1653
+
1654
+ def _package_json_paths(
1655
+ selected_stack: Mapping[str, Any],
1656
+ project_kind: Mapping[str, Any],
1657
+ ) -> list[str]:
1658
+ paths: list[str] = []
1659
+ backend_prefix = "backend/" if project_kind["isFullStack"] else ""
1660
+ frontend_prefix = "frontend/" if project_kind["isFullStack"] else ""
1661
+ if project_kind["hasBackend"] and str(selected_stack.get("backend") or "") in {"Express", "NestJS"}:
1662
+ paths.append(f"{backend_prefix}package.json")
1663
+ if project_kind["hasFrontend"]:
1664
+ paths.append(f"{frontend_prefix}package.json")
1665
+ return paths
1666
+
1667
+
1668
+ def _expected_package_scripts(
1669
+ package_json_path: str,
1670
+ selected_stack: Mapping[str, Any],
1671
+ project_kind: Mapping[str, Any],
1672
+ ) -> set[str]:
1673
+ if package_json_path.endswith("backend/package.json") or (
1674
+ not project_kind["isFullStack"] and project_kind["hasBackend"]
1675
+ ):
1676
+ backend = str(selected_stack.get("backend") or "")
1677
+ if backend in {"Express", "NestJS"}:
1678
+ return {"start"}
1679
+ return {"dev"}
1680
+
1681
+
1682
+ def _entry_validation_paths(
1683
+ selected_stack: Mapping[str, Any],
1684
+ project_kind: Mapping[str, Any],
1685
+ ) -> list[str]:
1686
+ paths: list[str] = []
1687
+ backend_prefix = "backend/" if project_kind["isFullStack"] else ""
1688
+ frontend_prefix = "frontend/" if project_kind["isFullStack"] else ""
1689
+ if project_kind["hasBackend"]:
1690
+ backend = str(selected_stack.get("backend") or "FastAPI")
1691
+ if backend in {"FastAPI", "Flask"}:
1692
+ paths.append(f"{backend_prefix}app/main.py")
1693
+ elif backend in {"Express", "NestJS"}:
1694
+ paths.append(f"{backend_prefix}server.js")
1695
+ elif backend == "Spring Boot":
1696
+ paths.append(f"{backend_prefix}src/main/java/com/example/demo/Application.java")
1697
+ if project_kind["hasFrontend"]:
1698
+ frontend = str(selected_stack.get("frontend") or "React")
1699
+ if frontend in {"React", "Next.js", "Vue"}:
1700
+ paths.extend([f"{frontend_prefix}src/main.jsx", f"{frontend_prefix}src/App.jsx"])
1701
+ else:
1702
+ paths.extend([f"{frontend_prefix}src/main.js", f"{frontend_prefix}index.html"])
1703
+ return paths
1704
+
1705
+
1706
+ def _backend_endpoint_paths(
1707
+ selected_stack: Mapping[str, Any],
1708
+ project_kind: Mapping[str, Any],
1709
+ ) -> list[str]:
1710
+ if not project_kind["hasBackend"]:
1711
+ return []
1712
+ backend_prefix = "backend/" if project_kind["isFullStack"] else ""
1713
+ backend = str(selected_stack.get("backend") or "FastAPI")
1714
+ if backend in {"FastAPI", "Flask"}:
1715
+ return [f"{backend_prefix}app/main.py", f"{backend_prefix}app/routers/health.py"]
1716
+ if backend in {"Express", "NestJS"}:
1717
+ return [f"{backend_prefix}server.js", f"{backend_prefix}src/controllers/appController.js"]
1718
+ if backend == "Spring Boot":
1719
+ return [f"{backend_prefix}src/main/java/com/example/demo/controller/AppController.java"]
1720
+ return []
1721
+
1722
+
1723
+ def _frontend_page_paths(
1724
+ selected_stack: Mapping[str, Any],
1725
+ project_kind: Mapping[str, Any],
1726
+ ) -> list[str]:
1727
+ if not project_kind["hasFrontend"]:
1728
+ return []
1729
+ frontend_prefix = "frontend/" if project_kind["isFullStack"] else ""
1730
+ frontend = str(selected_stack.get("frontend") or "React")
1731
+ if frontend in {"React", "Next.js", "Vue"}:
1732
+ return [f"{frontend_prefix}src/App.jsx", f"{frontend_prefix}src/pages/HomePage.jsx"]
1733
+ return [f"{frontend_prefix}index.html", f"{frontend_prefix}src/views/home.js"]
1734
+
1735
+
1736
+ def _valid_package_json(content: str, required_scripts: set[str]) -> bool:
1737
+ try:
1738
+ payload = json.loads(content)
1739
+ except json.JSONDecodeError:
1740
+ return False
1741
+ if not isinstance(payload, Mapping):
1742
+ return False
1743
+ scripts = payload.get("scripts")
1744
+ if not isinstance(scripts, Mapping):
1745
+ return False
1746
+ return required_scripts.issubset({str(key) for key in scripts.keys()})
1747
+
1748
+
1749
+ def _valid_entry_file(path: str, content: str) -> bool:
1750
+ text = content.strip()
1751
+ if not text:
1752
+ return False
1753
+ lower = path.lower()
1754
+ if lower.endswith("app/main.py"):
1755
+ return "FastAPI" in text and "app = FastAPI" in text and '@app.get("/")' in text
1756
+ if lower.endswith("server.js"):
1757
+ return "express" in text and 'app.use("/", indexRouter)' in text and "app.listen" in text
1758
+ if lower.endswith("src/main.jsx"):
1759
+ return "ReactDOM.createRoot" in text and 'from "./App"' in text
1760
+ if lower.endswith("src/app.jsx"):
1761
+ return "export default function App" in text
1762
+ if lower.endswith("src/main.js"):
1763
+ return "renderHomePage" in text and 'querySelector("#app")' in text
1764
+ if lower.endswith("index.html"):
1765
+ return '<div id="root"></div>' in text or '<div id="app"></div>' in text
1766
+ if lower.endswith("application.java"):
1767
+ return "@SpringBootApplication" in text and "SpringApplication.run" in text
1768
+ return True
1769
+
1770
+
1771
+ def _valid_backend_endpoint_file(path: str, content: str) -> bool:
1772
+ lower = path.lower()
1773
+ if lower.endswith("app/main.py"):
1774
+ return '"status": "ok"' in content and '"message": "Project is running"' in content
1775
+ if lower.endswith("routers/health.py"):
1776
+ return 'status="ok"' in content and 'message="Project is running"' in content
1777
+ if lower.endswith("server.js"):
1778
+ return 'app.use("/", indexRouter)' in content and "app.listen" in content
1779
+ if lower.endswith("appcontroller.js"):
1780
+ return 'status: "ok"' in content and 'message: "Project is running"' in content
1781
+ if lower.endswith("appcontroller.java"):
1782
+ return '"status", "ok"' in content and '"message", "Project is running"' in content
1783
+ return True
1784
+
1785
+
1786
+ def _python_compiles(content: str) -> bool:
1787
+ try:
1788
+ compile(content, "<generated>", "exec")
1789
+ except SyntaxError:
1790
+ return False
1791
+ return True
1792
+
1793
+
1794
+ def _build_safe_fallback_content(path: str, project_name: str) -> str:
1795
+ lower = path.lower()
1796
+ if lower.endswith(".py"):
1797
+ return f"""def generated_safe_summary() -> dict[str, str]:
1798
+ return {{"status": "ok", "message": "{project_name} safe fallback is loaded."}}
1799
+ """
1800
+ if lower.endswith(".jsx"):
1801
+ return f"""export default function SafeFallback() {{
1802
+ return <section className="card"><h2>{project_name}</h2><p>Safe fallback UI loaded.</p></section>;
1803
+ }}
1804
+ """
1805
+ if lower.endswith(".js"):
1806
+ return f"""export const safeFallback = {{
1807
+ status: "ok",
1808
+ message: "{project_name} safe fallback loaded"
1809
+ }};
1810
+ """
1811
+ if lower.endswith(".java"):
1812
+ return """package com.example.demo.service;
1813
+
1814
+ public class SafeFallback {
1815
+ public String summary() {
1816
+ return "Safe fallback loaded";
1817
+ }
1818
+ }
1819
+ """
1820
+ return f"# Safe fallback\n\n{project_name} restored this file to preserve a runnable starter.\n"
1821
+
1822
+
1823
+ def _build_java_scripts(target_dir: str) -> dict[str, str]:
1824
+ directory_prefix = "" if target_dir in {"", "."} else f"{target_dir}/"
1825
+ windows_prefix = "" if target_dir in {"", "."} else f"{target_dir}\\"
1826
+ return {
1827
+ "setup.bat": f"""@echo off
1828
+ setlocal
1829
+ where mvn >nul 2>nul && (
1830
+ pushd {windows_prefix or "."}
1831
+ call mvn install
1832
+ popd
1833
+ ) || (
1834
+ echo Maven not found. Skipping install.
1835
+ )
1836
+ """,
1837
+ "setup.sh": f"""#!/usr/bin/env bash
1838
+ set -e
1839
+ if command -v mvn >/dev/null 2>&1; then
1840
+ (cd {directory_prefix or "."} && mvn install)
1841
+ else
1842
+ echo "Maven not found. Skipping install."
1843
+ fi
1844
+ """,
1845
+ "run.bat": f"""@echo off
1846
+ setlocal
1847
+ pushd {windows_prefix or "."}
1848
+ call mvn spring-boot:run
1849
+ popd
1850
+ """,
1851
+ "run.sh": f"""#!/usr/bin/env bash
1852
+ set -e
1853
+ (cd {directory_prefix or "."} && mvn spring-boot:run)
1854
+ """,
1855
+ }
1856
+
1857
+
1858
+ def _normalize_preview_files(value: Any) -> list[dict[str, str]]:
1859
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)):
1860
+ return []
1861
+ files: list[dict[str, str]] = []
1862
+ for item in value:
1863
+ if not isinstance(item, Mapping):
1864
+ continue
1865
+ path = _clean_relative_path(item.get("path"))
1866
+ if not path:
1867
+ continue
1868
+ files.append(
1869
+ {
1870
+ "path": path,
1871
+ "content": _trim_content_lines(str(item.get("content") or ""), allow_long=True),
1872
+ }
1873
+ )
1874
+ return files
1875
+
1876
+
1877
+ def _merge_file_entries(
1878
+ primary: Sequence[Mapping[str, str]],
1879
+ secondary: Sequence[Mapping[str, str]],
1880
+ ) -> list[dict[str, str]]:
1881
+ merged: dict[str, dict[str, str]] = {}
1882
+ for file_entry in primary:
1883
+ merged[str(file_entry["path"])] = {
1884
+ "path": str(file_entry["path"]),
1885
+ "content": str(file_entry["content"]),
1886
+ }
1887
+ for file_entry in secondary:
1888
+ merged[str(file_entry["path"])] = {
1889
+ "path": str(file_entry["path"]),
1890
+ "content": str(file_entry["content"]),
1891
+ }
1892
+ return list(merged.values())
1893
+
1894
+
1895
+ def _clean_relative_path(value: Any) -> str:
1896
+ path = str(value or "").replace("\\", "/").strip().strip("/")
1897
+ if not path or path.startswith(".") or ".." in path.split("/"):
1898
+ return ""
1899
+ return path
1900
+
1901
+
1902
+ def _trim_content_lines(content: str, allow_long: bool = False) -> str:
1903
+ if allow_long:
1904
+ return content
1905
+ lines = content.splitlines()
1906
+ if len(lines) <= MAX_CUSTOM_FILE_LINES:
1907
+ return content
1908
+ return "\n".join(lines[:MAX_CUSTOM_FILE_LINES]).rstrip() + "\n"
1909
+
1910
+
1911
+ def _safe_component_name(value: str) -> str:
1912
+ parts = re.findall(r"[A-Za-z0-9]+", value)
1913
+ return "".join(part.capitalize() for part in parts) or "GeneratedComponent"
1914
+
1915
+
1916
+ def _safe_python_name(value: str) -> str:
1917
+ cleaned = re.sub(r"[^a-zA-Z0-9_]+", "_", value).strip("_").lower()
1918
+ return cleaned or "generated_item"
1919
+
1920
+
1921
+ def _safe_js_name(value: str) -> str:
1922
+ parts = re.findall(r"[A-Za-z0-9]+", value)
1923
+ if not parts:
1924
+ return "generatedItem"
1925
+ head = parts[0].lower()
1926
+ tail = "".join(part.capitalize() for part in parts[1:])
1927
+ return head + tail
1928
+
1929
+
1930
+ def _prefixed(prefix: str, path: str) -> str:
1931
+ base = Path(prefix) if prefix else Path()
1932
+ return (base / path).as_posix()
app/services/zip_service.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from pathlib import Path
5
+ from typing import Any
6
+ from uuid import uuid4
7
+ from zipfile import ZIP_DEFLATED, ZipFile
8
+
9
+ from .file_service import (
10
+ SYSTEM_FILENAMES,
11
+ build_file_tree_from_paths,
12
+ build_required_docs,
13
+ ensure_within_directory,
14
+ sanitize_relative_path,
15
+ slugify,
16
+ )
17
+
18
+
19
+ def create_project_zip(preview: dict[str, Any], generated_dir: Path) -> dict[str, str]:
20
+ generated_dir.mkdir(parents=True, exist_ok=True)
21
+
22
+ project_name = str(preview.get("projectName") or "Generated Project")
23
+ slug = slugify(project_name)
24
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
25
+ unique_suffix = uuid4().hex[:8]
26
+ bundle_name = f"{slug}-{timestamp}-{unique_suffix}"
27
+
28
+ project_dir = generated_dir / bundle_name
29
+ project_dir.mkdir(parents=True, exist_ok=False)
30
+
31
+ _write_project_source_files(project_dir, preview)
32
+ bundle_info: dict[str, Any] = {}
33
+
34
+ actual_paths = [
35
+ path.relative_to(project_dir).as_posix()
36
+ for path in sorted(project_dir.rglob("*"))
37
+ if path.is_file()
38
+ ]
39
+ bundle_info["actualFileTree"] = build_file_tree_from_paths(actual_paths)
40
+
41
+ required_docs = build_required_docs(preview, bundle_info)
42
+ full_paths = sorted(set(actual_paths + list(required_docs.keys())))
43
+ bundle_info["actualFileTree"] = build_file_tree_from_paths(full_paths)
44
+ required_docs = build_required_docs(preview, bundle_info)
45
+ for doc_name, content in required_docs.items():
46
+ target_path = project_dir / doc_name
47
+ _write_text_file(project_dir, target_path, content)
48
+
49
+ zip_path = generated_dir / f"{bundle_name}.zip"
50
+ with ZipFile(zip_path, "w", compression=ZIP_DEFLATED) as zip_file:
51
+ for file_path in sorted(project_dir.rglob("*")):
52
+ if file_path.is_file():
53
+ arcname = Path(bundle_name) / file_path.relative_to(project_dir)
54
+ zip_file.write(file_path, arcname=arcname)
55
+
56
+ return {
57
+ "filename": zip_path.name,
58
+ "downloadUrl": f"/downloads/{zip_path.name}",
59
+ }
60
+
61
+
62
+ def _write_project_source_files(project_dir: Path, preview: dict[str, Any]) -> None:
63
+ written_paths: set[Path] = set()
64
+ for file_entry in preview.get("files", []):
65
+ relative_path = sanitize_relative_path(str(file_entry.get("path", "")))
66
+ if relative_path in written_paths or (
67
+ relative_path.name in SYSTEM_FILENAMES and relative_path.parent == Path(".")
68
+ ):
69
+ continue
70
+
71
+ content = str(file_entry.get("content", ""))
72
+ target_path = project_dir / relative_path
73
+ _write_text_file(project_dir, target_path, content)
74
+ written_paths.add(relative_path)
75
+
76
+
77
+ def _write_text_file(project_dir: Path, target_path: Path, content: str) -> None:
78
+ target_path.parent.mkdir(parents=True, exist_ok=True)
79
+ if not ensure_within_directory(project_dir, target_path):
80
+ raise ValueError(f"Refusing to write outside the generated project folder: {target_path}")
81
+ target_path.write_text(content, encoding="utf-8")
app/static/app.js ADDED
@@ -0,0 +1,895 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const STACK_OPTIONS = {
2
+ language: ["Auto", "Python", "JavaScript", "TypeScript", "Java"],
3
+ frontend: ["Auto", "None", "HTML/CSS/JavaScript", "React", "Next.js", "Vue"],
4
+ backend: ["Auto", "None", "FastAPI", "Flask", "Express", "NestJS", "Spring Boot"],
5
+ database: ["Auto", "None", "SQLite", "PostgreSQL", "MySQL", "MongoDB"],
6
+ aiTools: ["Auto", "None", "Ollama", "OpenAI API", "LangChain"],
7
+ deployment: ["Auto", "None", "Render", "Railway", "Vercel", "Docker"],
8
+ };
9
+
10
+ const ideaInput = document.getElementById("ideaInput");
11
+ const suggestButton = document.getElementById("suggestButton");
12
+ const continueButton = document.getElementById("continueButton");
13
+ const skipQuestionsButton = document.getElementById("skipQuestionsButton");
14
+ const generateProjectButton = document.getElementById("generateProjectButton");
15
+ const regenerateButton = document.getElementById("regenerateButton");
16
+ const confirmButton = document.getElementById("confirmButton");
17
+ const clearButton = document.getElementById("clearButton");
18
+ const statusMessage = document.getElementById("statusMessage");
19
+ const generationModeSelect = document.getElementById("generationModeSelect");
20
+
21
+ const agentSection = document.getElementById("agentSection");
22
+ const advancedStackSection = document.getElementById("advancedStackSection");
23
+ const previewSection = document.getElementById("previewSection");
24
+ const downloadSection = document.getElementById("downloadSection");
25
+
26
+ const agentUnderstandingText = document.getElementById("agentUnderstandingText");
27
+ const agentAssumptionsList = document.getElementById("agentAssumptionsList");
28
+ const agentSuggestedStackList = document.getElementById("agentSuggestedStackList");
29
+ const questionCards = document.getElementById("questionCards");
30
+ const finalizeCard = document.getElementById("finalizeCard");
31
+ const finalizeSummaryText = document.getElementById("finalizeSummaryText");
32
+ const finalSelectedStackList = document.getElementById("finalSelectedStackList");
33
+ const finalRequirementsText = document.getElementById("finalRequirementsText");
34
+
35
+ const projectNameHeading = document.getElementById("projectNameHeading");
36
+ const detectedChoicesList = document.getElementById("detectedChoicesList");
37
+ const stackChips = document.getElementById("stackChips");
38
+ const selectedStackList = document.getElementById("selectedStackList");
39
+ const chosenStackList = document.getElementById("chosenStackList");
40
+ const assumptionsList = document.getElementById("assumptionsList");
41
+ const summaryText = document.getElementById("summaryText");
42
+ const problemStatementText = document.getElementById("problemStatementText");
43
+ const architectureList = document.getElementById("architectureList");
44
+ const packageRequirementsList = document.getElementById("packageRequirementsList");
45
+ const installCommandsList = document.getElementById("installCommandsList");
46
+ const runCommandsList = document.getElementById("runCommandsList");
47
+ const envVariablesList = document.getElementById("envVariablesList");
48
+ const requiredInputsBody = document.getElementById("requiredInputsBody");
49
+ const modulesList = document.getElementById("modulesList");
50
+ const fileTreeBlock = document.getElementById("fileTreeBlock");
51
+ const filesList = document.getElementById("filesList");
52
+ const downloadText = document.getElementById("downloadText");
53
+ const downloadLink = document.getElementById("downloadLink");
54
+
55
+ const stackSelects = {
56
+ language: document.getElementById("languageSelect"),
57
+ frontend: document.getElementById("frontendSelect"),
58
+ backend: document.getElementById("backendSelect"),
59
+ database: document.getElementById("databaseSelect"),
60
+ aiTools: document.getElementById("aiToolsSelect"),
61
+ deployment: document.getElementById("deploymentSelect"),
62
+ };
63
+
64
+ let baseIdea = "";
65
+ let agentAnalysis = null;
66
+ let agentAnswers = {};
67
+ let finalRequirements = "";
68
+ let currentPreview = null;
69
+ let selectedStack = getDefaultStackState();
70
+ let currentQuestionIndex = 0;
71
+ let currentQuestionDraft = "";
72
+ let showingSuggestion = false;
73
+
74
+ initializeStackSelectors();
75
+ refreshUiState();
76
+
77
+ suggestButton.addEventListener("click", handleStartAgent);
78
+ continueButton.addEventListener("click", handleContinueAgent);
79
+ skipQuestionsButton.addEventListener("click", handleSkipQuestions);
80
+ generateProjectButton.addEventListener("click", handleGenerateProject);
81
+ regenerateButton.addEventListener("click", handleRegenerate);
82
+ confirmButton.addEventListener("click", handleConfirmZip);
83
+ clearButton.addEventListener("click", resetAll);
84
+
85
+ Object.values(stackSelects).forEach((select) => {
86
+ select.addEventListener("change", handleStackChange);
87
+ });
88
+
89
+ function initializeStackSelectors() {
90
+ Object.entries(stackSelects).forEach(([key, select]) => {
91
+ STACK_OPTIONS[key].forEach((option) => {
92
+ const optionElement = document.createElement("option");
93
+ optionElement.value = option;
94
+ optionElement.textContent = option;
95
+ select.appendChild(optionElement);
96
+ });
97
+ });
98
+ applySelectedStackToControls(getDefaultStackState());
99
+ }
100
+
101
+ function getDefaultStackState() {
102
+ return {
103
+ language: "Auto",
104
+ frontend: "Auto",
105
+ backend: "Auto",
106
+ database: "Auto",
107
+ aiTools: "Auto",
108
+ deployment: "Auto",
109
+ };
110
+ }
111
+
112
+ function getQuestionList() {
113
+ return Array.isArray(agentAnalysis?.questions) ? agentAnalysis.questions : [];
114
+ }
115
+
116
+ function getCurrentQuestion() {
117
+ const questions = getQuestionList();
118
+ return questions[currentQuestionIndex] || null;
119
+ }
120
+
121
+ function getStoredAnswer(questionId) {
122
+ return typeof agentAnswers[questionId] === "string" ? agentAnswers[questionId] : "";
123
+ }
124
+
125
+ function setQuestionDraft(value) {
126
+ currentQuestionDraft = String(value || "");
127
+ }
128
+
129
+ function resetQuestionFlow() {
130
+ currentQuestionIndex = 0;
131
+ currentQuestionDraft = "";
132
+ showingSuggestion = false;
133
+ }
134
+
135
+ function collectSelectedStack() {
136
+ return {
137
+ language: stackSelects.language.value,
138
+ frontend: stackSelects.frontend.value,
139
+ backend: stackSelects.backend.value,
140
+ database: stackSelects.database.value,
141
+ aiTools: stackSelects.aiTools.value,
142
+ deployment: stackSelects.deployment.value,
143
+ };
144
+ }
145
+
146
+ function applySelectedStackToControls(stack) {
147
+ const safeStack = stack || getDefaultStackState();
148
+ Object.entries(stackSelects).forEach(([key, select]) => {
149
+ select.value = safeStack[key] || "Auto";
150
+ });
151
+ selectedStack = collectSelectedStack();
152
+ }
153
+
154
+ function handleStackChange() {
155
+ selectedStack = collectSelectedStack();
156
+ refreshUiState();
157
+ }
158
+
159
+ function setStatus(message, type) {
160
+ statusMessage.hidden = false;
161
+ statusMessage.className = `status-message ${type}`;
162
+ statusMessage.textContent = message;
163
+ }
164
+
165
+ function clearStatus() {
166
+ statusMessage.hidden = true;
167
+ statusMessage.className = "status-message";
168
+ statusMessage.textContent = "";
169
+ }
170
+
171
+ function setBusy(isBusy, message = "Working...") {
172
+ suggestButton.disabled = isBusy;
173
+ continueButton.disabled = isBusy || !agentAnalysis;
174
+ skipQuestionsButton.disabled = isBusy || !agentAnalysis;
175
+ generateProjectButton.disabled = isBusy || !finalRequirements;
176
+ regenerateButton.disabled = isBusy || !currentPreview;
177
+ confirmButton.disabled = isBusy || !currentPreview;
178
+ clearButton.disabled = isBusy && !baseIdea && !currentPreview && !agentAnalysis;
179
+ generationModeSelect.disabled = isBusy;
180
+ Object.values(stackSelects).forEach((select) => {
181
+ select.disabled = isBusy || !agentAnalysis;
182
+ });
183
+ if (isBusy) {
184
+ setStatus(message, "loading");
185
+ }
186
+ }
187
+
188
+ function refreshUiState() {
189
+ continueButton.disabled = !agentAnalysis;
190
+ skipQuestionsButton.disabled = !agentAnalysis;
191
+ generateProjectButton.disabled = !finalRequirements;
192
+ regenerateButton.disabled = !currentPreview;
193
+ confirmButton.disabled = !currentPreview;
194
+ clearButton.disabled = !baseIdea && !currentPreview && !agentAnalysis;
195
+ Object.values(stackSelects).forEach((select) => {
196
+ select.disabled = !agentAnalysis;
197
+ });
198
+
199
+ const questions = getQuestionList();
200
+ const hasQuestionFlow = !!agentAnalysis && questions.length > 0 && !finalRequirements;
201
+ if (hasQuestionFlow) {
202
+ continueButton.disabled = showingSuggestion;
203
+ continueButton.textContent = currentQuestionIndex >= questions.length - 1 ? "Finish Questions" : "Next";
204
+ } else if (!!agentAnalysis && !finalRequirements) {
205
+ continueButton.textContent = "Continue";
206
+ } else {
207
+ continueButton.textContent = "Next";
208
+ }
209
+ }
210
+
211
+ async function handleStartAgent() {
212
+ const idea = ideaInput.value.trim();
213
+ if (!idea) {
214
+ setStatus("Please enter a project idea before starting the agent.", "error");
215
+ return;
216
+ }
217
+
218
+ baseIdea = idea;
219
+ currentPreview = null;
220
+ finalRequirements = "";
221
+ agentAnswers = {};
222
+ resetQuestionFlow();
223
+ previewSection.hidden = true;
224
+ downloadSection.hidden = true;
225
+ finalizeCard.hidden = true;
226
+ setBusy(true, "Analyzing your idea...");
227
+
228
+ try {
229
+ const payload = await requestAgentAnalysis(idea);
230
+ agentAnalysis = payload;
231
+ selectedStack = payload.suggestedStack || getDefaultStackState();
232
+ applySelectedStackToControls(selectedStack);
233
+ renderAgentAnalysis(payload);
234
+ agentSection.hidden = false;
235
+ advancedStackSection.hidden = false;
236
+ setStatus("The agent reviewed your idea. Answer the questions one by one or skip and use the suggested defaults.", "success");
237
+ } catch (error) {
238
+ agentAnalysis = null;
239
+ agentSection.hidden = true;
240
+ advancedStackSection.hidden = true;
241
+ setStatus(error.message || "Could not start the agent.", "error");
242
+ } finally {
243
+ clearBusyState();
244
+ }
245
+ }
246
+
247
+ async function handleContinueAgent() {
248
+ if (!baseIdea || !agentAnalysis) {
249
+ setStatus("Start the agent before continuing.", "error");
250
+ return;
251
+ }
252
+
253
+ const question = getCurrentQuestion();
254
+ if (!question) {
255
+ await finalizeConversationOnly();
256
+ return;
257
+ }
258
+
259
+ const typedAnswer = currentQuestionDraft.trim();
260
+ if (!typedAnswer) {
261
+ showingSuggestion = true;
262
+ renderQuestionFlow();
263
+ setStatus("No answer was entered. Review the suggested default or edit your answer.", "info");
264
+ return;
265
+ }
266
+
267
+ storeQuestionAnswer(question.id, typedAnswer);
268
+ moveToNextQuestion();
269
+ }
270
+
271
+ async function handleSkipQuestions() {
272
+ if (!baseIdea || !agentAnalysis) {
273
+ setStatus("Start the agent before skipping questions.", "error");
274
+ return;
275
+ }
276
+
277
+ downloadSection.hidden = true;
278
+ setBusy(true, "Generating project with suggested defaults...");
279
+
280
+ try {
281
+ await finalizeAgentConversation({ fillDefaults: true });
282
+ await generatePreviewFromCurrentState(
283
+ "Preview ready. Review the generated project, regenerate if needed, then confirm to create the ZIP.",
284
+ );
285
+ } catch (error) {
286
+ currentPreview = null;
287
+ previewSection.hidden = true;
288
+ setStatus(error.message || "Could not generate the project from the suggested defaults.", "error");
289
+ } finally {
290
+ clearBusyState();
291
+ }
292
+ }
293
+
294
+ async function handleGenerateProject() {
295
+ if (!baseIdea || !finalRequirements) {
296
+ setStatus("Finalize the agent conversation before generating the project.", "error");
297
+ return;
298
+ }
299
+
300
+ downloadSection.hidden = true;
301
+ setBusy(true, "Generating project preview...");
302
+
303
+ try {
304
+ await generatePreviewFromCurrentState(
305
+ "Preview ready. Review the generated project, regenerate if needed, then confirm to create the ZIP.",
306
+ );
307
+ } catch (error) {
308
+ currentPreview = null;
309
+ previewSection.hidden = true;
310
+ setStatus(error.message || "Could not generate the project preview.", "error");
311
+ } finally {
312
+ clearBusyState();
313
+ }
314
+ }
315
+
316
+ async function handleRegenerate() {
317
+ if (!baseIdea || !currentPreview) {
318
+ setStatus("Generate a preview before regenerating with the selected stack.", "error");
319
+ return;
320
+ }
321
+
322
+ downloadSection.hidden = true;
323
+ setBusy(true, "Generating project preview...");
324
+
325
+ try {
326
+ await generatePreviewFromCurrentState(
327
+ "Preview regenerated with the latest requirements and selected stack.",
328
+ );
329
+ } catch (error) {
330
+ setStatus(error.message || "Could not regenerate the preview.", "error");
331
+ } finally {
332
+ clearBusyState();
333
+ }
334
+ }
335
+
336
+ async function handleConfirmZip() {
337
+ if (!currentPreview) {
338
+ setStatus("Generate a preview before creating a ZIP.", "error");
339
+ return;
340
+ }
341
+
342
+ setBusy(true, "Creating ZIP from the latest accepted preview...");
343
+
344
+ try {
345
+ const response = await fetch("/api/zip", {
346
+ method: "POST",
347
+ headers: {
348
+ "Content-Type": "application/json",
349
+ },
350
+ body: JSON.stringify({
351
+ preview: currentPreview,
352
+ }),
353
+ });
354
+
355
+ const payload = await response.json();
356
+ if (!response.ok) {
357
+ throw new Error(payload.detail || "Could not create the ZIP.");
358
+ }
359
+
360
+ downloadLink.href = payload.downloadUrl;
361
+ downloadLink.download = payload.filename;
362
+ downloadText.textContent = `Your generated project ZIP is ready: ${payload.filename}`;
363
+ downloadSection.hidden = false;
364
+ setStatus("ZIP created successfully. You can download it now.", "success");
365
+ } catch (error) {
366
+ setStatus(error.message || "Could not create the ZIP.", "error");
367
+ } finally {
368
+ clearBusyState();
369
+ }
370
+ }
371
+
372
+ async function requestAgentAnalysis(idea) {
373
+ const response = await fetch("/api/agent/analyze", {
374
+ method: "POST",
375
+ headers: {
376
+ "Content-Type": "application/json",
377
+ },
378
+ body: JSON.stringify({ idea }),
379
+ });
380
+
381
+ const payload = await response.json();
382
+ if (!response.ok) {
383
+ throw new Error(payload.detail || "Could not analyze the idea.");
384
+ }
385
+ return payload;
386
+ }
387
+
388
+ async function requestAgentFinalize(body) {
389
+ const response = await fetch("/api/agent/finalize", {
390
+ method: "POST",
391
+ headers: {
392
+ "Content-Type": "application/json",
393
+ },
394
+ body: JSON.stringify(body),
395
+ });
396
+
397
+ const payload = await response.json();
398
+ if (!response.ok) {
399
+ throw new Error(payload.detail || "Could not finalize the requirements.");
400
+ }
401
+ return payload;
402
+ }
403
+
404
+ async function requestPreview(body) {
405
+ const response = await fetch("/api/suggest", {
406
+ method: "POST",
407
+ headers: {
408
+ "Content-Type": "application/json",
409
+ },
410
+ body: JSON.stringify({
411
+ generationMode: generationModeSelect.value || "fast",
412
+ ...body,
413
+ }),
414
+ });
415
+
416
+ const payload = await response.json();
417
+ if (!response.ok) {
418
+ throw new Error(payload.detail || "Could not generate a preview.");
419
+ }
420
+ return payload;
421
+ }
422
+
423
+ function renderAgentAnalysis(analysis) {
424
+ agentUnderstandingText.textContent = analysis.understanding || "No understanding available.";
425
+ renderList(agentAssumptionsList, analysis.assumptions, "No assumptions recorded.");
426
+ renderStackSummary(agentSuggestedStackList, analysis.suggestedStack || getDefaultStackState());
427
+ resetQuestionFlow();
428
+ renderQuestionFlow();
429
+ }
430
+
431
+ function renderQuestionFlow() {
432
+ questionCards.replaceChildren();
433
+
434
+ const questions = getQuestionList();
435
+ if (questions.length === 0) {
436
+ const empty = document.createElement("p");
437
+ empty.className = "text-block";
438
+ empty.textContent = "No follow-up questions are needed. Continue to finalize the recommended stack or skip straight to generation.";
439
+ questionCards.appendChild(empty);
440
+ refreshUiState();
441
+ return;
442
+ }
443
+
444
+ const question = getCurrentQuestion();
445
+ if (!question) {
446
+ const completed = document.createElement("p");
447
+ completed.className = "text-block";
448
+ completed.textContent = "All important questions are complete. Continue to finalize your requirements.";
449
+ questionCards.appendChild(completed);
450
+ refreshUiState();
451
+ return;
452
+ }
453
+
454
+ if (!currentQuestionDraft && getStoredAnswer(question.id) && !showingSuggestion) {
455
+ setQuestionDraft(getStoredAnswer(question.id));
456
+ }
457
+
458
+ const card = document.createElement("article");
459
+ card.className = "question-card active-question-card";
460
+
461
+ const position = document.createElement("p");
462
+ position.className = "question-position";
463
+ position.textContent = `Question ${currentQuestionIndex + 1} of ${questions.length}`;
464
+
465
+ const title = document.createElement("h4");
466
+ title.textContent = question.question || "Question";
467
+
468
+ const reason = document.createElement("p");
469
+ reason.className = "question-reason";
470
+ reason.textContent = "Type your preference first. If you leave it blank, the agent will suggest a default and explain why.";
471
+
472
+ const inputLabel = document.createElement("label");
473
+ inputLabel.className = "field-label";
474
+ inputLabel.setAttribute("for", "activeQuestionInput");
475
+ inputLabel.textContent = "Your answer";
476
+
477
+ const input = document.createElement("input");
478
+ input.type = "text";
479
+ input.id = "activeQuestionInput";
480
+ input.className = "question-input";
481
+ input.placeholder = buildQuestionPlaceholder(question);
482
+ input.value = currentQuestionDraft;
483
+ input.addEventListener("input", (event) => {
484
+ setQuestionDraft(event.target.value);
485
+ });
486
+
487
+ card.append(position, title, reason, inputLabel, input);
488
+
489
+ if (Array.isArray(question.options) && question.options.length) {
490
+ const optionsHint = document.createElement("p");
491
+ optionsHint.className = "question-hint";
492
+ optionsHint.textContent = `Common choices: ${question.options.join(", ")}`;
493
+ card.appendChild(optionsHint);
494
+ }
495
+
496
+ if (showingSuggestion) {
497
+ const suggestionBlock = document.createElement("div");
498
+ suggestionBlock.className = "suggestion-block";
499
+
500
+ const suggestionTitle = document.createElement("p");
501
+ suggestionTitle.className = "suggestion-title";
502
+ suggestionTitle.textContent = `Suggested: ${question.default || "No default available"}`;
503
+
504
+ const suggestionReason = document.createElement("p");
505
+ suggestionReason.className = "suggestion-reason";
506
+ suggestionReason.textContent = question.reason || "This default keeps the starter simple and runnable.";
507
+
508
+ const suggestionActions = document.createElement("div");
509
+ suggestionActions.className = "question-actions";
510
+
511
+ const acceptButton = document.createElement("button");
512
+ acceptButton.type = "button";
513
+ acceptButton.className = "secondary-button";
514
+ acceptButton.textContent = "Accept Suggestion";
515
+ acceptButton.addEventListener("click", () => {
516
+ storeQuestionAnswer(question.id, question.default || "");
517
+ moveToNextQuestion();
518
+ });
519
+
520
+ const editButton = document.createElement("button");
521
+ editButton.type = "button";
522
+ editButton.className = "ghost-button";
523
+ editButton.textContent = "Edit Answer";
524
+ editButton.addEventListener("click", () => {
525
+ showingSuggestion = false;
526
+ renderQuestionFlow();
527
+ const inputElement = document.getElementById("activeQuestionInput");
528
+ inputElement?.focus();
529
+ });
530
+
531
+ suggestionActions.append(acceptButton, editButton);
532
+ suggestionBlock.append(suggestionTitle, suggestionReason, suggestionActions);
533
+ card.appendChild(suggestionBlock);
534
+ }
535
+
536
+ questionCards.appendChild(card);
537
+ refreshUiState();
538
+ }
539
+
540
+ function buildQuestionPlaceholder(question) {
541
+ if (Array.isArray(question.options) && question.options.length) {
542
+ return `Example: ${question.options[0]}`;
543
+ }
544
+ return "Type your answer or leave blank for a suggestion";
545
+ }
546
+
547
+ function storeQuestionAnswer(questionId, value) {
548
+ agentAnswers[questionId] = String(value || "").trim();
549
+ }
550
+
551
+ function moveToNextQuestion() {
552
+ showingSuggestion = false;
553
+ currentQuestionIndex += 1;
554
+ currentQuestionDraft = "";
555
+
556
+ if (currentQuestionIndex >= getQuestionList().length) {
557
+ renderQuestionFlow();
558
+ void finalizeConversationOnly();
559
+ return;
560
+ }
561
+
562
+ renderQuestionFlow();
563
+ clearStatus();
564
+ }
565
+
566
+ async function finalizeConversationOnly() {
567
+ setBusy(true, "Finalizing requirements...");
568
+
569
+ try {
570
+ await finalizeAgentConversation({ fillDefaults: false });
571
+ setStatus("Requirements finalized. Generate the project when you are ready.", "success");
572
+ } catch (error) {
573
+ setStatus(error.message || "Could not finalize the requirements.", "error");
574
+ } finally {
575
+ clearBusyState();
576
+ }
577
+ }
578
+
579
+ function buildAnswersPayload(fillDefaults) {
580
+ const answers = { ...agentAnswers };
581
+ if (!fillDefaults) {
582
+ return answers;
583
+ }
584
+
585
+ getQuestionList().forEach((question) => {
586
+ if (!answers[question.id]) {
587
+ answers[question.id] = question.default || "";
588
+ }
589
+ });
590
+ return answers;
591
+ }
592
+
593
+ function buildFinalizationSummary(finalizedStack, assumptions) {
594
+ const scope = [
595
+ finalizedStack.frontend && finalizedStack.frontend !== "None" ? finalizedStack.frontend : null,
596
+ finalizedStack.backend && finalizedStack.backend !== "None" ? finalizedStack.backend : null,
597
+ ].filter(Boolean).join(" + ");
598
+ const firstAssumption = Array.isArray(assumptions) && assumptions.length ? assumptions[0] : "";
599
+ return scope
600
+ ? `The starter is now aligned around ${scope}. ${firstAssumption}`.trim()
601
+ : `The starter requirements are finalized. ${firstAssumption}`.trim();
602
+ }
603
+
604
+ function renderFinalization(finalized) {
605
+ finalizeCard.hidden = false;
606
+ finalRequirementsText.textContent = finalized.finalRequirements || "No finalized requirements available.";
607
+ finalizeSummaryText.textContent = buildFinalizationSummary(
608
+ finalized.selectedStack || selectedStack,
609
+ finalized.assumptions || [],
610
+ );
611
+ renderStackSummary(finalSelectedStackList, finalized.selectedStack || selectedStack);
612
+ renderStackSummary(agentSuggestedStackList, finalized.selectedStack || selectedStack);
613
+ const mergedAssumptions = dedupeList([
614
+ ...(Array.isArray(agentAnalysis?.assumptions) ? agentAnalysis.assumptions : []),
615
+ ...(Array.isArray(finalized.assumptions) ? finalized.assumptions : []),
616
+ ]);
617
+ renderList(agentAssumptionsList, mergedAssumptions, "No assumptions recorded.");
618
+ refreshUiState();
619
+ }
620
+
621
+ function clearBusyState() {
622
+ suggestButton.disabled = false;
623
+ generationModeSelect.disabled = false;
624
+ refreshUiState();
625
+ }
626
+
627
+ async function finalizeAgentConversation({ fillDefaults }) {
628
+ const payload = await requestAgentFinalize({
629
+ idea: baseIdea,
630
+ answers: buildAnswersPayload(fillDefaults),
631
+ suggestedStack: collectSelectedStack(),
632
+ });
633
+ finalRequirements = payload.finalRequirements || "";
634
+ selectedStack = payload.selectedStack || selectedStack;
635
+ applySelectedStackToControls(selectedStack);
636
+ renderFinalization(payload);
637
+ return payload;
638
+ }
639
+
640
+ async function generatePreviewFromCurrentState(successMessage) {
641
+ selectedStack = collectSelectedStack();
642
+ const payload = await requestPreview({
643
+ idea: baseIdea,
644
+ selectedStack,
645
+ finalRequirements,
646
+ });
647
+ currentPreview = payload;
648
+ selectedStack = payload.selectedStack || selectedStack;
649
+ renderPreview(payload);
650
+ applySelectedStackToControls(selectedStack);
651
+ setStatus(successMessage, "success");
652
+ return payload;
653
+ }
654
+
655
+ function resetAll() {
656
+ baseIdea = "";
657
+ agentAnalysis = null;
658
+ agentAnswers = {};
659
+ finalRequirements = "";
660
+ currentPreview = null;
661
+ selectedStack = getDefaultStackState();
662
+ resetQuestionFlow();
663
+
664
+ ideaInput.value = "";
665
+ clearStatus();
666
+ applySelectedStackToControls(getDefaultStackState());
667
+ agentSection.hidden = true;
668
+ advancedStackSection.hidden = true;
669
+ advancedStackSection.open = false;
670
+ previewSection.hidden = true;
671
+ downloadSection.hidden = true;
672
+ finalizeCard.hidden = true;
673
+ generateProjectButton.disabled = true;
674
+ generationModeSelect.value = "fast";
675
+ generationModeSelect.disabled = false;
676
+ downloadLink.removeAttribute("href");
677
+ downloadLink.removeAttribute("download");
678
+ downloadText.textContent = "Your generated project ZIP is ready.";
679
+
680
+ agentUnderstandingText.textContent = "";
681
+ clearCollection(agentAssumptionsList);
682
+ clearCollection(agentSuggestedStackList);
683
+ questionCards.replaceChildren();
684
+ finalizeSummaryText.textContent = "";
685
+ clearCollection(finalSelectedStackList);
686
+ finalRequirementsText.textContent = "";
687
+
688
+ clearCollection(detectedChoicesList);
689
+ stackChips.replaceChildren();
690
+ clearCollection(selectedStackList);
691
+ clearCollection(chosenStackList);
692
+ clearCollection(assumptionsList);
693
+ clearCollection(architectureList);
694
+ clearCollection(packageRequirementsList);
695
+ clearCollection(installCommandsList);
696
+ clearCollection(runCommandsList);
697
+ clearCollection(envVariablesList);
698
+ requiredInputsBody.replaceChildren();
699
+ modulesList.replaceChildren();
700
+ filesList.replaceChildren();
701
+ fileTreeBlock.textContent = "";
702
+ summaryText.textContent = "";
703
+ problemStatementText.textContent = "";
704
+ projectNameHeading.textContent = "Generated Project";
705
+ refreshUiState();
706
+ }
707
+
708
+ function renderPreview(preview) {
709
+ previewSection.hidden = false;
710
+ projectNameHeading.textContent = preview.projectName || "Generated Project";
711
+ summaryText.textContent = preview.summary || "No summary available.";
712
+ problemStatementText.textContent = preview.problemStatement || "No problem statement available.";
713
+ fileTreeBlock.textContent = preview.fileTree || "No file tree available.";
714
+
715
+ renderList(detectedChoicesList, preview.detectedUserChoices, "No explicit user choices detected.");
716
+ renderStackChips(preview.selectedStack || getDefaultStackState());
717
+ renderStackSummary(selectedStackList, preview.selectedStack || getDefaultStackState());
718
+ renderList(chosenStackList, preview.chosenStack, "No chosen stack details available.");
719
+ renderList(assumptionsList, preview.assumptions, "No assumptions recorded.");
720
+ renderList(architectureList, preview.architecture, "No architecture details available.");
721
+ renderList(packageRequirementsList, preview.packageRequirements, "No package requirements available.");
722
+ renderList(installCommandsList, preview.installCommands, "No install commands available.");
723
+ renderList(runCommandsList, preview.runCommands, "No run commands available.");
724
+ renderRequiredInputs(preview.requiredInputs || []);
725
+ renderEnvVariables(preview.envVariables || []);
726
+ renderModules(preview.modules || []);
727
+ renderFiles(preview.files || []);
728
+ }
729
+
730
+ function renderStackChips(stack) {
731
+ stackChips.replaceChildren();
732
+ const featuredChips = [
733
+ ["language", "Language"],
734
+ ["frontend", "Frontend"],
735
+ ["backend", "Backend"],
736
+ ["database", "Database"],
737
+ ];
738
+
739
+ featuredChips.forEach(([key, label]) => {
740
+ const chip = document.createElement("span");
741
+ chip.className = "stack-chip";
742
+ chip.innerHTML = `<strong>${escapeHtml(label)}</strong> ${escapeHtml(stack[key] || "Auto")}`;
743
+ stackChips.appendChild(chip);
744
+ });
745
+ }
746
+
747
+ function renderList(element, items, fallback) {
748
+ clearCollection(element);
749
+ const safeItems = Array.isArray(items) && items.length ? items : [fallback];
750
+
751
+ safeItems.forEach((item) => {
752
+ const listItem = document.createElement("li");
753
+ listItem.textContent = item;
754
+ element.appendChild(listItem);
755
+ });
756
+ }
757
+
758
+ function renderStackSummary(element, stack) {
759
+ clearCollection(element);
760
+ const labels = {
761
+ language: "Language",
762
+ frontend: "Frontend",
763
+ backend: "Backend",
764
+ database: "Database",
765
+ aiTools: "AI / Tools",
766
+ deployment: "Deployment",
767
+ };
768
+
769
+ Object.entries(labels).forEach(([key, label]) => {
770
+ const listItem = document.createElement("li");
771
+ listItem.textContent = `${label}: ${stack[key] || "Auto"}`;
772
+ element.appendChild(listItem);
773
+ });
774
+ }
775
+
776
+ function renderEnvVariables(envVariables) {
777
+ clearCollection(envVariablesList);
778
+ if (!Array.isArray(envVariables) || envVariables.length === 0) {
779
+ renderList(envVariablesList, [], "No environment variables required.");
780
+ return;
781
+ }
782
+
783
+ envVariables.forEach((variable) => {
784
+ const listItem = document.createElement("li");
785
+ const description = variable.description ? ` - ${variable.description}` : "";
786
+ const value = variable.value ? ` = ${variable.value}` : "";
787
+ listItem.textContent = `${variable.name}${value}${description}`;
788
+ envVariablesList.appendChild(listItem);
789
+ });
790
+ }
791
+
792
+ function renderRequiredInputs(requiredInputs) {
793
+ requiredInputsBody.replaceChildren();
794
+ if (!Array.isArray(requiredInputs) || requiredInputs.length === 0) {
795
+ const row = document.createElement("tr");
796
+ row.innerHTML = `
797
+ <td colspan="5">No required inputs detected. Copy <code>.env.example</code> to <code>.env</code> if you want to override defaults later.</td>
798
+ `;
799
+ requiredInputsBody.appendChild(row);
800
+ return;
801
+ }
802
+
803
+ requiredInputs.forEach((item) => {
804
+ const row = document.createElement("tr");
805
+ row.innerHTML = `
806
+ <td><code>${escapeHtml(item.name || "")}</code></td>
807
+ <td>${item.required === false ? "Optional" : "Required"}</td>
808
+ <td><code>${escapeHtml(item.example || "") || "-"}</code></td>
809
+ <td>${escapeHtml(item.whereToAdd || ".env")}</td>
810
+ <td>${escapeHtml(item.purpose || "No purpose provided.")}</td>
811
+ `;
812
+ requiredInputsBody.appendChild(row);
813
+ });
814
+ }
815
+
816
+ function renderModules(modules) {
817
+ modulesList.replaceChildren();
818
+ if (!Array.isArray(modules) || modules.length === 0) {
819
+ const empty = document.createElement("p");
820
+ empty.className = "text-block";
821
+ empty.textContent = "No modules available.";
822
+ modulesList.appendChild(empty);
823
+ return;
824
+ }
825
+
826
+ modules.forEach((module) => {
827
+ const card = document.createElement("article");
828
+ card.className = "module-card";
829
+
830
+ const title = document.createElement("h4");
831
+ title.textContent = module.name || "Unnamed module";
832
+
833
+ const purpose = document.createElement("p");
834
+ purpose.textContent = module.purpose || "No purpose provided.";
835
+
836
+ const keyFiles = document.createElement("p");
837
+ keyFiles.className = "key-files";
838
+ const files = Array.isArray(module.keyFiles) && module.keyFiles.length
839
+ ? module.keyFiles.join(", ")
840
+ : "No key files provided.";
841
+ keyFiles.textContent = `Key files: ${files}`;
842
+
843
+ card.append(title, purpose, keyFiles);
844
+ modulesList.appendChild(card);
845
+ });
846
+ }
847
+
848
+ function renderFiles(files) {
849
+ filesList.replaceChildren();
850
+ if (!Array.isArray(files) || files.length === 0) {
851
+ const empty = document.createElement("p");
852
+ empty.className = "text-block";
853
+ empty.textContent = "No starter files were generated.";
854
+ filesList.appendChild(empty);
855
+ return;
856
+ }
857
+
858
+ files.forEach((file) => {
859
+ const entry = document.createElement("details");
860
+ entry.className = "file-entry";
861
+
862
+ const summary = document.createElement("summary");
863
+ summary.innerHTML = `
864
+ <span class="file-entry-header">
865
+ <span class="file-entry-dots"><span></span><span></span><span></span></span>
866
+ <span class="file-entry-path">${escapeHtml(file.path || "Unnamed file")}</span>
867
+ </span>
868
+ <span class="file-entry-tag">Source</span>
869
+ `;
870
+
871
+ const code = document.createElement("pre");
872
+ code.className = "code-block";
873
+ code.textContent = file.content || "";
874
+
875
+ entry.append(summary, code);
876
+ filesList.appendChild(entry);
877
+ });
878
+ }
879
+
880
+ function clearCollection(element) {
881
+ element.replaceChildren();
882
+ }
883
+
884
+ function dedupeList(items) {
885
+ return [...new Set((items || []).filter(Boolean))];
886
+ }
887
+
888
+ function escapeHtml(value) {
889
+ return String(value)
890
+ .replaceAll("&", "&amp;")
891
+ .replaceAll("<", "&lt;")
892
+ .replaceAll(">", "&gt;")
893
+ .replaceAll('"', "&quot;")
894
+ .replaceAll("'", "&#039;");
895
+ }
app/static/style.css ADDED
@@ -0,0 +1,648 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #f2f4f7;
3
+ --surface: #ffffff;
4
+ --surface-soft: #f8fafc;
5
+ --surface-strong: #eef4ff;
6
+ --text: #1f2937;
7
+ --muted: #667085;
8
+ --border: #d0d5dd;
9
+ --accent: #2563eb;
10
+ --accent-strong: #1d4ed8;
11
+ --success: #027a48;
12
+ --success-soft: #ecfdf3;
13
+ --danger: #b42318;
14
+ --danger-soft: #fef3f2;
15
+ --info: #175cd3;
16
+ --info-soft: #eff8ff;
17
+ --shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 10px 20px rgba(16, 24, 40, 0.06);
18
+ }
19
+
20
+ * {
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ body {
25
+ margin: 0;
26
+ min-height: 100vh;
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ font-family: "Segoe UI", Tahoma, Arial, sans-serif;
30
+ }
31
+
32
+ .page-shell {
33
+ width: min(1180px, calc(100% - 28px));
34
+ margin: 0 auto;
35
+ padding: 24px 0 40px;
36
+ }
37
+
38
+ .hero,
39
+ .panel {
40
+ border: 1px solid var(--border);
41
+ border-radius: 12px;
42
+ background: var(--surface);
43
+ box-shadow: var(--shadow);
44
+ }
45
+
46
+ .hero {
47
+ margin-bottom: 18px;
48
+ padding: 20px 22px;
49
+ background: var(--surface);
50
+ }
51
+
52
+ .eyebrow,
53
+ .section-label {
54
+ margin: 0 0 8px;
55
+ color: var(--accent);
56
+ font-size: 0.78rem;
57
+ font-weight: 700;
58
+ letter-spacing: 0.08em;
59
+ text-transform: uppercase;
60
+ }
61
+
62
+ .hero h1 {
63
+ margin: 0;
64
+ font-size: 1.9rem;
65
+ font-weight: 700;
66
+ }
67
+
68
+ .hero-copy {
69
+ margin: 10px 0 0;
70
+ color: var(--muted);
71
+ line-height: 1.6;
72
+ max-width: 780px;
73
+ }
74
+
75
+ .panel {
76
+ margin-top: 18px;
77
+ padding: 20px;
78
+ }
79
+
80
+ .section-heading {
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: flex-start;
84
+ gap: 16px;
85
+ margin-bottom: 16px;
86
+ }
87
+
88
+ .section-heading h2,
89
+ .download-panel h2 {
90
+ margin: 0;
91
+ font-size: 1.35rem;
92
+ font-weight: 600;
93
+ }
94
+
95
+ .section-note {
96
+ margin: 0;
97
+ color: var(--muted);
98
+ font-size: 0.92rem;
99
+ line-height: 1.5;
100
+ }
101
+
102
+ .field-label {
103
+ display: block;
104
+ margin-bottom: 8px;
105
+ font-size: 0.95rem;
106
+ font-weight: 600;
107
+ }
108
+
109
+ textarea,
110
+ select,
111
+ .question-input {
112
+ width: 100%;
113
+ border: 1px solid var(--border);
114
+ border-radius: 10px;
115
+ background: #fff;
116
+ color: var(--text);
117
+ font: 0.96rem/1.5 "Segoe UI", Tahoma, Arial, sans-serif;
118
+ }
119
+
120
+ textarea {
121
+ min-height: 190px;
122
+ resize: vertical;
123
+ padding: 14px 16px;
124
+ }
125
+
126
+ select,
127
+ .question-input {
128
+ padding: 11px 12px;
129
+ }
130
+
131
+ textarea:focus,
132
+ select:focus,
133
+ .question-input:focus,
134
+ details summary:focus {
135
+ outline: none;
136
+ border-color: var(--accent);
137
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
138
+ }
139
+
140
+ .compose-grid,
141
+ .stack-grid,
142
+ .preview-grid,
143
+ .agent-grid {
144
+ display: grid;
145
+ gap: 14px;
146
+ }
147
+
148
+ .compose-grid {
149
+ grid-template-columns: minmax(0, 280px);
150
+ }
151
+
152
+ .stack-grid {
153
+ grid-template-columns: repeat(3, minmax(0, 1fr));
154
+ }
155
+
156
+ .preview-grid,
157
+ .agent-grid {
158
+ grid-template-columns: repeat(2, minmax(0, 1fr));
159
+ gap: 16px;
160
+ }
161
+
162
+ .select-field {
163
+ display: grid;
164
+ gap: 6px;
165
+ }
166
+
167
+ .select-field span {
168
+ font-size: 0.9rem;
169
+ font-weight: 600;
170
+ }
171
+
172
+ .action-row,
173
+ .question-actions {
174
+ display: flex;
175
+ flex-wrap: wrap;
176
+ gap: 10px;
177
+ }
178
+
179
+ .action-row {
180
+ margin-top: 16px;
181
+ }
182
+
183
+ button {
184
+ border: 1px solid transparent;
185
+ border-radius: 10px;
186
+ padding: 10px 16px;
187
+ font: 600 0.94rem/1 "Segoe UI", Tahoma, Arial, sans-serif;
188
+ cursor: pointer;
189
+ }
190
+
191
+ button:disabled {
192
+ cursor: not-allowed;
193
+ opacity: 0.6;
194
+ }
195
+
196
+ .primary-button {
197
+ background: var(--accent);
198
+ color: #fff;
199
+ }
200
+
201
+ .primary-button:hover:not(:disabled) {
202
+ background: var(--accent-strong);
203
+ }
204
+
205
+ .secondary-button {
206
+ background: #344054;
207
+ color: #fff;
208
+ }
209
+
210
+ .success-button {
211
+ background: #039855;
212
+ color: #fff;
213
+ }
214
+
215
+ .ghost-button {
216
+ background: #fff;
217
+ color: var(--text);
218
+ border-color: var(--border);
219
+ }
220
+
221
+ .status-message {
222
+ margin-top: 14px;
223
+ padding: 12px 14px;
224
+ border: 1px solid transparent;
225
+ border-radius: 10px;
226
+ font-size: 0.93rem;
227
+ line-height: 1.5;
228
+ }
229
+
230
+ .status-message.loading,
231
+ .status-message.info {
232
+ background: var(--info-soft);
233
+ border-color: #b2ddff;
234
+ color: var(--info);
235
+ }
236
+
237
+ .status-message.success {
238
+ background: var(--success-soft);
239
+ border-color: #abefc6;
240
+ color: var(--success);
241
+ }
242
+
243
+ .status-message.error {
244
+ background: var(--danger-soft);
245
+ border-color: #fecdca;
246
+ color: var(--danger);
247
+ }
248
+
249
+ .agent-panel {
250
+ background: var(--surface);
251
+ }
252
+
253
+ .agent-bubble {
254
+ display: flex;
255
+ gap: 14px;
256
+ align-items: flex-start;
257
+ padding: 16px;
258
+ border: 1px solid #dbe7ff;
259
+ border-radius: 14px;
260
+ background: #f8fbff;
261
+ margin-bottom: 16px;
262
+ }
263
+
264
+ .agent-avatar {
265
+ width: 42px;
266
+ height: 42px;
267
+ border-radius: 999px;
268
+ background: var(--accent);
269
+ color: #fff;
270
+ display: grid;
271
+ place-items: center;
272
+ font-weight: 700;
273
+ flex: 0 0 auto;
274
+ }
275
+
276
+ .agent-copy {
277
+ min-width: 0;
278
+ }
279
+
280
+ .agent-name {
281
+ margin: 0 0 6px;
282
+ font-weight: 700;
283
+ }
284
+
285
+ .agent-inline-heading {
286
+ display: flex;
287
+ justify-content: space-between;
288
+ align-items: flex-start;
289
+ gap: 16px;
290
+ margin-bottom: 14px;
291
+ }
292
+
293
+ .question-cards {
294
+ display: grid;
295
+ gap: 12px;
296
+ }
297
+
298
+ .question-card {
299
+ padding: 14px 16px;
300
+ border: 1px solid #dbe7ff;
301
+ border-radius: 12px;
302
+ background: #fff;
303
+ }
304
+
305
+ .question-card h4 {
306
+ margin: 0 0 8px;
307
+ font-size: 1rem;
308
+ }
309
+
310
+ .question-position {
311
+ margin: 0 0 10px;
312
+ color: var(--accent);
313
+ font-size: 0.84rem;
314
+ font-weight: 700;
315
+ text-transform: uppercase;
316
+ letter-spacing: 0.04em;
317
+ }
318
+
319
+ .question-reason {
320
+ margin: 0 0 12px;
321
+ color: var(--muted);
322
+ line-height: 1.55;
323
+ }
324
+
325
+ .question-hint {
326
+ margin: 10px 0 0;
327
+ color: var(--muted);
328
+ font-size: 0.88rem;
329
+ line-height: 1.5;
330
+ }
331
+
332
+ .question-control {
333
+ display: grid;
334
+ gap: 8px;
335
+ }
336
+
337
+ .suggestion-block {
338
+ margin-top: 14px;
339
+ padding: 14px;
340
+ border: 1px solid #bfd4fe;
341
+ border-radius: 12px;
342
+ background: #eff8ff;
343
+ }
344
+
345
+ .suggestion-title {
346
+ margin: 0 0 8px;
347
+ color: var(--accent);
348
+ font-weight: 700;
349
+ }
350
+
351
+ .suggestion-reason {
352
+ margin: 0 0 12px;
353
+ color: var(--text);
354
+ line-height: 1.55;
355
+ }
356
+
357
+ .final-requirements-block {
358
+ margin: 0 0 14px;
359
+ white-space: pre-wrap;
360
+ padding: 14px;
361
+ border: 1px solid #d0d5dd;
362
+ border-radius: 10px;
363
+ background: #101828;
364
+ color: #f8fafc;
365
+ font: 0.92rem/1.6 Consolas, "Courier New", monospace;
366
+ }
367
+
368
+ .advanced-stack {
369
+ margin-top: 18px;
370
+ border: 1px solid var(--border);
371
+ border-radius: 12px;
372
+ background: #fff;
373
+ }
374
+
375
+ .advanced-stack summary {
376
+ padding: 14px 16px;
377
+ cursor: pointer;
378
+ font-weight: 600;
379
+ }
380
+
381
+ .advanced-stack .section-note,
382
+ .advanced-stack .stack-grid {
383
+ margin: 0 16px 16px;
384
+ }
385
+
386
+ .stack-chip-row {
387
+ display: flex;
388
+ flex-wrap: wrap;
389
+ gap: 8px;
390
+ margin-bottom: 16px;
391
+ }
392
+
393
+ .stack-chip {
394
+ display: inline-flex;
395
+ gap: 6px;
396
+ align-items: center;
397
+ padding: 6px 10px;
398
+ border: 1px solid #bfd4fe;
399
+ border-radius: 999px;
400
+ background: #eff8ff;
401
+ color: var(--info);
402
+ font-size: 0.85rem;
403
+ }
404
+
405
+ .card {
406
+ padding: 16px;
407
+ border: 1px solid var(--border);
408
+ border-radius: 12px;
409
+ background: var(--surface-soft);
410
+ }
411
+
412
+ .card.wide {
413
+ grid-column: 1 / -1;
414
+ }
415
+
416
+ .card h3 {
417
+ margin: 0 0 10px;
418
+ font-size: 1rem;
419
+ font-weight: 600;
420
+ }
421
+
422
+ .list-block,
423
+ .modules-list,
424
+ .files-list {
425
+ margin: 0;
426
+ padding: 0;
427
+ }
428
+
429
+ .list-block {
430
+ list-style: none;
431
+ display: grid;
432
+ gap: 8px;
433
+ }
434
+
435
+ .list-block li {
436
+ padding: 10px 12px;
437
+ border: 1px solid #eaecf0;
438
+ border-radius: 8px;
439
+ background: #fff;
440
+ line-height: 1.5;
441
+ }
442
+
443
+ .command-list li {
444
+ font-family: Consolas, "Courier New", monospace;
445
+ }
446
+
447
+ .text-block {
448
+ margin: 0;
449
+ line-height: 1.65;
450
+ }
451
+
452
+ .required-inputs-copy {
453
+ margin: 0 0 12px;
454
+ color: var(--muted);
455
+ line-height: 1.6;
456
+ }
457
+
458
+ .required-inputs-table-wrap {
459
+ overflow-x: auto;
460
+ border: 1px solid #eaecf0;
461
+ border-radius: 10px;
462
+ background: #fff;
463
+ }
464
+
465
+ .required-inputs-table {
466
+ width: 100%;
467
+ border-collapse: collapse;
468
+ min-width: 720px;
469
+ }
470
+
471
+ .required-inputs-table th,
472
+ .required-inputs-table td {
473
+ padding: 10px 12px;
474
+ border-bottom: 1px solid #eaecf0;
475
+ text-align: left;
476
+ vertical-align: top;
477
+ line-height: 1.5;
478
+ }
479
+
480
+ .required-inputs-table th {
481
+ background: #f9fafb;
482
+ font-size: 0.84rem;
483
+ font-weight: 700;
484
+ color: var(--muted);
485
+ text-transform: uppercase;
486
+ letter-spacing: 0.03em;
487
+ }
488
+
489
+ .required-inputs-table tr:last-child td {
490
+ border-bottom: 0;
491
+ }
492
+
493
+ .required-inputs-table code {
494
+ font-family: Consolas, "Courier New", monospace;
495
+ }
496
+
497
+ .module-card {
498
+ padding: 12px 14px;
499
+ border: 1px solid #eaecf0;
500
+ border-radius: 10px;
501
+ background: #fff;
502
+ }
503
+
504
+ .module-card + .module-card,
505
+ .file-entry + .file-entry {
506
+ margin-top: 10px;
507
+ }
508
+
509
+ .module-card h4 {
510
+ margin: 0;
511
+ font-size: 0.98rem;
512
+ }
513
+
514
+ .module-card p {
515
+ margin: 8px 0 10px;
516
+ color: var(--muted);
517
+ line-height: 1.6;
518
+ }
519
+
520
+ .key-files {
521
+ margin: 0;
522
+ color: var(--muted);
523
+ font-size: 0.88rem;
524
+ }
525
+
526
+ .code-block {
527
+ margin: 0;
528
+ overflow-x: auto;
529
+ padding: 14px;
530
+ border: 1px solid #d0d5dd;
531
+ border-radius: 10px;
532
+ background: #101828;
533
+ color: #f8fafc;
534
+ font: 0.92rem/1.6 Consolas, "Courier New", monospace;
535
+ }
536
+
537
+ .file-entry {
538
+ border: 1px solid #d0d5dd;
539
+ border-radius: 10px;
540
+ overflow: hidden;
541
+ background: #fff;
542
+ }
543
+
544
+ .file-entry summary {
545
+ padding: 12px 14px;
546
+ cursor: pointer;
547
+ list-style: none;
548
+ display: flex;
549
+ justify-content: space-between;
550
+ align-items: center;
551
+ gap: 10px;
552
+ background: #f9fafb;
553
+ }
554
+
555
+ .file-entry summary::-webkit-details-marker {
556
+ display: none;
557
+ }
558
+
559
+ .file-entry-header {
560
+ display: flex;
561
+ align-items: center;
562
+ gap: 8px;
563
+ min-width: 0;
564
+ }
565
+
566
+ .file-entry-dots {
567
+ display: inline-flex;
568
+ gap: 4px;
569
+ }
570
+
571
+ .file-entry-dots span {
572
+ width: 7px;
573
+ height: 7px;
574
+ border-radius: 999px;
575
+ background: #98a2b3;
576
+ }
577
+
578
+ .file-entry-path {
579
+ font: 0.9rem/1.4 Consolas, "Courier New", monospace;
580
+ word-break: break-all;
581
+ }
582
+
583
+ .file-entry-tag {
584
+ color: var(--muted);
585
+ font-size: 0.76rem;
586
+ font-weight: 700;
587
+ text-transform: uppercase;
588
+ }
589
+
590
+ .download-copy {
591
+ margin: 0;
592
+ line-height: 1.6;
593
+ }
594
+
595
+ .download-link {
596
+ display: inline-block;
597
+ margin-top: 12px;
598
+ padding: 10px 16px;
599
+ border-radius: 10px;
600
+ background: var(--accent);
601
+ color: #fff;
602
+ text-decoration: none;
603
+ font-weight: 600;
604
+ }
605
+
606
+ @media (max-width: 960px) {
607
+ .section-heading,
608
+ .agent-inline-heading {
609
+ flex-direction: column;
610
+ }
611
+
612
+ .stack-grid,
613
+ .preview-grid,
614
+ .agent-grid {
615
+ grid-template-columns: 1fr;
616
+ }
617
+
618
+ .card,
619
+ .card.wide {
620
+ grid-column: 1 / -1;
621
+ }
622
+ }
623
+
624
+ @media (max-width: 640px) {
625
+ .page-shell {
626
+ width: min(100% - 16px, 1180px);
627
+ padding: 16px 0 28px;
628
+ }
629
+
630
+ .panel,
631
+ .hero {
632
+ padding: 16px;
633
+ }
634
+
635
+ .action-row,
636
+ .question-actions {
637
+ flex-direction: column;
638
+ }
639
+
640
+ button,
641
+ .download-link {
642
+ width: 100%;
643
+ }
644
+
645
+ .compose-grid {
646
+ grid-template-columns: 1fr;
647
+ }
648
+ }
app/templates/index.html ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Project Agent</title>
7
+ <link rel="stylesheet" href="{{ request.url_for('static', path='/style.css') }}">
8
+ </head>
9
+ <body>
10
+ <main class="page-shell">
11
+ <section class="hero">
12
+ <p class="eyebrow">Project Agent</p>
13
+ <h1>Agent-guided project architecture for runnable starter apps.</h1>
14
+ <p class="hero-copy">
15
+ Describe your idea, let the agent identify missing architecture decisions,
16
+ review the recommended stack, then generate, validate, preview, and export.
17
+ </p>
18
+ </section>
19
+
20
+ <section class="panel compose-panel">
21
+ <div class="section-heading">
22
+ <div>
23
+ <p class="section-label">Idea Input</p>
24
+ <h2>Start with the product idea</h2>
25
+ </div>
26
+ <p class="section-note">The agent will ask only the questions that change architecture, dependencies, or required files.</p>
27
+ </div>
28
+
29
+ <label class="field-label" for="ideaInput">Describe what you want to build</label>
30
+ <textarea
31
+ id="ideaInput"
32
+ name="idea"
33
+ rows="8"
34
+ placeholder="Example:
35
+ - Build a beginner-friendly expense tracker web app for small teams
36
+ - Create an AI support dashboard and choose the best stack automatically
37
+ - Generate a React + FastAPI starter for a customer portal with admin access"
38
+ ></textarea>
39
+
40
+ <div class="compose-grid">
41
+ <label class="select-field">
42
+ <span>Generation Mode</span>
43
+ <select id="generationModeSelect">
44
+ <option value="fast">Fast Mode</option>
45
+ <option value="deep">Deep Mode</option>
46
+ </select>
47
+ </label>
48
+ </div>
49
+
50
+ <div class="action-row">
51
+ <button id="suggestButton" class="primary-button" type="button">Start Agent</button>
52
+ <button id="regenerateButton" class="secondary-button" type="button" disabled>Regenerate With Selected Stack</button>
53
+ <button id="confirmButton" class="success-button" type="button" disabled>Confirm And Create ZIP</button>
54
+ <button id="clearButton" class="ghost-button" type="button" disabled>Clear Preview</button>
55
+ </div>
56
+
57
+ <div id="statusMessage" class="status-message" hidden></div>
58
+ </section>
59
+
60
+ <section id="agentSection" class="panel agent-panel" hidden>
61
+ <div class="section-heading">
62
+ <div>
63
+ <p class="section-label">Agent Conversation</p>
64
+ <h2>Project Agent</h2>
65
+ </div>
66
+ <p class="section-note">The agent is guiding the architecture before generation.</p>
67
+ </div>
68
+
69
+ <div class="agent-bubble agent-intro">
70
+ <div class="agent-avatar">PA</div>
71
+ <div class="agent-copy">
72
+ <p class="agent-name">Project Agent</p>
73
+ <p id="agentUnderstandingText" class="text-block"></p>
74
+ </div>
75
+ </div>
76
+
77
+ <div class="agent-grid">
78
+ <article class="card">
79
+ <h3>Assumptions</h3>
80
+ <ul id="agentAssumptionsList" class="list-block"></ul>
81
+ </article>
82
+
83
+ <article class="card wide">
84
+ <h3>Suggested Stack</h3>
85
+ <ul id="agentSuggestedStackList" class="list-block"></ul>
86
+ </article>
87
+ </div>
88
+
89
+ <article class="card wide question-section">
90
+ <div class="agent-inline-heading">
91
+ <div>
92
+ <h3>Questions</h3>
93
+ <p class="section-note">Answer in your own words. If you leave it blank, the agent will suggest a default and explain why.</p>
94
+ </div>
95
+ <div class="question-actions">
96
+ <button id="continueButton" class="secondary-button" type="button" disabled>Next</button>
97
+ <button id="skipQuestionsButton" class="ghost-button" type="button" disabled>Skip All &amp; Use Suggested Defaults</button>
98
+ </div>
99
+ </div>
100
+ <div id="questionCards" class="question-cards"></div>
101
+ </article>
102
+
103
+ <article id="finalizeCard" class="card wide" hidden>
104
+ <h3>Finalized Requirements</h3>
105
+ <p id="finalizeSummaryText" class="text-block"></p>
106
+ <ul id="finalSelectedStackList" class="list-block"></ul>
107
+ <pre id="finalRequirementsText" class="final-requirements-block"></pre>
108
+ <button id="generateProjectButton" class="primary-button" type="button" disabled>Generate Project</button>
109
+ </article>
110
+
111
+ <details id="advancedStackSection" class="advanced-stack" hidden>
112
+ <summary>Advanced Stack Settings</summary>
113
+ <p class="section-note">You can still override the stack manually before generation or regeneration.</p>
114
+ <div class="stack-grid">
115
+ <label class="select-field">
116
+ <span>Language</span>
117
+ <select id="languageSelect"></select>
118
+ </label>
119
+ <label class="select-field">
120
+ <span>Frontend</span>
121
+ <select id="frontendSelect"></select>
122
+ </label>
123
+ <label class="select-field">
124
+ <span>Backend</span>
125
+ <select id="backendSelect"></select>
126
+ </label>
127
+ <label class="select-field">
128
+ <span>Database</span>
129
+ <select id="databaseSelect"></select>
130
+ </label>
131
+ <label class="select-field">
132
+ <span>AI / Tools</span>
133
+ <select id="aiToolsSelect"></select>
134
+ </label>
135
+ <label class="select-field">
136
+ <span>Deployment</span>
137
+ <select id="deploymentSelect"></select>
138
+ </label>
139
+ </div>
140
+ </details>
141
+ </section>
142
+
143
+ <section id="previewSection" class="panel preview-panel" hidden>
144
+ <div class="section-heading">
145
+ <div>
146
+ <p class="section-label">Preview</p>
147
+ <h2 id="projectNameHeading">Generated Project</h2>
148
+ </div>
149
+ <p class="section-note">The latest preview is used for ZIP creation.</p>
150
+ </div>
151
+
152
+ <div id="stackChips" class="stack-chip-row"></div>
153
+
154
+ <div class="preview-grid">
155
+ <article class="card wide">
156
+ <h3>Summary</h3>
157
+ <p id="summaryText" class="text-block"></p>
158
+ </article>
159
+
160
+ <article class="card">
161
+ <h3>Selected Stack</h3>
162
+ <ul id="selectedStackList" class="list-block"></ul>
163
+ </article>
164
+
165
+ <article class="card">
166
+ <h3>Chosen Stack Summary</h3>
167
+ <ul id="chosenStackList" class="list-block"></ul>
168
+ </article>
169
+
170
+ <article class="card">
171
+ <h3>Detected User Choices</h3>
172
+ <ul id="detectedChoicesList" class="list-block"></ul>
173
+ </article>
174
+
175
+ <article class="card">
176
+ <h3>Problem Statement</h3>
177
+ <p id="problemStatementText" class="text-block"></p>
178
+ </article>
179
+
180
+ <article class="card wide">
181
+ <h3>Architecture</h3>
182
+ <ul id="architectureList" class="list-block"></ul>
183
+ </article>
184
+
185
+ <article class="card wide">
186
+ <h3>Modules</h3>
187
+ <div id="modulesList" class="modules-list"></div>
188
+ </article>
189
+
190
+ <article class="card">
191
+ <h3>Package Requirements</h3>
192
+ <ul id="packageRequirementsList" class="list-block"></ul>
193
+ </article>
194
+
195
+ <article class="card">
196
+ <h3>Environment Variables</h3>
197
+ <ul id="envVariablesList" class="list-block"></ul>
198
+ </article>
199
+
200
+ <article class="card wide">
201
+ <h3>Required Inputs</h3>
202
+ <p class="required-inputs-copy">Fill these values in <code>.env</code> before running the project.</p>
203
+ <div class="required-inputs-table-wrap">
204
+ <table class="required-inputs-table">
205
+ <thead>
206
+ <tr>
207
+ <th>Name</th>
208
+ <th>Required</th>
209
+ <th>Example</th>
210
+ <th>Where To Add</th>
211
+ <th>Purpose</th>
212
+ </tr>
213
+ </thead>
214
+ <tbody id="requiredInputsBody"></tbody>
215
+ </table>
216
+ </div>
217
+ </article>
218
+
219
+ <article class="card">
220
+ <h3>Install Commands</h3>
221
+ <ul id="installCommandsList" class="list-block command-list"></ul>
222
+ </article>
223
+
224
+ <article class="card">
225
+ <h3>Run Commands</h3>
226
+ <ul id="runCommandsList" class="list-block command-list"></ul>
227
+ </article>
228
+
229
+ <article class="card wide">
230
+ <h3>Assumptions</h3>
231
+ <ul id="assumptionsList" class="list-block"></ul>
232
+ </article>
233
+
234
+ <article class="card wide">
235
+ <h3>File Tree</h3>
236
+ <pre id="fileTreeBlock" class="code-block"></pre>
237
+ </article>
238
+
239
+ <article class="card wide">
240
+ <h3>Generated Files</h3>
241
+ <div id="filesList" class="files-list"></div>
242
+ </article>
243
+ </div>
244
+ </section>
245
+
246
+ <section id="downloadSection" class="panel download-panel" hidden>
247
+ <div class="section-heading">
248
+ <div>
249
+ <p class="section-label">Download</p>
250
+ <h2>ZIP Ready</h2>
251
+ </div>
252
+ <p class="section-note">Download the latest confirmed preview.</p>
253
+ </div>
254
+ <p id="downloadText" class="download-copy">Your generated project ZIP is ready.</p>
255
+ <a id="downloadLink" class="download-link" href="#" download>Download ZIP</a>
256
+ </section>
257
+ </main>
258
+
259
+ <script src="{{ request.url_for('static', path='/app.js') }}" defer></script>
260
+ </body>
261
+ </html>
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ jinja2
4
+ httpx
5
+ python-dotenv
tests/__pycache__/test_agent_controller.cpython-314.pyc ADDED
Binary file (6.15 kB). View file
 
tests/test_agent_controller.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ import unittest
6
+ from unittest.mock import patch
7
+
8
+ from app.services.agent_controller import agent_controller
9
+
10
+
11
+ class AgentControllerTests(unittest.TestCase):
12
+ def test_analyze_idea_returns_stack_and_questions(self) -> None:
13
+ result = agent_controller.analyze_idea("Build a dashboard for a small team")
14
+
15
+ self.assertIn("understanding", result)
16
+ self.assertIn("suggestedStack", result)
17
+ self.assertIn("questions", result)
18
+ self.assertIsInstance(result["questions"], list)
19
+ self.assertTrue(result["suggestedStack"]["backend"])
20
+
21
+ def test_finalize_requirements_normalizes_partial_answers(self) -> None:
22
+ result = agent_controller.finalize_requirements(
23
+ "Build a starter app",
24
+ {"database": "postgres", "backend_framework": "node", "authentication": "yes"},
25
+ {
26
+ "language": "Auto",
27
+ "frontend": "React",
28
+ "backend": "FastAPI",
29
+ "database": "SQLite",
30
+ "aiTools": "Auto",
31
+ "deployment": "Render",
32
+ },
33
+ )
34
+
35
+ self.assertEqual(result["selectedStack"]["database"], "PostgreSQL")
36
+ self.assertEqual(result["selectedStack"]["backend"], "Express")
37
+ self.assertIn("authentication-ready", result["finalRequirements"])
38
+
39
+ def test_plan_project_structure_covers_full_stack(self) -> None:
40
+ context = agent_controller._build_idea_context(
41
+ "Build a customer portal",
42
+ selected_stack={
43
+ "language": "Python",
44
+ "frontend": "React",
45
+ "backend": "FastAPI",
46
+ "database": "SQLite",
47
+ "aiTools": "None",
48
+ "deployment": "Render",
49
+ },
50
+ final_requirements="Include a simple dashboard.",
51
+ )
52
+ plan = agent_controller.plan_project_structure(context, {})
53
+
54
+ self.assertEqual(plan.selected_stack["frontend"], "React")
55
+ self.assertEqual(plan.selected_stack["backend"], "FastAPI")
56
+ self.assertTrue(any(file["path"].startswith("frontend/") for file in plan.files))
57
+ self.assertTrue(any(file["path"].startswith("backend/") for file in plan.files))
58
+
59
+ def test_generate_files_falls_back_without_ollama(self) -> None:
60
+ with patch.dict(os.environ, {"OLLAMA_BASE_URL": ""}, clear=False):
61
+ preview = asyncio.run(
62
+ agent_controller.generate_files(
63
+ "Build a task tracker",
64
+ generation_mode="fast",
65
+ )
66
+ )
67
+
68
+ self.assertIn("projectName", preview)
69
+ self.assertTrue(any("fallback" in item.lower() for item in preview["assumptions"]))
70
+ self.assertTrue(preview["files"])
71
+
72
+
73
+ if __name__ == "__main__":
74
+ unittest.main()