"""StoryCode — understand the code you built, as a story. Gradio frontend. Flow: upload a .zip (or paste a file) -> static analysis builds the factual ProjectModel -> MiniCPM narrates it (map per file, reduce to a story) -> you read the Story, the Architecture Map, and what's Safe to Edit. Changing style/difficulty re-narrates only (fast); it never re-analyses the code. """ from __future__ import annotations import os import gradio as gr import config import db import diagram import narrate from analyzer import analyze_project from ingest import from_text, from_zip, from_github from schema import ProjectModel from ui import theme db.init_db() SAMPLE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "scripts", "sample_project") STYLE_CHOICES = [(f"{s.emoji} {s.label}", s.key) for s in config.STYLES] DIFF_CHOICES = [(d.label, d.key) for d in config.DIFFICULTIES] # --- core analysis ---------------------------------------------------------- def _ingest_input(zip_path: str | None, pasted: str | None, github_url: str | None): if zip_path: return from_zip(zip_path) if github_url and github_url.strip(): return from_github(github_url.strip()) if pasted and pasted.strip(): return from_text(pasted) raise gr.Error("Upload a .zip, paste a file, or enter a GitHub URL.") def _render_all(model: ProjectModel, story): secret = "" if model.secrets_found: n = len(model.secrets_found) secret = theme.banner( f"🔒 We spotted and hid {n} likely secret(s) " "(API keys / passwords) before analysing. Remove them from your code too.", "warn") skipped = "" if model.note: skipped = theme.banner(model.note, "info") return ( theme.story_html(story), theme.plain_panel_html(story), diagram.render_html(model), theme.safe_to_edit_html(model), theme.deps_html(model), gr.update(value=secret + skipped, visible=bool(secret or skipped)), ) def analyze(zip_path, pasted, github_url, style, difficulty, progress=gr.Progress()): ingested = _ingest_input(zip_path, pasted, github_url) if not ingested.files: raise gr.Error("No code files found in there. Supported: Python, JS/TS, HTML, CSS, JSON, YAML.") progress(0.05, desc="Reading your project structure…") model = analyze_project(ingested) def _p(i, total, path): progress(0.1 + 0.7 * i / max(1, total), desc=f"Understanding file {i} of {total}…") summaries = narrate.summarise_files(model, progress=_p) progress(0.9, desc="Writing your story…") story = narrate.tell_story(model, summaries, style, difficulty) out = _render_all(model, story) return (*out, model, summaries, gr.update(selected="story")) def analyze_sample(style, difficulty, progress=gr.Progress()): from ingest import from_folder ingested = from_folder(SAMPLE_DIR, name="doc-qa (sample)") progress(0.05, desc="Reading the sample project…") model = analyze_project(ingested) def _p(i, total, path): progress(0.1 + 0.7 * i / max(1, total), desc=f"Understanding file {i} of {total}…") summaries = narrate.summarise_files(model, progress=_p) progress(0.9, desc="Writing your story…") story = narrate.tell_story(model, summaries, style, difficulty) out = _render_all(model, story) return (*out, model, summaries, gr.update(selected="story")) def restyle(model: ProjectModel, summaries, style, difficulty): """Re-narrate with a new style/difficulty — no re-analysis.""" if model is None: return gr.update(), gr.update() story = narrate.tell_story(model, summaries, style, difficulty) return theme.story_html(story), theme.plain_panel_html(story) # --- layout ----------------------------------------------------------------- def build() -> gr.Blocks: with gr.Blocks(theme=theme.THEME, css=theme.css(), title="StoryCode", head=theme.MERMAID_HEAD) as demo: model_state = gr.State(None) summ_state = gr.State(None) gr.HTML(theme.header_html()) with gr.Row(): with gr.Column(scale=2): gr.HTML(theme.sources_html()) zip_in = gr.File(label="Project .zip", file_types=[".zip"], type="filepath") with gr.Accordion("…or paste a single file", open=False): paste_in = gr.Code(label="Paste code here", language="python", lines=10) with gr.Accordion("…or paste a GitHub URL", open=False): github_in = gr.Textbox( label="GitHub URL", placeholder="https://github.com/owner/repo", lines=1, ) with gr.Row(): go_btn = gr.Button("Tell me the story", variant="primary", elem_classes="primary", scale=2) sample_btn = gr.Button("Try the sample project", scale=1) with gr.Column(scale=1): style_in = gr.Radio(STYLE_CHOICES, value=config.DEFAULT_STYLE, label="Story style") diff_in = gr.Radio(DIFF_CHOICES, value=config.DEFAULT_DIFFICULTY, label="Explain it for…") notice = gr.HTML(visible=False) with gr.Tabs() as tabs: with gr.Tab("📖 The Story", id="story"): with gr.Row(): story_box = gr.HTML(elem_classes="sc-story-wrap") with gr.Column(scale=1, min_width=240): plain_box = gr.HTML() with gr.Tab("🗺️ Architecture Map", id="map"): gr.HTML('
Boxes are your files, grouped by job. ' "An arrow means “this file uses that one”.
") map_box = gr.HTML() with gr.Tab("🚦 Safe to Edit", id="safe"): safe_box = gr.HTML() with gr.Tab("📦 Dependencies", id="deps"): deps_box = gr.HTML() outputs = [story_box, plain_box, map_box, safe_box, deps_box, notice, model_state, summ_state, tabs] go_btn.click(analyze, inputs=[zip_in, paste_in, github_in, style_in, diff_in], outputs=outputs) sample_btn.click(analyze_sample, inputs=[style_in, diff_in], outputs=outputs) # Restyle live without re-analysing. for ctrl in (style_in, diff_in): ctrl.change(restyle, inputs=[model_state, summ_state, style_in, diff_in], outputs=[story_box, plain_box]) return demo if __name__ == "__main__": build().launch(show_api=False)