arcalab/prototyping
0
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()