""" app.py — Second Brain Gradio Application ========================================== Modular structure: core/database.py — SQLite persistence (users, tasks, life areas, goals, AI context) core/ai_engine.py — All Groq API calls (task parsing, scheduling, journaling) core/styles.py — CSS Run locally: pip install -r requirements.txt GROQ_API_KEY=gsk_... python app.py HuggingFace Spaces: Add GROQ_API_KEY in Space Settings → Repository Secrets """ import os import json as _json import gradio as gr from datetime import date, datetime # ── Core modules ────────────────────────────────────────────────────────────── from core.database import ( init_db, register_user, login_user, get_username, create_default_life_areas, save_goals, get_goals, get_life_areas, get_life_area_names, add_life_area, delete_life_area, get_tasks, save_task, assign_task_date, toggle_task_complete, delete_task, get_today_stats, load_user_context, save_user_context, spawn_due_habits, ) from core.ai_engine import ( init_groq, create_blank_context, parse_task_with_groq, generate_schedule, build_opening_question, get_next_journal_question, synthesize_journal, ) from core.styles import CSS # ── Boot ────────────────────────────────────────────────────────────────────── init_db() init_groq() # reads GROQ_API_KEY from env TASK_HEADERS = ["ID", "Done", "Title", "Area", "Urgency", "Importance", "Mind", "Min", "Deadline", "Scheduled"] # ═══════════════════════════════════════════════════════════════════════════════ # SHARED HELPERS # ═══════════════════════════════════════════════════════════════════════════════ def _ok(msg): return f'✓ {msg}' def _err(msg): return f'⚠ {msg}' def _info(msg): return f'ℹ {msg}' def _stat(val, label, color): return f'
{val}
{label}
' def _fmt_tasks(tasks: list) -> list: _today = date.today() rows = [] for t in tasks: dl = (t.get("deadline_date") or "").strip() if dl and dl.lower() not in ("null", "none", ""): try: diff = (date.fromisoformat(dl) - _today).days if diff < 0: dl = f"❌ {dl}" elif diff == 0: dl = "🚨 TODAY" elif diff == 1: dl = "⚠️ tomorrow" elif diff <= 3: dl = f"⏰ {dl}" except ValueError: pass else: dl = "—" sched = (t.get("scheduled_date") or "").strip() rows.append([ t["id"], "✅" if t["is_completed"] else "⬜", ("🔁 " if t["is_habit"] else "") + t["title"], t["life_area"] or "—", t["urgency"] or "—", t["importance"] or "—", t["state_of_mind"] or "—", str(t["time_estimate"]) + "m" if t["time_estimate"] else "—", dl, sched if sched else "⬜ unscheduled", ]) return rows def _area_choices(user_id): if not user_id: return ["All"] return ["All"] + get_life_area_names(user_id) def _ensure_context(user_id): """Load or create blank AI context for a user.""" ctx = load_user_context(user_id) if not ctx: ctx = create_blank_context(user_id) save_user_context(user_id, ctx) return ctx # ═══════════════════════════════════════════════════════════════════════════════ # AUTH HANDLERS # ═══════════════════════════════════════════════════════════════════════════════ def handle_login(username, password): uid, msg = login_user(username, password) if not uid: return None, "", _err(msg), gr.update(visible=True), gr.update(visible=False) spawn_due_habits(uid) uname = get_username(uid) return uid, uname, _ok(msg), gr.update(visible=False), gr.update(visible=True) def handle_register(username, password, wake, sleep, focus, goals_text): uid, msg = register_user(username, password) if not uid: return None, "", _err(msg), gr.update(visible=True), gr.update(visible=False) create_default_life_areas(uid) if goals_text.strip(): save_goals(uid, goals_text) prefs = {"wake_time": wake or "07:30", "sleep_time": sleep or "23:00", "focus_peak": focus or "Morning"} ctx = create_blank_context(uid, prefs) save_user_context(uid, ctx) spawn_due_habits(uid) uname = get_username(uid) return uid, uname, _ok(msg + " Logged in!"), gr.update(visible=False), gr.update(visible=True) def handle_logout(user_id): return None, "", gr.update(visible=True), gr.update(visible=False) # ═══════════════════════════════════════════════════════════════════════════════ # TODAY TAB HANDLERS # ═══════════════════════════════════════════════════════════════════════════════ def refresh_today(user_id): if not user_id: return [], _stat("—","Today","#a78bfa"), _stat("—","Done","#4ade80"), _stat("—","Left","#f87171") tasks = get_tasks(user_id, only_today=True) s = get_today_stats(user_id) c = "#4ade80" if s["remaining"] == 0 and s["total"] > 0 else "#f87171" return ( _fmt_tasks(tasks), _stat(s["total"], "Today", "#a78bfa"), _stat(s["done"], "Done", "#4ade80"), _stat(s["remaining"], "Left", c), ) def show_text_panel(): return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False) def show_voice_panel(): return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False) def show_plan_panel(): return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True) def hide_panels(): return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) def handle_parse_text(task_text, user_id): """Call Groq to classify a typed task, return pre-filled fields.""" if not task_text.strip(): return (gr.update(visible=False), "", "", "Work", "Not Urgent", "Important", "Easy", 30, str(date.today()), False, "Daily", "", gr.update(visible=False), [], "") ctx = _ensure_context(user_id) goals = get_goals(user_id) areas = get_life_area_names(user_id) if user_id else [] result = parse_task_with_groq(task_text, ctx, goals, areas) clarifications = result.get("clarifications_needed", []) clar_html = "" has_clarifications = bool(clarifications) if clarifications: items = "".join(f"
  • {q}
  • " for q in clarifications) clar_html = ( f'
    ' f'
    ⚠ Please clarify:
    ' f'
    ' ) area_choices = areas if areas else ["Work", "Health", "Finance", "Learning", "Personal", "Family", "Other"] area_val = result.get("life_area") or (area_choices[0] if area_choices else "Work") if area_val not in area_choices: area_choices = [area_val] + area_choices raw_dl = result.get("deadline_date") or "" dl_val = "" if str(raw_dl).lower() in ("null", "none", "n/a", "") else str(raw_dl) return ( gr.update(visible=True), result.get("title", task_text), clar_html, area_val, result.get("urgency", "Not Urgent"), result.get("importance", "Important"), result.get("state_of_mind", "Easy"), int(result.get("time_estimate") or 30), dl_val, False, "Daily", _ok("AI classified ✓ — answer questions above." if has_clarifications else "AI classified ✓"), gr.update(visible=has_clarifications), clarifications, task_text, ) def handle_clarification_reply(user_reply, original_task, clarifications, user_id): """Re-run Groq classification with the original task + clarification Q&A appended.""" if not user_reply.strip(): return (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(visible=True), "") ctx = _ensure_context(user_id) goals = get_goals(user_id) areas = get_life_area_names(user_id) if user_id else [] # Build enriched task description: original + each clarification + the user's answer q_block = "\n".join(f"Q: {q}" for q in clarifications) enriched = ( f"{original_task}\n\n" f"Additional context from user (answers to clarification questions):\n" f"{q_block}\n" f"A: {user_reply}" ) result = parse_task_with_groq(enriched, ctx, goals, areas) # If AI still has clarifications, show them; otherwise hide the clarification row remaining = result.get("clarifications_needed", []) if remaining: items = "".join(f"
  • {q}
  • " for q in remaining) clar_html_val = ( f'
    ' f'
    ⚠ Please clarify:
    ' f'
    ' ) clar_row_visible = True else: clar_html_val = '✓ Clarified — classification updated!' clar_row_visible = False area_choices = areas if areas else ["Work","Health","Finance","Learning","Personal","Family","Other"] area_val = result.get("life_area") or (area_choices[0] if area_choices else "Work") return ( result.get("title", original_task), clar_html_val, area_val, result.get("urgency", "Not Urgent"), result.get("importance", "Important"), result.get("state_of_mind", "Easy"), int(result.get("time_estimate") or 30), gr.update(visible=clar_row_visible), remaining, ) def handle_transcribe_voice(audio_path, user_id): """Whisper transcription then Groq parse — same pipeline as notebook.""" if audio_path is None: return gr.update(visible=False), _err("No audio recorded.") try: from faster_whisper import WhisperModel global _whisper_model if _whisper_model is None: _whisper_model = WhisperModel("small", device="cpu", compute_type="int8") segments, _ = _whisper_model.transcribe(audio_path) text = " ".join(seg.text for seg in segments).strip() return gr.update(visible=True, value=text), _ok(f'Transcribed: "{text[:60]}..."') except Exception as e: return gr.update(visible=False), _err(f"Transcription error: {e}") _whisper_model = None def handle_confirm_task(user_id, title, area, urgency, importance, state, time_est, deadline, is_habit, habit_interval): if not user_id: return _err("Not logged in."), [], _stat("—","Today","#a78bfa"), _stat("—","Done","#4ade80"), _stat("—","Left","#f87171"), [], gr.update(visible=False) if not title.strip(): return _err("Title cannot be empty."), [], _stat("—","Today","#a78bfa"), _stat("—","Done","#4ade80"), _stat("—","Left","#f87171"), [], gr.update(visible=True) task = { "title": title, "life_area": area, "urgency": urgency, "importance": importance, "state_of_mind": state, "time_estimate": int(time_est or 30), "deadline_date": (deadline or "").strip(), "is_habit": is_habit, "habit_interval": habit_interval if is_habit else "", } save_task(user_id, task, scheduled_date="") # saved unscheduled — planner assigns date rows, s1, s2, s3 = refresh_today(user_id) all_rows = _fmt_tasks(get_tasks(user_id)) return _ok("✓ Task saved! Use 'Plan My Day' to schedule it."), rows, s1, s2, s3, all_rows, gr.update(visible=False) def _render_sched_html(sched): COLOR = {"Flow": "#0ea5e9", "Easy": "#4ade80", "Quick": "#a78bfa", "Personal": "#f87171"} html = ( f'
    ' f'
    📅 {sched.get("schedule_date","Today")}
    ' f'
    {sched.get("day_summary","")}
    ' f'
    ' ) for t in sched.get("scheduled_tasks", []): sm = t.get("state_of_mind", "Easy") c = COLOR.get(sm, "#6366f1") html += ( f'
    ' f'
    {t.get("start_time","?")} – {t.get("end_time","?")}
    ' f'
    {t.get("title","")}
    ' f'
    {t.get("life_area","—")} · {sm} · {t.get("duration_minutes","?")}min
    ' f'
    {t.get("scheduling_reason","")}
    ' f'
    ' ) if sched.get("deferred_tasks"): html += '
    Deferred
    ' for t in sched["deferred_tasks"]: html += f'
    ✗ {t["title"]} — {t.get("reason","")}
    ' for w in sched.get("warnings", []): html += f'
    ⚠ {w}
    ' return html def handle_generate_schedule(user_id, prompt): """Step 1: generate proposal only. Nothing written to DB.""" if not user_id: return _err("Not logged in."), "", gr.update(visible=False) tasks = get_tasks(user_id, include_completed=False) if not tasks: return _err("No tasks found. Add some tasks first."), "", gr.update(visible=False) ctx = _ensure_context(user_id) goals = get_goals(user_id) try: sched = generate_schedule(ctx, tasks, prompt or "Schedule all my tasks intelligently across upcoming days.", goals) except Exception as e: return _err(f"Scheduling error: {e}"), "", gr.update(visible=False) if "error" in sched and "scheduled_tasks" not in sched: return _err(f"Scheduling failed: {sched.get('error')}"), "", gr.update(visible=False) html = _render_sched_html(sched) return html, _json.dumps(sched), gr.update(visible=True) def handle_confirm_schedule(user_id, pending_json): """Step 2a: apply the proposed schedule to the database.""" if not user_id or not pending_json: return _err("Nothing to confirm."), gr.update(visible=False), [], [] try: sched = _json.loads(pending_json) except Exception: return _err("Could not read proposal."), gr.update(visible=False), [], [] count = 0 sched_date = sched.get("schedule_date", str(date.today())) for t in sched.get("scheduled_tasks", []): tid = t.get("task_id") or t.get("id") if tid: try: assign_task_date(int(tid), user_id, sched_date) count += 1 except Exception: pass all_rows = _fmt_tasks(get_tasks(user_id)) today_rows = _fmt_tasks(get_tasks(user_id, only_today=True)) return _ok(f"✓ {count} tasks scheduled for {sched_date}!"), gr.update(visible=False), all_rows, today_rows def handle_refine_schedule(user_id, refinement, pending_json): """Step 2b: re-run with a refinement instruction.""" if not user_id or not refinement.strip(): return gr.update(), gr.update(), gr.update() hint = "" if pending_json: try: old = _json.loads(pending_json) hint = f"\n\nPrevious plan summary: {old.get('day_summary', '')}" except Exception: pass return handle_generate_schedule(user_id, refinement + hint) def handle_manual_assign(task_id, task_date, user_id): """Manually assign a date to a specific task.""" if not task_id or not task_date or not user_id: return _err("Enter task ID and date.") try: assign_task_date(int(task_id), user_id, task_date.strip()) return _ok(f"Task #{int(task_id)} assigned to {task_date.strip()}") except Exception as e: return _err(f"Error: {e}") def handle_toggle_today(task_id, user_id): if task_id and user_id: toggle_task_complete(int(task_id), user_id) rows, s1, s2, s3 = refresh_today(user_id) return rows, s1, s2, s3, _ok("Updated") def handle_delete_today(task_id, user_id): if task_id and user_id: delete_task(int(task_id), user_id) rows, s1, s2, s3 = refresh_today(user_id) return rows, s1, s2, s3, _ok("Deleted") # ═══════════════════════════════════════════════════════════════════════════════ # ALL TASKS TAB HANDLERS # ═══════════════════════════════════════════════════════════════════════════════ def refresh_all_tasks(user_id, filter_area="All", only_today=False): if not user_id: return [] return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today)) def handle_toggle_all(task_id, user_id, filter_area, only_today): if task_id and user_id: toggle_task_complete(int(task_id), user_id) return refresh_all_tasks(user_id, filter_area, only_today), _ok("Updated") def handle_delete_all(task_id, user_id, filter_area, only_today): if task_id and user_id: delete_task(int(task_id), user_id) return refresh_all_tasks(user_id, filter_area, only_today), _ok("Deleted") # ═══════════════════════════════════════════════════════════════════════════════ # JOURNAL TAB HANDLERS # ═══════════════════════════════════════════════════════════════════════════════ def handle_load_journal_tasks(user_id): if not user_id: return "

    Not logged in.

    ", gr.update(visible=False), [] tasks = get_tasks(user_id, only_today=True) if not tasks: return "

    No tasks for today.

    ", gr.update(visible=False), [] rows = "" state = [] for t in tasks: status = "✅" if t["is_completed"] else "⬜" actual = f" ({t['actual_duration']}m actual)" if t["actual_duration"] else "" rows += ( f'
    ' f'#{t["id"]}' f'{status}' f'{t["title"]}' f'{t["life_area"] or "—"}' f'{t["time_estimate"]}m{actual}' f'
    ' ) state.append({ "task_id": t["id"], "title": t["title"], "state_of_mind": t["state_of_mind"], "start_time": "", "end_time": "", "time_estimate": t["time_estimate"], "completed": bool(t["is_completed"]), "actual_duration": t["actual_duration"], }) return f'
    {rows}
    ', gr.update(visible=True), state def handle_journal_mark(task_id, actual_mins, user_id, tasks_state, mark_done: bool): if not task_id or not user_id: return tasks_state, _err("Enter a task ID.") tid = int(task_id) if mark_done: toggle_task_complete(tid, user_id, int(actual_mins) if actual_mins else None) else: toggle_task_complete(tid, user_id) for t in tasks_state: if t["task_id"] == tid: t["completed"] = mark_done if mark_done and actual_mins: t["actual_duration"] = int(actual_mins) return tasks_state, _ok("Marked ✅" if mark_done else "Unmarked") def handle_start_journal(user_id, tasks_state): if not user_id: return [], [], False, gr.update(interactive=False), gr.update(interactive=False), _err("Not logged in.") if not tasks_state: return [], [], False, gr.update(interactive=False), gr.update(interactive=False), _err("Load today's tasks first.") ctx = _ensure_context(user_id) opening = build_opening_question(ctx, tasks_state) chat = [{"role": "assistant", "content": opening["question"]}] hist = [{"role": "assistant", "content": opening["question"], "focus": opening["question_focus"]}] return ( chat, hist, True, gr.update(interactive=True), gr.update(interactive=True), _ok("Session started — type your answer below"), ) def handle_send_answer(user_id, answer, chat, hist, tasks_state, active): if not active or not answer.strip(): return chat, hist, "", "" chat = list(chat) + [{"role": "user", "content": answer}] hist = list(hist) + [{"role": "user", "content": answer}] ctx = _ensure_context(user_id) result = get_next_journal_question(ctx, tasks_state, hist) if result.get("session_complete"): chat = list(chat) + [{"role": "assistant", "content": "✅ That's enough for today. Click **Finish & Save** to update your profile."}] return chat, hist, "", _ok("Session complete — save when ready") next_q = result.get("question", "") hist.append({"role": "assistant", "content": next_q, "focus": result.get("question_focus", "")}) chat = list(chat) + [{"role": "assistant", "content": next_q}] return chat, hist, "", "" def handle_finish_journal(user_id, chat, hist, tasks_state): if not user_id or not hist: return chat, _err("Nothing to save.") ctx = _ensure_context(user_id) updated = synthesize_journal(ctx, tasks_state, hist) save_user_context(user_id, updated) total = len(tasks_state) done = sum(1 for t in tasks_state if t.get("completed")) rate = round(done / total * 100) if total else 0 notes = updated.get("learned_patterns", {}).get("notes", []) note_html = "".join(f"
  • {n}
  • " for n in notes[-3:]) if notes else "
  • No new patterns yet
  • " return ( chat, _ok(f"Insights saved! {done}/{total} tasks ({rate}%) completed.") + f'' ) def handle_restart_journal(): return [], [], False, gr.update(interactive=False), gr.update(interactive=False), "" # ═══════════════════════════════════════════════════════════════════════════════ # LIFE AREAS & GOALS HANDLERS # ═══════════════════════════════════════════════════════════════════════════════ def render_areas(user_id): if not user_id: return "", [], [] areas = get_life_areas(user_id) chips = "" for a in areas: chips += ( f'' f'{a["name"]} ' ) html = f'
    {chips}
    ' if chips else '

    No areas yet.

    ' names = [a["name"] for a in areas] return html, names, names def handle_add_area(user_id, name, color): ok, msg = add_life_area(user_id, name, color) css = "ok" if ok else "err" html, names, _ = render_areas(user_id) return ( f'{msg}', html, gr.update(choices=names, value=None), gr.update(value=""), ) def handle_del_area(user_id, name): if not name: return _err("Select an area first."), "", [] ok, msg = delete_life_area(user_id, name) html, names, _ = render_areas(user_id) return ( f'{msg}', html, gr.update(choices=names, value=None), ) def handle_save_goals(user_id, goals_text): if not user_id: return _err("Not logged in.") save_goals(user_id, goals_text) return _ok("Goals saved!") def load_goals_txt(user_id): if not user_id: return "" return "\n".join(get_goals(user_id)) # ═══════════════════════════════════════════════════════════════════════════════ # PREFERENCES HANDLERS # ═══════════════════════════════════════════════════════════════════════════════ def load_prefs(user_id): ctx = load_user_context(user_id) if user_id else None if not ctx: return "07:30", "23:00", "Morning", 10, 90 p = ctx.get("preferences", {}) return ( p.get("wake_time", "07:30"), p.get("sleep_time", "23:00"), p.get("focus_peak", "Morning"), p.get("break_duration_minutes", 10), p.get("max_flow_block_minutes", 90), ) def handle_save_prefs(user_id, wake, sleep, focus, brk, flow_max): if not user_id: return _err("Not logged in.") ctx = _ensure_context(user_id) ctx["preferences"].update({ "wake_time": wake or "07:30", "sleep_time": sleep or "23:00", "focus_peak": focus or "Morning", "break_duration_minutes": int(brk or 10), "max_flow_block_minutes": int(flow_max or 90), }) save_user_context(user_id, ctx) return _ok("Preferences saved!") def render_context_html(user_id): if not user_id: return "

    Not logged in.

    " ctx = load_user_context(user_id) if not ctx: return "

    No AI context yet. Complete a journaling session to start building it.

    " lp = ctx.get("learned_patterns", {}) sf = ctx.get("scheduling_feedback", {}) pref = ctx.get("preferences", {}) def row(label, val): return ( f'
    ' f'{label}' f'{val}' f'
    ' ) def lst(v): return ", ".join(v) if v else "—" html = ( f'
    ' f'
    ' f'
    Preferences
    ' f'{row("Wake", pref.get("wake_time","—"))}' f'{row("Sleep", pref.get("sleep_time","—"))}' f'{row("Peak focus", pref.get("focus_peak","—"))}' f'{row("Break", str(pref.get("break_duration_minutes","—")) + "min")}' f'{row("Max flow block", str(pref.get("max_flow_block_minutes","—")) + "min")}' f'
    ' f'
    ' f'
    Scheduling Stats
    ' f'{row("Days scheduled", sf.get("total_days_scheduled",0))}' f'{row("Avg completion", str(round(sf.get("avg_completion_rate",0)*100)) + "%")}' f'{row("Context version", ctx.get("version",1))}' f'{row("Last updated", (ctx.get("last_updated","—") or "—")[:16])}' f'
    ' f'
    ' f'
    ' f'
    Learned Patterns
    ' f'{row("Productive times", lst(lp.get("productive_times",[])))}' f'{row("Low energy times", lst(lp.get("low_energy_times",[])))}' f'{row("Avg task overrun", str(lp.get("avg_task_overrun_pct",0)) + "%")}' f'{row("Flow batchable", str(lp.get("flow_batch_capable","Unknown")))}' f'{row("Best morning areas", lst(lp.get("best_life_areas_morning",[])))}' f'{row("Skipped types", lst(lp.get("common_skipped_task_types",[])))}' f'
    ' ) notes = lp.get("notes", []) if notes: note_rows = "".join( f'
    • {n}
    ' for n in notes[-5:] ) html += f'
    AI Notes
    {note_rows}
    ' history = ctx.get("history_summary", []) if history: hist_rows = "".join( f'
    • {h}
    ' for h in history[-5:] ) html += f'
    Recent History
    {hist_rows}
    ' return html # ═══════════════════════════════════════════════════════════════════════════════ # CALENDAR VIEW # ═══════════════════════════════════════════════════════════════════════════════ def render_calendar(user_id): if not user_id: return "

    Sign in to view calendar.

    " tasks = get_tasks(user_id) if not tasks: return "

    No tasks yet.

    " _today = date.today() COLOR = {"Flow":"#0ea5e9","Easy":"#4ade80","Quick":"#a78bfa","Personal":"#f87171"} by_date, unscheduled = {}, [] for t in tasks: sd = (t.get("scheduled_date") or "").strip() if sd: by_date.setdefault(sd, []).append(t) else: unscheduled.append(t) html = "" for d in sorted(by_date.keys()): try: dt = date.fromisoformat(d) diff = (dt - _today).days if diff == 0: label = f"TODAY — {dt.strftime('%a, %b %d')}" elif diff == 1: label = f"TOMORROW — {dt.strftime('%a, %b %d')}" elif diff < 0: label = dt.strftime('%a, %b %d') + " (past)" else: label = dt.strftime('%a, %b %d') hdr = "#a78bfa" if diff == 0 else "#60a5fa" if diff > 0 else "#334155" except ValueError: label, hdr = d, "#60a5fa" html += (f'
    ' f'
    ' f'📅 {label}
    ') for t in by_date[d]: sm = t.get("state_of_mind","Easy") c = COLOR.get(sm,"#6366f1") done = t.get("is_completed") dl = (t.get("deadline_date") or "").strip() dl_badge = (f'due {dl}' ) if dl and dl.lower() not in ("null","none","") else "" html += (f'
    ' f'
    {"✅" if done else "⬜"} ' f'#{t["id"]} {t["title"]}{dl_badge}
    ' f'
    ' f'{t.get("life_area","—")} · {sm} · {t.get("time_estimate","?")}min
    ') html += '
    ' if unscheduled: html += ('
    ' '⬜ UNSCHEDULED — Use Plan My Day to assign dates
    ') for t in unscheduled: sm = t.get("state_of_mind","Easy") c = COLOR.get(sm,"#6366f1") dl = (t.get("deadline_date") or "").strip() dl_badge = (f'due {dl}' ) if dl and dl.lower() not in ("null","none","") else "" html += (f'
    ' f'
    #{t["id"]} {t["title"]}{dl_badge}
    ' f'
    ' f'{t.get("life_area","—")} · {sm} · {t.get("time_estimate","?")}min
    ') html += '
    ' return html or "

    No tasks.

    " # ═══════════════════════════════════════════════════════════════════════════════ # UI BUILD # ═══════════════════════════════════════════════════════════════════════════════ with gr.Blocks(title="🧠 Second Brain") as demo: # ── Session state ────────────────────────────────────────────────────────── user_id_state = gr.State(None) username_state = gr.State("") # Journal states journal_chat_state = gr.State([]) journal_hist_state = gr.State([]) journal_tasks_state = gr.State([]) journal_active = gr.State(False) # Clarification states clar_questions_state = gr.State([]) original_task_state = gr.State("") # ═════════════════════════════════════════════════════════════════════════ # AUTH SECTION # ═════════════════════════════════════════════════════════════════════════ with gr.Column(visible=True, elem_id="auth-card") as auth_section: gr.HTML('') gr.HTML('
    Your intelligent productivity companion
    ') with gr.Tabs(): # ── Sign In ──────────────────────────────────────────────────── with gr.Tab("Sign In"): login_user_in = gr.Textbox(label="Username", placeholder="your username") login_pass_in = gr.Textbox(label="Password", type="password", placeholder="••••••••") login_btn = gr.Button("Sign In", elem_classes="btn-primary") login_msg = gr.HTML("") # ── Create Account ───────────────────────────────────────────── with gr.Tab("Create Account"): reg_user_in = gr.Textbox(label="Username", placeholder="choose a username") reg_pass_in = gr.Textbox(label="Password (min 6 chars)", type="password", placeholder="••••••••") reg_wake = gr.Textbox(label="Wake time", value="07:30") reg_sleep = gr.Textbox(label="Sleep time", value="23:00") reg_focus = gr.Dropdown(label="Peak focus", choices=["Morning","Afternoon","Evening","Night"], value="Morning") reg_goals = gr.Textbox( label="Big-picture goals (one per line, optional)", lines=3, placeholder="Launch my startup\nGet fit\nLearn machine learning" ) reg_btn = gr.Button("Create Account", elem_classes="btn-primary") reg_msg = gr.HTML("") # ═════════════════════════════════════════════════════════════════════════ # MAIN APP SECTION # ═════════════════════════════════════════════════════════════════════════ with gr.Column(visible=False) as app_section: # ── Top header ───────────────────────────────────────────────────── with gr.Row(elem_id="top-header"): header_html = gr.HTML('
    🧠 Second Brain
    ') logout_btn = gr.Button("Sign Out", elem_classes="btn-secondary", scale=0) # ── Tabs ──────────────────────────────────────────────────────────── with gr.Tabs() as main_tabs: # ───────────────────────────────────────────────────────────── # TAB 1: TODAY # ───────────────────────────────────────────────────────────── with gr.Tab("📅 Today"): # Stats row with gr.Row(): stat_total = gr.HTML(_stat("—","Today","#a78bfa")) stat_done = gr.HTML(_stat("—","Done","#4ade80")) stat_remain = gr.HTML(_stat("—","Left","#f87171")) # Action buttons with gr.Row(): btn_add_text = gr.Button("✏️ Add Task (Text)", elem_classes="btn-primary", scale=3) btn_add_voice = gr.Button("🎙️ Add Task (Voice)", elem_classes="btn-accent", scale=3) btn_plan_day = gr.Button("🗓 Plan My Day", elem_classes="btn-secondary", scale=3) btn_refresh = gr.Button("↻", elem_classes="btn-secondary", scale=1) # ── Text add panel ──────────────────────────────────────── with gr.Column(visible=False, elem_classes="panel") as text_panel: gr.HTML('
    Describe Your Task
    ') task_input = gr.Textbox( label="", placeholder="e.g. Finish the client proposal by tomorrow, it's really important", lines=2 ) with gr.Row(): btn_parse = gr.Button("🤖 Parse with AI", elem_classes="btn-primary") btn_cancel_text = gr.Button("Cancel", elem_classes="btn-secondary") with gr.Column(visible=False) as parsed_panel: parse_status = gr.HTML("") clar_html = gr.HTML("") # ── Clarification reply row (shown only when AI has questions) ── with gr.Column(visible=False) as clar_reply_row: gr.HTML('
    Your Answer
    ') clar_reply_input = gr.Textbox( label="", placeholder="e.g. It's for a client deadline, very high priority, should take about 2 hours", lines=2 ) btn_clar_submit = gr.Button("🔄 Re-classify with my answer", elem_classes="btn-accent") gr.HTML('
    Review & Confirm
    ') with gr.Row(): f_title = gr.Textbox(label="Title", scale=3) f_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1) with gr.Row(): f_urgency = gr.Dropdown(label="Urgency", choices=["Urgent","Not Urgent","Habit"], value="Not Urgent") f_importance = gr.Dropdown(label="Importance", choices=["Move the Needle","Important","Not Important"], value="Important") f_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy") with gr.Row(): f_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1) f_date = gr.Textbox(label="Deadline (YYYY-MM-DD, optional)", placeholder="e.g. 2026-03-15", value="", scale=1) f_is_habit = gr.Checkbox(label="♻️ This is a habit", scale=1) f_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1) with gr.Row(): btn_confirm = gr.Button("✓ Save Task", elem_classes="btn-success") btn_discard = gr.Button("✗ Discard", elem_classes="btn-danger") save_msg = gr.HTML("") # ── Voice add panel ─────────────────────────────────────── with gr.Column(visible=False, elem_classes="panel") as voice_panel: gr.HTML('
    Record or Upload Audio
    ') gr.HTML('

    Whisper transcribes it, then AI classifies the task automatically.

    ') audio_input = gr.Audio(sources=["microphone","upload"], type="filepath", label="") transcribed = gr.Textbox(label="Transcribed text (editable before parsing)", visible=False, lines=2) with gr.Row(): btn_transcribe = gr.Button("🤖 Transcribe & Parse", elem_classes="btn-primary") btn_cancel_voice = gr.Button("Cancel", elem_classes="btn-secondary") voice_msg = gr.HTML("") # ── Plan My Day panel ───────────────────────────────────── with gr.Column(visible=False, elem_classes="panel") as plan_panel: gr.HTML('
    Plan My Day
    ') gr.HTML('

    AI schedules ALL your unscheduled tasks using your patterns and goals. Review before applying.

    ') plan_prompt = gr.Textbox( label="Instructions (optional)", placeholder="e.g. 'Schedule everything for this week' or 'I have a meeting at 2pm, focus on Work tasks'", lines=2 ) with gr.Row(): btn_gen_sched = gr.Button("🧠 Generate Plan", elem_classes="btn-primary") btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary") schedule_html = gr.HTML("") sched_pending = gr.State("") with gr.Column(visible=False) as sched_confirm_row: gr.HTML('
    ' '👆 Review the plan above, then:
    ') with gr.Row(): btn_sched_ok = gr.Button("✅ Confirm & Apply", elem_classes="btn-success", scale=2) btn_sched_discard = gr.Button("✗ Discard", elem_classes="btn-danger", scale=1) gr.HTML('
    💬 Or refine it:
    ') with gr.Row(): sched_refine_txt = gr.Textbox(label="", placeholder="e.g. Move Flow tasks to morning, skip Fridays", scale=4) btn_sched_refine = gr.Button("🔄 Re-plan", elem_classes="btn-accent", scale=1) gr.HTML('
    📌 Or manually assign a task:
    ') with gr.Row(): manual_tid = gr.Number(label="Task ID", precision=0, scale=1) manual_date = gr.Textbox(label="Date (YYYY-MM-DD)", placeholder=str(date.today()), scale=2) btn_manual = gr.Button("Assign Date", elem_classes="btn-secondary", scale=1) manual_msg = gr.HTML("") sched_status = gr.HTML("") # ── Today's task table ──────────────────────────────────── gr.HTML('
    Today\'s Tasks
    ') today_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True) with gr.Row(): today_done_id = gr.Number(label="Task ID — toggle done", precision=0, scale=2) btn_today_done = gr.Button("✅ Mark Done", elem_classes="btn-success", scale=2) today_del_id = gr.Number(label="Task ID — delete", precision=0, scale=2) btn_today_del = gr.Button("🗑 Delete", elem_classes="btn-danger", scale=2) today_action_msg = gr.HTML("") # ───────────────────────────────────────────────────────────── # TAB 2: ALL TASKS # ───────────────────────────────────────────────────────────── with gr.Tab("📋 All Tasks"): with gr.Row(): all_filter = gr.Dropdown(label="Filter by Area", choices=["All"], value="All", scale=4) all_today_chk = gr.Checkbox(label="Today only", value=False, scale=1) btn_all_ref = gr.Button("↻ Refresh", elem_classes="btn-secondary", scale=1) all_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True) with gr.Row(): all_done_id = gr.Number(label="Task ID — toggle done", precision=0, scale=2) btn_all_done = gr.Button("✅ Done", elem_classes="btn-success", scale=2) all_del_id = gr.Number(label="Task ID — delete", precision=0, scale=2) btn_all_del = gr.Button("🗑 Delete", elem_classes="btn-danger", scale=2) all_action_msg = gr.HTML("") # ───────────────────────────────────────────────────────────── # TAB 3: CALENDAR # ───────────────────────────────────────────────────────────── with gr.Tab("📆 Calendar"): gr.HTML('

    All tasks grouped by scheduled date. Unscheduled tasks appear at the bottom — use Plan My Day to assign them.

    ') with gr.Row(): btn_cal_refresh = gr.Button("↻ Refresh", elem_classes="btn-secondary", scale=1) calendar_html = gr.HTML("") # ───────────────────────────────────────────────────────────── # TAB 4: JOURNAL # ───────────────────────────────────────────────────────────── with gr.Tab("📓 Journal"): gr.HTML('

    Mark today\'s tasks complete, then reflect. The AI updates your profile to improve tomorrow\'s schedule.

    ') # Step 1 — Mark tasks with gr.Column(elem_classes="panel"): gr.HTML('
    Step 1 — Mark Tasks Complete
    ') journal_task_list = gr.HTML('

    Click "Load Tasks" to begin.

    ') btn_load_tasks = gr.Button("Load Today's Tasks", elem_classes="btn-secondary") with gr.Row(visible=False) as journal_mark_row: j_task_id = gr.Number(label="Task ID", precision=0, scale=2) j_actual_min = gr.Number(label="Actual time (min)", precision=0, value=0, scale=2) btn_j_done = gr.Button("✅ Mark Complete", elem_classes="btn-success", scale=2) btn_j_undone = gr.Button("↩ Unmark", elem_classes="btn-secondary", scale=1) journal_mark_msg = gr.HTML("") # Step 2 — Chat with gr.Column(elem_classes="panel"): gr.HTML('
    Step 2 — Reflect
    ') journal_chatbot = gr.Chatbot(label="", height=380) with gr.Row(): btn_j_start = gr.Button("▶ Start Reflection", elem_classes="btn-primary", scale=2) btn_j_finish = gr.Button("✓ Finish & Save Insights", elem_classes="btn-success", scale=2) btn_j_restart = gr.Button("↺ Restart", elem_classes="btn-secondary", scale=1) j_answer = gr.Textbox(label="Your answer", placeholder="Type here…", lines=2, interactive=False) btn_j_send = gr.Button("Send →", elem_classes="btn-accent", interactive=False) journal_msg = gr.HTML("") # ───────────────────────────────────────────────────────────── # TAB 4: LIFE AREAS & GOALS # ───────────────────────────────────────────────────────────── with gr.Tab("🗂 Areas & Goals"): gr.HTML('
    Your Life Areas
    ') areas_display = gr.HTML("") with gr.Row(): new_area_name = gr.Textbox(label="New area name", placeholder="e.g. Side Project", scale=3) new_area_color = gr.ColorPicker(label="Color", value="#6366f1", scale=1) btn_add_area = gr.Button("Add", elem_classes="btn-primary", scale=1) area_msg = gr.HTML("") gr.HTML('
    Remove Area
    ') with gr.Row(): del_area_dd = gr.Dropdown(label="Select area to remove", choices=[], scale=4) btn_del_area = gr.Button("Remove", elem_classes="btn-danger", scale=1) del_area_msg = gr.HTML("") gr.HTML('
    ') gr.HTML('
    Big-Picture Goals
    ') gr.HTML('

    The AI uses these when scheduling and prioritising your tasks.

    ') goals_input = gr.Textbox(label="One goal per line", lines=5, placeholder="Launch my SaaS\nRun 5K\nLearn ML") btn_save_goals = gr.Button("Save Goals", elem_classes="btn-primary") goals_msg = gr.HTML("") # ───────────────────────────────────────────────────────────── # TAB 5: PREFERENCES # ───────────────────────────────────────────────────────────── with gr.Tab("⚙️ Preferences"): gr.HTML('
    Scheduling Preferences
    ') with gr.Row(): pref_wake = gr.Textbox(label="Wake time", placeholder="07:30", scale=1) pref_sleep = gr.Textbox(label="Sleep time", placeholder="23:00", scale=1) pref_focus = gr.Dropdown(label="Peak focus", choices=["Morning","Afternoon","Evening","Night"], value="Morning", scale=1) with gr.Row(): pref_break = gr.Number(label="Break between tasks (min)", value=10, minimum=0, scale=1) pref_flow_max = gr.Number(label="Max Flow block (min)", value=90, minimum=30, scale=1) btn_save_prefs = gr.Button("Save Preferences", elem_classes="btn-primary") prefs_msg = gr.HTML("") gr.HTML('
    ') gr.HTML('
    AI Memory
    ') gr.HTML('

    Everything the AI has learned about you. Updates after each journaling session.

    ') ctx_display = gr.HTML("") btn_ref_ctx = gr.Button("↻ Refresh", elem_classes="btn-secondary") # ═══════════════════════════════════════════════════════════════════════ # EVENT WIRING # ═══════════════════════════════════════════════════════════════════════ # ── Auth ────────────────────────────────────────────────────────────── def _post_login(uid, uname): if not uid: return uid, uname spawn_due_habits(uid) return uid, uname login_btn.click( handle_login, [login_user_in, login_pass_in], [user_id_state, username_state, login_msg, auth_section, app_section] ).then( lambda uid, uname: f'
    🧠 Second Brain
    👤 {uname}
    ', [user_id_state, username_state], [header_html] ).then( refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain] ).then( lambda uid: _fmt_tasks(get_tasks(uid)) if uid else [], [user_id_state], [all_df] ).then( render_calendar, [user_id_state], [calendar_html] ).then( lambda uid: render_areas(uid)[0:2], [user_id_state], [areas_display, del_area_dd] ).then( load_goals_txt, [user_id_state], [goals_input] ).then( lambda uid: load_prefs(uid), [user_id_state], [pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max] ) reg_btn.click( handle_register, [reg_user_in, reg_pass_in, reg_wake, reg_sleep, reg_focus, reg_goals], [user_id_state, username_state, reg_msg, auth_section, app_section] ).then( lambda uid, uname: f'
    🧠 Second Brain
    👤 {uname}
    ', [user_id_state, username_state], [header_html] ).then( refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain] ).then( lambda uid: _fmt_tasks(get_tasks(uid)) if uid else [], [user_id_state], [all_df] ).then( render_calendar, [user_id_state], [calendar_html] ).then( lambda uid: render_areas(uid)[0:2], [user_id_state], [areas_display, del_area_dd] ) logout_btn.click( handle_logout, [user_id_state], [user_id_state, username_state, auth_section, app_section] ) # ── Today tab ───────────────────────────────────────────────────────── btn_add_text.click(show_text_panel, outputs=[text_panel, voice_panel, plan_panel]) btn_add_voice.click(show_voice_panel, outputs=[text_panel, voice_panel, plan_panel]) btn_plan_day.click(show_plan_panel, outputs=[text_panel, voice_panel, plan_panel]) btn_cancel_text.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel]) btn_cancel_voice.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel]) btn_cancel_plan.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel]) btn_refresh.click( refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain] ) # Show habit interval dropdown only when habit checked f_is_habit.change( lambda v: gr.update(visible=v), [f_is_habit], [f_interval] ) btn_parse.click( handle_parse_text, [task_input, user_id_state], [parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance, f_state, f_time, f_date, f_is_habit, f_interval, parse_status, clar_reply_row, clar_questions_state, original_task_state] ) btn_clar_submit.click( handle_clarification_reply, [clar_reply_input, original_task_state, clar_questions_state, user_id_state], [f_title, clar_html, f_area, f_urgency, f_importance, f_state, f_time, clar_reply_row, clar_questions_state] ).then( lambda: "", outputs=[clar_reply_input] ) btn_transcribe.click( handle_transcribe_voice, [audio_input, user_id_state], [transcribed, voice_msg] ) btn_discard.click( lambda: gr.update(visible=False), outputs=[parsed_panel] ) btn_confirm.click( handle_confirm_task, [user_id_state, f_title, f_area, f_urgency, f_importance, f_state, f_time, f_date, f_is_habit, f_interval], [save_msg, today_df, stat_total, stat_done, stat_remain, all_df, parsed_panel] ).then(render_calendar, [user_id_state], [calendar_html]) btn_gen_sched.click( handle_generate_schedule, [user_id_state, plan_prompt], [schedule_html, sched_pending, sched_confirm_row] ) btn_sched_ok.click( handle_confirm_schedule, [user_id_state, sched_pending], [sched_status, sched_confirm_row, all_df, today_df] ).then(render_calendar, [user_id_state], [calendar_html]) btn_sched_discard.click( lambda: ("", gr.update(visible=False)), outputs=[schedule_html, sched_confirm_row] ) btn_sched_refine.click( handle_refine_schedule, [user_id_state, sched_refine_txt, sched_pending], [schedule_html, sched_pending, sched_confirm_row] ).then(lambda: "", outputs=[sched_refine_txt]) btn_manual.click( handle_manual_assign, [manual_tid, manual_date, user_id_state], [manual_msg] ).then( lambda uid: _fmt_tasks(get_tasks(uid)) if uid else [], [user_id_state], [all_df] ).then(render_calendar, [user_id_state], [calendar_html]) btn_today_done.click( handle_toggle_today, [today_done_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg] ).then( lambda uid: _fmt_tasks(get_tasks(uid)) if uid else [], [user_id_state], [all_df] ).then(render_calendar, [user_id_state], [calendar_html]) btn_today_del.click( handle_delete_today, [today_del_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg] ).then( lambda uid: _fmt_tasks(get_tasks(uid)) if uid else [], [user_id_state], [all_df] ).then(render_calendar, [user_id_state], [calendar_html]) # ── Calendar tab ────────────────────────────────────────────────────────── btn_cal_refresh.click(render_calendar, [user_id_state], [calendar_html]) # ── All Tasks tab ────────────────────────────────────────────────────── btn_all_ref.click( refresh_all_tasks, [user_id_state, all_filter, all_today_chk], [all_df] ) all_filter.change( refresh_all_tasks, [user_id_state, all_filter, all_today_chk], [all_df] ) all_today_chk.change( refresh_all_tasks, [user_id_state, all_filter, all_today_chk], [all_df] ) btn_all_done.click( handle_toggle_all, [all_done_id, user_id_state, all_filter, all_today_chk], [all_df, all_action_msg] ).then(render_calendar, [user_id_state], [calendar_html]) btn_all_del.click( handle_delete_all, [all_del_id, user_id_state, all_filter, all_today_chk], [all_df, all_action_msg] ).then(render_calendar, [user_id_state], [calendar_html]) # ── Journal tab ──────────────────────────────────────────────────────── btn_load_tasks.click( handle_load_journal_tasks, [user_id_state], [journal_task_list, journal_mark_row, journal_tasks_state] ) btn_j_done.click( lambda tid, mins, uid, st: handle_journal_mark(tid, mins, uid, st, True), [j_task_id, j_actual_min, user_id_state, journal_tasks_state], [journal_tasks_state, journal_mark_msg] ).then( handle_load_journal_tasks, [user_id_state], [journal_task_list, journal_mark_row, journal_tasks_state] ) btn_j_undone.click( lambda tid, mins, uid, st: handle_journal_mark(tid, mins, uid, st, False), [j_task_id, j_actual_min, user_id_state, journal_tasks_state], [journal_tasks_state, journal_mark_msg] ).then( handle_load_journal_tasks, [user_id_state], [journal_task_list, journal_mark_row, journal_tasks_state] ) btn_j_start.click( handle_start_journal, [user_id_state, journal_tasks_state], [journal_chatbot, journal_hist_state, journal_active, j_answer, btn_j_send, journal_msg] ) btn_j_send.click( handle_send_answer, [user_id_state, j_answer, journal_chatbot, journal_hist_state, journal_tasks_state, journal_active], [journal_chatbot, journal_hist_state, j_answer, journal_msg] ) j_answer.submit( handle_send_answer, [user_id_state, j_answer, journal_chatbot, journal_hist_state, journal_tasks_state, journal_active], [journal_chatbot, journal_hist_state, j_answer, journal_msg] ) btn_j_finish.click( handle_finish_journal, [user_id_state, journal_chatbot, journal_hist_state, journal_tasks_state], [journal_chatbot, journal_msg] ) btn_j_restart.click( handle_restart_journal, outputs=[journal_chatbot, journal_hist_state, journal_active, j_answer, btn_j_send, journal_msg] ) # ── Areas & Goals tab ───────────────────────────────────────────────── btn_add_area.click( handle_add_area, [user_id_state, new_area_name, new_area_color], [area_msg, areas_display, del_area_dd, new_area_name] ).then( lambda uid: gr.update(choices=_area_choices(uid), value="All"), [user_id_state], [all_filter] ) btn_del_area.click( handle_del_area, [user_id_state, del_area_dd], [del_area_msg, areas_display, del_area_dd] ).then( lambda uid: gr.update(choices=_area_choices(uid), value="All"), [user_id_state], [all_filter] ) btn_save_goals.click( handle_save_goals, [user_id_state, goals_input], [goals_msg] ) # ── Preferences tab ─────────────────────────────────────────────────── btn_save_prefs.click( handle_save_prefs, [user_id_state, pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max], [prefs_msg] ) btn_ref_ctx.click( render_context_html, [user_id_state], [ctx_display] ) # Refresh AI context display whenever the refresh button is clicked # (Gradio Tabs.select doesn't support conditional output, so we use the button) # ── Launch ───────────────────────────────────────────────────────────────────── if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860, css=CSS)