diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,1212 +1,1183 @@ """ 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 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, + get_tasks, save_task, 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, smart_schedule_tasks, + 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() +init_groq() # reads GROQ_API_KEY from env -TASK_HEADERS = ["ID", "Done", "Title", "Area", "Urgency", "Importance", "Mind", "Min", "Deadline", "Scheduled"] -_whisper_model = None +TASK_HEADERS = ["ID", "Done", "Title", "Area", "Urgency", "Importance", "Mind", "Min", "Date"] -# ============================================================================= +# ═══════════════════════════════════════════════════════════════════════════════ # SHARED HELPERS -# ============================================================================= +# ═══════════════════════════════════════════════════════════════════════════════ -def _ok(msg): return f'\u2713 {msg}' -def _err(msg): return f'\u26a0 {msg}' -def _info(msg): return f'\u2139 {msg}' +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): +def _fmt_tasks(tasks: list) -> list: rows = [] for t in tasks: - deadline = t.get("deadline_date") or "" - if deadline: - try: - dl = date.fromisoformat(deadline) - days_left = (dl - date.today()).days - if days_left < 0: deadline = f"\u274c {deadline}" - elif days_left == 0: deadline = "\U0001f6a8 TODAY" - elif days_left == 1: deadline = "\u26a0\ufe0f tmrw" - elif days_left <= 3: deadline = f"\u23f0 {deadline}" - except ValueError: - pass rows.append([ t["id"], - "\u2705" if t["is_completed"] else "\u2b1c", - ("\U0001f501 " if t["is_habit"] else "") + t["title"], - t["life_area"] or "\u2014", - t["urgency"] or "\u2014", - t["importance"] or "\u2014", - t["state_of_mind"] or "\u2014", - str(t["time_estimate"]) + "m" if t["time_estimate"] else "\u2014", - deadline or "\u2014", - t["scheduled_date"] or "\u2014 unscheduled", + "✅" 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 "—", + t["scheduled_date"] or "—", ]) return rows def _area_choices(user_id): - return ["All"] + (get_life_area_names(user_id) if user_id else []) + 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 -def _transcribe(audio_path): - global _whisper_model - from faster_whisper import WhisperModel - if _whisper_model is None: - _whisper_model = WhisperModel("small", device="cpu", compute_type="int8") - segments, _ = _whisper_model.transcribe(audio_path) - return " ".join(seg.text for seg in segments).strip() - -def _build_clar_html(clarifications): - if not clarifications: return "" - items = "".join(f"
  • {q}
  • " for q in clarifications) - return ( - f'
    ' - f'
    \u26a0 Please clarify:
    ' - f'
    ' - ) -def _run_parse(task_text, user_id): - 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) - clars = result.get("clarifications_needed", []) - area_choices = areas or ["Work","Health","Finance","Learning","Personal","Family","Other"] - area_val = result.get("life_area") or area_choices[0] - if area_val not in area_choices: - area_choices = [area_val] + area_choices - return result, _build_clar_html(clars), bool(clars), area_val, clars - -def _parse_result_to_outputs(result, clar_html, has_clar, area_val, clars, task_text, status=""): - if not status: - status = _ok("AI classified your task \u2014 answer the questions above." if has_clar else "AI classified your task \u2713") - # Extract deadline from AI result - deadline = result.get("deadline_date") or "" - if deadline and deadline.lower() in ("null","none","n/a",""): deadline = "" - 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), - deadline, - False, "Daily", - status, - gr.update(visible=has_clar), - clars, task_text, - ) - -_EMPTY_PARSE = ( - gr.update(visible=False), "", "", "Work", "Not Urgent", "Important", - "Easy", 30, "", False, "Daily", "", - gr.update(visible=False), [], "", -) - - -# ============================================================================= -# AUTH -# ============================================================================= +# ═══════════════════════════════════════════════════════════════════════════════ +# 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) - return uid, get_username(uid), _ok(msg), gr.update(visible=False), gr.update(visible=True) + 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) - ctx = create_blank_context(uid, {"wake_time": wake or "07:30", "sleep_time": sleep or "23:00", "focus_peak": focus or "Morning"}) + 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) - return uid, get_username(uid), _ok(msg + " Logged in!"), gr.update(visible=False), gr.update(visible=True) + 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 / ALL TASKS -# ============================================================================= +# ═══════════════════════════════════════════════════════════════════════════════ +# TODAY TAB HANDLERS +# ═══════════════════════════════════════════════════════════════════════════════ def refresh_today(user_id): if not user_id: - return [], _stat("\u2014","Today","#a78bfa"), _stat("\u2014","Done","#4ade80"), _stat("\u2014","Left","#f87171") + 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) + 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 refresh_all_tasks_auto(user_id): - if not user_id: return [] - return _fmt_tasks(get_tasks(user_id)) +def show_voice_panel(): + return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False) -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 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) -# ── Text parse ───────────────────────────────────────────────────────────────── def handle_parse_text(task_text, user_id): - if not task_text.strip(): return _EMPTY_PARSE - return _parse_result_to_outputs(*_run_parse(task_text, user_id), task_text) + """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 + + 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), + str(date.today()), + False, + "Daily", + _ok("AI classified your task — please answer the clarification questions above." if has_clarifications else "AI classified your task"), + gr.update(visible=has_clarifications), # clar_reply_row + clarifications, # clar_questions_state + task_text, # original_task_state + ) + 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(),)*8 + (gr.update(visible=True), []) - q_block = "\n".join(f"Q: {q}" for q in clarifications) - enriched = f"{original_task}\n\nUser clarification:\n{q_block}\nA: {user_reply}" - result, clar_html_val, still_has, area_val, remaining = _run_parse(enriched, user_id) - if not still_has: - clar_html_val = '\u2713 Classification updated!' - deadline = result.get("deadline_date") or "" - if deadline and deadline.lower() in ("null","none","n/a",""): deadline = "" + 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), - deadline, gr.update(visible=still_has), remaining, + 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_clar_voice(audio_path, original_task, clarifications, user_id): - if not audio_path: - return (gr.update(),)*8 + (gr.update(visible=True), []) - try: - reply = _transcribe(audio_path) - return handle_clarification_reply(reply, original_task, clarifications, user_id) - except Exception: - return (gr.update(),)*8 + (gr.update(visible=True), clarifications) -def handle_voice_parse(audio_path, user_id): +def handle_transcribe_voice(audio_path, user_id): + """Whisper transcription then Groq parse — same pipeline as notebook.""" if audio_path is None: - return _EMPTY_PARSE[:11] + (_err("No audio recorded."),) + _EMPTY_PARSE[12:] + return gr.update(visible=False), _err("No audio recorded.") try: - text = _transcribe(audio_path) + 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 _EMPTY_PARSE[:11] + (_err(f"Transcription error: {e}"),) + _EMPTY_PARSE[12:] - if not text: - return _EMPTY_PARSE[:11] + (_err("Could not hear anything. Try again."),) + _EMPTY_PARSE[12:] - result, clar_html, has_clar, area_val, clars = _run_parse(text, user_id) - preview = text[:50] + ("\u2026" if len(text) > 50 else "") - status = _ok(f'Heard: "{preview}" \u2014 classified \u2713' + (" Please answer questions." if has_clar else "")) - return _parse_result_to_outputs(result, clar_html, has_clar, area_val, clars, text, status) + 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): + time_est, sched_date, is_habit, habit_interval): if not user_id: - return _err("Not logged in."), [], _stat("\u2014","Today","#a78bfa"), _stat("\u2014","Done","#4ade80"), _stat("\u2014","Left","#f87171"), [], gr.update(visible=False) + 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("\u2014","Today","#a78bfa"), _stat("\u2014","Done","#4ade80"), _stat("\u2014","Left","#f87171"), [], gr.update(visible=True) - save_task(user_id, { - "title": title, "life_area": area, "urgency": urgency, "importance": importance, - "state_of_mind": state, "time_estimate": int(time_est or 30), - "deadline_date": deadline.strip() if deadline else "", - "is_habit": is_habit, "habit_interval": habit_interval if is_habit else "", - }, scheduled_date="") + 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), + "is_habit": is_habit, + "habit_interval": habit_interval if is_habit else "", + } + save_task(user_id, task, sched_date) rows, s1, s2, s3 = refresh_today(user_id) - all_rows = refresh_all_tasks_auto(user_id) - return _ok("Task saved! Use \u2018Plan My Tasks\u2019 to schedule it."), rows, s1, s2, s3, all_rows, gr.update(visible=False) + return _ok("Task saved!"), rows, s1, s2, s3, gr.update(visible=False) -# ── Quick toggle/delete from table (uses selected row ID) ───────────────────── -def handle_action_today(evt: gr.SelectData, tasks_data, user_id, action): - """Called when a row is clicked in today_df then action button pressed.""" - if not user_id or tasks_data is None or len(tasks_data) == 0: return refresh_today(user_id) + (_ok(""),) - # evt.index is [row, col]; grab the ID from col 0 - try: - row_idx = evt.index[0] if hasattr(evt, 'index') else 0 - task_id = int(tasks_data[row_idx][0]) - except Exception: - return refresh_today(user_id) + (_ok("Select a row first"),) - if action == "done": toggle_task_complete(task_id, user_id) - else: delete_task(task_id, user_id) - rows, s1, s2, s3 = refresh_today(user_id) - return rows, s1, s2, s3, _ok("Done" if action=="done" else "Deleted") +def handle_generate_schedule(user_id, prompt): + if not user_id: + return _err("Not logged in.") + tasks = get_tasks(user_id, only_today=True, include_completed=False) + if not tasks: + return _err("No incomplete tasks today to schedule.") + + ctx = _ensure_context(user_id) + goals = get_goals(user_id) + sched = generate_schedule(ctx, tasks, prompt or "Schedule my day sensibly.", goals) + + if "error" in sched and "scheduled_tasks" not in sched: + return _err(f"Scheduling failed: {sched.get('error')}") -def handle_toggle_today_id(task_id, user_id): - if task_id and user_id: toggle_task_complete(int(task_id), user_id) + 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_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_id(task_id, user_id): - if task_id and user_id: delete_task(int(task_id), user_id) + +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") -def handle_toggle_all_id(task_id, user_id): - if task_id and user_id: toggle_task_complete(int(task_id), user_id) - return _fmt_tasks(get_tasks(user_id)), _ok("Updated") - -def handle_delete_all_id(task_id, user_id): - if task_id and user_id: delete_task(int(task_id), user_id) - return _fmt_tasks(get_tasks(user_id)), _ok("Deleted") +# ═══════════════════════════════════════════════════════════════════════════════ +# ALL TASKS TAB HANDLERS +# ═══════════════════════════════════════════════════════════════════════════════ -# ============================================================================= -# SMART TASK SCHEDULER — with confirm flow -# ============================================================================= - -def handle_smart_schedule(user_id, prompt): - """Step 1: Generate proposals and show them for user review. Don't apply yet.""" +def refresh_all_tasks(user_id, filter_area="All", only_today=False): if not user_id: - return _err("Not logged in."), "", gr.update(visible=False), gr.update(visible=False), [] + return [] + return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today)) - unscheduled = get_tasks(user_id, only_unscheduled=True, include_completed=False) - if not unscheduled: - all_tasks = get_tasks(user_id, include_completed=False) - if not all_tasks: - return _err("No tasks yet. Add some tasks first."), "", gr.update(visible=False), gr.update(visible=False), [] - unscheduled = all_tasks - prompt = (prompt or "") + " (All tasks already have dates; feel free to reassign.)" - ctx = _ensure_context(user_id) - goals = get_goals(user_id) - now = datetime.now() +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") - result = smart_schedule_tasks( - tasks=unscheduled, user_context=ctx, user_goals=goals, - scheduling_prompt=prompt or "Schedule my tasks intelligently across the next week.", - current_dt=now, - ) - # Build PREVIEW html — do NOT apply to DB yet - COLOR = {"Flow":"#0ea5e9","Easy":"#4ade80","Quick":"#a78bfa","Personal":"#f87171"} - task_lookup = {t["id"]: t for t in unscheduled} - assignments = result.get("assignments", []) +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") - html = ( - f'
    ' - f'
    ' - f'\U0001f9e0 AI Proposal \u2014 {len(assignments)} task(s) scheduled
    ' - f'

    {result.get("summary","")}

    ' - f'

    \u26a0 Review below, then Confirm & Apply or refine with a follow-up prompt.

    ' - f'
    ' - ) - by_date = {} - for a in assignments: - by_date.setdefault(a.get("assigned_date","?"), []).append(a) - - for d in sorted(by_date.keys()): - try: - dl = date.fromisoformat(d) - dfn = (dl - date.today()).days - if dfn == 0: lbl = f"{d} (Today)" - elif dfn == 1: lbl = f"{d} (Tomorrow)" - else: lbl = f"{d} ({dl.strftime('%A')})" - except ValueError: - lbl = d - html += f'
    \U0001f4c5 {lbl}
    ' - for a in by_date[d]: - tid = a.get("task_id") - task = task_lookup.get(int(tid) if tid else -1, {}) - sm = task.get("state_of_mind","Easy") - c = COLOR.get(sm,"#6366f1") - st = a.get("start_time","") - et = a.get("end_time","") - time_block = f'{st}\u2013{et}  ' if st and et else "" - dl_str = "" - if task.get("deadline_date"): - dl_str = f'  |\u23f3 due {task["deadline_date"]}' - html += ( - f'
    ' - f'
    ' - f'
    {a.get("title","")}
    {time_block}
    ' - f'
    {task.get("life_area") or "—"} · {sm} · {task.get("time_estimate") or "?"}min{dl_str}
    ' - f'
    \U0001f4a1 {a.get("reasoning","")}
    ' - f'
    ' - ) - - skipped = result.get("skipped",[]) - if skipped: - html += '
    Not scheduled
    ' - for s in skipped: - html += f'
    \u2717 {s.get("title","")} \u2014 {s.get("reason","")}
    ' - - for w in result.get("warnings",[]): - html += f'
    \u26a0 {w}
    ' - - # Store result in JSON for confirm step - pending_json = json.dumps(result) - return html, pending_json, gr.update(visible=True), gr.update(visible=True), [] - -def handle_confirm_schedule(user_id, pending_json, all_df_placeholder): - """Step 2: User clicked Confirm — apply assignments to DB.""" - if not user_id or not pending_json: - return _err("Nothing to confirm."), [], gr.update(visible=False), gr.update(visible=False) - try: - result = json.loads(pending_json) - except Exception: - return _err("Could not parse proposal."), [], gr.update(visible=False), gr.update(visible=False) - - count = 0 - for a in result.get("assignments",[]): - try: - assign_task_date(int(a["task_id"]), user_id, a["assigned_date"]) - count += 1 - except Exception: - pass - - all_rows = refresh_all_tasks_auto(user_id) - return _ok(f"\u2713 {count} tasks scheduled!"), all_rows, gr.update(visible=False), gr.update(visible=False) - -def handle_refine_schedule(user_id, new_prompt, pending_json): - """Step 2 alt: User typed a refinement — re-run scheduler with the old result as context.""" - if not user_id or not new_prompt.strip(): - return gr.update(), gr.update(), gr.update(), gr.update(), gr.update() - combined = new_prompt - if pending_json: - try: - old = json.loads(pending_json) - combined = f"{new_prompt}\n\n(Previous proposal summary: {old.get('summary','')})" - except Exception: - pass - return handle_smart_schedule(user_id, combined) - - -# ============================================================================= -# JOURNAL -# ============================================================================= - -def _task_list_md(tasks): - if not tasks: return "*No tasks found.*" - lines = [] - for t in tasks: - tid = t.get("task_id") or t.get("id") - status = "\u2705" if (t.get("is_completed") or t.get("completed")) else "\u2b1c" - dur = f" \u00b7 {t['actual_duration']}m actual" if t.get("actual_duration") else "" - lines.append(f"{status} **#{tid}** {t['title']} _{t.get('life_area','')} \u00b7 {t.get('time_estimate','')}m{dur}_") - return "\n".join(lines) - -def _parse_ids(text, valid_ids): - import re as _re - return [int(n) for n in _re.findall(r'\b(\d+)\b', text) if int(n) in valid_ids] - -def _bot(text): - """Return assistant message tuple for Gradio Chatbot (tuple mode).""" - return (None, text) - -def _usr(text): - """Return user message tuple for Gradio Chatbot (tuple mode).""" - return (text, None) - -def handle_start_journal(user_id): - blank_outputs = ([], [], False, - gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), - [], "marking", "") +# ═══════════════════════════════════════════════════════════════════════════════ +# JOURNAL TAB HANDLERS +# ═══════════════════════════════════════════════════════════════════════════════ + +def handle_load_journal_tasks(user_id): if not user_id: - return ([_bot("\u26a0 Please sign in first.")],) + blank_outputs[1:] + return "

    Not logged in.

    ", gr.update(visible=False), [] + tasks = get_tasks(user_id, only_today=True) if not tasks: - return ([_bot("No tasks scheduled for today. Add and schedule tasks first, then come back to reflect!")],) + blank_outputs[1:] - - tasks_state = [{ - "task_id": t["id"], "title": t["title"], "life_area": t.get("life_area",""), - "state_of_mind": t.get("state_of_mind",""), "time_estimate": t.get("time_estimate",30), - "completed": bool(t["is_completed"]), "actual_duration": t.get("actual_duration"), - } for t in tasks] - - total = len(tasks_state) - completed = sum(1 for t in tasks_state if t["completed"]) - task_md = _task_list_md(tasks_state) - - if completed == total and total > 0: - opener = f"\U0001f389 You knocked out all **{total}** tasks today \u2014 great work!\n\n{task_md}\n\nDid the day feel productive or were you grinding through it?" - elif completed == 0: - opener = (f"Here's what was on your plate today:\n\n{task_md}\n\n" - f"None are marked complete yet. Tell me which ones you finished " - f"(e.g. *\"I did #1 and #3\"*), or say **skip** to start reflecting.") + 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: - incomplete = [t for t in tasks_state if not t["completed"]] - inc_str = ", ".join(f"#{t['task_id']}" for t in incomplete[:4]) - opener = (f"Here's today's list:\n\n{task_md}\n\n" - f"Done: **{completed}/{total}**. Still open: {inc_str}. " - f"Tell me if you finished any more, or say **skip** to reflect.") - - chat = [_bot(opener)] - hist = [{"role": "assistant", "content": opener, "focus": "task_review"}] + 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.") + + try: + ctx = _ensure_context(user_id) + opening = build_opening_question(ctx, tasks_state) + except Exception as e: + return ( + [{"role": "assistant", "content": f"⚠ Could not start session: {e}"}], + [], False, + gr.update(interactive=False), gr.update(interactive=False), + _err(f"Start failed: {e}") + ) + 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), gr.update(interactive=True), - tasks_state, "marking", "", + gr.update(interactive=True), + gr.update(interactive=True), + _ok("Session started — type your answer below"), ) -def handle_journal_message(user_id, user_text, audio_path, chat, hist, tasks_state, active, phase): - voice_used = False - if audio_path is not None: - try: - transcribed = _transcribe(audio_path) - if transcribed: - user_text = transcribed - voice_used = True - except Exception as e: - chat = list(chat) + [_bot(f"\u26a0 Transcription error: {e}. Please type instead.")] - return chat, hist, "", None, tasks_state, active, phase, "" - - if not active or not user_text.strip(): - return list(chat), hist, "", None, tasks_state, active, phase, "" - - display_text = f"\U0001f3a4 *{user_text}*" if voice_used else user_text - chat = list(chat) + [_usr(display_text)] - hist = list(hist) + [{"role": "user", "content": user_text}] - - # Phase: marking tasks - if phase == "marking": - valid_ids = {t["task_id"] for t in tasks_state} - mentioned = _parse_ids(user_text, valid_ids) - updated = [] - for t in tasks_state: - if t["task_id"] in mentioned and not t["completed"]: - toggle_task_complete(t["task_id"], user_id) - t["completed"] = True - updated.append(t["title"]) - - titles_str = ", ".join(updated) - confirm = (f"Got it \u2014 marked **{titles_str}** as complete \u2705\n\n" if updated else "") - skip_words = {"done","skip","reflect","none","nothing","no more","start","go","next","continue","all"} - wants_skip = any(w in user_text.lower() for w in skip_words) - done_count = sum(1 for t in tasks_state if t["completed"]) - - if wants_skip or done_count == len(tasks_state) or (updated and done_count > 0): - ctx = _ensure_context(user_id) - opening = build_opening_question(ctx, tasks_state) - bridge = confirm + opening["question"] - chat = list(chat) + [_bot(bridge)] - hist.append({"role": "assistant", "content": bridge, "focus": opening["question_focus"]}) - return chat, hist, "", None, tasks_state, active, "reflecting", "" - else: - remaining = [t for t in tasks_state if not t["completed"]] - rem_str = " | ".join(f"#{t['task_id']} {t['title']}" for t in remaining[:5]) - follow = confirm + f"Still pending: {rem_str}\n\nAnything else done? Or say **skip** to start reflecting." - chat = list(chat) + [_bot(follow)] - hist.append({"role": "assistant", "content": follow, "focus": "task_marking"}) - return chat, hist, "", None, tasks_state, active, "marking", "" - - # Phase: reflecting - if phase == "reflecting": - try: - ctx = _ensure_context(user_id) - result = get_next_journal_question(ctx, tasks_state, hist) - except Exception as e: - chat = list(chat) + [_bot(f"\u26a0 AI error: {e}. Try again.")] - return chat, hist, "", None, tasks_state, active, phase, "" - - if result.get("session_complete"): - closing = "That's a solid reflection \u2014 I have plenty to work with. \U0001f9e0 Hit **Finish & Save** to lock in your insights!" - chat = list(chat) + [_bot(closing)] - hist.append({"role": "assistant", "content": closing, "focus": "complete"}) - return chat, hist, "", None, tasks_state, active, "done", _ok("Session complete \u2014 click Finish & Save") - - next_q = result.get("question") or "Anything else you'd like to reflect on?" - chat = list(chat) + [_bot(next_q)] - hist.append({"role": "assistant", "content": next_q, "focus": result.get("question_focus","")}) - return chat, hist, "", None, tasks_state, active, "reflecting", "" - - # Phase: done - chat = list(chat) + [_bot("Session complete! Click **Finish & Save** to save your insights.")] - return chat, hist, "", None, tasks_state, active, "done", "" +def handle_send_answer(user_id, answer, chat, hist, tasks_state, active): + if not active or not answer.strip(): + return chat, hist, "", "" + + # Append user message as dict (Gradio 4.44 requires dict format for Chatbot) + chat = list(chat) + [{"role": "user", "content": answer}] + hist = list(hist) + [{"role": "user", "content": answer}] + + ctx = _ensure_context(user_id) + try: + result = get_next_journal_question(ctx, tasks_state, hist) + except Exception as e: + chat = list(chat) + [{"role": "assistant", "content": f"⚠ AI error: {e}. Please try again."}] + return chat, hist, "", _err(f"AI error: {e}") + + if result.get("session_complete"): + bot_reply = "✅ That's enough for today. Click **Finish & Save** to update your profile." + hist.append({"role": "assistant", "content": bot_reply}) + chat = list(chat) + [{"role": "assistant", "content": bot_reply}] + return chat, hist, "", _ok("Session complete — save when ready") + + next_q = result.get("question", "Anything else you'd like to reflect on?") + 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 list(chat) + [_bot("\u26a0 Nothing to save yet.")], "" + return chat, _err("Nothing to save.") + try: ctx = _ensure_context(user_id) updated = synthesize_journal(ctx, tasks_state, hist) save_user_context(user_id, updated) except Exception as e: - return list(chat) + [_bot(f"\u26a0 Save failed: {e}")], _err("Save failed") - 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",[]) - notes_md = "\n".join(f"\u2022 {n}" for n in notes[-3:]) if notes else "\u2022 Keep reflecting to build patterns" - summary = (f"\u2705 **Insights saved!** {done}/{total} tasks ({rate}%) complete today.\n\n" - f"**What I learned:**\n{notes_md}\n\n" - f"I'll use this to make tomorrow's scheduling smarter. Great work! \U0001f319") - return list(chat) + [_bot(summary)], _ok(f"Saved! {done}/{total} tasks ({rate}%)") + return chat, _err(f"Save failed: {e}") + + 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), gr.update(interactive=False), [], "marking", "" + return [], [], False, gr.update(interactive=False), gr.update(interactive=False), "" -# ============================================================================= -# AREAS & GOALS -# ============================================================================= +# ═══════════════════════════════════════════════════════════════════════════════ +# LIFE AREAS & GOALS HANDLERS +# ═══════════════════════════════════════════════════════════════════════════════ -def render_areas_html(user_id): - if not user_id: return "" - areas = get_life_areas(user_id) - tasks = get_tasks(user_id) - # Count tasks per area - counts = {} - for t in tasks: - a = t.get("life_area","") - if a: counts[a] = counts.get(a,0) + 1 - max_count = max(counts.values()) if counts else 1 - if not areas: - return '

    No areas yet. Add one below.

    ' - html = '
    ' +def render_areas(user_id): + if not user_id: + return "", [], [] + areas = get_life_areas(user_id) + chips = "" for a in areas: - name = a["name"] - color = a["color"] - cnt = counts.get(name, 0) - bar_w = max(4, round(cnt / max_count * 100)) if max_count else 4 - html += ( - f'
    ' - f'
    ' - f'
    {name}
    ' - f'
    ' - f'
    ' - f'
    {cnt} task{"s" if cnt!=1 else ""}
    ' - f'
    ' - f'' - f'
    ' + chips += ( + f'' + f'{a["name"]} ' ) - html += '
    ' - return html + html = f'
    {chips}
    ' if chips else '

    No areas yet.

    ' + names = [a["name"] for a in areas] + return html, names, names -def render_areas_list(user_id): - if not user_id: return "", [] - areas = get_life_areas(user_id) - tasks_all = get_tasks(user_id) - counts = {} - for t in tasks_all: - a = t.get("life_area","") - if a: counts[a] = counts.get(a,0) + 1 - max_count = max(counts.values()) if counts else 1 - - if not areas: - html = '

    No areas yet. Add one below.

    ' - else: - html = '
    ' - for a in areas: - name = a["name"] - color = a["color"] - cnt = counts.get(name, 0) - bar_w = max(4, round(cnt / max_count * 100)) if max_count else 4 - html += ( - f'
    ' - f'
    ' - f'
    {name}
    ' - f'
    ' - f'
    ' - f'
    {cnt} task{"s" if cnt!=1 else ""}
    ' - f'
    ' - f'' - f'
    ' - ) - html += '
    ' - names = [a["name"] for a in areas] - return html, names def handle_add_area(user_id, name, color): ok, msg = add_life_area(user_id, name, color) - html, names = render_areas_list(user_id) - ct = "#4ade80" if ok else "#f87171" - return f'{msg}', html, gr.update(choices=names, value=None), gr.update(value="") + 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."), "" - delete_life_area(user_id, name) - html, names = render_areas_list(user_id) - return _ok(f'"{name}" removed.'), html + 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(uid=user_id, goals_text=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): - return "\n".join(get_goals(user_id)) if user_id else "" + if not user_id: + return "" + return "\n".join(get_goals(user_id)) -# ============================================================================= -# PREFERENCES & AI MEMORY -# ============================================================================= +# ═══════════════════════════════════════════════════════════════════════════════ +# 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) + 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.") + 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)}) + 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.

    " + if not user_id: + return "

    Not logged in.

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

    No AI context yet. Complete a journal session to build it.

    " - lp = ctx.get("learned_patterns",{}) - sf = ctx.get("scheduling_feedback",{}) - pref = ctx.get("preferences",{}) - _ver = ctx.get("version",1) - _upd = (ctx.get("last_updated") or "")[:10] - - # Notes with timestamps (notes are stored as strings; newer ones may have timestamps embedded) - notes = lp.get("notes",[]) - history = ctx.get("history_summary",[]) - rates = sf.get("last_7_day_completion_rates",[]) - avg_rate = round(sf.get("avg_completion_rate",0)*100) - overrun = lp.get("avg_task_overrun_pct",0) - days = sf.get("total_days_scheduled",0) - - # ── Mini SVG bar chart for last-7-day completion ─────────────────────── - chart_html = "" - if rates: - bar_w = 28 - gap = 6 - chart_w = len(rates)*(bar_w+gap) - chart_h = 60 - bars = "" - for idx, r in enumerate(rates[-7:]): - x = idx*(bar_w+gap) - h = max(4, round(r*chart_h)) - y = chart_h - h - col = "#4ade80" if r >= 0.7 else "#f59e0b" if r >= 0.4 else "#f87171" - pct = round(r*100) - bars += (f'' - f'{pct}%') - chart_html = ( - f'
    ' - f'
    Last {len(rates[-7:])} Day Completion
    ' - f'{bars}' + 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 "—" - # ── Overrun pill ─────────────────────────────────────────────────────── - overrun_col = "#4ade80" if overrun <= 10 else "#f59e0b" if overrun <= 30 else "#f87171" - - # ── Stats row ───────────────────────────────────────────────────────── - def _pill(val, label, color): - return (f'
    ' - f'
    {val}
    ' - f'
    {label}
    ') - - stats_row = ( - f'
    ' - f'{_pill(days,"Days tracked","#a78bfa")}' - f'{_pill(str(avg_rate)+"%","Avg completion","#4ade80")}' - f'{_pill(str(overrun)+"%","Task overrun",overrun_col)}' - f'{_pill(str(lp.get("flow_batch_capable","?")), "Flow batching","#60a5fa")}' + 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'
    ' ) - # ── AI Notes with timestamps ─────────────────────────────────────────── - notes_html = "" + notes = lp.get("notes", []) if notes: - notes_html = '
    AI Insights
    ' - for n in notes[-8:]: - # Notes may be plain strings or "YYYY-MM-DD: text" - if ": " in str(n) and str(n)[:10].count("-") == 2: - ts, txt = n.split(": ",1) - else: - ts, txt = _upd, str(n) - notes_html += ( - f'
    ' - f'{ts}' - f'\u2022 {txt}' - f'
    ' - ) - - # ── History summary ──────────────────────────────────────────────────── - hist_html = "" + note_rows = "".join( + f'
    • {n}
    ' + for n in notes[-5:] + ) + html += f'
    AI Notes
    {note_rows}
    ' + + history = ctx.get("history_summary", []) if history: - hist_html = '
    Session History
    ' - for h in history[-5:]: - hist_html += f'
    \u2022 {h}
    ' + hist_rows = "".join( + f'
    • {h}
    ' + for h in history[-5:] + ) + html += f'
    Recent History
    {hist_rows}
    ' - html = ( - f'
    ' - f'
    v{_ver} \u00b7 last updated {_upd}
    ' - f'{stats_row}' - f'{chart_html}' - f'{notes_html}' - f'{hist_html}' - f'
    ' - ) return html -# ============================================================================= -# UI -# ============================================================================= +# ═══════════════════════════════════════════════════════════════════════════════ +# UI BUILD +# ═══════════════════════════════════════════════════════════════════════════════ -with gr.Blocks(title="\U0001f9e0 Second Brain") as demo: +with gr.Blocks(title="🧠 Second Brain") as demo: - user_id_state = gr.State(None) - username_state = gr.State("") - # Journal + # ── 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) - journal_phase_state = gr.State("marking") - # Clarification — text panel + # Clarification states clar_questions_state = gr.State([]) original_task_state = gr.State("") - # Clarification — voice panel - vclar_questions_state = gr.State([]) - voriginal_task_state = gr.State("") - # Scheduler pending state - sched_pending_state = gr.State("") - # Selected task ids - today_sel_state = gr.State(None) - all_sel_state = gr.State(None) - - # ── AUTH ───────────────────────────────────────────────────────────────��─ + + # ═════════════════════════════════════════════════════════════════════════ + # AUTH SECTION + # ═════════════════════════════════════════════════════════════════════════ with gr.Column(visible=True, elem_id="auth-card") as auth_section: - gr.HTML('') + 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="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022") + 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") - 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 (optional)", lines=3, placeholder="Launch my startup\nGet fit\nLearn ML") - reg_btn = gr.Button("Create Account", elem_classes="btn-primary") - reg_msg = gr.HTML("") - - # ── MAIN APP ───────────────────────────────────────────────────────────── + 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('
    \U0001f9e0 Second Brain
    \u2014
    ') + header_html = gr.HTML('
    🧠 Second Brain
    ') logout_btn = gr.Button("Sign Out", elem_classes="btn-secondary", scale=0) - with gr.Tabs(): + # ── Tabs ──────────────────────────────────────────────────────────── + with gr.Tabs() as main_tabs: - # ── TAB 1: TODAY ──────────────────���───────────────────────────── - with gr.Tab("\U0001f4c5 Today"): + # ───────────────────────────────────────────────────────────── + # TAB 1: TODAY + # ───────────────────────────────────────────────────────────── + with gr.Tab("📅 Today"): + # Stats row with gr.Row(): - stat_total = gr.HTML(_stat("\u2014","Today","#a78bfa")) - stat_done = gr.HTML(_stat("\u2014","Done","#4ade80")) - stat_remain = gr.HTML(_stat("\u2014","Left","#f87171")) + 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("\u270f\ufe0f Add Task (Text)", elem_classes="btn-primary", scale=3) - btn_add_voice = gr.Button("\U0001f399\ufe0f Add Task (Voice)", elem_classes="btn-accent", scale=3) - btn_plan_day = gr.Button("\U0001f9e0 Plan My Tasks", elem_classes="btn-secondary", scale=3) - btn_refresh = gr.Button("\u21bb", elem_classes="btn-secondary", scale=1) + 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 capture panel ──────────────────────────────────── + # ── 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 proposal by Friday, it's really urgent", lines=2) + 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("\U0001f916 Parse with AI", elem_classes="btn-primary") - btn_cancel_text = gr.Button("Cancel", elem_classes="btn-secondary") + 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('
    Answer the questions above
    ') - with gr.Row(): - clar_text_input = gr.Textbox(label="Type your answer", lines=2, scale=4) - clar_audio_input = gr.Audio(sources=["microphone"], type="filepath", label="\U0001f3a4", scale=1) - btn_clar_submit = gr.Button("\U0001f504 Re-classify", elem_classes="btn-accent") + 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
    ') - gr.HTML('

    Deadline is auto-extracted from your text. Date will be assigned by the AI scheduler.

    ') 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, allow_custom_value=True) + f_area = gr.Dropdown(label="Life Area", + choices=["Work","Health","Finance","Learning","Personal","Family","Other"], + scale=1, allow_custom_value=True) 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") + 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_deadline = gr.Textbox(label="\U0001f3af Deadline (YYYY-MM-DD)", placeholder="auto-filled or type", scale=2) - f_is_habit = gr.Checkbox(label="\u267b\ufe0f Habit", scale=1) - f_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1) + f_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1) + f_date = gr.Textbox(label="Scheduled Date", value=str(date.today()), 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("\u2713 Save Task", elem_classes="btn-success") - btn_discard = gr.Button("\u2717 Discard", elem_classes="btn-danger") + btn_confirm = gr.Button("✓ Save Task", elem_classes="btn-success") + btn_discard = gr.Button("✗ Discard", elem_classes="btn-danger") save_msg = gr.HTML("") - # ── Voice capture panel ─────────────────────────────────── + # ── Voice add panel ─────────────────────────────────────── with gr.Column(visible=False, elem_classes="panel") as voice_panel: - gr.HTML('
    Add Task by Voice
    ') - gr.HTML('

    Say your task naturally including any deadline \u2014 AI extracts it all.

    ') - voice_audio_input = gr.Audio(sources=["microphone","upload"], type="filepath", label="") + 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_voice_parse = gr.Button("\U0001f916 Transcribe & Classify", elem_classes="btn-primary") - btn_cancel_voice = gr.Button("Cancel", elem_classes="btn-secondary") - voice_status = gr.HTML("") - with gr.Column(visible=False) as voice_parsed_panel: - voice_transcribed_txt = gr.Textbox(label="Transcribed text (editable)", lines=1) - voice_clar_html = gr.HTML("") - with gr.Column(visible=False) as voice_clar_row: - gr.HTML('
    Answer the questions above
    ') - with gr.Row(): - vclar_text = gr.Textbox(label="Type your answer", lines=2, scale=4) - vclar_audio = gr.Audio(sources=["microphone"], type="filepath", label="\U0001f3a4", scale=1) - btn_vclar_submit = gr.Button("\U0001f504 Re-classify", elem_classes="btn-accent") - gr.HTML('
    Review & Confirm
    ') - with gr.Row(): - vf_title = gr.Textbox(label="Title", scale=3) - vf_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1, allow_custom_value=True) - with gr.Row(): - vf_urgency = gr.Dropdown(label="Urgency", choices=["Urgent","Not Urgent","Habit"], value="Not Urgent") - vf_importance = gr.Dropdown(label="Importance", choices=["Move the Needle","Important","Not Important"], value="Important") - vf_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy") - with gr.Row(): - vf_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1) - vf_deadline = gr.Textbox(label="\U0001f3af Deadline (YYYY-MM-DD)", placeholder="auto-filled or type", scale=2) - vf_is_habit = gr.Checkbox(label="\u267b\ufe0f Habit", scale=1) - vf_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1) - with gr.Row(): - btn_voice_confirm = gr.Button("\u2713 Save Task", elem_classes="btn-success") - btn_voice_discard = gr.Button("\u2717 Discard", elem_classes="btn-danger") - voice_save_msg = gr.HTML("") + 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 Tasks panel ─────────────────────────────────── + # ── Plan My Day panel ───────────────────────────────────── with gr.Column(visible=False, elem_classes="panel") as plan_panel: - gr.HTML('
    Plan My Tasks
    ') - gr.HTML('

    ' - 'The AI reads your unscheduled tasks, patterns, goals and current time \u2014 ' - 'proposes a schedule with time blocks. Review before applying.

    ') + gr.HTML('
    Plan My Day
    ') + gr.HTML('

    The AI reads your tasks, context, and goals to build the optimal schedule.

    ') plan_prompt = gr.Textbox( - label="Tell the AI what you want", - placeholder="e.g. 'Clear today, schedule from tomorrow' or 'Focus on Work tasks this week, I have Thursday free'", - lines=2 + label="Any fixed events or preferences today?", + placeholder="e.g. I have a meeting at 10am, want deep work in the morning, done by 7pm", + lines=3 ) with gr.Row(): - btn_gen_sched = gr.Button("\U0001f9e0 Generate Proposal", elem_classes="btn-primary") - btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary") + btn_gen_sched = gr.Button("✨ Generate Schedule", elem_classes="btn-primary") + btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary") schedule_html = gr.HTML("") - # Confirm/Refine row — hidden until proposal generated - with gr.Column(visible=False) as sched_confirm_row: - gr.HTML('
    ' - '\u26a0 Review the proposal above. Confirm to apply, or type a refinement.
    ') - with gr.Row(): - btn_sched_confirm = gr.Button("\u2713 Confirm & Apply", elem_classes="btn-success", scale=2) - btn_sched_cancel = gr.Button("\u2717 Discard", elem_classes="btn-danger", scale=1) - refine_prompt = gr.Textbox(label="Or refine (optional)", placeholder="e.g. 'Move the Flow tasks to morning'", lines=1) - btn_refine = gr.Button("\U0001f504 Re-generate with refinement", elem_classes="btn-accent") - sched_status = gr.HTML("") - + # ── Today's task table ──────────────────────────────────── gr.HTML('
    Today\'s Tasks
    ') today_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True) - gr.HTML('

    Enter task ID then click action:

    ') with gr.Row(): - today_task_id = gr.Number(label="Task ID", precision=0, scale=1, minimum=1) - btn_today_done = gr.Button("\u2705 Mark Done", elem_classes="btn-success", scale=2) - btn_today_del = gr.Button("\U0001f5d1 Delete", elem_classes="btn-danger", scale=2) + 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("\U0001f4cb All Tasks"): - gr.HTML('

    All tasks including unscheduled. Enter ID then click action.

    ') + # ───────────────────────────────────────────────────────────── + # 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=3) - all_today_chk = gr.Checkbox(label="Today only", value=False, scale=1) - all_unsched = gr.Checkbox(label="Unscheduled only", value=False, scale=1) - btn_all_ref = gr.Button("\u21bb Refresh", elem_classes="btn-secondary", scale=1) + 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_task_id = gr.Number(label="Task ID", precision=0, scale=1, minimum=1) - btn_all_done = gr.Button("\u2705 Done", elem_classes="btn-success", scale=2) - btn_all_del = gr.Button("\U0001f5d1 Delete", elem_classes="btn-danger", scale=2) + 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: JOURNAL ──────────────────────────────────────────── - with gr.Tab("\U0001f4d3 Journal"): - gr.HTML('

    End-of-day reflection. Chat naturally \u2014 AI marks tasks, asks questions, updates your profile.

    ') - journal_chatbot = gr.Chatbot(label="", height=440) - with gr.Row(): - j_answer = gr.Textbox(label="", placeholder="Type or use the mic\u2026", lines=1, scale=5, interactive=False, show_label=False) - j_audio_in = gr.Audio(sources=["microphone"], type="filepath", label="\U0001f3a4", scale=1, interactive=False) - btn_j_send = gr.Button("Send \u2192", elem_classes="btn-accent", scale=1, interactive=False) - with gr.Row(): - btn_j_start = gr.Button("\u25b6 Start Session", elem_classes="btn-primary", scale=3) - btn_j_finish = gr.Button("\u2713 Finish & Save", elem_classes="btn-success", scale=3) - btn_j_restart = gr.Button("\u21ba Reset", elem_classes="btn-secondary", scale=1) - journal_msg = gr.HTML("") - - # ── TAB 4: AREAS & GOALS ────────────────────────────────────── - with gr.Tab("\U0001f5c2 Areas & Goals"): + # ───────────────────────────────────────────────────────────── + # TAB 3: 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("") - gr.HTML('
    To remove an area, select it from the dropdown and click Remove:
    ') - with gr.Row(): - del_area_dd = gr.Dropdown(label="Select area to remove", choices=[], scale=4, allow_custom_value=False) - btn_del_area = gr.Button("\u2717 Remove", elem_classes="btn-danger", scale=1) - del_area_msg = gr.HTML("") - gr.HTML('
    Add New Area
    ') + with gr.Row(): - new_area_name = gr.Textbox(label="Name", placeholder="e.g. Side Project", scale=3) + 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) + 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 factors these into scheduling and prioritisation.

    ') - 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("\u2699\ufe0f Preferences"): + 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) + 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('

    Stats, charts, and insights the AI has learned about you. Updates after each journal session.

    ') - ctx_display = gr.HTML("") - btn_ref_ctx = gr.Button("\u21bb Refresh", elem_classes="btn-secondary") + 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 - # ========================================================================= + # ═══════════════════════════════════════════════════════════════════════ - def _header_html(uid, uname): - return f'
    \U0001f9e0 Second Brain
    \U0001f464 {uname}
    ' + # ── Auth ────────────────────────────────────────────────────────────── - # Auth - login_btn.click(handle_login, [login_user_in, login_pass_in], + 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(_header_html, [user_id_state, username_state], [header_html] - ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain] - ).then(refresh_all_tasks_auto, [user_id_state], [all_df] - ).then(lambda uid: render_areas_list(uid), [user_id_state], [areas_display, del_area_dd] - ).then(load_prefs, [user_id_state], [pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max] - ).then(load_goals_txt, [user_id_state], [goals_input]) - - reg_btn.click(handle_register, [reg_user_in, reg_pass_in, reg_wake, reg_sleep, reg_focus, reg_goals], + ).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: 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(_header_html, [user_id_state, username_state], [header_html] - ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain] - ).then(refresh_all_tasks_auto, [user_id_state], [all_df] - ).then(lambda uid: render_areas_list(uid), [user_id_state], [areas_display, del_area_dd]) + ).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: 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] + ) - logout_btn.click(handle_logout, [user_id_state], [user_id_state, username_state, auth_section, app_section]) + # ── Today tab ───────────────────────────────────────────────────────── - # Panels - btn_add_text.click(show_text_panel, outputs=[text_panel, voice_panel, plan_panel]) + 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]) - f_is_habit.change(lambda v: gr.update(visible=v), [f_is_habit], [f_interval]) - vf_is_habit.change(lambda v: gr.update(visible=v), [vf_is_habit], [vf_interval]) + 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] + ) - # ── Text task flow ──────────────────────────────────────────────────── - _TEXT_PARSE_OUT = [parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance, - f_state, f_time, f_deadline, f_is_habit, f_interval, parse_status, - clar_reply_row, clar_questions_state, original_task_state] - _TEXT_CLAR_OUT = [f_title, clar_html, f_area, f_urgency, f_importance, - f_state, f_time, f_deadline, clar_reply_row, clar_questions_state] + 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_parse.click(handle_parse_text, [task_input, user_id_state], _TEXT_PARSE_OUT) btn_clar_submit.click( handle_clarification_reply, - [clar_text_input, original_task_state, clar_questions_state, user_id_state], - _TEXT_CLAR_OUT - ).then(lambda: ("", None), outputs=[clar_text_input, clar_audio_input]) - clar_audio_input.change( - handle_clar_voice, - [clar_audio_input, original_task_state, clar_questions_state, user_id_state], - _TEXT_CLAR_OUT - ).then(lambda: None, outputs=[clar_audio_input]) - 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_deadline, f_is_habit, f_interval], - [save_msg, today_df, stat_total, stat_done, stat_remain, all_df, parsed_panel] + [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] ) - # ── Voice task flow ─────────────────────────────────────────────────── - _VOICE_PARSE_OUT = [voice_parsed_panel, voice_transcribed_txt, - voice_clar_html, vf_area, vf_urgency, vf_importance, - vf_state, vf_time, vf_deadline, vf_is_habit, vf_interval, - voice_status, voice_clar_row, vclar_questions_state, voriginal_task_state] - _VOICE_CLAR_OUT = [vf_title, voice_clar_html, vf_area, vf_urgency, vf_importance, - vf_state, vf_time, vf_deadline, voice_clar_row, vclar_questions_state] + btn_transcribe.click( + handle_transcribe_voice, + [audio_input, user_id_state], + [transcribed, voice_msg] + ) - btn_voice_parse.click(handle_voice_parse, [voice_audio_input, user_id_state], _VOICE_PARSE_OUT) - btn_vclar_submit.click( - handle_clarification_reply, - [vclar_text, voriginal_task_state, vclar_questions_state, user_id_state], - _VOICE_CLAR_OUT - ).then(lambda: ("", None), outputs=[vclar_text, vclar_audio]) - vclar_audio.change( - handle_clar_voice, - [vclar_audio, voriginal_task_state, vclar_questions_state, user_id_state], - _VOICE_CLAR_OUT - ).then(lambda: None, outputs=[vclar_audio]) - btn_voice_discard.click(lambda: gr.update(visible=False), outputs=[voice_parsed_panel]) - btn_voice_confirm.click( - handle_confirm_task, - [user_id_state, vf_title, vf_area, vf_urgency, vf_importance, vf_state, vf_time, vf_deadline, vf_is_habit, vf_interval], - [voice_save_msg, today_df, stat_total, stat_done, stat_remain, all_df, voice_parsed_panel] + btn_discard.click( + lambda: gr.update(visible=False), + outputs=[parsed_panel] ) - # ── Scheduler with confirm flow ─────────────────────────────────────── - # Step 1: generate proposal (5 outputs) - _SCHED_PROP_OUTS = [schedule_html, sched_pending_state, sched_confirm_row, sched_confirm_row, all_df] + 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, parsed_panel] + ) btn_gen_sched.click( - handle_smart_schedule, + handle_generate_schedule, [user_id_state, plan_prompt], - [schedule_html, sched_pending_state, sched_confirm_row, btn_sched_confirm, all_df] + [schedule_html] ) - # Step 2a: confirm → apply - btn_sched_confirm.click( - handle_confirm_schedule, - [user_id_state, sched_pending_state, all_df], - [sched_status, all_df, sched_confirm_row, btn_sched_confirm] - ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]) - - # Step 2b: discard - btn_sched_cancel.click( - lambda: ("", gr.update(visible=False), gr.update(visible=False)), - outputs=[schedule_html, sched_confirm_row, btn_sched_confirm] - ) - - # Step 2c: refine - btn_refine.click( - handle_refine_schedule, - [user_id_state, refine_prompt, sched_pending_state], - [schedule_html, sched_pending_state, sched_confirm_row, btn_sched_confirm, all_df] - ).then(lambda: "", outputs=[refine_prompt]) - - # ── Today task actions ──────────────────────────────────────────────── btn_today_done.click( - handle_toggle_today_id, [today_task_id, user_id_state], + handle_toggle_today, + [today_done_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg] ) btn_today_del.click( - handle_delete_today_id, [today_task_id, user_id_state], + handle_delete_today, + [today_del_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg] ) - # ── All Tasks ─────────────────────────────────────────────────────��─── - def refresh_all_filtered(user_id, filter_area, only_today, only_unsched): - if not user_id: return [] - return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today, only_unscheduled=only_unsched)) - - btn_all_ref.click(refresh_all_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df]) - all_filter.change(refresh_all_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df]) - all_today_chk.change(refresh_all_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df]) - all_unsched.change(refresh_all_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df]) - - btn_all_done.click(handle_toggle_all_id, [all_task_id, user_id_state], [all_df, all_action_msg]) - btn_all_del.click(handle_delete_all_id, [all_task_id, user_id_state], [all_df, all_action_msg]) - - # ── Journal ─────────────────────────────────────────────────────────── - _J_START_OUT = [journal_chatbot, journal_hist_state, journal_active, - j_answer, btn_j_send, j_audio_in, journal_tasks_state, - journal_phase_state, journal_msg] - _J_MSG_IN = [user_id_state, j_answer, j_audio_in, journal_chatbot, - journal_hist_state, journal_tasks_state, journal_active, journal_phase_state] - _J_MSG_OUT = [journal_chatbot, journal_hist_state, j_answer, j_audio_in, - journal_tasks_state, journal_active, journal_phase_state, journal_msg] - - btn_j_start.click(handle_start_journal, [user_id_state], _J_START_OUT) - btn_j_send.click(handle_journal_message, _J_MSG_IN, _J_MSG_OUT) - j_answer.submit(handle_journal_message, _J_MSG_IN, _J_MSG_OUT) - j_audio_in.change(handle_journal_message, _J_MSG_IN, _J_MSG_OUT) + # ── 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] + ) + btn_all_del.click( + handle_delete_all, + [all_del_id, user_id_state, all_filter, all_today_chk], + [all_df, all_action_msg] + ) + + # ── 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=_J_START_OUT) - # ── Areas & Goals ───────────────────────────────────────���───────────── + 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], + 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]) + ).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] - ).then(lambda uid: render_areas_list(uid)[1], [user_id_state], [del_area_dd] - ).then(lambda uid: gr.update(choices=_area_choices(uid), value="All"), [user_id_state], [all_filter]) + 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] + ) - btn_save_goals.click(handle_save_goals, [user_id_state, goals_input], [goals_msg]) + # ── Preferences tab ─────────────────────────────────────────────────── - # ── Preferences ─────────────────────────────────────────────────────── 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]) + 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) \ No newline at end of file