Spaces:
Sleeping
Sleeping
| """ | |
| VentureForge Gradio UI for HuggingFace Spaces | |
| ================================================ | |
| Web interface for the autonomous multi-agent startup discovery system. | |
| """ | |
| import json | |
| import time | |
| from pathlib import Path | |
| from typing import Literal | |
| import gradio as gr | |
| from src.config import settings | |
| from src.main import make_initial_state | |
| from src.run_controller import ( | |
| is_cancel_requested, | |
| is_run_active, | |
| poll_state, | |
| request_cancel, | |
| start_run, | |
| ) | |
| from src.state.schema import PipelineStage | |
| # ============================================================================= | |
| # DOMAIN RECOMMENDATIONS | |
| # ============================================================================= | |
| DOMAIN_RECOMMENDATIONS = [ | |
| "developer tools", | |
| "healthcare", | |
| "finance & fintech", | |
| "education", | |
| "food service", | |
| "e-commerce", | |
| "marketing & social media", | |
| "real estate", | |
| "transportation", | |
| "AI & machine learning", | |
| "productivity tools", | |
| "fashion & retail", | |
| "sports & fitness", | |
| "agriculture", | |
| "content creation", | |
| ] | |
| # Track previous stage for notifications | |
| _previous_stage = {} | |
| # ============================================================================= | |
| # DOWNLOAD UTILITIES | |
| # ============================================================================= | |
| def download_pain_points(state) -> str: | |
| """Generate downloadable markdown file for pain points.""" | |
| if not state or not state.pain_points: | |
| return None | |
| content = f"# Pain Points - {state.domain}\n\n" | |
| content += f"**Run ID:** {state.run_id}\n" | |
| content += f"**Total Pain Points:** {len(state.pain_points)}\n" | |
| content += f"**Passed Rubric:** {len(state.filtered_pain_points)}\n\n" | |
| content += "---\n\n" | |
| content += format_pain_points(state) | |
| # Save to temp file | |
| output_path = Path(settings.cache_dir) / f"pain_points_{state.run_id}.md" | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| output_path.write_text(content, encoding="utf-8") | |
| return str(output_path) | |
| def download_ideas(state) -> str: | |
| """Generate downloadable markdown file for ideas.""" | |
| if not state or not state.ideas: | |
| return None | |
| content = f"# Startup Ideas - {state.domain}\n\n" | |
| content += f"**Run ID:** {state.run_id}\n" | |
| content += f"**Total Ideas:** {len(state.ideas)}\n\n" | |
| content += "---\n\n" | |
| content += format_ideas(state) | |
| output_path = Path(settings.cache_dir) / f"ideas_{state.run_id}.md" | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| output_path.write_text(content, encoding="utf-8") | |
| return str(output_path) | |
| def download_scored_ideas(state) -> str: | |
| """Generate downloadable markdown file for scored ideas.""" | |
| if not state or not state.scored_ideas: | |
| return None | |
| content = f"# Scored Ideas - {state.domain}\n\n" | |
| content += f"**Run ID:** {state.run_id}\n" | |
| content += f"**Total Scored Ideas:** {len(state.scored_ideas)}\n\n" | |
| content += "---\n\n" | |
| content += format_scored_ideas(state) | |
| output_path = Path(settings.cache_dir) / f"scored_ideas_{state.run_id}.md" | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| output_path.write_text(content, encoding="utf-8") | |
| return str(output_path) | |
| def download_pitches(state) -> str: | |
| """Generate downloadable markdown file for pitch briefs.""" | |
| if not state or not state.pitch_briefs: | |
| return None | |
| content = f"# Pitch Briefs - {state.domain}\n\n" | |
| content += f"**Run ID:** {state.run_id}\n" | |
| content += f"**Total Pitches:** {len(state.pitch_briefs)}\n\n" | |
| content += "---\n\n" | |
| content += format_pitches(state) | |
| output_path = Path(settings.cache_dir) / f"pitches_{state.run_id}.md" | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| output_path.write_text(content, encoding="utf-8") | |
| return str(output_path) | |
| # ============================================================================= | |
| # FORMATTING UTILITIES | |
| # ============================================================================= | |
| def format_state_summary(state) -> str: | |
| """Format a summary of the current pipeline state with visual progress.""" | |
| if state is None: | |
| return "No active run. Click **Start Discovery** to begin." | |
| status_emoji = { | |
| PipelineStage.IDLE: "π€", | |
| PipelineStage.MINING: "βοΈ", | |
| PipelineStage.GENERATING: "π‘", | |
| PipelineStage.SCORING: "π", | |
| PipelineStage.WRITING: "βοΈ", | |
| PipelineStage.CRITIQUING: "π", | |
| PipelineStage.REVISING: "π", | |
| PipelineStage.COMPLETED: "β ", | |
| PipelineStage.FAILED: "β", | |
| PipelineStage.CANCELLED: "π", | |
| } | |
| emoji = status_emoji.get(state.current_stage, "β") | |
| # Build visual pipeline progress | |
| pipeline_stages = [ | |
| ("βοΈ Mining", PipelineStage.MINING, len(state.pain_points) > 0), | |
| ("π‘ Generating", PipelineStage.GENERATING, len(state.ideas) > 0), | |
| ("π Scoring", PipelineStage.SCORING, len(state.scored_ideas) > 0), | |
| ("βοΈ Writing", PipelineStage.WRITING, len(state.pitch_briefs) > 0), | |
| ("π Critiquing", PipelineStage.CRITIQUING, len(state.critiques) > 0), | |
| ] | |
| progress_bar = [] | |
| completed_count = 0 | |
| for label, stage, completed in pipeline_stages: | |
| if state.current_stage == stage: | |
| progress_bar.append(f"**π {label}**") # Current stage | |
| elif completed: | |
| progress_bar.append(f"β {label}") # Completed | |
| completed_count += 1 | |
| else: | |
| progress_bar.append(f"β¬ {label}") # Not started | |
| progress_line = " β ".join(progress_bar) | |
| # Calculate progress percentage | |
| total_stages = len(pipeline_stages) | |
| progress_pct = int((completed_count / total_stages) * 100) | |
| # Format agent timings | |
| timing_lines = [] | |
| if state.agent_timings: | |
| for agent, duration in state.agent_timings.items(): | |
| timing_lines.append(f" - {agent}: {duration:.1f}s") | |
| timing_str = "\n".join(timing_lines) if timing_lines else " No timings yet" | |
| # Recent events (last 3) | |
| event_lines = [] | |
| if state.events: | |
| for event in state.events[-3:]: | |
| event_emoji = {"info": "βΉοΈ", "warning": "β οΈ", "error": "β"}.get(event.kind, "π") | |
| event_lines.append(f" {event_emoji} **{event.agent}**: {event.message}") | |
| event_str = "\n".join(event_lines) if event_lines else " No events yet" | |
| return ( | |
| f"## {emoji} Current Stage: {state.current_stage.value.upper()}\n\n" | |
| f"### Pipeline Progress ({progress_pct}% complete)\n" | |
| f"{progress_line}\n\n" | |
| f"---\n\n" | |
| f"**π Run ID:** `{state.run_id}`\n" | |
| f"**π Domain:** {state.domain}\n\n" | |
| f"### Metrics\n" | |
| f"- π« Pain Points: **{len(state.pain_points)}** (passed rubric: {len(state.filtered_pain_points)})\n" | |
| f"- π‘ Ideas: **{len(state.ideas)}**\n" | |
| f"- π Scored Ideas: **{len(state.scored_ideas)}**\n" | |
| f"- βοΈ Pitch Briefs: **{len(state.pitch_briefs)}**\n" | |
| f"- π Critiques: **{len(state.critiques)}**\n\n" | |
| f"### Agent Timings\n" | |
| f"{timing_str}\n\n" | |
| f"### Recent Events\n" | |
| f"{event_str}\n" | |
| ) | |
| def format_pain_points(state) -> str: | |
| """Format pain points for display.""" | |
| if not state or not state.pain_points: | |
| return "No pain points yet" | |
| output = [] | |
| for i, pp in enumerate(state.pain_points, 1): | |
| evidence_count = len(pp.evidence) if hasattr(pp, 'evidence') else 1 | |
| output.append( | |
| f"### {i}. {pp.title} ({evidence_count} source{'s' if evidence_count > 1 else ''})\n" | |
| f"**Description:** {pp.description}\n" | |
| f"**Passes Rubric:** {pp.passes_rubric}\n\n" | |
| ) | |
| # Show all evidence sources | |
| if hasattr(pp, 'evidence') and pp.evidence: | |
| output.append("**Evidence:**\n") | |
| for j, ev in enumerate(pp.evidence, 1): | |
| output.append( | |
| f"{j}. [{ev.source.value}]({ev.source_url})\n" | |
| f" > \"{ev.raw_quote}\"\n\n" | |
| ) | |
| else: | |
| # Backward compatibility | |
| output.append( | |
| f"**Source:** {pp.source.value} | [Link]({pp.source_url})\n" | |
| f"**Quote:** \"{pp.raw_quote}\"\n\n" | |
| ) | |
| return "".join(output) | |
| def format_ideas(state) -> str: | |
| """Format ideas for display.""" | |
| if not state or not state.ideas: | |
| return "No ideas yet" | |
| output = [] | |
| for i, idea in enumerate(state.ideas, 1): | |
| output.append( | |
| f"### {i}. {idea.title}\n" | |
| f"**One-liner:** {idea.one_liner}\n" | |
| f"**Target User:** {idea.target_user}\n" | |
| f"**Problem:** {idea.problem}\n" | |
| f"**Solution:** {idea.solution}\n" | |
| f"**Key Features:**\n" | |
| ) | |
| for feature in idea.key_features: | |
| output.append(f"- {feature}\n") | |
| output.append("\n") | |
| return "".join(output) | |
| def format_scored_ideas(state) -> str: | |
| """Format scored ideas with rubrics.""" | |
| if not state or not state.scored_ideas: | |
| return "No scored ideas yet" | |
| output = [] | |
| for i, scored in enumerate(state.scored_ideas, 1): | |
| idea = next((idea for idea in state.ideas if idea.id == scored.idea_id), None) | |
| idea_title = idea.title if idea else "Unknown" | |
| output.append( | |
| f"### {i}. {idea_title}\n" | |
| f"**Verdict:** {scored.verdict.upper()} | **Yes Count:** {scored.yes_count}/8\n" | |
| f"**Core Assumption:** {scored.core_assumption}\n" | |
| f"**One Risk:** {scored.one_risk}\n\n" | |
| ) | |
| if scored.fatal_flaws: | |
| output.append("**Fatal Flaws:**\n") | |
| for flaw in scored.fatal_flaws: | |
| output.append(f"- {flaw.severity.upper()}: {flaw.flaw}\n") | |
| output.append("\n") | |
| output.append( | |
| "**Feasibility:**\n" | |
| f"- Manual-first: {scored.feasibility_rubric.can_be_solved_manually_first}\n" | |
| f"- Schlep advantage: {scored.feasibility_rubric.has_schlep_or_unsexy_advantage}\n" | |
| f"- 2-3 person MVP in 6mo: {scored.feasibility_rubric.can_2_3_person_team_build_mvp_in_6_months}\n\n" | |
| ) | |
| output.append( | |
| "**Demand:**\n" | |
| f"- Addresses 2+ pain points: {scored.demand_rubric.addresses_at_least_2_pain_points}\n" | |
| f"- Painkiller not vitamin: {scored.demand_rubric.is_painkiller_not_vitamin}\n" | |
| f"- Clear early adopters: {scored.demand_rubric.has_clear_vein_of_early_adopters}\n\n" | |
| ) | |
| output.append( | |
| "**Novelty:**\n" | |
| f"- Differentiated: {scored.novelty_rubric.differentiated_from_current_behavior}\n" | |
| f"- Path out of niche: {scored.novelty_rubric.has_path_out_of_niche}\n\n" | |
| ) | |
| output.append("---\n\n") | |
| return "".join(output) | |
| def format_pitches(state) -> str: | |
| """Format pitch briefs for display.""" | |
| if not state or not state.pitch_briefs: | |
| return "No pitches yet" | |
| output = [] | |
| for i, pitch in enumerate(state.pitch_briefs, 1): | |
| output.append( | |
| f"### {i}. {pitch.title}\n" | |
| f"**Tagline:** {pitch.tagline}\n" | |
| f"**Target User:** {pitch.target_user}\n" | |
| f"**Revision Count:** {pitch.revision_count}\n\n" | |
| ) | |
| if pitch.evidence_links: | |
| output.append("**Evidence Links:**\n") | |
| for link in pitch.evidence_links: | |
| output.append(f"- [{link}]({link})\n") | |
| output.append("\n") | |
| output.append(f"## Full Pitch\n\n{pitch.markdown_content}\n\n---\n\n") | |
| return "".join(output) | |
| def format_critiques(state) -> str: | |
| """Format critiques for display.""" | |
| if not state or not state.critiques: | |
| return "No critiques yet" | |
| output = [] | |
| for i, critique in enumerate(state.critiques, 1): | |
| idea = next((idea for idea in state.ideas if idea.id == critique.idea_id), None) | |
| idea_title = idea.title if idea else "Unknown" | |
| status_emoji = "β " if critique.approval_status == "approved" else "π" | |
| output.append( | |
| f"### {i}. {idea_title}\n" | |
| f"{status_emoji} **Status:** {critique.approval_status.upper()}\n" | |
| f"**Target Agent:** {critique.target_agent}\n" | |
| f"**All Checks Pass:** {critique.all_pass}\n\n" | |
| ) | |
| if critique.failing_checks: | |
| output.append("**Failing Checks:**\n") | |
| for check in critique.failing_checks: | |
| output.append(f"- {check}\n") | |
| output.append("\n") | |
| output.append(f"**Feedback:** {critique.revision_feedback}\n\n---\n\n") | |
| return "".join(output) | |
| def export_json(state) -> str: | |
| """Export state as JSON.""" | |
| if state is None: | |
| return "" | |
| return json.dumps(state.model_dump(mode="json", exclude_none=True), indent=2) | |
| def export_markdown(state) -> str: | |
| """Export state as Markdown report.""" | |
| if state is None: | |
| return "" | |
| md = f"# VentureForge Report\n\n" | |
| md += f"**Domain:** {state.domain}\n" | |
| md += f"**Run ID:** {state.run_id}\n" | |
| md += f"**Final Stage:** {state.current_stage}\n\n" | |
| md += "## Summary\n\n" | |
| md += f"- Pain Points: {len(state.pain_points)}\n" | |
| md += f"- Ideas: {len(state.ideas)}\n" | |
| md += f"- Scored Ideas: {len(state.scored_ideas)}\n" | |
| md += f"- Pitches: {len(state.pitch_briefs)}\n" | |
| md += f"- Critiques: {len(state.critiques)}\n\n" | |
| if state.pain_points: | |
| md += "## Pain Points\n\n" | |
| md += format_pain_points(state) | |
| if state.ideas: | |
| md += "## Ideas\n\n" | |
| md += format_ideas(state) | |
| if state.scored_ideas: | |
| md += "## Scored Ideas\n\n" | |
| md += format_scored_ideas(state) | |
| if state.pitch_briefs: | |
| md += "## Pitch Briefs\n\n" | |
| md += format_pitches(state) | |
| if state.critiques: | |
| md += "## Critiques\n\n" | |
| md += format_critiques(state) | |
| return md | |
| # ============================================================================= | |
| # PIPELINE CONTROL | |
| # ============================================================================= | |
| def start_pipeline( | |
| domain: str, | |
| max_pain_points: int, | |
| ideas_per_run: int, | |
| top_n_pitches: int, | |
| max_revisions: int, | |
| ) -> tuple[str, str, str, str, str, str, str, str]: | |
| """Start the pipeline and return initial UI state.""" | |
| try: | |
| run_id = start_run( | |
| domain=domain, | |
| max_pain_points=max_pain_points, | |
| ideas_per_run=ideas_per_run, | |
| ) | |
| # Poll for initial state | |
| for _ in range(10): # Wait up to 5 seconds for initial state | |
| time.sleep(0.5) | |
| state = poll_state(run_id) | |
| if state is not None: | |
| break | |
| if state is None: | |
| return ( | |
| run_id, | |
| "π Pipeline starting...", | |
| "Initializing...", | |
| "Initializing...", | |
| "Initializing...", | |
| "Initializing...", | |
| "Initializing...", | |
| f"β Pipeline started successfully! Run ID: {run_id}", | |
| ) | |
| return ( | |
| run_id, | |
| format_state_summary(state), | |
| format_pain_points(state), | |
| format_ideas(state), | |
| format_scored_ideas(state), | |
| format_pitches(state), | |
| format_critiques(state), | |
| f"β Pipeline started successfully! Run ID: {run_id}", | |
| ) | |
| except Exception as e: | |
| error_msg = f"β Error starting pipeline: {str(e)}" | |
| return ( | |
| "", | |
| error_msg, | |
| error_msg, | |
| error_msg, | |
| error_msg, | |
| error_msg, | |
| error_msg, | |
| error_msg, | |
| ) | |
| def update_progress(run_id: str) -> tuple[str, str, str, str, str, str]: | |
| """Poll for state updates and return new UI state with notifications.""" | |
| global _previous_stage | |
| if not run_id or not is_run_active(): | |
| return gr.skip() | |
| state = poll_state(run_id) | |
| if state is None: | |
| return gr.skip() | |
| # Check for stage changes and send notifications | |
| prev_stage = _previous_stage.get(run_id) | |
| current_stage = state.current_stage | |
| if prev_stage != current_stage: | |
| _previous_stage[run_id] = current_stage | |
| # Send notifications for important stage transitions | |
| if current_stage == PipelineStage.COMPLETED: | |
| gr.Info(f"β Pipeline completed! Generated {len(state.pitch_briefs)} pitch brief(s).") | |
| elif current_stage == PipelineStage.FAILED: | |
| gr.Warning(f"β Pipeline failed. Check the error log for details.") | |
| elif current_stage == PipelineStage.CANCELLED: | |
| gr.Info("π Pipeline cancelled by user.") | |
| elif current_stage == PipelineStage.GENERATING and len(state.pain_points) > 0: | |
| gr.Info(f"βοΈ Mining complete! Found {len(state.pain_points)} pain points.") | |
| elif current_stage == PipelineStage.SCORING and len(state.ideas) > 0: | |
| gr.Info(f"π‘ Generated {len(state.ideas)} startup ideas.") | |
| elif current_stage == PipelineStage.WRITING and len(state.scored_ideas) > 0: | |
| gr.Info(f"π Scored {len(state.scored_ideas)} ideas.") | |
| return ( | |
| format_state_summary(state), | |
| format_pain_points(state), | |
| format_ideas(state), | |
| format_scored_ideas(state), | |
| format_pitches(state), | |
| format_critiques(state), | |
| ) | |
| def stop_pipeline(run_id: str) -> str: | |
| """Request pipeline cancellation.""" | |
| if run_id: | |
| request_cancel(run_id) | |
| return "βΉοΈ Stop requested. The pipeline will complete the current step and stop gracefully." | |
| return "βΉοΈ No active run to stop." | |
| def clear_cache() -> str: | |
| """Clear the LangGraph checkpoint cache and scraper caches.""" | |
| import shutil | |
| from pathlib import Path | |
| # Check if any runs are active | |
| if is_run_active(): | |
| return "β Cannot clear cache while a run is active. Please stop the run first." | |
| cache_dir = Path(settings.cache_dir) | |
| if cache_dir.exists(): | |
| try: | |
| # Remove all cache contents | |
| shutil.rmtree(cache_dir) | |
| # Recreate empty cache directory | |
| cache_dir.mkdir(parents=True, exist_ok=True) | |
| return "β Cache cleared successfully! All previous run data has been deleted." | |
| except PermissionError as e: | |
| return f"β Cache is locked (database in use). Stop the app and clear manually: rm -rf {cache_dir}" | |
| except Exception as e: | |
| return f"β Error clearing cache: {str(e)}" | |
| return "βΉοΈ Cache directory does not exist." | |
| # ============================================================================= | |
| # UI CONSTRUCTION | |
| # ============================================================================= | |
| def create_ui() -> gr.Blocks: | |
| """Create and return the Gradio UI.""" | |
| with gr.Blocks(title="VentureForge - AI Startup Discovery") as app: | |
| gr.Markdown( | |
| """ | |
| # π VentureForge | |
| ### AI-Powered Startup Discovery System | |
| Enter a domain to discover startup ideas using autonomous multi-agent analysis. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| domain_recommendation = gr.Dropdown( | |
| choices=DOMAIN_RECOMMENDATIONS, | |
| label="Domain Recommendations", | |
| info="Select a recommended domain or type your own below", | |
| ) | |
| domain_input = gr.Textbox( | |
| label="Domain", | |
| placeholder="e.g., developer tools, healthcare, fintech", | |
| value="developer tools", | |
| ) | |
| with gr.Column(scale=1): | |
| start_btn = gr.Button("π Start Discovery", variant="primary", size="lg") | |
| stop_btn = gr.Button("βΉοΈ Stop", variant="stop") | |
| clear_cache_btn = gr.Button("ποΈ Clear Cache", variant="secondary") | |
| with gr.Accordion("βοΈ Advanced Settings", open=False): | |
| with gr.Row(): | |
| max_pain_points = gr.Slider( | |
| minimum=5, | |
| maximum=10, | |
| value=5, | |
| step=1, | |
| label="Max Pain Points", | |
| ) | |
| ideas_per_run = gr.Slider( | |
| minimum=1, | |
| maximum=5, | |
| value=2, | |
| step=1, | |
| label="Ideas per Run", | |
| ) | |
| top_n_pitches = gr.Slider( | |
| minimum=1, | |
| maximum=5, | |
| value=3, | |
| step=1, | |
| label="Top N Pitches", | |
| ) | |
| max_revisions = gr.Slider( | |
| minimum=0, | |
| maximum=5, | |
| value=settings.max_revisions, | |
| step=1, | |
| label="Max Revisions", | |
| ) | |
| run_id_display = gr.Textbox(label="Run ID", interactive=False, visible=True) | |
| with gr.Tabs() as tabs: | |
| with gr.TabItem("π Progress"): | |
| progress_display = gr.Markdown("No active run") | |
| with gr.TabItem("π« Pain Points"): | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| pain_points_display = gr.Markdown("No pain points yet") | |
| with gr.Column(scale=1): | |
| download_pain_points_btn = gr.DownloadButton( | |
| "π₯ Download", | |
| variant="secondary", | |
| size="sm", | |
| ) | |
| with gr.TabItem("π‘ Ideas"): | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| ideas_display = gr.Markdown("No ideas yet") | |
| with gr.Column(scale=1): | |
| download_ideas_btn = gr.DownloadButton( | |
| "π₯ Download", | |
| variant="secondary", | |
| size="sm", | |
| ) | |
| with gr.TabItem("π Scored Ideas"): | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| scored_ideas_display = gr.Markdown("No scored ideas yet") | |
| with gr.Column(scale=1): | |
| download_scored_btn = gr.DownloadButton( | |
| "π₯ Download", | |
| variant="secondary", | |
| size="sm", | |
| ) | |
| with gr.TabItem("π Pitches"): | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| pitches_display = gr.Markdown("No pitches yet") | |
| with gr.Column(scale=1): | |
| download_pitches_btn = gr.DownloadButton( | |
| "π₯ Download", | |
| variant="secondary", | |
| size="sm", | |
| ) | |
| with gr.TabItem("π Critiques"): | |
| critiques_display = gr.Markdown("No critiques yet") | |
| with gr.TabItem("π₯ Export"): | |
| with gr.Row(): | |
| export_json_btn = gr.Button("Export JSON", variant="secondary") | |
| export_md_btn = gr.Button("Export Markdown", variant="secondary") | |
| export_output = gr.Code(label="Exported Data", language="json", interactive=False) | |
| status_message = gr.Textbox( | |
| label="Status", | |
| interactive=False, | |
| visible=True, | |
| value="Ready to start. Select a domain and click 'Start Discovery'.", | |
| ) | |
| # Event handlers | |
| domain_recommendation.change( | |
| fn=lambda x: x if x else "", | |
| inputs=[domain_recommendation], | |
| outputs=[domain_input], | |
| ) | |
| start_btn.click( | |
| fn=start_pipeline, | |
| inputs=[domain_input, max_pain_points, ideas_per_run, top_n_pitches, max_revisions], | |
| outputs=[ | |
| run_id_display, | |
| progress_display, | |
| pain_points_display, | |
| ideas_display, | |
| scored_ideas_display, | |
| pitches_display, | |
| critiques_display, | |
| status_message, | |
| ], | |
| ) | |
| stop_btn.click( | |
| fn=stop_pipeline, | |
| inputs=[run_id_display], | |
| outputs=[status_message], | |
| ) | |
| clear_cache_btn.click( | |
| fn=clear_cache, | |
| inputs=[], | |
| outputs=[status_message], | |
| ) | |
| # Auto-refresh progress every 2 seconds when run is active | |
| timer = gr.Timer(2.0) | |
| timer.tick( | |
| fn=update_progress, | |
| inputs=[run_id_display], | |
| outputs=[ | |
| progress_display, | |
| pain_points_display, | |
| ideas_display, | |
| scored_ideas_display, | |
| pitches_display, | |
| critiques_display, | |
| ], | |
| ) | |
| export_json_btn.click( | |
| fn=lambda rid: export_json(poll_state(rid)), | |
| inputs=[run_id_display], | |
| outputs=[export_output], | |
| ) | |
| export_md_btn.click( | |
| fn=lambda rid: export_markdown(poll_state(rid)), | |
| inputs=[run_id_display], | |
| outputs=[export_output], | |
| ) | |
| # Download button handlers | |
| download_pain_points_btn.click( | |
| fn=lambda rid: download_pain_points(poll_state(rid)), | |
| inputs=[run_id_display], | |
| ) | |
| download_ideas_btn.click( | |
| fn=lambda rid: download_ideas(poll_state(rid)), | |
| inputs=[run_id_display], | |
| ) | |
| download_scored_btn.click( | |
| fn=lambda rid: download_scored_ideas(poll_state(rid)), | |
| inputs=[run_id_display], | |
| ) | |
| download_pitches_btn.click( | |
| fn=lambda rid: download_pitches(poll_state(rid)), | |
| inputs=[run_id_display], | |
| ) | |
| return app | |
| # Create the demo for HuggingFace Spaces | |
| demo = create_ui() | |
| def main() -> None: | |
| """Launch the Gradio UI locally.""" | |
| demo.launch( | |
| server_name=settings.gradio_host, | |
| server_port=settings.gradio_port, | |
| share=False, | |
| theme=gr.themes.Soft(), | |
| ) | |
| if __name__ == "__main__": | |
| main() | |