diff --git "a/app/services/ai_service.py" "b/app/services/ai_service.py"
new file mode 100644--- /dev/null
+++ "b/app/services/ai_service.py"
@@ -0,0 +1,3233 @@
+from __future__ import annotations
+
+import json
+import logging
+import os
+import re
+import time
+from pathlib import Path
+from typing import Any, Mapping, Sequence
+
+import httpx
+from dotenv import load_dotenv
+
+from .file_service import (
+ build_preview_file_tree,
+ finalize_preview_files,
+)
+
+
+load_dotenv()
+
+DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434"
+DEFAULT_OLLAMA_MODEL = "qwen2.5-coder"
+FAST_PLANNER_TIMEOUT_SECONDS = 22.0
+DEEP_PREVIEW_TIMEOUT_SECONDS = 70.0
+MIN_CUSTOM_PASS_SECONDS = 8.0
+MAX_CUSTOM_FILES = 8
+MAX_CUSTOM_FILE_LINES = 300
+FAST_PLANNER_RESPONSE_BYTES_LIMIT = 48 * 1024
+REQUIRED_PLANNER_KEYS = {
+ "projectName",
+ "projectType",
+ "selectedStack",
+ "modules",
+ "packageRequirements",
+ "installCommands",
+ "runCommands",
+ "requiredInputs",
+ "envVariables",
+ "customFiles",
+}
+
+logger = logging.getLogger(__name__)
+
+STACK_FIELDS = (
+ "language",
+ "frontend",
+ "backend",
+ "database",
+ "aiTools",
+ "deployment",
+)
+STACK_DEFAULTS = {
+ "language": "Auto",
+ "frontend": "Auto",
+ "backend": "Auto",
+ "database": "Auto",
+ "aiTools": "Auto",
+ "deployment": "Auto",
+}
+NONE_LIKE = {"", "Auto", "None"}
+YES_ANSWERS = {"yes", "y", "true", "1", "required", "needed"}
+NO_ANSWERS = {"no", "n", "false", "0", "none", "not needed"}
+BOOLEAN_QUESTION_IDS = {"authentication", "admin_dashboard", "email_smtp", "payment_system"}
+SUPPORTED_SCOPE_VALUES = {"full-stack", "backend-only", "frontend-only"}
+SUPPORTED_LANGUAGE_VALUES = {"Python", "JavaScript", "TypeScript", "Java"}
+SUPPORTED_FRONTEND_VALUES = {"React", "Next.js", "Vue", "HTML/CSS/JavaScript", "None"}
+SUPPORTED_BACKEND_VALUES = {"FastAPI", "Flask", "Express", "NestJS", "Spring Boot"}
+SUPPORTED_DATABASE_VALUES = {"SQLite", "PostgreSQL", "MySQL", "MongoDB", "None"}
+SUPPORTED_AI_VALUES = {"None", "Ollama", "OpenAI API", "LangChain"}
+SUPPORTED_DEPLOYMENT_VALUES = {"Render", "Railway", "Vercel", "Docker", "None"}
+SUPPORTED_COMPLEXITY_VALUES = {"Simple", "Standard", "Advanced"}
+SUPPORTED_ROLE_VALUES = {
+ "Single user / no roles",
+ "Basic roles (admin + user)",
+ "Custom roles needed",
+}
+CANONICAL_ANSWER_ALIASES = {
+ "project_scope": {
+ "full stack": "full-stack",
+ "full-stack": "full-stack",
+ "fullstack": "full-stack",
+ "frontend and backend": "full-stack",
+ "backend only": "backend-only",
+ "backend-only": "backend-only",
+ "api only": "backend-only",
+ "service only": "backend-only",
+ "frontend only": "frontend-only",
+ "frontend-only": "frontend-only",
+ "ui only": "frontend-only",
+ },
+ "language": {
+ "python": "Python",
+ "py": "Python",
+ "javascript": "JavaScript",
+ "js": "JavaScript",
+ "typescript": "TypeScript",
+ "ts": "TypeScript",
+ "java": "Java",
+ },
+ "frontend_framework": {
+ "react": "React",
+ "next": "Next.js",
+ "nextjs": "Next.js",
+ "next.js": "Next.js",
+ "vue": "Vue",
+ "vanilla": "HTML/CSS/JavaScript",
+ "html": "HTML/CSS/JavaScript",
+ "html/css/javascript": "HTML/CSS/JavaScript",
+ "plain html": "HTML/CSS/JavaScript",
+ "none": "None",
+ "no frontend": "None",
+ },
+ "backend_framework": {
+ "fastapi": "FastAPI",
+ "flask": "Flask",
+ "express": "Express",
+ "node": "Express",
+ "node.js": "Express",
+ "nestjs": "NestJS",
+ "nest": "NestJS",
+ "spring": "Spring Boot",
+ "spring boot": "Spring Boot",
+ "none": "None",
+ "no backend": "None",
+ },
+ "database": {
+ "sqlite": "SQLite",
+ "postgres": "PostgreSQL",
+ "postgresql": "PostgreSQL",
+ "postgre sql": "PostgreSQL",
+ "mysql": "MySQL",
+ "mongo": "MongoDB",
+ "mongodb": "MongoDB",
+ "none": "None",
+ "no database": "None",
+ },
+ "ai_integration": {
+ "none": "None",
+ "no": "None",
+ "ollama": "Ollama",
+ "openai": "OpenAI API",
+ "openai api": "OpenAI API",
+ "langchain": "LangChain",
+ },
+ "deployment_target": {
+ "render": "Render",
+ "railway": "Railway",
+ "vercel": "Vercel",
+ "docker": "Docker",
+ "container": "Docker",
+ "none": "None",
+ },
+ "complexity_level": {
+ "simple": "Simple",
+ "basic": "Simple",
+ "beginner": "Simple",
+ "standard": "Standard",
+ "normal": "Standard",
+ "default": "Standard",
+ "advanced": "Advanced",
+ "complex": "Advanced",
+ },
+ "user_roles": {
+ "single": "Single user / no roles",
+ "single user": "Single user / no roles",
+ "no roles": "Single user / no roles",
+ "admin and user": "Basic roles (admin + user)",
+ "admin + user": "Basic roles (admin + user)",
+ "basic roles": "Basic roles (admin + user)",
+ "custom roles": "Custom roles needed",
+ "custom": "Custom roles needed",
+ "manager roles": "Custom roles needed",
+ },
+}
+
+KEYWORD_MAP = {
+ "language": {
+ "Python": ("python", "fastapi", "flask", "django"),
+ "JavaScript": ("javascript", "node", "express"),
+ "TypeScript": ("typescript", "next.js", "nextjs", "nest", "nestjs"),
+ "Java": ("java", "spring", "spring boot"),
+ },
+ "frontend": {
+ "React": ("react",),
+ "Next.js": ("next.js", "nextjs"),
+ "Vue": ("vue",),
+ "HTML/CSS/JavaScript": ("html", "css", "javascript", "vanilla js", "frontend"),
+ "None": ("api only", "backend only", "cli", "script"),
+ },
+ "backend": {
+ "FastAPI": ("fastapi",),
+ "Flask": ("flask",),
+ "Express": ("express", "node api"),
+ "NestJS": ("nestjs", "nest js"),
+ "Spring Boot": ("spring boot", "spring"),
+ "None": ("static site", "frontend only"),
+ },
+ "database": {
+ "SQLite": ("sqlite",),
+ "PostgreSQL": ("postgres", "postgresql"),
+ "MySQL": ("mysql",),
+ "MongoDB": ("mongodb", "mongo"),
+ "None": ("no database", "without database", "in memory"),
+ },
+ "aiTools": {
+ "Ollama": ("ollama", "local llm", "qwen", "llama"),
+ "OpenAI API": ("openai", "gpt"),
+ "LangChain": ("langchain",),
+ "None": ("no ai", "without ai"),
+ },
+ "deployment": {
+ "Render": ("render",),
+ "Railway": ("railway",),
+ "Vercel": ("vercel",),
+ "Docker": ("docker", "container"),
+ "None": ("no deployment",),
+ },
+}
+
+
+async def generate_project_preview(
+ idea: str,
+ selected_stack: dict[str, str] | None = None,
+ generation_mode: str = "fast",
+ final_requirements: str = "",
+) -> dict[str, Any]:
+ from .agent_controller import agent_controller
+
+ return await agent_controller.generate_files(
+ idea,
+ selected_stack,
+ generation_mode,
+ final_requirements,
+ )
+
+
+def prepare_preview_for_output(preview: dict[str, Any]) -> dict[str, Any]:
+ base_idea = (
+ str(preview.get("problemStatement") or "").strip()
+ or str(preview.get("summary") or "").strip()
+ or str(preview.get("projectName") or "").strip()
+ or "Generated project"
+ )
+ requested_stack = normalize_stack_selection(preview.get("selectedStack"))
+ return normalize_preview(preview, base_idea, requested_stack, "fast")
+
+
+async def generate_project_plan(
+ idea: str,
+ requested_stack: dict[str, str],
+ generation_mode: str,
+ deadline: float,
+) -> dict[str, Any]:
+ timeout_seconds = remaining_time(deadline)
+ if timeout_seconds <= 0:
+ raise TimeoutError("Preview time budget was exhausted before generation started.")
+
+ prompt = build_planning_prompt(idea, requested_stack, generation_mode)
+ planner_payload = await call_ollama_json(
+ prompt,
+ timeout_seconds,
+ enforce_compact_output=(generation_mode == "fast"),
+ response_kind="planner",
+ )
+ return validate_planner_payload(planner_payload)
+
+
+async def generate_deep_custom_files(
+ idea: str,
+ project_name: str,
+ selected_stack: dict[str, str],
+ custom_manifest: list[dict[str, str]],
+ timeout_seconds: float,
+) -> list[dict[str, str]]:
+ prompt = build_custom_files_prompt(idea, project_name, selected_stack, custom_manifest)
+ payload = await call_ollama_json(
+ prompt,
+ timeout_seconds,
+ enforce_compact_output=False,
+ response_kind="custom",
+ )
+ files = normalize_files(payload.get("files"))
+
+ allowed_paths = {item["path"] for item in custom_manifest}
+ trimmed: list[dict[str, str]] = []
+ for file_entry in files:
+ if file_entry["path"] not in allowed_paths:
+ continue
+ content = trim_content_lines(file_entry["content"])
+ trimmed.append({"path": file_entry["path"], "content": content})
+ return trimmed
+
+
+async def call_ollama_json(
+ prompt: str,
+ timeout_seconds: float,
+ *,
+ enforce_compact_output: bool,
+ response_kind: str,
+) -> dict[str, Any]:
+ configured_base_url = str(os.getenv("OLLAMA_BASE_URL") or "").strip()
+ if not configured_base_url:
+ raise RuntimeError(
+ "AI generation is unavailable because OLLAMA_BASE_URL is not configured."
+ )
+
+ base_url = configured_base_url.rstrip("/")
+ model = os.getenv("OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL).strip() or DEFAULT_OLLAMA_MODEL
+ safe_timeout = max(1.0, timeout_seconds)
+ request_timeout = httpx.Timeout(
+ connect=min(3.5, safe_timeout),
+ read=safe_timeout,
+ write=min(5.0, safe_timeout),
+ pool=min(5.0, safe_timeout),
+ )
+
+ try:
+ async with httpx.AsyncClient(timeout=request_timeout) as client:
+ response = await client.post(
+ f"{base_url}/api/generate",
+ json={"model": model, "prompt": prompt, "stream": False},
+ )
+ except httpx.HTTPError as exc:
+ raise RuntimeError(
+ f"AI generation is unavailable because Ollama could not be reached at {base_url}."
+ ) from exc
+
+ if response.status_code >= 400:
+ detail = response.text.strip() or f"HTTP {response.status_code}"
+ raise RuntimeError(f"Ollama returned an error: {detail}")
+
+ payload = response.json()
+ raw_text = str(payload.get("response") or "").strip()
+ if not raw_text:
+ raise RuntimeError("Ollama returned an empty response.")
+ if enforce_compact_output and len(raw_text.encode("utf-8")) > FAST_PLANNER_RESPONSE_BYTES_LIMIT:
+ raise ValueError(
+ f"{response_kind.capitalize()} response exceeded the compact output limit for Fast Mode."
+ )
+
+ return parse_model_json(raw_text)
+
+
+def build_planning_prompt(
+ idea: str,
+ selected_stack: dict[str, str],
+ generation_mode: str,
+) -> str:
+ stack_summary = "\n".join(
+ f"- {field}: {selected_stack.get(field, 'Auto')}" for field in STACK_FIELDS
+ )
+
+ return f"""
+You are Project Agent.
+
+Return compact JSON only. No markdown. No commentary.
+
+Goal:
+- Plan a 100% runnable starter project.
+- Do not generate the entire project.
+- The backend will create standard files from templates.
+
+Rules:
+1. Preserve user-specified language, frameworks, tools, and deployment choices.
+2. Respect any non-Auto selectedStack values exactly.
+3. Choose beginner-friendly defaults only for missing stack categories.
+4. For full-stack output, plan both frontend and backend.
+5. Do not include README, dependency manifests, setup scripts, run scripts, env files, config boilerplate, or installed libraries in customFiles.
+6. customFiles must contain only project-specific business logic files, max {MAX_CUSTOM_FILES}.
+7. Each custom file should be worth generating because it contains app-specific logic or UI, not generic boilerplate.
+8. Detect required external inputs and return them in requiredInputs.
+9. If the project uses auth, database, email, payments, AI providers, or OAuth, include the needed keys in requiredInputs.
+10. Do not claim this is a production-finished application. It is a 100% runnable starter project.
+
+Return this exact top-level shape:
+{{
+ "projectName": "string",
+ "projectType": "frontend-only | backend-only | full-stack",
+ "selectedStack": {{
+ "language": "string",
+ "frontend": "string",
+ "backend": "string",
+ "database": "string",
+ "aiTools": "string",
+ "deployment": "string"
+ }},
+ "modules": [
+ {{
+ "name": "string",
+ "purpose": "string",
+ "keyFiles": ["string"]
+ }}
+ ],
+ "packageRequirements": ["string"],
+ "installCommands": ["string"],
+ "runCommands": ["string"],
+ "requiredInputs": [
+ {{
+ "name": "string",
+ "required": true,
+ "example": "string",
+ "whereToAdd": ".env",
+ "purpose": "string"
+ }}
+ ],
+ "envVariables": [
+ {{
+ "name": "string",
+ "value": "string",
+ "description": "string"
+ }}
+ ],
+ "customFiles": [
+ {{
+ "path": "relative/path",
+ "purpose": "why this file is needed"
+ }}
+ ]
+}}
+
+Mode:
+- generationMode: {generation_mode}
+- Fast Mode should keep customFiles compact and rely on backend templates for standard files.
+- Deep Mode may propose richer custom business files, but still max {MAX_CUSTOM_FILES}.
+
+Selected stack:
+{stack_summary}
+
+Project idea:
+{idea}
+""".strip()
+
+
+def build_custom_files_prompt(
+ idea: str,
+ project_name: str,
+ selected_stack: dict[str, str],
+ custom_manifest: list[dict[str, str]],
+) -> str:
+ manifest_json = json.dumps(custom_manifest, indent=2)
+ stack_json = json.dumps(selected_stack, indent=2)
+ return f"""
+You are Project Agent.
+
+Generate only the custom business-logic files listed below.
+Return JSON only in this shape:
+{{
+ "files": [
+ {{
+ "path": "relative/path",
+ "content": "file content"
+ }}
+ ]
+}}
+
+Rules:
+1. Generate only the listed files.
+2. Do not generate standard boilerplate, dependency manifests, docs, setup scripts, or env files.
+3. Keep each file under {MAX_CUSTOM_FILE_LINES} lines.
+4. Use the selected stack exactly.
+5. Produce runnable starter logic, not empty placeholders.
+
+Project name: {project_name}
+Project idea: {idea}
+Selected stack:
+{stack_json}
+
+Requested custom files:
+{manifest_json}
+""".strip()
+
+
+def parse_model_json(raw_text: str) -> dict[str, Any]:
+ text = strip_markdown_fences(raw_text)
+ candidate = extract_json_object(text)
+ if not candidate:
+ raise ValueError("Could not recover a JSON object from the AI response.")
+
+ parsed = json.loads(candidate)
+ if not isinstance(parsed, dict):
+ raise ValueError("AI response JSON was not an object.")
+ return parsed
+
+
+def strip_markdown_fences(text: str) -> str:
+ stripped = text.strip()
+ if stripped.startswith("```"):
+ stripped = re.sub(r"^```[a-zA-Z0-9_-]*\s*", "", stripped)
+ stripped = re.sub(r"\s*```$", "", stripped)
+ return stripped.strip()
+
+
+def extract_json_object(text: str) -> str:
+ start = text.find("{")
+ if start == -1:
+ return ""
+
+ depth = 0
+ in_string = False
+ escape = False
+ for index in range(start, len(text)):
+ char = text[index]
+ if in_string:
+ if escape:
+ escape = False
+ elif char == "\\":
+ escape = True
+ elif char == '"':
+ in_string = False
+ continue
+
+ if char == '"':
+ in_string = True
+ elif char == "{":
+ depth += 1
+ elif char == "}":
+ depth -= 1
+ if depth == 0:
+ return text[start : index + 1]
+
+ return ""
+
+
+def build_fallback_preview(
+ idea: str,
+ requested_stack: dict[str, str],
+ generation_mode: str,
+ reason: str,
+ generation_context: str | None = None,
+) -> dict[str, Any]:
+ preview = normalize_preview(
+ {},
+ idea,
+ requested_stack,
+ generation_mode,
+ requirements_context=generation_context or idea,
+ )
+ fallback_note = (
+ "Fast Mode returned a complete template-based preview because AI generation was unavailable."
+ if generation_mode == "fast"
+ else "Deep Mode AI enrichment was unavailable, so a complete template-based preview was returned."
+ )
+ preview["assumptions"] = dedupe_list(
+ [
+ fallback_note,
+ f"Template fallback preview was generated because the AI planner could not complete in time or returned invalid output: {reason}",
+ *preview.get("assumptions", []),
+ ]
+ )
+ return preview
+
+
+def build_generation_context(
+ idea: str,
+ final_requirements: str = "",
+ generation_mode: str = "fast",
+) -> str:
+ cleaned_idea = idea.strip()
+ cleaned_requirements = final_requirements.strip()
+ if not cleaned_requirements:
+ return cleaned_idea
+ if generation_mode == "fast":
+ return (
+ f"Idea: {cleaned_idea}\n"
+ "Final requirements:\n"
+ f"{cleaned_requirements}"
+ ).strip()
+ return (
+ f"Original idea:\n{cleaned_idea}\n\n"
+ "Finalized requirements from the agent conversation:\n"
+ f"{cleaned_requirements}"
+ ).strip()
+
+
+def analyze_project_idea(idea: str) -> dict[str, Any]:
+ detected_choices = detect_user_choices(idea)
+ suggested_stack = resolve_selected_stack(
+ idea,
+ normalize_stack_selection({}),
+ None,
+ detected_choices,
+ )
+ project_kind = determine_project_kind(suggested_stack, infer_declared_project_type(idea))
+ questions = build_agent_questions(idea, suggested_stack, project_kind)
+ return {
+ "understanding": build_agent_understanding(idea, suggested_stack, project_kind),
+ "assumptions": build_agent_analysis_assumptions(suggested_stack, project_kind, questions),
+ "suggestedStack": suggested_stack,
+ "stackReasons": build_stack_reasons(suggested_stack, project_kind),
+ "questions": questions,
+ "detectedProjectType": project_kind["label"],
+ "confidence": compute_agent_confidence(idea, detected_choices, questions, project_kind),
+ }
+
+
+def finalize_agent_requirements(
+ idea: str,
+ answers: Mapping[str, Any] | None,
+ suggested_stack: Mapping[str, Any] | None,
+) -> dict[str, Any]:
+ normalized_answers = normalize_agent_answers(answers)
+ selected_stack = normalize_stack_selection(suggested_stack)
+ selected_stack = apply_agent_answers_to_stack(idea, selected_stack, normalized_answers)
+ project_kind = determine_project_kind(selected_stack, normalized_answers.get("project_scope"))
+ final_requirements = build_final_requirements_summary(
+ idea,
+ normalized_answers,
+ selected_stack,
+ project_kind,
+ )
+ assumptions = build_agent_finalize_assumptions(
+ normalized_answers,
+ selected_stack,
+ project_kind,
+ )
+ return {
+ "finalRequirements": final_requirements,
+ "selectedStack": selected_stack,
+ "assumptions": assumptions,
+ }
+
+
+def infer_declared_project_type(idea: str) -> str:
+ lowered = idea.lower()
+ if any(token in lowered for token in ("full stack", "full-stack", "frontend and backend")):
+ return "full-stack"
+ if any(token in lowered for token in ("backend only", "api only", "service only")):
+ return "backend-only"
+ if any(token in lowered for token in ("frontend only", "landing page only", "static site")):
+ return "frontend-only"
+ return ""
+
+
+def build_agent_understanding(
+ idea: str,
+ suggested_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> str:
+ description = idea.strip().rstrip(".")
+ if len(description) > 180:
+ description = description[:177].rstrip() + "..."
+ stack_bits: list[str] = []
+ if project_kind["hasFrontend"]:
+ stack_bits.append(suggested_stack["frontend"])
+ if project_kind["hasBackend"]:
+ stack_bits.append(suggested_stack["backend"])
+ stack_text = " + ".join(bit for bit in stack_bits if bit not in NONE_LIKE) or suggested_stack["language"]
+ return (
+ f"I understood your idea as a {project_kind['label']} project focused on {description}. "
+ f"My starting recommendation is {stack_text} with template-generated runtime structure so you can get to a runnable starter quickly."
+ )
+
+
+def build_agent_analysis_assumptions(
+ suggested_stack: dict[str, str],
+ project_kind: dict[str, Any],
+ questions: list[dict[str, Any]],
+) -> list[str]:
+ assumptions = [
+ "Questions are limited to decisions that change architecture, dependencies, or required files.",
+ "If you skip the questions, the suggested stack and defaults will be used to generate the project.",
+ ]
+ if project_kind["isFullStack"]:
+ assumptions.append("The app currently looks like a full-stack build, so both frontend and backend are planned by default.")
+ elif project_kind["hasBackend"]:
+ assumptions.append("The current recommendation leans backend-first and will keep the frontend optional unless you add it.")
+ else:
+ assumptions.append("The current recommendation treats this as a frontend-focused starter unless you add backend needs.")
+ if questions:
+ assumptions.append("Each question starts with your own input first. If you leave it blank, the agent will suggest a default and explain the benefit.")
+ if suggested_stack.get("deployment") == "Render":
+ assumptions.append("Render is used as a deployment default when no target is mentioned explicitly.")
+ return dedupe_list(assumptions)
+
+
+def build_stack_reasons(
+ suggested_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> list[str]:
+ reasons: list[str] = []
+ backend = suggested_stack.get("backend", "Auto")
+ frontend = suggested_stack.get("frontend", "Auto")
+ database = suggested_stack.get("database", "Auto")
+ deployment = suggested_stack.get("deployment", "Auto")
+ ai_tools = suggested_stack.get("aiTools", "Auto")
+
+ if backend == "FastAPI":
+ reasons.append("FastAPI is a beginner-friendly Python API framework that pairs well with the current runnable starter templates.")
+ elif backend == "Express":
+ reasons.append("Express keeps a Node-based backend lightweight and easy to extend for API-first projects.")
+ elif backend == "Spring Boot":
+ reasons.append("Spring Boot was chosen because the idea strongly suggests a Java-based backend stack.")
+
+ if frontend == "React":
+ reasons.append("React with Vite gives a fast local frontend starter and aligns with the existing component templates.")
+ elif frontend == "HTML/CSS/JavaScript":
+ reasons.append("A lightweight HTML/CSS/JavaScript frontend keeps the starter simple when a framework was not clearly required.")
+ elif frontend == "None":
+ reasons.append("No frontend was selected because the current idea reads like an API, worker, or backend-only service.")
+
+ if database == "SQLite":
+ reasons.append("SQLite keeps local setup simple for a runnable starter when the app does not need heavier infrastructure yet.")
+ elif database == "PostgreSQL":
+ reasons.append("PostgreSQL is a strong default when the app looks multi-user, relational, or production-oriented.")
+ elif database == "None":
+ reasons.append("No database was selected because the current idea can start without persistent storage.")
+
+ if ai_tools == "Ollama":
+ reasons.append("Ollama stays the local-first AI default when the idea explicitly suggests AI features.")
+ elif ai_tools == "None":
+ reasons.append("No AI tool was added because the current idea does not require model integration.")
+
+ if deployment == "Render":
+ reasons.append("Render is the default deployment target because it is simple for web apps and matches the current starter flow.")
+ elif deployment == "Docker":
+ reasons.append("Docker is selected because the idea or deployment preference points toward containerized delivery.")
+
+ if project_kind["isFullStack"]:
+ reasons.append("Both frontend and backend are included because the idea appears to need a user interface and a service layer.")
+ return dedupe_list(reasons)
+
+
+def build_agent_questions(
+ idea: str,
+ suggested_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> list[dict[str, Any]]:
+ lowered = idea.lower()
+ questions: list[dict[str, Any]] = []
+
+ def add_question(
+ question_id: str,
+ question: str,
+ question_type: str,
+ default: str,
+ reason: str,
+ options: Sequence[str] | None = None,
+ ) -> None:
+ questions.append(
+ {
+ "id": question_id,
+ "question": question,
+ "type": question_type,
+ "options": list(options or []),
+ "default": default,
+ "reason": reason,
+ }
+ )
+
+ has_explicit_frontend = bool(match_keyword("frontend", lowered))
+ has_explicit_backend = bool(match_keyword("backend", lowered))
+
+ if not infer_declared_project_type(idea) and not (has_explicit_frontend and has_explicit_backend):
+ add_question(
+ "project_scope",
+ "Do you need both a frontend and a backend for this project?",
+ "choice",
+ project_kind["label"],
+ "If you leave this blank, I will suggest the current project scope because it keeps the runnable starter aligned with the folders and runtime scripts your idea appears to need.",
+ ["full-stack", "backend-only", "frontend-only"],
+ )
+
+ if not match_keyword("backend", lowered) and project_kind["hasBackend"]:
+ add_question(
+ "backend_framework",
+ "Which backend framework do you prefer?",
+ "choice",
+ suggested_stack["backend"],
+ f"If you leave this blank, I will suggest {suggested_stack['backend']} because it matches the current starter templates and keeps backend setup straightforward.",
+ ["FastAPI", "Flask", "Express", "NestJS", "Spring Boot"],
+ )
+
+ if not match_keyword("frontend", lowered) and project_kind["hasFrontend"]:
+ add_question(
+ "frontend_framework",
+ "Which frontend framework should the starter use?",
+ "choice",
+ suggested_stack["frontend"],
+ f"If you leave this blank, I will suggest {suggested_stack['frontend']} because it gives the cleanest starter structure for the current UI needs.",
+ ["React", "Next.js", "Vue", "HTML/CSS/JavaScript", "None"],
+ )
+
+ if not match_keyword("language", lowered) and not match_keyword("backend", lowered):
+ add_question(
+ "language",
+ "Do you want to lock in a preferred programming language?",
+ "choice",
+ suggested_stack["language"],
+ f"If you leave this blank, I will suggest {suggested_stack['language']} because it fits the recommended stack and keeps the starter consistent.",
+ ["Python", "JavaScript", "TypeScript", "Java"],
+ )
+
+ if project_kind["hasBackend"] and not match_keyword("database", lowered):
+ add_question(
+ "database",
+ "Which database should the starter prepare for?",
+ "choice",
+ suggested_stack["database"],
+ f"If you leave this blank, I will suggest {suggested_stack['database']} because it gives the best balance of simple setup and starter reliability for this project.",
+ ["SQLite", "PostgreSQL", "MySQL", "MongoDB", "None"],
+ )
+
+ if project_kind["hasBackend"] and not _context_mentions_any(lowered, ("auth", "authentication", "login", "signup", "jwt")):
+ add_question(
+ "authentication",
+ "Do you need authentication in the starter?",
+ "boolean",
+ "No",
+ "If you leave this blank, I will suggest No so the starter stays simpler unless your app clearly needs protected user flows.",
+ ["Yes", "No"],
+ )
+
+ if project_kind["hasBackend"] and not _context_mentions_any(lowered, ("role", "roles", "permissions", "admin", "staff", "manager")):
+ add_question(
+ "user_roles",
+ "Do you need multiple user roles?",
+ "choice",
+ "Single user / no roles",
+ "If you leave this blank, I will suggest a single-user or no-roles setup so the starter stays easy to run before you add permissions logic.",
+ ["Single user / no roles", "Basic roles (admin + user)", "Custom roles needed"],
+ )
+
+ if project_kind["hasFrontend"] and not _context_mentions_any(lowered, ("admin dashboard", "admin panel", "back office")):
+ add_question(
+ "admin_dashboard",
+ "Should the starter include an admin dashboard path?",
+ "boolean",
+ "No",
+ "If you leave this blank, I will suggest No so the starter focuses on the main product flow before adding admin-specific pages.",
+ ["Yes", "No"],
+ )
+
+ if project_kind["hasBackend"] and not _context_mentions_any(lowered, ("email", "smtp", "mail", "newsletter", "verification email", "contact form")):
+ add_question(
+ "email_smtp",
+ "Will the project need email or SMTP support?",
+ "boolean",
+ "No",
+ "If you leave this blank, I will suggest No so the starter avoids extra SMTP setup unless email is part of the core workflow.",
+ ["Yes", "No"],
+ )
+
+ if project_kind["hasBackend"] and not _context_mentions_any(lowered, ("payment", "payments", "stripe", "checkout", "subscription", "billing")):
+ add_question(
+ "payment_system",
+ "Will the project need payment processing?",
+ "boolean",
+ "No",
+ "If you leave this blank, I will suggest No so the starter avoids payment keys and billing setup unless your app clearly needs them.",
+ ["Yes", "No"],
+ )
+
+ if project_kind["hasBackend"] and not _context_mentions_any(lowered, ("api", "integration", "webhook", "third-party", "external service")):
+ add_question(
+ "external_apis",
+ "Do you already know any external APIs or third-party services this project must connect to?",
+ "text",
+ "None",
+ "If you leave this blank, I will assume no required external APIs so the starter stays self-contained and quicker to run locally.",
+ )
+
+ if (
+ not match_keyword("aiTools", lowered)
+ and _context_mentions_any(
+ lowered,
+ ("ai", "assistant", "agent", "chatbot", "llm", "openai", "ollama", "summar", "recommendation"),
+ )
+ ):
+ add_question(
+ "ai_integration",
+ "Do you want AI integration in the starter?",
+ "choice",
+ "None",
+ "If you leave this blank, I will suggest no AI integration unless the idea clearly needs model tooling and provider keys.",
+ ["None", "Ollama", "OpenAI API", "LangChain"],
+ )
+
+ if not match_keyword("deployment", lowered):
+ add_question(
+ "deployment_target",
+ "Where do you expect to deploy this project first?",
+ "choice",
+ suggested_stack["deployment"],
+ f"If you leave this blank, I will suggest {suggested_stack['deployment']} because it matches the current deployment defaults and generated starter files.",
+ ["Render", "Railway", "Vercel", "Docker", "None"],
+ )
+
+ if len(re.findall(r"[A-Za-z0-9]+", idea)) < 18:
+ add_question(
+ "complexity_level",
+ "How ambitious should the first starter be?",
+ "choice",
+ "Standard",
+ "If you leave this blank, I will suggest Standard so the starter includes useful structure without becoming heavy too early.",
+ ["Simple", "Standard", "Advanced"],
+ )
+
+ return questions
+
+
+def compute_agent_confidence(
+ idea: str,
+ detected_choices: list[str],
+ questions: list[dict[str, Any]],
+ project_kind: dict[str, Any],
+) -> int:
+ score = 35
+ word_count = len(re.findall(r"[A-Za-z0-9]+", idea))
+ if word_count >= 12:
+ score += 15
+ elif word_count >= 6:
+ score += 8
+
+ score += min(len(detected_choices) * 8, 32)
+ if infer_declared_project_type(idea):
+ score += 8
+ if project_kind["isFullStack"]:
+ score += 4
+ score -= min(len(questions) * 3, 24)
+ return max(20, min(95, score))
+
+
+def normalize_agent_answers(answers: Mapping[str, Any] | None) -> dict[str, str]:
+ if not isinstance(answers, Mapping):
+ return {}
+ normalized: dict[str, str] = {}
+ for key, value in answers.items():
+ question_id = str(key).strip()
+ normalized[question_id] = normalize_agent_answer_value(question_id, value)
+ return normalized
+
+
+def apply_agent_answers_to_stack(
+ idea: str,
+ selected_stack: dict[str, str],
+ answers: Mapping[str, str],
+) -> dict[str, str]:
+ resolved = normalize_stack_selection(selected_stack)
+ scope = normalize_project_scope(
+ answers.get("project_scope", infer_declared_project_type(idea) or determine_project_kind(resolved)["label"])
+ )
+ scope_value = scope.strip().lower()
+
+ frontend_choice = supported_answer(answers.get("frontend_framework"), SUPPORTED_FRONTEND_VALUES)
+ backend_choice = supported_answer(answers.get("backend_framework"), SUPPORTED_BACKEND_VALUES)
+ language_choice = supported_answer(answers.get("language"), SUPPORTED_LANGUAGE_VALUES)
+ database_choice = supported_answer(answers.get("database"), SUPPORTED_DATABASE_VALUES)
+ deployment_choice = supported_answer(answers.get("deployment_target"), SUPPORTED_DEPLOYMENT_VALUES)
+ ai_choice = supported_answer(answers.get("ai_integration"), SUPPORTED_AI_VALUES)
+
+ if frontend_choice:
+ resolved["frontend"] = frontend_choice
+ if backend_choice:
+ resolved["backend"] = backend_choice
+ if language_choice:
+ resolved["language"] = language_choice
+ if database_choice:
+ resolved["database"] = database_choice
+ if deployment_choice:
+ resolved["deployment"] = deployment_choice
+ if ai_choice:
+ resolved["aiTools"] = ai_choice
+
+ if scope_value == "backend-only":
+ resolved["frontend"] = "None"
+ if resolved["backend"] in NONE_LIKE:
+ resolved["backend"] = infer_backend(idea.lower())
+ elif scope_value == "frontend-only":
+ resolved["backend"] = "None"
+ resolved["database"] = "None"
+ if resolved["frontend"] in NONE_LIKE:
+ resolved["frontend"] = infer_frontend(idea.lower(), "None")
+ else:
+ if resolved["frontend"] in NONE_LIKE:
+ resolved["frontend"] = "React"
+ if resolved["backend"] in NONE_LIKE:
+ resolved["backend"] = "FastAPI"
+ if resolved["database"] in {"Auto", ""}:
+ resolved["database"] = infer_database(idea.lower(), resolved["backend"])
+
+ if resolved["language"] in {"Auto", ""}:
+ resolved["language"] = infer_language(idea.lower(), resolved["frontend"], resolved["backend"])
+ return resolved
+
+
+def build_final_requirements_summary(
+ idea: str,
+ answers: Mapping[str, str],
+ selected_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> str:
+ lines = [
+ f"Original idea: {idea.strip()}",
+ f"Project scope: {project_kind['label']}",
+ "Selected stack:",
+ f"- Language: {selected_stack['language']}",
+ f"- Frontend: {selected_stack['frontend']}",
+ f"- Backend: {selected_stack['backend']}",
+ f"- Database: {selected_stack['database']}",
+ f"- AI / Tools: {selected_stack['aiTools']}",
+ f"- Deployment: {selected_stack['deployment']}",
+ ]
+
+ details: list[str] = []
+ if _is_yes_answer(answers.get("authentication")):
+ details.append("Include authentication-ready structure and required secrets.")
+ if _is_yes_answer(answers.get("admin_dashboard")):
+ details.append("Include admin-oriented navigation or management surface in the starter.")
+ if _is_yes_answer(answers.get("email_smtp")):
+ details.append("Prepare SMTP/email configuration and service wiring.")
+ if _is_yes_answer(answers.get("payment_system")):
+ details.append("Prepare payment provider integration points and required keys.")
+ if answers.get("user_roles") and answers.get("user_roles") != "Single user / no roles":
+ details.append(f"Support user access model: {answers['user_roles']}.")
+ if answers.get("external_apis") and answers.get("external_apis").lower() not in {"", "none", "no"}:
+ details.append(f"Prepare integration boundaries for: {answers['external_apis']}.")
+ if answers.get("complexity_level"):
+ details.append(f"Target complexity level: {answers['complexity_level']}.")
+ if answers.get("ai_integration") and answers.get("ai_integration") not in {"", "None"}:
+ details.append(f"Prepare AI integration around: {answers['ai_integration']}.")
+ details.extend(build_unmapped_preference_notes(answers, selected_stack))
+
+ if details:
+ lines.append("Architectural requirements:")
+ lines.extend(f"- {item}" for item in details)
+ else:
+ lines.append("Architectural requirements:")
+ lines.append("- Use the suggested defaults for unspecified decisions and keep the starter runnable-first.")
+
+ return "\n".join(lines).strip()
+
+
+def build_agent_finalize_assumptions(
+ answers: Mapping[str, str],
+ selected_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> list[str]:
+ assumptions = [
+ "Final requirements were synthesized from the original idea, your answers, and the selected stack.",
+ "Suggested defaults were used only where you left a question blank, so generation can continue without extra back-and-forth.",
+ ]
+ if project_kind["isFullStack"]:
+ assumptions.append("Frontend and backend will both be generated in separate folders.")
+ if _is_yes_answer(answers.get("authentication")):
+ assumptions.append("Authentication-related secrets and starter environment variables will be included where needed.")
+ if selected_stack.get("deployment") == "Docker":
+ assumptions.append("Docker-oriented deployment files will be included where the starter templates support them.")
+ return dedupe_list(assumptions)
+
+
+def _is_yes_answer(value: str | None) -> bool:
+ return str(value or "").strip().lower() in YES_ANSWERS
+
+
+def normalize_agent_answer_value(question_id: str, value: Any) -> str:
+ text = str(value or "").strip()
+ if not text:
+ return ""
+
+ lowered = text.lower()
+ if question_id in BOOLEAN_QUESTION_IDS:
+ if lowered in YES_ANSWERS:
+ return "Yes"
+ if lowered in NO_ANSWERS:
+ return "No"
+
+ if question_id == "project_scope":
+ return normalize_project_scope(text)
+
+ alias_map = CANONICAL_ANSWER_ALIASES.get(question_id, {})
+ if lowered in alias_map:
+ return alias_map[lowered]
+
+ if question_id == "user_roles":
+ if "admin" in lowered and "user" in lowered:
+ return "Basic roles (admin + user)"
+ if "custom" in lowered or "manager" in lowered or "permission" in lowered:
+ return "Custom roles needed"
+ if "single" in lowered or "no role" in lowered:
+ return "Single user / no roles"
+
+ if question_id == "external_apis" and lowered in NO_ANSWERS:
+ return "None"
+
+ return text
+
+
+def normalize_project_scope(value: Any) -> str:
+ text = str(value or "").strip()
+ if not text:
+ return ""
+ lowered = text.lower()
+ return CANONICAL_ANSWER_ALIASES["project_scope"].get(lowered, text)
+
+
+def supported_answer(value: str | None, supported_values: set[str]) -> str:
+ text = str(value or "").strip()
+ return text if text in supported_values else ""
+
+
+def build_unmapped_preference_notes(
+ answers: Mapping[str, str],
+ selected_stack: Mapping[str, str],
+) -> list[str]:
+ notes: list[str] = []
+ mappings = [
+ ("language", "Language", SUPPORTED_LANGUAGE_VALUES, "language"),
+ ("frontend_framework", "Frontend framework", SUPPORTED_FRONTEND_VALUES, "frontend"),
+ ("backend_framework", "Backend framework", SUPPORTED_BACKEND_VALUES, "backend"),
+ ("database", "Database", SUPPORTED_DATABASE_VALUES, "database"),
+ ("ai_integration", "AI integration", SUPPORTED_AI_VALUES, "aiTools"),
+ ("deployment_target", "Deployment target", SUPPORTED_DEPLOYMENT_VALUES, "deployment"),
+ ]
+
+ for answer_key, label, supported_values, stack_key in mappings:
+ raw_value = str(answers.get(answer_key, "")).strip()
+ if not raw_value or raw_value in supported_values:
+ continue
+ notes.append(
+ f"{label} preference noted: {raw_value}. The runnable starter keeps {selected_stack.get(stack_key, 'the suggested default')} as the safe template-backed default for now."
+ )
+ return notes
+
+
+def preview_budget_seconds(generation_mode: str) -> float:
+ return FAST_PLANNER_TIMEOUT_SECONDS if generation_mode == "fast" else DEEP_PREVIEW_TIMEOUT_SECONDS
+
+
+def validate_planner_payload(payload: Any) -> dict[str, Any]:
+ if not isinstance(payload, Mapping):
+ raise ValueError("Planner response was not a JSON object.")
+
+ missing_keys = sorted(REQUIRED_PLANNER_KEYS.difference(payload.keys()))
+ if missing_keys:
+ raise ValueError(f"Planner response was partial and missed required keys: {', '.join(missing_keys)}")
+
+ if not isinstance(payload.get("selectedStack"), Mapping):
+ raise ValueError("Planner response did not include a valid selectedStack object.")
+ if not isinstance(payload.get("modules"), Sequence) or isinstance(payload.get("modules"), (str, bytes, bytearray)):
+ raise ValueError("Planner response did not include a valid modules list.")
+ if not isinstance(payload.get("packageRequirements"), Sequence) or isinstance(payload.get("packageRequirements"), (str, bytes, bytearray)):
+ raise ValueError("Planner response did not include a valid packageRequirements list.")
+ if not isinstance(payload.get("installCommands"), Sequence) or isinstance(payload.get("installCommands"), (str, bytes, bytearray)):
+ raise ValueError("Planner response did not include a valid installCommands list.")
+ if not isinstance(payload.get("runCommands"), Sequence) or isinstance(payload.get("runCommands"), (str, bytes, bytearray)):
+ raise ValueError("Planner response did not include a valid runCommands list.")
+ if not isinstance(payload.get("requiredInputs"), Sequence) or isinstance(payload.get("requiredInputs"), (str, bytes, bytearray)):
+ raise ValueError("Planner response did not include a valid requiredInputs list.")
+ if not isinstance(payload.get("envVariables"), Sequence) or isinstance(payload.get("envVariables"), (str, bytes, bytearray)):
+ raise ValueError("Planner response did not include a valid envVariables list.")
+ if not isinstance(payload.get("customFiles"), Sequence) or isinstance(payload.get("customFiles"), (str, bytes, bytearray)):
+ raise ValueError("Planner response did not include a valid customFiles list.")
+
+ normalized_payload = dict(payload)
+ normalized_payload["customFiles"] = list(payload.get("customFiles", []))[:MAX_CUSTOM_FILES]
+ return normalized_payload
+
+
+def normalize_preview(
+ raw_preview: Mapping[str, Any] | None,
+ idea: str,
+ requested_stack: dict[str, str] | None = None,
+ generation_mode: str = "fast",
+ requirements_context: str | None = None,
+) -> dict[str, Any]:
+ raw = dict(raw_preview or {})
+ mode = normalize_generation_mode(generation_mode)
+ requested = normalize_stack_selection(requested_stack)
+ planning_context = requirements_context or idea
+ detected_choices = dedupe_list(
+ normalize_string_list(raw.get("detectedUserChoices")) or detect_user_choices(idea)
+ )
+ selected_stack = resolve_selected_stack(
+ idea=idea,
+ requested_stack=requested,
+ model_stack=raw.get("selectedStack"),
+ detected_choices=detected_choices,
+ )
+ project_kind = determine_project_kind(selected_stack, raw.get("projectType"))
+ project_name = clean_project_name(raw.get("projectName"), idea)
+
+ modules = merge_modules(
+ normalize_modules(raw.get("modules")),
+ build_default_modules(selected_stack, project_kind),
+ )
+ required_inputs = merge_required_inputs(
+ normalize_required_inputs(raw.get("requiredInputs")),
+ build_required_inputs(planning_context, selected_stack, project_kind, modules),
+ )
+ env_variables = merge_env_variables(
+ normalize_env_variables(raw.get("envVariables")),
+ required_inputs_to_env_variables(required_inputs),
+ )
+ package_requirements = dedupe_list(
+ normalize_string_list(raw.get("packageRequirements"))
+ + build_package_requirements(selected_stack, project_kind)
+ )
+ install_commands = dedupe_list(
+ normalize_string_list(raw.get("installCommands"))
+ + build_install_commands(selected_stack, project_kind)
+ )
+ run_commands = dedupe_list(
+ normalize_string_list(raw.get("runCommands"))
+ + build_run_commands(selected_stack, project_kind)
+ )
+
+ custom_manifest = normalize_custom_manifest(raw.get("customFiles"), selected_stack, project_kind)
+ validated_files = finalize_preview_files(
+ project_name=project_name,
+ selected_stack=selected_stack,
+ project_kind=project_kind,
+ custom_manifest=custom_manifest,
+ raw_files=raw.get("files"),
+ )
+
+ summary = (
+ str(raw.get("summary") or "").strip()
+ or build_summary(project_name, project_kind, selected_stack, mode)
+ )
+ problem_statement = (
+ str(raw.get("problemStatement") or "").strip()
+ or idea.strip()
+ or f"Build a starter project for {project_name}."
+ )
+ assumptions = dedupe_list(
+ normalize_string_list(raw.get("assumptions"))
+ + build_assumptions(selected_stack, project_kind, requested, mode, bool(custom_manifest))
+ )
+ architecture = dedupe_list(
+ normalize_string_list(raw.get("architecture"))
+ + build_architecture(selected_stack, project_kind)
+ )
+ chosen_stack = build_chosen_stack(selected_stack)
+
+ file_tree = build_preview_file_tree(validated_files, include_env_example=bool(env_variables))
+
+ return {
+ "projectName": project_name,
+ "detectedUserChoices": detected_choices,
+ "selectedStack": selected_stack,
+ "chosenStack": chosen_stack,
+ "assumptions": assumptions,
+ "summary": summary,
+ "problemStatement": problem_statement,
+ "architecture": architecture,
+ "modules": modules,
+ "packageRequirements": package_requirements,
+ "installCommands": install_commands,
+ "runCommands": run_commands,
+ "requiredInputs": required_inputs,
+ "envVariables": env_variables,
+ "fileTree": file_tree,
+ "files": validated_files,
+ }
+
+
+def apply_custom_file_overrides(
+ preview: dict[str, Any], custom_files: list[dict[str, str]]
+) -> dict[str, Any]:
+ merged_files = merge_file_entries(preview.get("files", []), custom_files)
+ selected_stack = normalize_stack_selection(preview.get("selectedStack"))
+ project_kind = determine_project_kind(selected_stack)
+ validated_files = finalize_preview_files(
+ project_name=str(preview.get("projectName") or "Generated Project"),
+ selected_stack=selected_stack,
+ project_kind=project_kind,
+ raw_files=merged_files,
+ )
+ env_variables = normalize_env_variables(preview.get("envVariables"))
+
+ preview["files"] = validated_files
+ preview["fileTree"] = build_preview_file_tree(
+ validated_files,
+ include_env_example=bool(env_variables),
+ )
+ return preview
+
+
+def normalize_generation_mode(value: Any) -> str:
+ return "deep" if str(value or "").strip().lower() == "deep" else "fast"
+
+
+def remaining_time(deadline: float) -> float:
+ return max(0.0, deadline - time.monotonic())
+
+
+def normalize_stack_selection(selection: Any) -> dict[str, str]:
+ data = selection if isinstance(selection, Mapping) else {}
+ normalized: dict[str, str] = {}
+ for field in STACK_FIELDS:
+ value = str(data.get(field) or STACK_DEFAULTS[field]).strip()
+ normalized[field] = value or STACK_DEFAULTS[field]
+ return normalized
+
+
+def resolve_selected_stack(
+ idea: str,
+ requested_stack: dict[str, str],
+ model_stack: Any,
+ detected_choices: list[str],
+) -> dict[str, str]:
+ model_selection = normalize_stack_selection(model_stack)
+ resolved = normalize_stack_selection(requested_stack)
+ lowered_idea = idea.lower()
+
+ backend = pick_stack_value(
+ requested_stack.get("backend"),
+ model_selection.get("backend"),
+ infer_backend(lowered_idea),
+ )
+ frontend = pick_stack_value(
+ requested_stack.get("frontend"),
+ model_selection.get("frontend"),
+ infer_frontend(lowered_idea, backend),
+ )
+ language = pick_stack_value(
+ requested_stack.get("language"),
+ model_selection.get("language"),
+ infer_language(lowered_idea, frontend, backend),
+ )
+ database = pick_stack_value(
+ requested_stack.get("database"),
+ model_selection.get("database"),
+ infer_database(lowered_idea, backend),
+ )
+ ai_tools = pick_stack_value(
+ requested_stack.get("aiTools"),
+ model_selection.get("aiTools"),
+ infer_ai_tools(lowered_idea),
+ )
+ deployment = pick_stack_value(
+ requested_stack.get("deployment"),
+ model_selection.get("deployment"),
+ infer_deployment(lowered_idea),
+ )
+
+ resolved.update(
+ {
+ "language": language,
+ "frontend": frontend,
+ "backend": backend,
+ "database": database,
+ "aiTools": ai_tools,
+ "deployment": deployment,
+ }
+ )
+
+ if not detected_choices and all(value == "Auto" for value in requested_stack.values()):
+ resolved["backend"] = resolved["backend"] if resolved["backend"] != "Auto" else "FastAPI"
+ resolved["frontend"] = resolved["frontend"] if resolved["frontend"] != "Auto" else "React"
+ resolved["language"] = resolved["language"] if resolved["language"] != "Auto" else "Python"
+ resolved["database"] = resolved["database"] if resolved["database"] != "Auto" else "SQLite"
+ resolved["aiTools"] = resolved["aiTools"] if resolved["aiTools"] != "Auto" else "None"
+ resolved["deployment"] = resolved["deployment"] if resolved["deployment"] != "Auto" else "Render"
+
+ return resolved
+
+
+def pick_stack_value(requested: str | None, suggested: str | None, inferred: str) -> str:
+ for candidate in (requested, suggested):
+ if candidate is not None and str(candidate).strip() not in {"", "Auto"}:
+ return str(candidate).strip()
+ return inferred
+
+
+def infer_language(idea: str, frontend: str, backend: str) -> str:
+ detected = match_keyword("language", idea)
+ if detected:
+ return detected
+ if backend in {"FastAPI", "Flask"}:
+ return "Python"
+ if backend == "Express":
+ return "JavaScript"
+ if backend == "NestJS":
+ return "TypeScript"
+ if backend == "Spring Boot":
+ return "Java"
+ if frontend in {"React", "Next.js", "Vue", "HTML/CSS/JavaScript"}:
+ return "JavaScript"
+ return "Python"
+
+
+def infer_frontend(idea: str, backend: str) -> str:
+ detected = match_keyword("frontend", idea)
+ if detected:
+ return detected
+ if any(token in idea for token in ("api only", "backend only", "worker", "cli", "automation")):
+ return "None"
+ if any(token in idea for token in ("dashboard", "web app", "portal", "browser", "frontend", "website")):
+ return "React"
+ if backend in {"FastAPI", "Express", "NestJS", "Spring Boot"}:
+ return "React"
+ return "None"
+
+
+def infer_backend(idea: str) -> str:
+ detected = match_keyword("backend", idea)
+ if detected:
+ return detected
+ if any(token in idea for token in ("frontend only", "static site", "landing page only")):
+ return "None"
+ if "spring" in idea or "java" in idea:
+ return "Spring Boot"
+ if "node" in idea or "express" in idea:
+ return "Express"
+ return "FastAPI"
+
+
+def infer_database(idea: str, backend: str) -> str:
+ detected = match_keyword("database", idea)
+ if detected:
+ return detected
+ if any(token in idea for token in ("no database", "without database", "in memory")):
+ return "None"
+ if backend in {"FastAPI", "Flask"}:
+ return "SQLite"
+ if backend in {"Express", "NestJS", "Spring Boot"}:
+ return "PostgreSQL"
+ return "None"
+
+
+def infer_ai_tools(idea: str) -> str:
+ detected = match_keyword("aiTools", idea)
+ if detected:
+ return detected
+ if any(token in idea for token in ("ai", "assistant", "agent", "generator", "chatbot", "llm")):
+ return "Ollama"
+ return "None"
+
+
+def infer_deployment(idea: str) -> str:
+ detected = match_keyword("deployment", idea)
+ return detected or "Render"
+
+
+def match_keyword(category: str, idea: str) -> str:
+ for value, keywords in KEYWORD_MAP[category].items():
+ if any(keyword in idea for keyword in keywords):
+ return value
+ return ""
+
+
+def detect_user_choices(idea: str) -> list[str]:
+ lowered = idea.lower()
+ detected: list[str] = []
+ labels = {
+ "language": "Language",
+ "frontend": "Frontend",
+ "backend": "Backend",
+ "database": "Database",
+ "aiTools": "AI / Tools",
+ "deployment": "Deployment",
+ }
+ for category, mapping in KEYWORD_MAP.items():
+ for value, keywords in mapping.items():
+ if any(keyword in lowered for keyword in keywords):
+ detected.append(f"{labels[category]}: {value}")
+ break
+ return dedupe_list(detected)
+
+
+def determine_project_kind(
+ selected_stack: dict[str, str],
+ declared_type: Any = None,
+) -> dict[str, Any]:
+ declared = str(declared_type or "").strip().lower()
+ frontend = selected_stack.get("frontend", "None")
+ backend = selected_stack.get("backend", "None")
+ has_frontend = frontend not in NONE_LIKE
+ has_backend = backend not in NONE_LIKE
+
+ if declared == "frontend-only":
+ has_backend = False
+ elif declared == "backend-only":
+ has_frontend = False
+ elif declared == "full-stack":
+ has_frontend = True
+ has_backend = True
+
+ is_full_stack = has_frontend and has_backend
+ if is_full_stack:
+ label = "full-stack"
+ minimum_files = 15
+ elif has_backend:
+ label = "backend-only"
+ minimum_files = 8
+ else:
+ label = "frontend-only"
+ minimum_files = 6
+
+ return {
+ "hasFrontend": has_frontend,
+ "hasBackend": has_backend,
+ "isFullStack": is_full_stack,
+ "label": label,
+ "minimumFiles": minimum_files,
+ }
+
+
+def clean_project_name(raw_name: Any, idea: str) -> str:
+ if isinstance(raw_name, str) and raw_name.strip():
+ return raw_name.strip()[:80]
+
+ words = re.findall(r"[A-Za-z0-9]+", idea)
+ if not words:
+ return "Project Agent Output"
+ return " ".join(word.capitalize() for word in words[:4])
+
+
+def build_summary(
+ project_name: str,
+ project_kind: dict[str, Any],
+ selected_stack: dict[str, str],
+ generation_mode: str,
+) -> str:
+ stack_bits = []
+ if project_kind["hasFrontend"]:
+ stack_bits.append(selected_stack["frontend"])
+ if project_kind["hasBackend"]:
+ stack_bits.append(selected_stack["backend"])
+ stack_summary = " + ".join(bit for bit in stack_bits if bit not in NONE_LIKE) or selected_stack["language"]
+ mode_label = "Fast Mode" if generation_mode == "fast" else "Deep Mode"
+ return (
+ f"{project_name} is a 100% runnable starter project built around {stack_summary}. "
+ f"{mode_label} uses backend templates for all standard project structure so output quality stays complete while generation remains fast."
+ )
+
+
+def build_assumptions(
+ selected_stack: dict[str, str],
+ project_kind: dict[str, Any],
+ requested_stack: dict[str, str],
+ generation_mode: str,
+ has_custom_manifest: bool,
+) -> list[str]:
+ assumptions: list[str] = []
+ if all(value == "Auto" for value in requested_stack.values()):
+ assumptions.append(
+ "Some stack choices were inferred automatically to provide a complete beginner-friendly starter."
+ )
+ if generation_mode == "fast":
+ assumptions.append(
+ "Fast Mode used backend templates for standard files so project completeness was preserved without waiting for the model to write boilerplate."
+ )
+ else:
+ assumptions.append(
+ "Deep Mode keeps the same complete standard structure as Fast Mode and uses extra model time only for project-specific custom files."
+ )
+ if project_kind["isFullStack"]:
+ assumptions.append("The project is split into frontend and backend folders to keep the full-stack boundary explicit.")
+ assumptions.append(
+ "This output is a 100% runnable starter project after `.env` is filled, setup is run, and the run script is started. Business logic can still be customized afterward."
+ )
+ if selected_stack.get("database") == "SQLite":
+ assumptions.append("SQLite was chosen as a lightweight local-first database default.")
+ if has_custom_manifest:
+ assumptions.append("Project-specific custom files were layered on top of the standard stack templates.")
+ return assumptions
+
+
+def build_architecture(
+ selected_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> list[str]:
+ architecture: list[str] = []
+ if project_kind["hasFrontend"]:
+ architecture.append(
+ f"{selected_stack['frontend']} handles the user-facing workflows, starter pages, and client-side integration points."
+ )
+ if project_kind["hasBackend"]:
+ architecture.append(
+ f"{selected_stack['backend']} provides the API surface, routing, services, and configuration layer."
+ )
+ if selected_stack.get("database") not in NONE_LIKE:
+ architecture.append(
+ f"{selected_stack['database']} is configured as the primary persistence layer through environment-driven settings."
+ )
+ if selected_stack.get("aiTools") not in NONE_LIKE:
+ architecture.append(
+ f"{selected_stack['aiTools']} integration is isolated behind service boundaries so model/provider settings can evolve independently."
+ )
+ architecture.append("Setup, run scripts, and dependency manifests are generated server-side for a consistent starter layout.")
+ return architecture
+
+
+def build_default_modules(
+ selected_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> list[dict[str, Any]]:
+ modules: list[dict[str, Any]] = []
+ if project_kind["hasFrontend"]:
+ base = "frontend/" if project_kind["isFullStack"] else ""
+ modules.append(
+ {
+ "name": "Frontend Experience",
+ "purpose": "Provides the main user interface, starter pages, and client-side state or API hooks.",
+ "keyFiles": [
+ f"{base}src/App.jsx" if selected_stack["frontend"] in {"React", "Next.js", "Vue"} else f"{base}index.html",
+ f"{base}src/pages/HomePage.jsx" if selected_stack["frontend"] in {"React", "Next.js", "Vue"} else f"{base}src/views/home.js",
+ f"{base}src/services/api.js",
+ ],
+ }
+ )
+ if project_kind["hasBackend"]:
+ base = "backend/" if project_kind["isFullStack"] else ""
+ if selected_stack["backend"] in {"FastAPI", "Flask"}:
+ key_files = [f"{base}app/main.py", f"{base}app/routers/items.py", f"{base}app/services/item_service.py"]
+ elif selected_stack["backend"] in {"Express", "NestJS"}:
+ key_files = [f"{base}server.js", f"{base}src/routes/items.js", f"{base}src/services/itemService.js"]
+ else:
+ key_files = [f"{base}pom.xml", f"{base}src/main/java/com/example/demo/service/AppService.java"]
+ modules.append(
+ {
+ "name": "Backend API",
+ "purpose": "Provides the routes, services, and integration-ready backend surface.",
+ "keyFiles": key_files,
+ }
+ )
+ if selected_stack.get("database") not in NONE_LIKE:
+ base = "backend/" if project_kind["isFullStack"] else ""
+ modules.append(
+ {
+ "name": "Persistence Layer",
+ "purpose": "Supplies data models, configuration, and starter persistence wiring.",
+ "keyFiles": [
+ f"{base}app/database.py" if selected_stack["backend"] in {"FastAPI", "Flask"} else f"{base}src/models/itemModel.js"
+ ],
+ }
+ )
+ return modules
+
+
+def build_package_requirements(
+ selected_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> list[str]:
+ packages: list[str] = []
+ if project_kind["hasBackend"]:
+ backend = selected_stack["backend"]
+ database = selected_stack["database"]
+ if backend == "FastAPI":
+ packages.extend(
+ ["fastapi", "uvicorn[standard]", "pydantic", "pydantic-settings", "sqlalchemy", "python-dotenv"]
+ )
+ if database == "SQLite":
+ packages.append("aiosqlite")
+ elif database == "PostgreSQL":
+ packages.append("psycopg[binary]")
+ elif backend == "Flask":
+ packages.extend(["flask", "python-dotenv", "sqlalchemy"])
+ elif backend in {"Express", "NestJS"}:
+ packages.extend(["express", "cors", "dotenv"])
+ if database == "PostgreSQL":
+ packages.append("pg")
+ elif database == "MongoDB":
+ packages.append("mongoose")
+ elif backend == "Spring Boot":
+ packages.extend(
+ [
+ "org.springframework.boot:spring-boot-starter-web",
+ "org.springframework.boot:spring-boot-starter-data-jpa",
+ ]
+ )
+ if project_kind["hasFrontend"]:
+ frontend = selected_stack["frontend"]
+ if frontend in {"React", "Next.js", "Vue"}:
+ packages.extend(["react", "react-dom", "vite"])
+ elif frontend == "HTML/CSS/JavaScript":
+ packages.append("vite")
+ ai_tools = selected_stack.get("aiTools", "None")
+ if ai_tools == "Ollama":
+ packages.append("ollama")
+ elif ai_tools == "LangChain":
+ packages.append("langchain")
+ return dedupe_list(packages)
+
+
+def build_install_commands(
+ selected_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> list[str]:
+ if project_kind["isFullStack"]:
+ return ["setup.bat", "./setup.sh"]
+ if project_kind["hasBackend"] and selected_stack["backend"] == "Spring Boot":
+ return ["setup.bat", "./setup.sh", "mvn install"]
+ if project_kind["hasFrontend"] and not project_kind["hasBackend"]:
+ return ["setup.bat", "./setup.sh", "npm install"]
+ return ["setup.bat", "./setup.sh"]
+
+
+def build_run_commands(
+ selected_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> list[str]:
+ commands = ["run.bat", "./run.sh"]
+ if project_kind["isFullStack"]:
+ return commands
+ if project_kind["hasBackend"]:
+ backend = selected_stack["backend"]
+ if backend in {"FastAPI", "Flask"}:
+ commands.append("python -m uvicorn app.main:app --reload")
+ elif backend in {"Express", "NestJS"}:
+ commands.append("npm start")
+ elif backend == "Spring Boot":
+ commands.append("mvn spring-boot:run")
+ elif project_kind["hasFrontend"]:
+ commands.append("npm run dev")
+ return commands
+
+
+def build_required_inputs(
+ idea: str,
+ selected_stack: dict[str, str],
+ project_kind: dict[str, Any],
+ modules: list[dict[str, Any]],
+) -> list[dict[str, Any]]:
+ context_parts = [idea]
+ for module in modules:
+ context_parts.append(str(module.get("name") or ""))
+ context_parts.append(str(module.get("purpose") or ""))
+ context_parts.extend(normalize_string_list(module.get("keyFiles")))
+ lowered = " ".join(part.lower() for part in context_parts if part)
+
+ required_inputs: list[dict[str, Any]] = []
+ if project_kind["hasBackend"]:
+ required_inputs.append(
+ {
+ "name": "APP_ENV",
+ "required": False,
+ "example": "development",
+ "whereToAdd": ".env",
+ "purpose": "Application environment name.",
+ }
+ )
+
+ database = selected_stack.get("database")
+ if database == "SQLite":
+ required_inputs.append(
+ {
+ "name": "DATABASE_URL",
+ "required": True,
+ "example": "sqlite:///./app.db",
+ "whereToAdd": ".env",
+ "purpose": "SQLite connection string for local development.",
+ }
+ )
+ elif database in {"PostgreSQL", "MySQL"}:
+ example = (
+ "postgresql://postgres:postgres@localhost:5432/app_db"
+ if database == "PostgreSQL"
+ else "mysql://root:password@localhost:3306/app_db"
+ )
+ required_inputs.append(
+ {
+ "name": "DATABASE_URL",
+ "required": True,
+ "example": example,
+ "whereToAdd": ".env",
+ "purpose": f"{database} connection string for the backend database.",
+ }
+ )
+ elif database == "MongoDB":
+ required_inputs.append(
+ {
+ "name": "MONGODB_URI",
+ "required": True,
+ "example": "mongodb://localhost:27017/app_db",
+ "whereToAdd": ".env",
+ "purpose": "MongoDB connection string for the backend database.",
+ }
+ )
+
+ if _context_mentions_any(lowered, ("auth", "authentication", "login", "signup", "jwt", "token", "session")):
+ required_inputs.append(
+ {
+ "name": "JWT_SECRET",
+ "required": True,
+ "example": "change-me-super-secret",
+ "whereToAdd": ".env",
+ "purpose": "Secret used to sign authentication tokens.",
+ }
+ )
+
+ if _context_mentions_any(lowered, ("email", "smtp", "mail", "newsletter", "verification email", "contact form")):
+ required_inputs.extend(
+ [
+ {
+ "name": "SMTP_HOST",
+ "required": True,
+ "example": "smtp.gmail.com",
+ "whereToAdd": ".env",
+ "purpose": "SMTP email server host.",
+ },
+ {
+ "name": "SMTP_PORT",
+ "required": True,
+ "example": "587",
+ "whereToAdd": ".env",
+ "purpose": "SMTP email server port.",
+ },
+ {
+ "name": "SMTP_USER",
+ "required": True,
+ "example": "notifications@example.com",
+ "whereToAdd": ".env",
+ "purpose": "SMTP account username or sender login.",
+ },
+ {
+ "name": "SMTP_PASSWORD",
+ "required": True,
+ "example": "app-password",
+ "whereToAdd": ".env",
+ "purpose": "SMTP account password or app password.",
+ },
+ ]
+ )
+
+ if _context_mentions_any(lowered, ("payment", "payments", "stripe", "checkout", "billing", "subscription")):
+ key_name = "STRIPE_SECRET_KEY" if "stripe" in lowered else "PAYMENT_SECRET_KEY"
+ required_inputs.append(
+ {
+ "name": key_name,
+ "required": True,
+ "example": "sk_test_1234567890",
+ "whereToAdd": ".env",
+ "purpose": "Secret key for payment provider API requests.",
+ }
+ )
+
+ ai_tools = selected_stack.get("aiTools")
+ if ai_tools == "Ollama" or _context_mentions_any(lowered, ("ollama", "local llm", "qwen", "llama")):
+ required_inputs.extend(
+ [
+ {
+ "name": "OLLAMA_BASE_URL",
+ "required": True,
+ "example": "http://localhost:11434",
+ "whereToAdd": ".env",
+ "purpose": "Base URL for the local Ollama server.",
+ },
+ {
+ "name": "OLLAMA_MODEL",
+ "required": True,
+ "example": "qwen2.5-coder:latest",
+ "whereToAdd": ".env",
+ "purpose": "Ollama model name to use for local inference.",
+ },
+ ]
+ )
+ if ai_tools == "OpenAI API" or _context_mentions_any(lowered, ("openai", "gpt-4", "gpt", "chatgpt")):
+ required_inputs.append(
+ {
+ "name": "OPENAI_API_KEY",
+ "required": True,
+ "example": "sk-proj-1234567890",
+ "whereToAdd": ".env",
+ "purpose": "API key for hosted OpenAI requests.",
+ }
+ )
+
+ if _context_mentions_any(lowered, ("oauth", "google login", "github login", "social login", "openid")):
+ required_inputs.extend(
+ [
+ {
+ "name": "OAUTH_CLIENT_ID",
+ "required": True,
+ "example": "your-client-id",
+ "whereToAdd": ".env",
+ "purpose": "OAuth client identifier for external sign-in.",
+ },
+ {
+ "name": "OAUTH_CLIENT_SECRET",
+ "required": True,
+ "example": "your-client-secret",
+ "whereToAdd": ".env",
+ "purpose": "OAuth client secret for external sign-in.",
+ },
+ ]
+ )
+
+ if project_kind["isFullStack"]:
+ required_inputs.append(
+ {
+ "name": "VITE_API_BASE_URL",
+ "required": False,
+ "example": "http://localhost:8000",
+ "whereToAdd": ".env",
+ "purpose": "Frontend base URL for backend API calls.",
+ }
+ )
+
+ return dedupe_required_inputs(required_inputs)
+
+
+def required_inputs_to_env_variables(
+ required_inputs: list[dict[str, Any]],
+) -> list[dict[str, str]]:
+ env_vars: list[dict[str, str]] = []
+ for item in required_inputs:
+ name = str(item.get("name") or "").strip()
+ if not name:
+ continue
+ env_vars.append(
+ {
+ "name": name,
+ "value": str(item.get("example") or "").strip(),
+ "description": str(item.get("purpose") or "").strip(),
+ }
+ )
+ return env_vars
+
+
+def normalize_custom_manifest(
+ value: Any,
+ selected_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> list[dict[str, str]]:
+ manifest: list[dict[str, str]] = []
+ if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
+ for item in value:
+ if len(manifest) >= MAX_CUSTOM_FILES:
+ break
+ if isinstance(item, str):
+ path = clean_relative_path(item)
+ purpose = "Project-specific custom logic."
+ elif isinstance(item, Mapping):
+ path = clean_relative_path(item.get("path"))
+ purpose = str(item.get("purpose") or item.get("description") or "Project-specific custom logic.").strip()
+ else:
+ continue
+ if not path or is_standard_file_path(path):
+ continue
+ manifest.append({"path": path, "purpose": purpose})
+
+ if manifest:
+ return dedupe_manifest(manifest)
+
+ defaults: list[dict[str, str]] = []
+ if project_kind["isFullStack"]:
+ if selected_stack["frontend"] not in NONE_LIKE:
+ defaults.append(
+ {
+ "path": "frontend/src/pages/DashboardPage.jsx",
+ "purpose": "Primary project-specific frontend page for the main user workflow.",
+ }
+ )
+ if selected_stack["backend"] in {"FastAPI", "Flask"}:
+ defaults.append(
+ {
+ "path": "backend/app/services/domain_service.py",
+ "purpose": "Project-specific backend domain service with starter business rules.",
+ }
+ )
+ elif selected_stack["backend"] in {"Express", "NestJS"}:
+ defaults.append(
+ {
+ "path": "backend/src/services/domainService.js",
+ "purpose": "Project-specific backend domain service with starter business rules.",
+ }
+ )
+ elif project_kind["hasBackend"]:
+ if selected_stack["backend"] in {"FastAPI", "Flask"}:
+ defaults.append(
+ {
+ "path": "app/services/domain_service.py",
+ "purpose": "Project-specific backend domain service with starter business rules.",
+ }
+ )
+ elif selected_stack["backend"] in {"Express", "NestJS"}:
+ defaults.append(
+ {
+ "path": "src/services/domainService.js",
+ "purpose": "Project-specific backend domain service with starter business rules.",
+ }
+ )
+ elif selected_stack["backend"] == "Spring Boot":
+ defaults.append(
+ {
+ "path": "src/main/java/com/example/demo/service/DomainService.java",
+ "purpose": "Project-specific backend service with starter business rules.",
+ }
+ )
+ elif project_kind["hasFrontend"]:
+ if selected_stack["frontend"] in {"React", "Next.js", "Vue"}:
+ defaults.append(
+ {
+ "path": "src/pages/DashboardPage.jsx",
+ "purpose": "Primary project-specific frontend page for the main workflow.",
+ }
+ )
+ else:
+ defaults.append(
+ {
+ "path": "src/views/dashboard.js",
+ "purpose": "Primary project-specific frontend view for the main workflow.",
+ }
+ )
+ return defaults[:MAX_CUSTOM_FILES]
+
+
+def build_standard_files(
+ project_name: str,
+ selected_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> list[dict[str, str]]:
+ files: dict[str, str] = {}
+ if project_kind["isFullStack"]:
+ if project_kind["hasFrontend"]:
+ files.update(build_frontend_files(selected_stack["frontend"], project_name, "frontend"))
+ if project_kind["hasBackend"]:
+ files.update(build_backend_files(selected_stack, project_name, "backend"))
+ files.update(build_root_scripts(selected_stack, project_kind))
+ elif project_kind["hasBackend"]:
+ files.update(build_backend_files(selected_stack, project_name, ""))
+ files.update(build_root_scripts(selected_stack, project_kind))
+ else:
+ files.update(build_frontend_files(selected_stack["frontend"], project_name, ""))
+ files.update(build_root_scripts(selected_stack, project_kind))
+ return [{"path": path, "content": content} for path, content in files.items()]
+
+
+def build_custom_template_files(
+ manifest: list[dict[str, str]],
+ project_name: str,
+ selected_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> list[dict[str, str]]:
+ files: list[dict[str, str]] = []
+ for item in manifest[:MAX_CUSTOM_FILES]:
+ path = item["path"]
+ purpose = item["purpose"]
+ content = build_custom_template_content(path, purpose, project_name, selected_stack, project_kind)
+ files.append({"path": path, "content": trim_content_lines(content)})
+ return files
+
+
+def build_custom_template_content(
+ path: str,
+ purpose: str,
+ project_name: str,
+ selected_stack: dict[str, str],
+ project_kind: dict[str, Any],
+) -> str:
+ filename = Path(path).name
+ stem = Path(path).stem
+ pretty_name = stem.replace("_", " ").replace("-", " ").title()
+ extension = Path(path).suffix.lower()
+
+ if extension in {".jsx", ".tsx"}:
+ if "page" in stem.lower() or "/pages/" in path:
+ return f"""const cards = [
+ {{
+ title: "{pretty_name} Overview",
+ detail: "{purpose}"
+ }},
+ {{
+ title: "Starter Workflow",
+ detail: "Use this page to connect forms, API requests, and user-facing business actions."
+ }}
+];
+
+export default function {safe_component_name(stem)}() {{
+ return (
+ {purpose} {purpose}{pretty_name}
+
+ {{cards.map((card) => (
+
+ {pretty_name}
+
Generated by Project Agent
+This frontend is ready for your first feature slice.
+API health source: {projectHealth}
+Generated by Project Agent
+This starter is ready for your first feature slice.
+