Spaces:
Runtime error
Runtime error
| import os | |
| import gradio as gr | |
| import json | |
| import base64 | |
| from datetime import datetime | |
| from io import BytesIO | |
| from pathlib import Path | |
| from theme import create_theme, CUSTOM_CSS | |
| from data import ( | |
| ensure_dirs, | |
| load_decisions, | |
| create_decision_record, | |
| get_open_decisions, | |
| get_decision_by_id, | |
| resolve_decision as resolve_decision_data, | |
| export_decisions, | |
| CARDS_DIR, | |
| ) | |
| from prompts import ( | |
| FOLLOW_UP_SYSTEM, | |
| FOLLOW_UP_NEXT, | |
| CATEGORIZE_PROMPT, | |
| PREDICT_PROMPT, | |
| MOMENT_CARD_PROMPT, | |
| PATTERN_ANALYSIS_PROMPT, | |
| IMAGE_DESCRIBE_PROMPT, | |
| ) | |
| from models import ( | |
| generate_text, | |
| transcribe_audio, | |
| describe_image, | |
| generate_moment_card, | |
| ) | |
| ensure_dirs() | |
| LOADING_MESSAGES = [ | |
| "Compiling life choicesβ¦", | |
| "Running static analysis on your decisionsβ¦", | |
| "Checking for dependency conflictsβ¦", | |
| "Generating memory snapshotβ¦", | |
| ] | |
| # ββ helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _format_qa(qa_list: list[dict]) -> str: | |
| if not qa_list: | |
| return "(no follow-up yet)" | |
| return "\n".join(f"Q: {q['question']}\nA: {q['answer']}" for q in qa_list) | |
| def _parse_json(text: str, fallback): | |
| clean = text.strip() | |
| if clean.startswith("```"): | |
| clean = clean.split("\n", 1)[-1].rsplit("```", 1)[0].strip() | |
| try: | |
| return json.loads(clean) | |
| except (json.JSONDecodeError, ValueError): | |
| return fallback | |
| def _card_thumbnail_b64(path: str | None, size: int = 96) -> str: | |
| if not path or not Path(path).exists(): | |
| return "" | |
| from PIL import Image as PILImage | |
| img = PILImage.open(path) | |
| img.thumbnail((size, size)) | |
| buf = BytesIO() | |
| img.save(buf, format="PNG") | |
| encoded = base64.b64encode(buf.getvalue()).decode() | |
| return ( | |
| f'<img src="data:image/png;base64,{encoded}" ' | |
| f'class="commit-card-thumb" />' | |
| ) | |
| def _initial_state() -> dict: | |
| return { | |
| "raw_input": "", | |
| "input_type": "text", | |
| "qa": [], | |
| "step": 0, | |
| "image_description": None, | |
| "category_data": None, | |
| "predictions": None, | |
| "card_prompt": None, | |
| "card_path": None, | |
| } | |
| # ββ analysis pipeline ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _run_categorize(decision: str, qa_ctx: str) -> dict: | |
| prompt = CATEGORIZE_PROMPT.format(decision=decision, qa_context=qa_ctx) | |
| raw = generate_text([{"role": "user", "content": prompt}], max_tokens=200) | |
| return _parse_json(raw, { | |
| "category": "lifestyle", | |
| "subcategory": "general", | |
| "severity": 5, | |
| "status_emoji": "π§", | |
| }) | |
| def _run_predict(decision: str, cat: str, sev: int, qa_ctx: str) -> list: | |
| prompt = PREDICT_PROMPT.format( | |
| decision=decision, category=cat, severity=sev, qa_context=qa_ctx, | |
| ) | |
| raw = generate_text([{"role": "user", "content": prompt}], max_tokens=500) | |
| return _parse_json(raw, [ | |
| {"outcome": "Uncertain outcome", "probability": "medium", | |
| "valence": "neutral", "timeframe": "months"}, | |
| ]) | |
| def _run_card_prompt(decision: str, category: str, tone: str) -> str: | |
| prompt = MOMENT_CARD_PROMPT.format( | |
| decision=decision, category=category, tone=tone, | |
| ) | |
| return generate_text([{"role": "user", "content": prompt}], max_tokens=150) | |
| # ββ HTML builders ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _build_analysis_html(cat_data: dict, predictions: list) -> str: | |
| category = cat_data.get("category", "unknown") | |
| severity = cat_data.get("severity", 5) | |
| emoji = cat_data.get("status_emoji", "π§") | |
| sev_class = "low" if severity <= 3 else "medium" if severity <= 6 else "high" | |
| preds = "" | |
| for p in predictions: | |
| v = p.get("valence", "neutral") | |
| icon = {"negative": "π", "positive": "β¨"}.get(v, "π§") | |
| cls = {"negative": "bug", "positive": "feature"}.get(v, "neutral") | |
| preds += ( | |
| f'<div class="prediction-item prediction-{cls}">' | |
| f'{icon} {p["outcome"]} ' | |
| f'<span style="opacity:.6">({p.get("probability","")}, ' | |
| f'{p.get("timeframe","")})</span></div>' | |
| ) | |
| return f""" | |
| <div class="results-panel"> | |
| <h3>{emoji} Debug Report Complete</h3> | |
| <div style="margin:12px 0"> | |
| <span class="category-badge branch-{category}"> | |
| [{category.upper()}] | |
| </span> | |
| <span style="color:#8b949e;margin-left:8px"> | |
| {cat_data.get("subcategory","")} | |
| </span> | |
| </div> | |
| <div style="margin:8px 0"> | |
| <span style="color:#8b949e">Severity:</span> | |
| <div class="severity-bar"> | |
| <div class="severity-fill severity-{sev_class}" | |
| style="width:{severity*10}%"></div> | |
| </div> | |
| <span style="color:#e6edf3;font-weight:600">{severity}/10</span> | |
| </div> | |
| <h4 style="color:#e6edf3;margin:16px 0 8px">Predicted Consequences</h4> | |
| {preds} | |
| </div>""" | |
| def _build_timeline_html(decisions: list) -> str: | |
| if not decisions: | |
| return ( | |
| '<div class="empty-state">' | |
| '<div class="icon">π</div>' | |
| "<p>No commits yet. Head to <b>New Commit</b> to log your first " | |
| "life decision.</p></div>" | |
| ) | |
| entries = [] | |
| for d in reversed(decisions): | |
| meta = d.get("debug_metadata", {}) | |
| h = meta.get("commit_hash", "0000000") | |
| branch = meta.get("branch", "unknown") | |
| emoji = meta.get("status_emoji", "π§") | |
| status = d.get("status", "open") | |
| resolved_cls = " resolved" if status == "resolved" else "" | |
| try: | |
| dt = datetime.fromisoformat(d["timestamp"]) | |
| date_str = dt.strftime("%b %d, %Y %H:%M") | |
| except Exception: | |
| date_str = d.get("timestamp", "") | |
| msg = d.get("raw_input", "")[:100] | |
| if len(d.get("raw_input", "")) > 100: | |
| msg += "β¦" | |
| preds = "" | |
| for p in d.get("consequence_predictions", [])[:3]: | |
| v = p.get("valence", "neutral") | |
| icon = {"negative": "π", "positive": "β¨"}.get(v, "π§") | |
| cls = {"negative": "bug", "positive": "feature"}.get(v, "neutral") | |
| preds += ( | |
| f'<div class="prediction-item prediction-{cls}">' | |
| f"{icon} {p.get('outcome','')}</div>" | |
| ) | |
| thumb = _card_thumbnail_b64(d.get("moment_card_path")) | |
| if status == "resolved" and d.get("outcome"): | |
| ov = d["outcome"].get("actual_valence", "mixed") | |
| outcome_html = ( | |
| f'<div class="outcome-badge outcome-{ov}">' | |
| f'RESOLVED: {d["outcome"].get("description","")[:60]}</div>' | |
| ) | |
| else: | |
| outcome_html = ( | |
| '<div class="outcome-badge outcome-pending">' | |
| "β³ Outcome pending</div>" | |
| ) | |
| entries.append(f""" | |
| <div class="commit-entry{resolved_cls}"> | |
| <div class="commit-header"> | |
| <span class="commit-hash">{h}</span> | |
| <span class="commit-branch branch-{branch}"> | |
| [{branch.upper()}] | |
| </span> | |
| <span class="commit-message">{emoji} {msg}</span> | |
| <span class="commit-date">{date_str}</span> | |
| </div> | |
| <div class="commit-body"> | |
| {preds} | |
| {thumb} | |
| {outcome_html} | |
| </div> | |
| </div>""") | |
| return f'<div class="git-log-container">{"".join(entries)}</div>' | |
| # ββ chart helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _category_chart(decisions: list): | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| cats: dict[str, int] = {} | |
| for d in decisions: | |
| c = d.get("category", "unknown") | |
| cats[c] = cats.get(c, 0) + 1 | |
| fig, ax = plt.subplots(figsize=(5, 4)) | |
| fig.patch.set_facecolor("#161b22") | |
| ax.set_facecolor("#161b22") | |
| if not cats: | |
| ax.text( | |
| 0.5, 0.5, "No data yet", | |
| ha="center", va="center", color="#484f58", fontsize=14, | |
| ) | |
| ax.axis("off") | |
| return fig | |
| color_map = { | |
| "career": "#58a6ff", "financial": "#f0883e", "health": "#3fb950", | |
| "relationship": "#db61a2", "education": "#a06ef6", | |
| "housing": "#79c0ff", "lifestyle": "#d29922", "creative": "#ff7b72", | |
| } | |
| labels = list(cats.keys()) | |
| sizes = list(cats.values()) | |
| colors = [color_map.get(l, "#8b949e") for l in labels] | |
| ax.pie( | |
| sizes, | |
| labels=[l.capitalize() for l in labels], | |
| colors=colors, | |
| autopct="%1.0f%%", | |
| textprops={"color": "#e6edf3", "fontsize": 10}, | |
| ) | |
| return fig | |
| def _compute_stats(decisions: list) -> tuple: | |
| total = len(decisions) | |
| open_n = sum(1 for d in decisions if d.get("status") == "open") | |
| resolved = total - open_n | |
| accs = [ | |
| d["outcome"]["prediction_accuracy"] | |
| for d in decisions | |
| if d.get("outcome") and d["outcome"].get("prediction_accuracy") is not None | |
| ] | |
| avg_acc = f"{sum(accs)/len(accs)*100:.0f}%" if accs else "--" | |
| return total, open_n, resolved, avg_acc | |
| def _stat_html(value, label): | |
| return ( | |
| f'<div class="stat-card">' | |
| f'<div class="stat-value">{value}</div>' | |
| f'<div class="stat-label">{label}</div></div>' | |
| ) | |
| # ββ build app ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| theme = create_theme() | |
| with gr.Blocks(title="LifeLog", theme=theme, css=CUSTOM_CSS) as app: | |
| # Header | |
| gr.HTML(""" | |
| <div class="app-header"> | |
| <div class="app-title">π§ LifeLog</div> | |
| <div class="app-subtitle"> | |
| $ git commit -m "a debugger for your life decisions" | |
| </div> | |
| <div class="app-meta"> | |
| all models β€ 4B params Β· tiny titan eligible Β· v1.0 | |
| </div> | |
| </div> | |
| """) | |
| state = gr.State(_initial_state()) | |
| with gr.Tabs() as tabs: | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 1 β NEW COMMIT | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π New Commit", id="tab-commit"): | |
| gr.HTML('<div class="section-header">$ git add life-decision</div>') | |
| with gr.Row(equal_height=True): | |
| with gr.Column(scale=1): | |
| text_input = gr.Textbox( | |
| label="π Type It", | |
| placeholder=( | |
| "What decision did you make? " | |
| "What crossroads are you at?" | |
| ), | |
| lines=4, | |
| ) | |
| text_btn = gr.Button("Log Decision", variant="primary") | |
| with gr.Column(scale=1): | |
| audio_input = gr.Audio( | |
| label="ποΈ Speak It", | |
| sources=["microphone"], | |
| type="filepath", | |
| ) | |
| audio_btn = gr.Button( | |
| "Transcribe & Log", variant="primary", | |
| ) | |
| with gr.Column(scale=1): | |
| image_input = gr.Image( | |
| label="π· Upload It", type="filepath", | |
| ) | |
| image_ctx = gr.Textbox( | |
| label="Context", | |
| placeholder="e.g. 'This is the job offer I received'", | |
| lines=2, | |
| ) | |
| image_btn = gr.Button( | |
| "Analyze & Log", variant="primary", | |
| ) | |
| # Follow-up conversation | |
| chat_col = gr.Column(visible=False) | |
| with chat_col: | |
| gr.HTML( | |
| '<div class="section-header">' | |
| "π Debugging Session</div>" | |
| ) | |
| chatbot = gr.Chatbot( | |
| label="Follow-up Questions", | |
| height=320, | |
| type="messages", | |
| ) | |
| with gr.Row(): | |
| user_resp = gr.Textbox( | |
| placeholder="Your answerβ¦", | |
| label="", | |
| scale=4, | |
| show_label=False, | |
| ) | |
| submit_btn = gr.Button("Submit", variant="primary", scale=1) | |
| # Results (no visibility wrapper β content presence is the cue) | |
| analysis_html = gr.HTML() | |
| with gr.Row(): | |
| moment_img = gr.Image( | |
| label="π¨ Your Moment Card", height=400, | |
| visible=False, | |
| ) | |
| save_btn = gr.Button( | |
| "πΎ Save to Timeline", variant="primary", size="lg", | |
| visible=False, | |
| ) | |
| save_status = gr.HTML() | |
| # ββ event handlers ββ | |
| def _start_session(text: str, st: dict, input_type: str = "text"): | |
| if not text or not text.strip(): | |
| gr.Warning("Please enter a decision first.") | |
| return None, st, gr.skip() | |
| st = _initial_state() | |
| st["raw_input"] = text.strip() | |
| st["input_type"] = input_type | |
| msgs = [ | |
| {"role": "system", "content": FOLLOW_UP_SYSTEM}, | |
| {"role": "user", "content": ( | |
| f"Decision logged: {text.strip()}\n\n" | |
| "Ask follow-up question #1 of 3 (ROOT CAUSE)." | |
| )}, | |
| ] | |
| question = generate_text(msgs) | |
| st["current_question"] = question | |
| chat = [ | |
| {"role": "assistant", "content": ( | |
| f"**Debugging session started for commit:** " | |
| f"`{text.strip()[:80]}`\n\n{question}" | |
| )}, | |
| ] | |
| return chat, st, gr.Column(visible=True) | |
| def start_text(text, st): | |
| return _start_session(text, st, "text") | |
| def start_voice(audio, st): | |
| if not audio: | |
| gr.Warning("Please record audio first.") | |
| return gr.skip(), None, st, gr.skip() | |
| transcript = transcribe_audio(audio) | |
| chat, st, cv = _start_session(transcript, st, "voice") | |
| return transcript, chat, st, cv | |
| def start_image(img, ctx, st): | |
| if not img: | |
| gr.Warning("Please upload an image first.") | |
| return gr.skip(), None, st, gr.skip() | |
| desc = describe_image(img, IMAGE_DESCRIBE_PROMPT) | |
| st_new = _initial_state() | |
| st_new["image_description"] = desc | |
| combined = ( | |
| f"{ctx.strip()}\n\n[Image analysis: {desc}]" | |
| if ctx and ctx.strip() else desc | |
| ) | |
| chat, st_new, cv = _start_session( | |
| combined, st_new, "image", | |
| ) | |
| return combined, chat, st_new, cv | |
| def handle_followup(user_msg, chat_history, st): | |
| if not user_msg or not user_msg.strip(): | |
| return ( | |
| "", chat_history, st, | |
| gr.skip(), gr.skip(), gr.skip(), gr.skip(), | |
| ) | |
| st["qa"].append({ | |
| "question": st.get("current_question", ""), | |
| "answer": user_msg.strip(), | |
| }) | |
| st["step"] += 1 | |
| chat_history = list(chat_history) | |
| chat_history.append({"role": "user", "content": user_msg.strip()}) | |
| if st["step"] < 3: | |
| qa_ctx = _format_qa(st["qa"]) | |
| prompt = FOLLOW_UP_NEXT.format( | |
| decision=st["raw_input"], | |
| qa_context=qa_ctx, | |
| question_number=st["step"] + 1, | |
| ) | |
| next_q = generate_text([ | |
| {"role": "system", "content": FOLLOW_UP_SYSTEM}, | |
| {"role": "user", "content": prompt}, | |
| ]) | |
| st["current_question"] = next_q | |
| chat_history.append( | |
| {"role": "assistant", "content": next_q}, | |
| ) | |
| return ( | |
| "", chat_history, st, | |
| gr.skip(), gr.skip(), gr.skip(), gr.skip(), | |
| ) | |
| # ββ all 3 questions done β run analysis ββ | |
| chat_history.append({ | |
| "role": "assistant", | |
| "content": "β All questions answered. Compiling debug reportβ¦", | |
| }) | |
| qa_ctx = _format_qa(st["qa"]) | |
| cat_data = _run_categorize(st["raw_input"], qa_ctx) | |
| category = cat_data.get("category", "lifestyle") | |
| severity = cat_data.get("severity", 5) | |
| predictions = _run_predict( | |
| st["raw_input"], category, severity, qa_ctx, | |
| ) | |
| pos = sum(1 for p in predictions if p.get("valence") == "positive") | |
| neg = sum(1 for p in predictions if p.get("valence") == "negative") | |
| tone = ( | |
| "hopeful and optimistic" if pos > neg | |
| else "tense and uncertain" if neg > pos | |
| else "contemplative and balanced" | |
| ) | |
| card_prompt = _run_card_prompt(st["raw_input"], category, tone) | |
| card_image = generate_moment_card(card_prompt) | |
| card_name = f"card_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" | |
| card_path = str(CARDS_DIR / card_name) | |
| card_image.save(card_path) | |
| st["category_data"] = cat_data | |
| st["predictions"] = predictions | |
| st["card_prompt"] = card_prompt | |
| st["card_path"] = card_path | |
| section_hdr = ( | |
| '<div class="section-header">' | |
| "π Analysis Results</div>" | |
| ) | |
| html = section_hdr + _build_analysis_html(cat_data, predictions) | |
| return ( | |
| "", chat_history, st, | |
| html, gr.Image(value=card_image, visible=True), | |
| gr.Button(visible=True), gr.skip(), | |
| ) | |
| def save_to_timeline(st): | |
| if not st.get("category_data"): | |
| return ( | |
| '<p style="color:#f85149">No data to save.</p>', | |
| st, | |
| ) | |
| cd = st["category_data"] | |
| record = create_decision_record( | |
| raw_input=st["raw_input"], | |
| input_type=st["input_type"], | |
| follow_up_qa=st["qa"], | |
| category=cd.get("category", "lifestyle"), | |
| subcategory=cd.get("subcategory", "general"), | |
| severity=cd.get("severity", 5), | |
| status_emoji=cd.get("status_emoji", "π§"), | |
| consequence_predictions=st.get("predictions", []), | |
| moment_card_prompt=st.get("card_prompt", ""), | |
| moment_card_path=st.get("card_path"), | |
| image_description=st.get("image_description"), | |
| ) | |
| h = record["debug_metadata"]["commit_hash"] | |
| return ( | |
| f'<p style="color:#3fb950">β Commit ' | |
| f'<span class="commit-hash">{h}</span> saved to timeline.' | |
| f"</p>", | |
| _initial_state(), | |
| ) | |
| # Wire events | |
| text_btn.click( | |
| start_text, | |
| inputs=[text_input, state], | |
| outputs=[chatbot, state, chat_col], | |
| ) | |
| text_input.submit( | |
| start_text, | |
| inputs=[text_input, state], | |
| outputs=[chatbot, state, chat_col], | |
| ) | |
| audio_btn.click( | |
| start_voice, | |
| inputs=[audio_input, state], | |
| outputs=[text_input, chatbot, state, chat_col], | |
| ) | |
| image_btn.click( | |
| start_image, | |
| inputs=[image_input, image_ctx, state], | |
| outputs=[text_input, chatbot, state, chat_col], | |
| ) | |
| submit_btn.click( | |
| handle_followup, | |
| inputs=[user_resp, chatbot, state], | |
| outputs=[ | |
| user_resp, chatbot, state, | |
| analysis_html, moment_img, | |
| save_btn, save_status, | |
| ], | |
| ) | |
| user_resp.submit( | |
| handle_followup, | |
| inputs=[user_resp, chatbot, state], | |
| outputs=[ | |
| user_resp, chatbot, state, | |
| analysis_html, moment_img, | |
| save_btn, save_status, | |
| ], | |
| ) | |
| save_btn.click( | |
| save_to_timeline, | |
| inputs=[state], | |
| outputs=[save_status, state], | |
| ) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 2 β GIT LOG | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Git Log", id="tab-log") as tab_log: | |
| gr.HTML( | |
| '<div class="section-header">' | |
| "$ git log --oneline --graph</div>" | |
| ) | |
| with gr.Row(): | |
| cat_filter = gr.Dropdown( | |
| choices=[ | |
| "All", "career", "financial", "health", | |
| "relationship", "education", "housing", | |
| "lifestyle", "creative", | |
| ], | |
| value="All", | |
| label="Filter by branch", | |
| scale=2, | |
| ) | |
| refresh_log_btn = gr.Button("π Refresh", scale=1) | |
| timeline_html = gr.HTML() | |
| card_gallery = gr.Gallery( | |
| label="π¨ Moment Cards", columns=4, height=280, | |
| ) | |
| def refresh_log(cat): | |
| decisions = load_decisions() | |
| if cat != "All": | |
| decisions = [ | |
| d for d in decisions if d.get("category") == cat | |
| ] | |
| html = _build_timeline_html(decisions) | |
| cards = [ | |
| (d["moment_card_path"], | |
| d.get("debug_metadata", {}).get("commit_hash", "")) | |
| for d in reversed(decisions) | |
| if d.get("moment_card_path") | |
| and Path(d["moment_card_path"]).exists() | |
| ] | |
| return html, cards | |
| refresh_log_btn.click( | |
| refresh_log, inputs=[cat_filter], | |
| outputs=[timeline_html, card_gallery], | |
| ) | |
| cat_filter.change( | |
| refresh_log, inputs=[cat_filter], | |
| outputs=[timeline_html, card_gallery], | |
| ) | |
| tab_log.select( | |
| refresh_log, inputs=[cat_filter], | |
| outputs=[timeline_html, card_gallery], | |
| ) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 3 β DEBUG DASHBOARD | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Debug Dashboard", id="tab-dash") as tab_dash: | |
| gr.HTML( | |
| '<div class="section-header">' | |
| "$ ./run_diagnostics.sh</div>" | |
| ) | |
| with gr.Row(): | |
| s_total = gr.HTML(_stat_html(0, "Total Commits")) | |
| s_open = gr.HTML(_stat_html(0, "Open")) | |
| s_resolved = gr.HTML(_stat_html(0, "Resolved")) | |
| s_accuracy = gr.HTML(_stat_html("--", "Prediction Accuracy")) | |
| chart = gr.Plot(label="Category Distribution") | |
| analyze_btn = gr.Button( | |
| "π Run Pattern Analysis", variant="primary", size="lg", | |
| ) | |
| pattern_md = gr.Markdown( | |
| "*Click 'Run Pattern Analysis' to generate your debug " | |
| "reportβ¦*" | |
| ) | |
| export_btn = gr.Button("π₯ Export All Data (JSON)", size="sm") | |
| export_file = gr.File(label="Download", visible=False) | |
| def refresh_dash(): | |
| decisions = load_decisions() | |
| total, open_n, resolved, acc = _compute_stats(decisions) | |
| fig = _category_chart(decisions) | |
| return ( | |
| _stat_html(total, "Total Commits"), | |
| _stat_html(open_n, "Open"), | |
| _stat_html(resolved, "Resolved"), | |
| _stat_html(acc, "Prediction Accuracy"), | |
| fig, | |
| ) | |
| def run_patterns(): | |
| decisions = load_decisions() | |
| if len(decisions) < 2: | |
| return ( | |
| "**Need at least 2 decisions to analyze patterns.** " | |
| "Log more decisions in the New Commit tab." | |
| ) | |
| summary = [ | |
| { | |
| "decision": d["raw_input"][:200], | |
| "category": d.get("category"), | |
| "severity": d.get("severity"), | |
| "predictions": d.get("consequence_predictions", []), | |
| "status": d.get("status"), | |
| "outcome": d.get("outcome"), | |
| "timestamp": d.get("timestamp"), | |
| } | |
| for d in decisions | |
| ] | |
| prompt = PATTERN_ANALYSIS_PROMPT.format( | |
| decisions_json=json.dumps(summary, indent=2), | |
| ) | |
| return generate_text( | |
| [{"role": "user", "content": prompt}], max_tokens=1000, | |
| ) | |
| def do_export(): | |
| path = Path("data") / "lifelog_export.json" | |
| path.write_text(export_decisions(), encoding="utf-8") | |
| return gr.File(value=str(path), visible=True) | |
| analyze_btn.click( | |
| refresh_dash, | |
| outputs=[s_total, s_open, s_resolved, s_accuracy, chart], | |
| ).then(run_patterns, outputs=[pattern_md]) | |
| tab_dash.select( | |
| refresh_dash, | |
| outputs=[s_total, s_open, s_resolved, s_accuracy, chart], | |
| ) | |
| export_btn.click(do_export, outputs=[export_file]) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 4 β RESOLVE | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("β Resolve", id="tab-resolve") as tab_resolve: | |
| gr.HTML( | |
| '<div class="section-header">' | |
| "$ git merge --resolve life-decision</div>" | |
| ) | |
| dec_dropdown = gr.Dropdown( | |
| label="Select a decision to resolve", | |
| choices=[], | |
| interactive=True, | |
| ) | |
| refresh_dec_btn = gr.Button("π Refresh List", size="sm") | |
| dec_detail = gr.HTML() | |
| outcome_text = gr.Textbox( | |
| label="What actually happened?", | |
| placeholder="Describe the outcomeβ¦", | |
| lines=3, | |
| ) | |
| outcome_valence = gr.Radio( | |
| choices=["positive", "negative", "mixed"], | |
| label="How did it turn out?", | |
| value="mixed", | |
| ) | |
| resolve_btn = gr.Button( | |
| "π§ Close This Bug/Feature", variant="primary", | |
| ) | |
| resolve_status = gr.HTML() | |
| def refresh_open(): | |
| items = get_open_decisions() | |
| choices = [ | |
| ( | |
| f"{d['debug_metadata']['commit_hash']} " | |
| f"[{d.get('category','?').upper()}] " | |
| f"{d['raw_input'][:55]}", | |
| d["id"], | |
| ) | |
| for d in items | |
| ] | |
| return gr.Dropdown(choices=choices, value=None) | |
| def show_detail(did): | |
| if not did: | |
| return "" | |
| d = get_decision_by_id(did) | |
| if not d: | |
| return "" | |
| qa = "".join( | |
| f"<p><b>Q:</b> {q['question']}<br>" | |
| f"<b>A:</b> {q['answer']}</p>" | |
| for q in d.get("follow_up_qa", []) | |
| ) | |
| preds = "".join( | |
| f"<div>{'π' if p.get('valence')=='negative' else 'β¨' if p.get('valence')=='positive' else 'π§'} " | |
| f"{p.get('outcome','')} ({p.get('probability','')}, " | |
| f"{p.get('timeframe','')})</div>" | |
| for p in d.get("consequence_predictions", []) | |
| ) | |
| return f""" | |
| <div class="results-panel"> | |
| <h4>{d['debug_metadata']['status_emoji']} Commit | |
| {d['debug_metadata']['commit_hash']}</h4> | |
| <p><b>Decision:</b> {d['raw_input']}</p> | |
| <p><b>Category:</b> | |
| [{d.get('category','?').upper()}] Β· | |
| Severity: {d.get('severity','?')}/10</p> | |
| <h5 style="margin-top:12px">Follow-up Discussion</h5> | |
| {qa} | |
| <h5 style="margin-top:12px">Predicted Consequences</h5> | |
| {preds} | |
| </div>""" | |
| def do_resolve(did, desc, valence): | |
| if not did: | |
| return '<p style="color:#f85149">Select a decision.</p>' | |
| if not desc or not desc.strip(): | |
| return '<p style="color:#f85149">Describe the outcome.</p>' | |
| resolve_decision_data(did, desc.strip(), valence) | |
| return ( | |
| f'<p style="color:#3fb950">β Decision resolved as ' | |
| f"<b>{valence}</b>. Timeline updated.</p>" | |
| ) | |
| refresh_dec_btn.click(refresh_open, outputs=[dec_dropdown]) | |
| tab_resolve.select(refresh_open, outputs=[dec_dropdown]) | |
| dec_dropdown.change( | |
| show_detail, inputs=[dec_dropdown], outputs=[dec_detail], | |
| ) | |
| resolve_btn.click( | |
| do_resolve, | |
| inputs=[dec_dropdown, outcome_text, outcome_valence], | |
| outputs=[resolve_status], | |
| ) | |
| if __name__ == "__main__": | |
| app.launch( | |
| allowed_paths=["data/cards"], | |
| ) | |