File size: 6,889 Bytes
71d239c 7603aa2 71d239c 7603aa2 71d239c 7603aa2 71d239c 7603aa2 71d239c 7603aa2 71d239c 7603aa2 71d239c 7603aa2 62bb55b 71d239c 62bb55b 71d239c 62bb55b 71d239c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | """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 <strong>hid {n} likely secret(s)</strong> "
"(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('<div class="sc-legend">Boxes are your files, grouped by job. '
"An arrow means “this file uses that one”.</div>")
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)
|