"""whatfirst-small — a Gradio app that turns a messy brain-dump (or a photo of a to-do list) into a transparently-ranked "do this first" list, using a small local model for the messy-language part and a deterministic, legible engine for the ranking. Capture -> AI structures each task (impact / readiness / effort / due) -> the scoring engine ranks -> you can correct any score and it re-ranks live. """ from __future__ import annotations import os from datetime import datetime import gradio as gr import llm import score SAMPLE_DUMP = """email the landlord about the lease renewal, kind of important and due Friday finish the Q3 board deck — big deal, maybe 4 hours of work, needs to be done by next Wednesday 5 min: cancel the unused gym membership look into switching the team to the new CI, no rush, not sure where to start book the dentist, due tomorrow reply to Sam's thread, quick one draft the hiring plan — important but I'm blocked until we agree on budget""" FORMULA_BLURB = """ Two scores compete and the higher one shows: - **do = (Impact · Urgency · Readiness_eff · QuickWin) / 10** — the case for doing it now. - **prep = (Impact · Urgency · (10 − Readiness)) / 10 · 0.7 · QuickWin** — the case for de-risking it first (wins only when a task is valuable but not ready). **Urgency** climbs as a deadline nears and *explodes* once it's within a day, so a looming deadline can't be buried by a shiny far-off task. **QuickWin** rewards short, ready tasks. Deadlines act as a *constraint*, not just a term: anything overdue, or that genuinely won't finish in time given everything ahead of it, is lifted above the value pack. Every number is shown — nothing is a black box. """ DF_HEADERS = ["#", "Task", "Score", "Due", "Flag", "Why", "I", "R", "Eff·h"] DF_DATATYPES = ["number", "str", "str", "str", "str", "str", "number", "number", "number"] # ── Branding, borrowed from the full what-first.com app ─────────────────────── # Indigo → blue gradient, slate/ink text, Roboto — the palette the marketing # site and the demo video (demo/compose.py) share. INDIGO, BLUE = "#4F46E5", "#2563EB" SLATE, INK, MUTED = "#1E293B", "#111827", "#5A6473" # Gold is the site's accent (demo/compose.py); GOLD_LT reads brighter on the # dark hero gradient, where the darker brand gold would muddy. GOLD, GOLD_LT = "#D97706", "#FBBF24" THEME = gr.themes.Soft( primary_hue=gr.themes.colors.indigo, secondary_hue=gr.themes.colors.blue, neutral_hue=gr.themes.colors.slate, font=[gr.themes.GoogleFont("Roboto"), "system-ui", "sans-serif"], ).set( button_primary_background_fill=f"linear-gradient(90deg, {INDIGO}, {BLUE})", button_primary_background_fill_hover=f"linear-gradient(90deg, {BLUE}, {INDIGO})", button_primary_text_color="white", block_title_text_weight="600", ) CSS = f""" #wf-hero {{ background: linear-gradient(120deg, {INK}, {INDIGO} 55%, {BLUE}); border-radius: 16px; padding: 26px 30px; margin-bottom: 8px; border: 1px solid rgba(255, 255, 255, 0.08); }} /* Gradio's prose styles set explicit heading/text colors, so force white here. */ #wf-hero, #wf-hero h1, #wf-hero p, #wf-hero em {{ color: #fff !important; }} #wf-hero h1 {{ margin: 0 0 6px; font-weight: 700; letter-spacing: -0.5px; }} #wf-hero h1 .wf-accent {{ color: {GOLD_LT} !important; }} #wf-hero p {{ margin: 0; opacity: 0.95; font-size: 1.02em; line-height: 1.5; }} #wf-hero strong {{ color: {GOLD_LT} !important; font-weight: 700; }} #wf-hero a {{ color: {GOLD_LT} !important; text-decoration: underline; text-underline-offset: 2px; }} #wf-first {{ color: {SLATE}; font-weight: 600; }} """ def _label(t: dict) -> str: return f"{t['id']} · {t['title']}" def render(tasks: list[dict]): """tasks -> (header_md, dataframe_rows, dropdown_choices).""" if not tasks: return "### Add a brain-dump or a photo, then hit **Prioritize**.", [], [] now = datetime.now() ranked = score.rank_active(tasks, now) risk = score.deadline_risk_map(ranked, now) rows = [] for i, t in enumerate(ranked): c = score.score_components(t) flag = score.deadline_status(t, now) or risk.get(t["id"], "") rows.append([ i + 1, t["title"], score.format_score(c["display"]), score.due_label(t.get("due_date"), now) if t.get("due_date") else "—", flag, score.explain(t, now) + (" · edited" if t.get("edited") else ""), round(c["I"]), round(c["R"]), round(t["effort_hours"], 2), ]) top = ranked[0] header = ( f"### ▶ Do this first: {top['title']}" f" · score {score.format_score(score.priority(top))}" ) return header, rows, [_label(t) for t in ranked] def prioritize(text, image, tasks): # A generator: the first yield lands instantly so the ~60s CPU inference # doesn't look like a frozen app. Gradio streams each yield to the UI. if not llm.is_ready(): yield tasks, "### ⏳ The local model is still loading — give it a moment and try again.", [], gr.update() return hint = "" if text and text.strip(): n = len([ln for ln in text.splitlines() if ln.strip()]) hint = f" ≈{n} item{'s' if n != 1 else ''}" yield tasks, f"### ⏳ Reading and scoring{hint} on the local model… (≈60s on free CPU)", [], gr.update() collected = [] if text and text.strip(): collected += llm.parse_braindump(text) if image is not None: collected += llm.extract_from_image(image) for i, t in enumerate(collected): t["id"] = f"t{i}" if not collected: yield collected, "### Nothing actionable found — try a different dump or photo.", [], gr.update(choices=[], value=None) return header, rows, choices = render(collected) yield collected, header, rows, gr.update(choices=choices, value=(choices[0] if choices else None)) def load_task(sel, tasks): t = _find(sel, tasks) if not t: return 5, 8, 1.0 return t["impact"], t["readiness"], t["effort_hours"] def apply_edit(sel, impact, readiness, effort, tasks): t = _find(sel, tasks) if t: t["impact"] = int(impact) t["readiness"] = int(readiness) t["effort_hours"] = float(effort) t["edited"] = True header, rows, choices = render(tasks) keep = sel if sel in choices else (choices[0] if choices else None) return tasks, header, rows, gr.update(choices=choices, value=keep) def resuggest(sel, tasks): t = _find(sel, tasks) if not t or not llm.is_ready(): return gr.update(), gr.update(), gr.update() s = llm.score_task(t["title"], t.get("notes") or "", t.get("category") or "") if not s: return t["impact"], t["readiness"], t["effort_hours"] return s["impact"], s["readiness"], s["effort_hours"] def _find(sel, tasks): if not sel: return None tid = sel.split(" · ", 1)[0] return next((x for x in tasks if x["id"] == tid), None) with gr.Blocks(title="whatfirst-small", theme=THEME, css=CSS) as demo: gr.HTML( "
Dump everything on your mind — or snap a photo of your to-do list — and a " "small local model turns it into structured tasks. A " "transparent scoring engine then tells you what to do first, " "and shows its work. No cloud, no API keys. The full app lives at " "what-first.com.
" "