CoolFace
Apppublic

arcalab/prototyping

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
app.py1217 linesDownload Raw Back to root
1# pyright: reportMissingImports=false, reportMissingTypeStubs=false, reportExplicitAny=false, reportAny=false, reportUnknownVariableType=false, reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnusedCallResult=false2import json3import os4import time5from typing import Any, Optional6 7import pandas as pd8import requests9import streamlit as st10 11 12APP_TITLE = "AI Prototype Planner"13CAPABILITY_DESCRIPTION = "End to End Applied AI Prototyping — Rapid prototyping of full AI experiences. Connect data sources with model layers and interfaces for validation and experimentation."14RATE_LIMIT_REQUESTS = 815RATE_LIMIT_WINDOW_SECONDS = 6016GEMINI_MODEL = "gemini-2.5-flash"17 18PRIMARY_CAPABILITIES = [19    "Text Generation",20    "Image/Video",21    "Speech/Audio",22    "Structured Data/Analytics",23    "Multi-modal",24    "Agents/Automation",25]26 27TARGET_USERS = [28    "Internal team",29    "B2B customers",30    "B2C consumers",31    "Developer API",32]33 34ARCHITECTURE_STAGES = [35    "Data Sources",36    "Processing",37    "Model Layer",38    "API Layer",39    "Frontend",40]41 42 43def model_catalog() -> dict[str, list[dict[str, str]]]:44    return {45        "Text Generation": [46            {"model": "GPT-4o", "provider": "OpenAI", "capability_match": "Strong reasoning + tools", "cost_1k_requests": "$28.00", "latency": "Medium", "pros": "High quality + mature ecosystem", "cons": "Premium pricing"},47            {"model": "Claude 3.5 Sonnet", "provider": "Anthropic", "capability_match": "Long-context instruction following", "cost_1k_requests": "$22.00", "latency": "Medium", "pros": "Reliable complex writing", "cons": "Tier-dependent limits"},48            {"model": "Gemini 1.5 Pro", "provider": "Google", "capability_match": "Large context + multimodal", "cost_1k_requests": "$18.00", "latency": "Medium", "pros": "Great context/cost balance", "cons": "May require strict prompting"},49        ],50        "Image/Video": [51            {"model": "SDXL", "provider": "Stability AI", "capability_match": "Image generation and editing", "cost_1k_requests": "$8.00", "latency": "Medium", "pros": "Flexible and open workflow", "cons": "Prompt tuning overhead"},52            {"model": "GPT-4o mini vision", "provider": "OpenAI", "capability_match": "Visual understanding", "cost_1k_requests": "$10.00", "latency": "Low", "pros": "Fast image analysis", "cons": "Not specialized for generation"},53            {"model": "Gemini 1.5 Flash", "provider": "Google", "capability_match": "Video-aware extraction", "cost_1k_requests": "$9.00", "latency": "Low", "pros": "Fast and cost-efficient", "cons": "Lower quality than premium tiers"},54        ],55        "Speech/Audio": [56            {"model": "Whisper-large-v3", "provider": "OpenAI", "capability_match": "Speech-to-text baseline", "cost_1k_requests": "$6.00", "latency": "Low", "pros": "High transcription accuracy", "cons": "Needs downstream NLU layer"},57            {"model": "Gemini 1.5 Pro", "provider": "Google", "capability_match": "Audio + text reasoning", "cost_1k_requests": "$18.00", "latency": "Medium", "pros": "Unified multimodal pipeline", "cons": "High cost for always-on streams"},58            {"model": "Llama 3.1 70B Instruct", "provider": "Meta / Hosted", "capability_match": "Post-ASR intent handling", "cost_1k_requests": "$7.00", "latency": "Medium", "pros": "Self-host flexibility", "cons": "MLOps complexity"},59        ],60        "Structured Data/Analytics": [61            {"model": "Claude 3 Haiku", "provider": "Anthropic", "capability_match": "Fast BI commentary", "cost_1k_requests": "$5.00", "latency": "Low", "pros": "Low-latency narrative insights", "cons": "Less deep reasoning"},62            {"model": "GPT-4o", "provider": "OpenAI", "capability_match": "Executive reporting", "cost_1k_requests": "$28.00", "latency": "Medium", "pros": "Best-in-class summaries", "cons": "Can be expensive at scale"},63            {"model": "Llama 3.1 8B Instruct", "provider": "Meta / Hosted", "capability_match": "Private analytics copilot", "cost_1k_requests": "$3.00", "latency": "Low", "pros": "Low marginal cost", "cons": "Requires quality tuning"},64        ],65        "Multi-modal": [66            {"model": "Gemini 1.5 Pro", "provider": "Google", "capability_match": "Cross-modal context fusion", "cost_1k_requests": "$18.00", "latency": "Medium", "pros": "Native multimodal input", "cons": "Prompt format discipline required"},67            {"model": "GPT-4o", "provider": "OpenAI", "capability_match": "High quality multimodal responses", "cost_1k_requests": "$28.00", "latency": "Medium", "pros": "Great tool ecosystem", "cons": "Higher cost"},68            {"model": "Claude 3.5 Sonnet", "provider": "Anthropic", "capability_match": "Document + visual reasoning", "cost_1k_requests": "$22.00", "latency": "Medium", "pros": "Strong reliability", "cons": "Fewer native media workflows"},69        ],70        "Agents/Automation": [71            {"model": "GPT-4o", "provider": "OpenAI", "capability_match": "Function/tool orchestration", "cost_1k_requests": "$28.00", "latency": "Medium", "pros": "Excellent function calling", "cons": "Costs climb with chain depth"},72            {"model": "Claude 3.5 Sonnet", "provider": "Anthropic", "capability_match": "Long-horizon planning", "cost_1k_requests": "$22.00", "latency": "Medium", "pros": "Strong consistency", "cons": "Schema-control needed"},73            {"model": "Llama 3.1 70B Instruct", "provider": "Meta / Hosted", "capability_match": "On-prem orchestration", "cost_1k_requests": "$7.00", "latency": "Medium", "pros": "Deployment control", "cons": "Ops burden"},74        ],75    }76 77 78def template_library() -> dict[str, dict[str, Any]]:79    return {80        "Text Generation": {"sources": ["Knowledge Base", "Support Tickets", "Product Docs"], "ingestion": "Batch sync + webhook deltas", "preprocessing": "Chunking, dedupe, metadata, embeddings", "storage": "Object store + vector DB", "serving": "RAG API + moderation", "frontend": "Chat assistant with handoff", "team": "1 PM, 1 AI, 1 Backend, 1 Frontend"},81        "Image/Video": {"sources": ["Uploads", "Media feeds", "Moderation queue"], "ingestion": "Event-driven media workers", "preprocessing": "Frame extraction, resizing, safety features", "storage": "Object store + metadata index", "serving": "Async inference gateway", "frontend": "Ops moderation dashboard", "team": "1 PM, 1 CV, 1 Backend, 1 Trust/Safety"},82        "Speech/Audio": {"sources": ["Call recordings", "Mobile streams", "CRM logs"], "ingestion": "Realtime collector + batch import", "preprocessing": "Noise reduction, diarization, transcript cleanup", "storage": "Audio lake + transcript warehouse", "serving": "Realtime intent API", "frontend": "Voice-enabled workflow app", "team": "1 PM, 1 Speech, 1 Mobile, 1 Backend"},83        "Structured Data/Analytics": {"sources": ["Warehouse", "SaaS exports", "Event tracking"], "ingestion": "Daily ETL + KPI stream", "preprocessing": "Validation, feature derivation, null handling", "storage": "Warehouse mart + feature cache", "serving": "Analytics API + insight jobs", "frontend": "KPI dashboard", "team": "1 PM, 1 Data Engineer, 1 Backend, 1 BI/Frontend"},84        "Multi-modal": {"sources": ["Documents", "Images", "Audio", "App events"], "ingestion": "Unified ingest bus + parsers", "preprocessing": "OCR, ASR, segmentation, embeddings", "storage": "Object + vector + metadata DB", "serving": "Multimodal orchestrator", "frontend": "Unified analyst workspace", "team": "1 PM, 2 AI, 1 Platform, 1 Frontend"},85        "Agents/Automation": {"sources": ["SOP docs", "System APIs", "Task events"], "ingestion": "Connector framework", "preprocessing": "Tool schema normalization + policy tags", "storage": "Knowledge store + run-history DB", "serving": "Agent runtime + guardrails", "frontend": "Automation cockpit", "team": "1 PM, 1 Agent Eng, 1 Backend, 1 Security"},86    }87 88 89@st.cache_data90def load_sample_concepts() -> list[dict[str, Any]]:91    path = "data/sample_concepts.jsonl"92    records: list[dict[str, Any]] = []93    try:94        with open(path, "r", encoding="utf-8") as handle:95            for line in handle:96                line = line.strip()97                if not line:98                    continue99                try:100                    obj = json.loads(line)101                except json.JSONDecodeError:102                    continue103                if isinstance(obj, dict):104                    records.append(obj)105    except OSError:106        return []107    return records108 109 110def complexity_weight(capability: str) -> float:111    return {112        "Text Generation": 1.0,113        "Image/Video": 1.25,114        "Speech/Audio": 1.2,115        "Structured Data/Analytics": 0.9,116        "Multi-modal": 1.45,117        "Agents/Automation": 1.35,118    }.get(capability, 1.0)119 120 121def risk_note(target_users: str) -> str:122    if target_users == "Internal team":123        return "Access controls, SSO, and audit logs."124    if target_users == "B2B customers":125        return "Tenant isolation, SLOs, and support runbook."126    if target_users == "B2C consumers":127        return "Safety, abuse detection, and high-traffic reliability."128    return "Versioned APIs, quotas, and auth hardening."129 130 131def estimate_monthly_requests(target_users: str, budget: int) -> int:132    baseline = {133        "Internal team": 90000,134        "B2B customers": 220000,135        "B2C consumers": 850000,136        "Developer API": 500000,137    }.get(target_users, 100000)138    scale = max(0.35, min(2.2, budget / 100000))139    return int(baseline * scale)140 141 142def architecture_template(capability: str, target_users: str) -> list[dict[str, str]]:143    profile = template_library().get(capability, template_library()["Text Generation"])144    api_layer = {145        "Internal team": "Internal API gateway + SSO",146        "B2B customers": "Tenant-aware API gateway + RBAC",147        "B2C consumers": "Public API + anti-abuse checks",148        "Developer API": "Versioned API gateway + key management",149    }.get(target_users, "API gateway")150    return [151        {"stage": "Data Sources", "tech": ", ".join(profile["sources"]), "details": "Primary data systems feeding context and events."},152        {"stage": "Processing", "tech": profile["preprocessing"], "details": "Normalize, enrich, and filter input signals."},153        {"stage": "Model Layer", "tech": "Hybrid routing across fast + premium models", "details": "Use quality/cost-aware model switching."},154        {"stage": "API Layer", "tech": api_layer, "details": "Authentication, observability, policy checks, and versioning."},155        {"stage": "Frontend", "tech": profile["frontend"], "details": "User-facing interface with feedback collection."},156    ]157 158 159def weekly_plan(capability: str, timeline_weeks: int, target_users: str) -> list[dict[str, str]]:160    phases = [161        "Discovery + scope lock",162        "Data integration",163        "Model baseline + evaluations",164        "API and orchestration",165        "UX integration + user tests",166        "Hardening + observability",167        "Pilot launch",168    ]169    team = template_library().get(capability, template_library()["Text Generation"]).get("team", "Cross-functional AI team")170    stride = max(1, int(round(max(2, timeline_weeks) / 6)))171    rows: list[dict[str, str]] = []172    for wk in range(1, timeline_weeks + 1):173        phase = phases[min((wk - 1) // stride, len(phases) - 1)]174        deliverable = f"Week {wk} delivery for {phase.lower()}"175        if wk == 1:176            deliverable = "Problem framing, KPI targets, and architecture brief"177        if wk == timeline_weeks:178            deliverable = "Pilot release, handover docs, and support plan"179        rows.append(180            {181                "Week": str(wk),182                "Milestone": phase,183                "Deliverables": deliverable,184                "Team Allocation": team,185                "Risk Control": risk_note(target_users),186            }187        )188    return rows189 190 191def estimate_costs(capability: str, target_users: str, budget: int, timeline_weeks: int) -> dict[str, Any]:192    weight = complexity_weight(capability)193    req = estimate_monthly_requests(target_users, budget)194    compute = int((budget * 0.18 * weight) + (timeline_weeks * 1100))195    storage = int((budget * 0.05) + (timeline_weeks * 320))196    model_api = int((req / 1000) * (8 + (weight * 4)))197    development = int((budget * 0.52) + (timeline_weeks * 2100 * weight))198    security = int((budget * 0.08) + (timeline_weeks * 420))199    subtotal = compute + storage + model_api + development + security200    contingency = int(subtotal * 0.15)201    total = subtotal + contingency202    fit = "Within target" if total <= budget * 1.1 else "Above target"203    return {204        "compute": compute,205        "storage": storage,206        "model_api": model_api,207        "development": development,208        "security": security,209        "contingency": contingency,210        "subtotal": subtotal,211        "total": total,212        "fit": fit,213    }214 215 216def parse_json_object(text: str) -> Optional[dict[str, Any]]:217    if not text:218        return None219    start = text.find("{")220    end = text.rfind("}")221    if start == -1 or end == -1 or end <= start:222        return None223    try:224        data = json.loads(text[start : end + 1])225    except json.JSONDecodeError:226        return None227    if isinstance(data, dict):228        return data229    return None230 231 232def check_rate_limit() -> tuple[bool, int]:233    now = time.time()234    current = [ts for ts in st.session_state.get("request_timestamps", []) if (now - ts) <= RATE_LIMIT_WINDOW_SECONDS]235    st.session_state["request_timestamps"] = current236    if len(current) >= RATE_LIMIT_REQUESTS:237        retry = int(RATE_LIMIT_WINDOW_SECONDS - (now - current[0]))238        return False, max(retry, 1)239    current.append(now)240    st.session_state["request_timestamps"] = current241    return True, 0242 243 244def init_state() -> None:245    defaults: dict[str, Any] = {246        "concept_description": "",247        "primary_capability": PRIMARY_CAPABILITIES[0],248        "target_users": TARGET_USERS[1],249        "budget": 75000,250        "timeline_weeks": 12,251        "unlocked_step": 1,252        "request_timestamps": [],253        "architecture_rows": [],254        "data_pipeline": {},255        "model_options": [],256        "selected_model_name": "",257        "build_plan_rows": [],258        "cost_inputs": {},259        "cost_summary": {},260        "ai_strategy_note": "",261        "ai_architecture_text": "",262        "final_plan": None,263        "final_plan_source": "",264        "sample_cursor": 0,265    }266    for key, value in defaults.items():267        if key not in st.session_state:268            st.session_state[key] = value269 270 271def clear_dynamic_widget_keys() -> None:272    keys = list(st.session_state.keys())273    for key in keys:274        if isinstance(key, str) and (key.startswith("arch_tech_") or key.startswith("arch_details_")):275            del st.session_state[key]276        if isinstance(key, str) and key.startswith("pipeline_"):277            del st.session_state[key]278    for transient in ["build_plan_editor", "move_week_pick"]:279        if transient in st.session_state:280            del st.session_state[transient]281 282 283def apply_sample(samples: list[dict[str, Any]]) -> None:284    if not samples:285        return286    index = int(st.session_state.get("sample_cursor", 0)) % len(samples)287    sample = samples[index]288    st.session_state["sample_cursor"] = (index + 1) % len(samples)289    st.session_state["concept_description"] = str(sample.get("description", ""))290    st.session_state["primary_capability"] = str(sample.get("primary_capability", PRIMARY_CAPABILITIES[0]))291    st.session_state["target_users"] = str(sample.get("target_users", TARGET_USERS[0]))292    st.session_state["budget"] = int(sample.get("budget", 75000))293    st.session_state["timeline_weeks"] = int(sample.get("timeline_weeks", 12))294    st.session_state["unlocked_step"] = 1295    st.session_state["final_plan"] = None296    st.session_state["final_plan_source"] = ""297    clear_dynamic_widget_keys()298    seed_step_two_state(reset_models=True)299    seed_step_three_state()300 301 302def seed_step_two_state(reset_models: bool) -> None:303    capability = str(st.session_state.get("primary_capability", PRIMARY_CAPABILITIES[0]))304    target = str(st.session_state.get("target_users", TARGET_USERS[0]))305    arch = architecture_template(capability, target)306    pipeline_profile = template_library().get(capability, template_library()["Text Generation"])307    pipeline = {308        "sources": list(pipeline_profile.get("sources", [])),309        "ingestion": str(pipeline_profile.get("ingestion", "")),310        "preprocessing": str(pipeline_profile.get("preprocessing", "")),311        "storage": str(pipeline_profile.get("storage", "")),312        "serving": str(pipeline_profile.get("serving", "")),313    }314    st.session_state["architecture_rows"] = arch315    st.session_state["data_pipeline"] = pipeline316    if reset_models:317        options = [dict(row) for row in model_catalog().get(capability, model_catalog()["Text Generation"])]318        st.session_state["model_options"] = options319        st.session_state["selected_model_name"] = options[0]["model"] if options else ""320    clear_dynamic_widget_keys()321 322 323def seed_step_three_state() -> None:324    capability = str(st.session_state.get("primary_capability", PRIMARY_CAPABILITIES[0]))325    target = str(st.session_state.get("target_users", TARGET_USERS[0]))326    budget = int(st.session_state.get("budget", 75000))327    timeline = int(st.session_state.get("timeline_weeks", 12))328    st.session_state["build_plan_rows"] = weekly_plan(capability, timeline, target)329    costs = estimate_costs(capability, target, budget, timeline)330    st.session_state["cost_inputs"] = {331        "compute": costs["compute"],332        "storage": costs["storage"],333        "model_api": costs["model_api"],334        "development": costs["development"],335        "security": costs["security"],336    }337    recalculate_cost_summary()338 339 340def recalculate_cost_summary() -> None:341    cost_inputs = st.session_state.get("cost_inputs", {})342    budget = int(st.session_state.get("budget", 0))343    compute = int(cost_inputs.get("compute", 0))344    storage = int(cost_inputs.get("storage", 0))345    model_api = int(cost_inputs.get("model_api", 0))346    development = int(cost_inputs.get("development", 0))347    security = int(cost_inputs.get("security", 0))348    subtotal = compute + storage + model_api + development + security349    contingency = int(subtotal * 0.15)350    total = subtotal + contingency351    fit = "Within target" if total <= budget * 1.1 else "Above target"352    st.session_state["cost_summary"] = {353        "line_items": [354            {"category": "Compute", "estimate_usd": compute, "notes": "Inference + orchestration resources"},355            {"category": "Storage", "estimate_usd": storage, "notes": "Object, metadata, and vector storage"},356            {"category": "Model API", "estimate_usd": model_api, "notes": "Primary model inference and retries"},357            {"category": "Development", "estimate_usd": development, "notes": "Product + engineering implementation effort"},358            {"category": "Security", "estimate_usd": security, "notes": "Guardrails, logging, monitoring, compliance"},359            {"category": "Contingency (15%)", "estimate_usd": contingency, "notes": "Scope and traffic variance"},360        ],361        "subtotal": subtotal,362        "contingency": contingency,363        "total": total,364        "budget": budget,365        "fit": fit,366    }367 368 369def ensure_architecture_widget_defaults() -> None:370    rows = st.session_state.get("architecture_rows", [])371    for idx, row in enumerate(rows):372        tech_key = f"arch_tech_{idx}"373        detail_key = f"arch_details_{idx}"374        if tech_key not in st.session_state:375            st.session_state[tech_key] = str(row.get("tech", ""))376        if detail_key not in st.session_state:377            st.session_state[detail_key] = str(row.get("details", ""))378 379 380def sync_architecture_from_widgets() -> None:381    rows = st.session_state.get("architecture_rows", [])382    updated: list[dict[str, str]] = []383    for idx, row in enumerate(rows):384        updated.append(385            {386                "stage": str(row.get("stage", ARCHITECTURE_STAGES[idx] if idx < len(ARCHITECTURE_STAGES) else "Stage")),387                "tech": str(st.session_state.get(f"arch_tech_{idx}", row.get("tech", ""))),388                "details": str(st.session_state.get(f"arch_details_{idx}", row.get("details", ""))),389            }390        )391    st.session_state["architecture_rows"] = updated392 393 394def ensure_pipeline_widget_defaults() -> None:395    pipeline = st.session_state.get("data_pipeline", {})396    sources = pipeline.get("sources", [])397    if not isinstance(sources, list):398        sources = []399    if "pipeline_sources" not in st.session_state:400        st.session_state["pipeline_sources"] = ", ".join([str(item) for item in sources])401    for key in ["ingestion", "preprocessing", "storage", "serving"]:402        widget_key = f"pipeline_{key}"403        if widget_key not in st.session_state:404            st.session_state[widget_key] = str(pipeline.get(key, ""))405 406 407def sync_pipeline_from_widgets() -> None:408    source_text = str(st.session_state.get("pipeline_sources", ""))409    sources = [part.strip() for part in source_text.split(",") if part.strip()]410    st.session_state["data_pipeline"] = {411        "sources": sources,412        "ingestion": str(st.session_state.get("pipeline_ingestion", "")),413        "preprocessing": str(st.session_state.get("pipeline_preprocessing", "")),414        "storage": str(st.session_state.get("pipeline_storage", "")),415        "serving": str(st.session_state.get("pipeline_serving", "")),416    }417 418 419def build_architecture_diagram_text(architecture_rows: list[dict[str, str]]) -> str:420    segments: list[str] = []421    for row in architecture_rows:422        stage = str(row.get("stage", "Stage"))423        tech = str(row.get("tech", ""))424        segments.append(f"[{stage}] {tech}")425    return "\n  -> ".join(segments)426 427 428def heuristic_strategy_note(inputs: dict[str, Any], selected_model: str) -> str:429    return (430        f"Design around a modular API-first stack for {inputs['target_users']} so the prototype can validate outcomes quickly. "431        f"Use {selected_model} as the primary model path while keeping routing abstraction in place for cost and quality tuning."432    )433 434 435def gemini_strategy(inputs: dict[str, Any], architecture_text: str) -> Optional[dict[str, Any]]:436    api_key = os.getenv("GEMINI_API_KEY")437    if not api_key:438        return None439    endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL}:generateContent"440    schema = {441        "strategy_note": "string",442        "architecture_diagram": "string",443        "execution_focus": ["string"],444    }445    prompt = f"""446You are a principal AI product architect.447Return only valid JSON.448 449Draft a concise but actionable strategy output for this prototype planning workflow.450 451Concept:452{inputs['concept_description']}453 454Primary capability: {inputs['primary_capability']}455Target users: {inputs['target_users']}456Budget: {inputs['budget']}457Timeline weeks: {inputs['timeline_weeks']}458Selected model: {inputs['selected_model']}459 460Current architecture draft:461{architecture_text}462 463Rules:464- strategy_note should be 2-4 sentences, practical and specific.465- architecture_diagram should preserve 5-stage order: Data Sources -> Processing -> Model Layer -> API Layer -> Frontend.466- execution_focus should include exactly 3 bullet-ready phrases.467 468Schema:469{json.dumps(schema)}470""".strip()471    payload = {472        "contents": [{"parts": [{"text": prompt}]}],473        "generationConfig": {"temperature": 0.2, "maxOutputTokens": 1500, "responseMimeType": "application/json"},474    }475    try:476        response = requests.post(f"{endpoint}?key={api_key}", json=payload, timeout=45)477        if response.status_code != 200:478            return None479        data = response.json()480        candidates = data.get("candidates", [])481        if not candidates:482            return None483        parts = candidates[0].get("content", {}).get("parts", [])484        text = "\n".join([str(part.get("text", "")) for part in parts if isinstance(part, dict)]).strip()485        parsed = parse_json_object(text)486        if not parsed:487            return None488        return parsed489    except Exception:490        return None491 492 493def assemble_final_plan(strategy_source: str) -> dict[str, Any]:494    concept = str(st.session_state.get("concept_description", "")).strip()495    capability = str(st.session_state.get("primary_capability", PRIMARY_CAPABILITIES[0]))496    target = str(st.session_state.get("target_users", TARGET_USERS[0]))497    budget = int(st.session_state.get("budget", 0))498    timeline = int(st.session_state.get("timeline_weeks", 0))499    selected_model_name = str(st.session_state.get("selected_model_name", ""))500    model_options = st.session_state.get("model_options", [])501    architecture_rows = st.session_state.get("architecture_rows", [])502    pipeline = st.session_state.get("data_pipeline", {})503    build_rows = st.session_state.get("build_plan_rows", [])504    cost_summary = st.session_state.get("cost_summary", {})505    selected_model = {}506    for model in model_options:507        if str(model.get("model", "")) == selected_model_name:508            selected_model = model509            break510    if not selected_model and model_options:511        selected_model = model_options[0]512    strategy_note = str(st.session_state.get("ai_strategy_note", "")).strip()513    architecture_diagram = str(st.session_state.get("ai_architecture_text", "")).strip()514    if not strategy_note:515        strategy_note = heuristic_strategy_note(516            {517                "concept_description": concept,518                "primary_capability": capability,519                "target_users": target,520                "budget": budget,521                "timeline_weeks": timeline,522            },523            str(selected_model.get("model", "Primary model")),524        )525    if not architecture_diagram:526        architecture_diagram = build_architecture_diagram_text(architecture_rows)527    return {528        "capability_description": CAPABILITY_DESCRIPTION,529        "concept": concept,530        "primary_capability": capability,531        "target_users": target,532        "budget": budget,533        "timeline_weeks": timeline,534        "strategy_note": strategy_note,535        "architecture_diagram": architecture_diagram,536        "architecture": architecture_rows,537        "selected_model": selected_model,538        "model_options": model_options,539        "data_pipeline": pipeline,540        "build_plan": build_rows,541        "cost_estimate": cost_summary,542        "source": strategy_source,543    }544 545 546def plan_tab_labels(unlocked_step: int) -> list[str]:547    labels = [548        "1. Define Your Concept",549        "2. Architecture & Model Selection",550        "3. Build Plan & Timeline",551        "4. Prototype Plan",552    ]553    output: list[str] = []554    for idx, label in enumerate(labels, start=1):555        if idx > unlocked_step:556            output.append(f"{label}  LOCKED")557        else:558            output.append(label)559    return output560 561 562def tab_lock_css(unlocked_step: int) -> str:563    if unlocked_step <= 1:564        lock_selector = "button:nth-child(n+2)"565    elif unlocked_step == 2:566        lock_selector = "button:nth-child(n+3)"567    elif unlocked_step == 3:568        lock_selector = "button:nth-child(4)"569    else:570        lock_selector = ""571    if not lock_selector:572        return ""573    return f"""574    .stTabs [data-baseweb=\"tab-list\"] {lock_selector} {{575        opacity: 0.42;576        pointer-events: none;577        filter: grayscale(1);578        border-color: rgba(117, 126, 145, 0.35) !important;579    }}580    """581 582 583def apply_theme(unlocked_step: int) -> None:584    st.markdown(585        f"""586        <style>587        @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=DM+Sans:wght@400;500;700&display=swap');588        :root {{589            --bg-0: #070b12;590            --bg-1: #0d131f;591            --bg-2: #121b2b;592            --line-soft: rgba(98, 123, 160, 0.42);593            --line-hard: rgba(112, 171, 233, 0.55);594            --text-main: #e5edf8;595            --text-soft: #9db1cb;596            --accent: #7bc8ff;597            --accent-warm: #f4bc69;598            --ok: #8ada91;599            --warn: #ff8f8f;600        }}601        .stApp {{602            background:603                radial-gradient(circle at 85% -5%, rgba(42, 77, 119, 0.32), transparent 34%),604                radial-gradient(circle at -10% 15%, rgba(151, 95, 31, 0.16), transparent 28%),605                linear-gradient(165deg, var(--bg-0), var(--bg-1) 58%, var(--bg-2));606            color: var(--text-main);607            font-family: "DM Sans", sans-serif;608        }}609        h1, h2, h3 {{610            font-family: "Cormorant Garamond", serif !important;611            letter-spacing: 0.02em;612        }}613        .hero-shell {{614            border: 1px solid var(--line-soft);615            background: linear-gradient(160deg, rgba(16, 27, 45, 0.94), rgba(8, 14, 24, 0.94));616            border-radius: 16px;617            padding: 1.0rem 1.2rem 1.2rem 1.2rem;618            box-shadow: 0 18px 42px rgba(3, 8, 20, 0.52);619            margin-bottom: 0.8rem;620            animation: riseIn 420ms ease-out;621        }}622        .hero-kicker {{623            text-transform: uppercase;624            letter-spacing: 0.15em;625            color: var(--accent);626            font-size: 0.72rem;627            opacity: 0.9;628        }}629        .hero-title {{630            color: #f3f8ff;631            font-size: 2.25rem;632            line-height: 1.0;633            margin: 0.25rem 0 0.55rem 0;634        }}635        .hero-copy {{636            color: var(--text-soft);637            font-size: 0.98rem;638            max-width: 74ch;639            line-height: 1.45;640        }}641        .chipline {{642            display: flex;643            flex-wrap: wrap;644            gap: 0.4rem;645            margin-top: 0.6rem;646        }}647        .chip {{648            border: 1px solid rgba(116, 176, 232, 0.42);649            border-radius: 999px;650            padding: 0.16rem 0.62rem;651            font-size: 0.73rem;652            color: #c3daf2;653            background: rgba(19, 31, 51, 0.78);654        }}655        .step-note {{656            border: 1px solid rgba(84, 102, 132, 0.45);657            border-left: 3px solid var(--accent);658            border-radius: 12px;659            background: rgba(15, 23, 36, 0.72);660            padding: 0.78rem 0.95rem;661            margin-bottom: 0.95rem;662            color: #b3c6de;663            line-height: 1.35;664        }}665        .locked-hint {{666            border: 1px dashed rgba(118, 132, 154, 0.45);667            background: rgba(13, 20, 31, 0.65);668            border-radius: 12px;669            padding: 0.95rem;670            color: #9db0c8;671        }}672        .arch-flow {{673            display: flex;674            gap: 0.55rem;675            overflow-x: auto;676            padding-bottom: 0.6rem;677        }}678        .arch-node {{679            min-width: 180px;680            max-width: 220px;681            border: 1px solid var(--line-soft);682            border-radius: 13px;683            background: linear-gradient(150deg, rgba(16, 26, 43, 0.94), rgba(9, 15, 24, 0.88));684            padding: 0.7rem;685        }}686        .arch-stage {{687            text-transform: uppercase;688            letter-spacing: 0.09em;689            color: var(--accent);690            font-size: 0.66rem;691            margin-bottom: 0.28rem;692        }}693        .arch-tech {{694            color: #dde8f6;695            font-size: 0.84rem;696            line-height: 1.32;697        }}698        .arch-arrow {{699            font-size: 1.3rem;700            color: #98aec8;701            margin-top: 2.0rem;702        }}703        .cost-box {{704            border: 1px solid var(--line-soft);705            border-radius: 12px;706            padding: 0.7rem 0.9rem;707            background: rgba(14, 22, 36, 0.76);708            margin-top: 0.4rem;709        }}710        .status-ok {{ color: var(--ok); font-weight: 700; }}711        .status-warn {{ color: var(--warn); font-weight: 700; }}712        .linkish button {{713            border: none !important;714            background: transparent !important;715            color: #82cdfd !important;716            text-decoration: underline;717            padding-left: 0 !important;718            padding-right: 0 !important;719            min-height: auto !important;720            height: auto !important;721            font-size: 0.85rem !important;722        }}723        .stTabs [data-baseweb="tab-list"] button {{724            background: rgba(14, 22, 34, 0.75);725            border: 1px solid rgba(102, 135, 177, 0.33);726            border-radius: 9px 9px 0 0;727            margin-right: 0.3rem;728            color: #cfdef2;729            transition: transform 180ms ease, opacity 180ms ease;730        }}731        .stTabs [data-baseweb="tab-list"] button[aria-selected="true"] {{732            border-color: var(--line-hard);733            color: #f0f6ff;734            transform: translateY(-1px);735            background: linear-gradient(180deg, rgba(20, 34, 55, 0.95), rgba(12, 21, 34, 0.95));736        }}737        {tab_lock_css(unlocked_step)}738        @keyframes riseIn {{739            0% {{ opacity: 0; transform: translateY(6px); }}740            100% {{ opacity: 1; transform: translateY(0); }}741        }}742        </style>743        """,744        unsafe_allow_html=True,745    )746 747 748def render_header() -> None:749    st.markdown(750        f"""751        <div class="hero-shell">752            <div class="hero-kicker">Applied AI Planning Studio</div>753            <div class="hero-title">End to End Applied AI Prototyping</div>754            <div class="hero-copy">{CAPABILITY_DESCRIPTION}</div>755            <div class="chipline">756                <span class="chip">4-Step Workflow</span>757                <span class="chip">Architecture + Model Planning</span>758                <span class="chip">Timeline + Cost Controls</span>759                <span class="chip">JSON Export</span>760            </div>761        </div>762        """,763        unsafe_allow_html=True,764    )765 766 767def render_architecture_preview() -> None:768    rows = st.session_state.get("architecture_rows", [])769    if not rows:770        return771    blocks: list[str] = []772    for idx, row in enumerate(rows[:5]):773        stage = str(row.get("stage", "Stage"))774        tech = str(row.get("tech", ""))775        blocks.append(f"<div class='arch-node'><div class='arch-stage'>{stage}</div><div class='arch-tech'>{tech}</div></div>")776        if idx < 4:777            blocks.append("<div class='arch-arrow'>→</div>")778    st.markdown(f"<div class='arch-flow'>{''.join(blocks)}</div>", unsafe_allow_html=True)779 780 781def render_locked_step(step_name: str, required_step: int) -> None:782    st.markdown(783        f"<div class='locked-hint'><strong>{step_name}</strong> unlocks after completing Step {required_step - 1}. Continue the workflow in order.</div>",784        unsafe_allow_html=True,785    )786 787 788def render_step_one(samples: list[dict[str, Any]]) -> None:789    st.markdown("<div class='step-note'><strong>Step 1</strong>: Define your concept, audience, budget envelope, and delivery timeline. Then unlock architecture design.</div>", unsafe_allow_html=True)790    link_col, _ = st.columns([1.2, 4.8])791    with link_col:792        st.markdown("<div class='linkish'>", unsafe_allow_html=True)793        if st.button("Try with sample", key="step1_try_sample"):794            apply_sample(samples)795            st.rerun()796        st.markdown("</div>", unsafe_allow_html=True)797 798    st.text_area(799        "Describe the AI product or experience you want to prototype",800        key="concept_description",801        height=180,802        placeholder="Start from zero: problem, expected behavior, user journey, constraints, and what success looks like.",803    )804 805    c1, c2 = st.columns(2)806    with c1:807        st.selectbox("Primary capability", PRIMARY_CAPABILITIES, key="primary_capability")808        st.selectbox("Target users", TARGET_USERS, key="target_users")809    with c2:810        st.slider("Budget constraint (USD)", min_value=5000, max_value=500000, step=5000, key="budget", format="$%d")811        st.slider("Timeline (weeks)", min_value=2, max_value=24, step=1, key="timeline_weeks")812 813    if st.button("Design Architecture →", type="primary", use_container_width=True, key="step1_next"):814        if not str(st.session_state.get("concept_description", "")).strip():815            st.warning("Add a concept description to continue.")816            return817        seed_step_two_state(reset_models=True)818        seed_step_three_state()819        st.session_state["unlocked_step"] = max(2, int(st.session_state.get("unlocked_step", 1)))820        st.session_state["final_plan"] = None821        st.session_state["final_plan_source"] = ""822        st.success("Step 2 unlocked: architecture and model selection ready.")823 824 825def render_step_two() -> None:826    unlocked = int(st.session_state.get("unlocked_step", 1))827    if unlocked < 2:828        render_locked_step("Step 2: Architecture & Model Selection", 2)829        return830    st.markdown("<div class='step-note'><strong>Step 2</strong>: Tune each architecture stage, compare model choices, and shape the data pipeline before planning build execution.</div>", unsafe_allow_html=True)831    ensure_architecture_widget_defaults()832    ensure_pipeline_widget_defaults()833 834    st.markdown("#### Proposed 5-stage architecture")835    render_architecture_preview()836 837    for idx, row in enumerate(st.session_state.get("architecture_rows", [])[:5]):838        stage = str(row.get("stage", ARCHITECTURE_STAGES[idx]))839        with st.container(border=True):840            st.markdown(f"**{stage}**")841            st.text_input("Tech choice", key=f"arch_tech_{idx}")842            st.text_area("Implementation focus", key=f"arch_details_{idx}", height=78)843    sync_architecture_from_widgets()844 845    st.markdown("#### Model comparison")846    options = st.session_state.get("model_options", [])847    if not isinstance(options, list):848        options = []849    if options:850        st.dataframe(pd.DataFrame(options), use_container_width=True, hide_index=True)851    else:852        st.warning("No model options available.")853 854    model_names = [str(item.get("model", "")) for item in options if str(item.get("model", "")).strip()]855    if model_names:856        current = str(st.session_state.get("selected_model_name", ""))857        if current not in model_names:858            st.session_state["selected_model_name"] = model_names[0]859        st.selectbox("Select primary model", model_names, key="selected_model_name")860 861    with st.expander("Add custom model entry"):862        m1, m2 = st.columns(2)863        with m1:864            custom_model = st.text_input("Model name", key="custom_model_name")865            custom_provider = st.text_input("Provider", key="custom_model_provider")866            custom_cost = st.text_input("Cost per 1k requests", key="custom_model_cost", placeholder="$12.00")867        with m2:868            custom_match = st.text_input("Capability match", key="custom_model_match")869            custom_latency = st.selectbox("Latency", ["Low", "Medium", "High"], key="custom_model_latency")870            custom_pros = st.text_input("Pros", key="custom_model_pros")871            custom_cons = st.text_input("Cons", key="custom_model_cons")872        if st.button("Add model", key="add_custom_model"):873            name = str(custom_model).strip()874            if not name:875                st.warning("Model name is required.")876            else:877                new_entry = {878                    "model": name,879                    "provider": str(custom_provider).strip() or "Custom",880                    "capability_match": str(custom_match).strip() or "Custom fit",881                    "cost_1k_requests": str(custom_cost).strip() or "$0.00",882                    "latency": str(custom_latency),883                    "pros": str(custom_pros).strip() or "Custom option",884                    "cons": str(custom_cons).strip() or "Needs validation",885                }886                updated_models = [dict(item) for item in options]887                updated_models.append(new_entry)888                st.session_state["model_options"] = updated_models889                st.session_state["selected_model_name"] = name890                st.success(f"Added model option: {name}")891                st.rerun()892 893    st.markdown("#### Data pipeline overview")894    st.text_input("Sources (comma separated)", key="pipeline_sources")895    p1, p2 = st.columns(2)896    with p1:897        st.text_area("Ingestion", key="pipeline_ingestion", height=88)898        st.text_area("Preprocessing", key="pipeline_preprocessing", height=88)899    with p2:900        st.text_area("Storage", key="pipeline_storage", height=88)901        st.text_area("Serving", key="pipeline_serving", height=88)902    sync_pipeline_from_widgets()903 904    if st.button("Plan Build →", type="primary", use_container_width=True, key="step2_next"):905        sync_architecture_from_widgets()906        sync_pipeline_from_widgets()907        if not st.session_state.get("build_plan_rows"):908            seed_step_three_state()909        st.session_state["unlocked_step"] = max(3, int(st.session_state.get("unlocked_step", 1)))910        st.success("Step 3 unlocked: build planning and budget controls ready.")911 912 913def move_week(direction: str) -> None:914    rows = [dict(item) for item in st.session_state.get("build_plan_rows", [])]915    if not rows:916        return917    pick = str(st.session_state.get("move_week_pick", "")).strip()918    idx = -1919    for i, row in enumerate(rows):920        if str(row.get("Week", "")).strip() == pick:921            idx = i922            break923    if idx == -1:924        return925    if direction == "up" and idx > 0:926        rows[idx - 1], rows[idx] = rows[idx], rows[idx - 1]927    if direction == "down" and idx < len(rows) - 1:928        rows[idx + 1], rows[idx] = rows[idx], rows[idx + 1]929    for i, row in enumerate(rows, start=1):930        row["Week"] = str(i)931    st.session_state["build_plan_rows"] = rows932 933 934def add_week_row() -> None:935    rows = [dict(item) for item in st.session_state.get("build_plan_rows", [])]936    next_week = len(rows) + 1937    team = template_library().get(st.session_state.get("primary_capability", PRIMARY_CAPABILITIES[0]), template_library()["Text Generation"]).get("team", "Cross-functional team")938    rows.append(939        {940            "Week": str(next_week),941            "Milestone": f"Phase {next_week}",942            "Deliverables": "Define delivery output",943            "Team Allocation": str(team),944            "Risk Control": risk_note(str(st.session_state.get("target_users", TARGET_USERS[0]))),945        }946    )947    st.session_state["build_plan_rows"] = rows948    st.session_state["timeline_weeks"] = len(rows)949 950 951def remove_week_row() -> None:952    rows = [dict(item) for item in st.session_state.get("build_plan_rows", [])]953    if len(rows) <= 1:954        return955    rows = rows[:-1]956    for i, row in enumerate(rows, start=1):957        row["Week"] = str(i)958    st.session_state["build_plan_rows"] = rows959    st.session_state["timeline_weeks"] = len(rows)960 961 962def render_step_three() -> None:963    unlocked = int(st.session_state.get("unlocked_step", 1))964    if unlocked < 3:965        render_locked_step("Step 3: Build Plan & Timeline", 3)966        return967    st.markdown("<div class='step-note'><strong>Step 3</strong>: Edit weekly milestones, reorder phases, refine team allocations, and tune cost assumptions with live budget fit.</div>", unsafe_allow_html=True)968 969    rows = st.session_state.get("build_plan_rows", [])970    if not rows:971        seed_step_three_state()972        rows = st.session_state.get("build_plan_rows", [])973 974    top1, top2, top3, top4 = st.columns([1.2, 1.2, 1.6, 2.0])975    with top1:976        if st.button("Add Week", key="add_week_btn"):977            add_week_row()978            st.rerun()979    with top2:980        if st.button("Remove Week", key="remove_week_btn"):981            remove_week_row()982            st.rerun()983    with top3:984        week_options = [str(item.get("Week", "")) for item in st.session_state.get("build_plan_rows", [])]985        if week_options:986            st.selectbox("Move week", week_options, key="move_week_pick")987    with top4:988        b1, b2 = st.columns(2)989        with b1:990            if st.button("Move Up", key="move_up_btn"):991                move_week("up")992                st.rerun()993        with b2:994            if st.button("Move Down", key="move_down_btn"):995                move_week("down")996                st.rerun()997 998    edited = st.data_editor(999        pd.DataFrame(st.session_state.get("build_plan_rows", [])),1000        key="build_plan_editor",1001        use_container_width=True,1002        hide_index=True,1003        num_rows="dynamic",1004        column_config={1005            "Week": st.column_config.TextColumn(width="small"),1006            "Milestone": st.column_config.TextColumn(width="medium"),1007            "Deliverables": st.column_config.TextColumn(width="large"),1008            "Team Allocation": st.column_config.TextColumn(width="large"),1009            "Risk Control": st.column_config.TextColumn(width="large"),1010        },1011    )1012    edited_rows = edited.fillna("").to_dict("records")1013    normalized: list[dict[str, str]] = []1014    for i, row in enumerate(edited_rows, start=1):1015        normalized.append(1016            {1017                "Week": str(i),1018                "Milestone": str(row.get("Milestone", "")).strip() or f"Phase {i}",1019                "Deliverables": str(row.get("Deliverables", "")).strip() or "Define delivery output",1020                "Team Allocation": str(row.get("Team Allocation", "")).strip() or "Cross-functional team",1021                "Risk Control": str(row.get("Risk Control", "")).strip() or risk_note(str(st.session_state.get("target_users", TARGET_USERS[0]))),1022            }1023        )1024    st.session_state["build_plan_rows"] = normalized1025    st.session_state["timeline_weeks"] = len(normalized)1026 1027    st.markdown("#### Cost breakdown")1028    cost_inputs = st.session_state.get("cost_inputs", {})1029    c1, c2, c3, c4, c5 = st.columns(5)1030    with c1:1031        compute = st.number_input("Compute", min_value=0, step=500, value=int(cost_inputs.get("compute", 0)), key="cost_compute")1032    with c2:1033        storage = st.number_input("Storage", min_value=0, step=500, value=int(cost_inputs.get("storage", 0)), key="cost_storage")1034    with c3:1035        model_api = st.number_input("Model API", min_value=0, step=500, value=int(cost_inputs.get("model_api", 0)), key="cost_model_api")1036    with c4:1037        development = st.number_input("Development", min_value=0, step=500, value=int(cost_inputs.get("development", 0)), key="cost_development")1038    with c5:1039        security = st.number_input("Security", min_value=0, step=500, value=int(cost_inputs.get("security", 0)), key="cost_security")1040 1041    st.session_state["cost_inputs"] = {1042        "compute": int(compute),1043        "storage": int(storage),1044        "model_api": int(model_api),1045        "development": int(development),1046        "security": int(security),1047    }1048    recalculate_cost_summary()1049 1050    summary = st.session_state.get("cost_summary", {})1051    fit = str(summary.get("fit", "Unknown"))1052    status_class = "status-ok" if fit == "Within target" else "status-warn"1053    st.markdown(1054        f"""1055        <div class='cost-box'>1056            <div><strong>Subtotal:</strong> ${int(summary.get('subtotal', 0)):,}</div>1057            <div><strong>Contingency (15%):</strong> ${int(summary.get('contingency', 0)):,}</div>1058            <div><strong>Running Total:</strong> ${int(summary.get('total', 0)):,}</div>1059            <div><strong>Budget Target:</strong> ${int(summary.get('budget', 0)):,}</div>1060            <div><strong>Status:</strong> <span class='{status_class}'>{fit}</span></div>1061        </div>1062        """,1063        unsafe_allow_html=True,1064    )1065 1066    if st.button("Generate Full Plan →", type="primary", use_container_width=True, key="step3_next"):1067        allowed, retry = check_rate_limit()1068        if not allowed:1069            st.error(f"Rate limit reached. Try again in {retry}s ({RATE_LIMIT_REQUESTS}/{RATE_LIMIT_WINDOW_SECONDS}s).")1070            return1071        sync_architecture_from_widgets()1072        sync_pipeline_from_widgets()1073        architecture_text = build_architecture_diagram_text(st.session_state.get("architecture_rows", []))1074        selected_model_name = str(st.session_state.get("selected_model_name", ""))1075        ai_input = {1076            "concept_description": str(st.session_state.get("concept_description", "")).strip(),1077            "primary_capability": str(st.session_state.get("primary_capability", PRIMARY_CAPABILITIES[0])),1078            "target_users": str(st.session_state.get("target_users", TARGET_USERS[0])),1079            "budget": int(st.session_state.get("budget", 0)),1080            "timeline_weeks": int(st.session_state.get("timeline_weeks", 0)),1081            "selected_model": selected_model_name,1082        }1083        with st.spinner("Synthesizing final strategy and validating full plan..."):1084            ai_result = gemini_strategy(ai_input, architecture_text)1085        if ai_result:1086            st.session_state["ai_strategy_note"] = str(ai_result.get("strategy_note", "")).strip()1087            ai_arch = str(ai_result.get("architecture_diagram", "")).strip()1088            st.session_state["ai_architecture_text"] = ai_arch if ai_arch else architecture_text1089            source = f"Gemini ({GEMINI_MODEL})"1090        else:1091            st.session_state["ai_strategy_note"] = ""1092            st.session_state["ai_architecture_text"] = architecture_text1093            source = "Heuristic fallback"1094        st.session_state["final_plan"] = assemble_final_plan(source)1095        st.session_state["final_plan_source"] = source1096        st.session_state["unlocked_step"] = max(4, int(st.session_state.get("unlocked_step", 1)))1097        st.success("Step 4 unlocked: final prototype plan generated.")1098 1099 1100def reset_workflow_state() -> None:1101    keep_timestamps = list(st.session_state.get("request_timestamps", []))1102    for key in list(st.session_state.keys()):1103        del st.session_state[key]1104    init_state()1105    st.session_state["request_timestamps"] = keep_timestamps1106    clear_dynamic_widget_keys()1107 1108 1109def render_step_four() -> None:1110    unlocked = int(st.session_state.get("unlocked_step", 1))1111    if unlocked < 4:1112        render_locked_step("Step 4: Prototype Plan", 4)1113        return1114    st.markdown("<div class='step-note'><strong>Step 4</strong>: Consolidated strategy, architecture, selected model, pipeline, timeline, and budget estimates with export-ready JSON.</div>", unsafe_allow_html=True)1115 1116    plan = st.session_state.get("final_plan")1117    if not isinstance(plan, dict):1118        plan = assemble_final_plan("Heuristic fallback")1119        st.session_state["final_plan"] = plan1120 1121    source = str(plan.get("source", st.session_state.get("final_plan_source", "Heuristic fallback")))1122    if source.startswith("Gemini"):1123        st.success(f"Plan source: {source}")1124    else:1125        st.info(f"Plan source: {source}")1126 1127    st.markdown("#### Strategy note")1128    st.write(str(plan.get("strategy_note", "")))1129 1130    st.markdown("#### Architecture diagram")1131    st.code(str(plan.get("architecture_diagram", "")), language="text")1132 1133    st.markdown("#### Model choices")1134    selected = plan.get("selected_model", {})1135    selected_name = str(selected.get("model", ""))1136    if selected_name:1137        st.markdown(f"**Selected model:** `{selected_name}`")1138    model_options = plan.get("model_options", [])1139    if isinstance(model_options, list) and model_options:1140        st.dataframe(pd.DataFrame(model_options), use_container_width=True, hide_index=True)1141 1142    st.markdown("#### Data pipeline")1143    pipeline = plan.get("data_pipeline", {}) if isinstance(plan.get("data_pipeline", {}), dict) else {}1144    p_sources = pipeline.get("sources", [])1145    if isinstance(p_sources, list) and p_sources:1146        st.markdown(", ".join([f"`{str(item)}`" for item in p_sources]))1147    st.write(f"Ingestion: {pipeline.get('ingestion', '-')}")1148    st.write(f"Preprocessing: {pipeline.get('preprocessing', '-')}")1149    st.write(f"Storage: {pipeline.get('storage', '-')}")1150    st.write(f"Serving: {pipeline.get('serving', '-')}")1151 1152    st.markdown("#### Build timeline")1153    build_plan = plan.get("build_plan", [])1154    if isinstance(build_plan, list) and build_plan:1155        st.dataframe(pd.DataFrame(build_plan), use_container_width=True, hide_index=True)1156 1157    st.markdown("#### Cost estimate")1158    cost = plan.get("cost_estimate", {}) if isinstance(plan.get("cost_estimate", {}), dict) else {}1159    c1, c2, c3 = st.columns(3)1160    with c1:1161        st.metric("Target budget", f"${int(cost.get('budget', 0)):,}")1162    with c2:1163        st.metric("Estimated total", f"${int(cost.get('total', 0)):,}")1164    with c3:1165        st.metric("Budget fit", str(cost.get("fit", "Unknown")))1166    line_items = cost.get("line_items", [])1167    if isinstance(line_items, list) and line_items:1168        st.dataframe(pd.DataFrame(line_items), use_container_width=True, hide_index=True)1169 1170    payload = json.dumps(plan, indent=2)1171    d1, d2 = st.columns(2)1172    with d1:1173        st.download_button(1174            "Download as JSON",1175            data=payload,1176            file_name="prototype_plan.json",1177            mime="application/json",1178            use_container_width=True,1179        )1180    with d2:1181        if st.button("Plan New Prototype", use_container_width=True, key="plan_new_prototype"):1182            reset_workflow_state()1183            st.rerun()1184 1185 1186def render_dataset_expander(samples: list[dict[str, Any]]) -> None:1187    st.divider()1188    with st.expander("Sample concept dataset"):1189        if samples:1190            st.dataframe(pd.DataFrame(samples), use_container_width=True, hide_index=True)1191        else:1192            st.write("No records found in data/sample_concepts.jsonl")1193 1194 1195def main() -> None:1196    st.set_page_config(page_title=APP_TITLE, page_icon="🧪", layout="wide")1197    init_state()1198    samples = load_sample_concepts()1199    apply_theme(int(st.session_state.get("unlocked_step", 1)))1200    render_header()

Showing the first 1,200 of 1217 lines. Download the file for the rest.