Spaces:
Runtime error
Runtime error
| """ | |
| 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 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, 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", "Date"] | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SHARED HELPERS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _ok(msg): return f'<span style="color:#4ade80;font-size:13px">β {msg}</span>' | |
| def _err(msg): return f'<span style="color:#f87171;font-size:13px">β {msg}</span>' | |
| def _info(msg): return f'<span style="color:#60a5fa;font-size:13px">βΉ {msg}</span>' | |
| def _stat(val, label, color): | |
| return f'<div class="stat-card"><div class="stat-num" style="color:{color}">{val}</div><div class="stat-label">{label}</div></div>' | |
| def _fmt_tasks(tasks: list) -> list: | |
| rows = [] | |
| for t in tasks: | |
| 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 "β", | |
| t["scheduled_date"] or "β", | |
| ]) | |
| 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", "") | |
| 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 = "" | |
| if clarifications: | |
| items = "".join(f"<li style='margin:4px 0'>{q}</li>" for q in clarifications) | |
| clar_html = ( | |
| f'<div style="margin:10px 0;padding:12px;background:#0c0f1a;' | |
| f'border-left:3px solid #a78bfa;border-radius:8px">' | |
| f'<div style="color:#a78bfa;font-size:11px;font-weight:600;margin-bottom:6px">β Please clarify:</div>' | |
| f'<ul style="color:#94a3b8;font-size:13px;margin:0;padding-left:16px">{items}</ul></div>' | |
| ) | |
| 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"), | |
| ) | |
| 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: | |
| import whisper | |
| global _whisper_model | |
| if _whisper_model is None: | |
| _whisper_model = whisper.load_model("small") | |
| result = _whisper_model.transcribe(audio_path) | |
| text = result["text"].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, sched_date, 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), | |
| "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) | |
| return _ok("Task saved!"), rows, s1, s2, s3, gr.update(visible=False) | |
| 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')}") | |
| COLOR = {"Flow": "#0ea5e9", "Easy": "#4ade80", "Quick": "#a78bfa", "Personal": "#f87171"} | |
| html = ( | |
| f'<div style="margin-bottom:14px">' | |
| f'<span style="color:#a78bfa;font-size:12px;font-weight:600">π {sched.get("schedule_date","Today")}</span>' | |
| f'<p style="color:#475569;font-size:13px;margin:6px 0 0">{sched.get("day_summary","")}</p>' | |
| f'</div>' | |
| ) | |
| for t in sched.get("scheduled_tasks", []): | |
| sm = t.get("state_of_mind", "Easy") | |
| c = COLOR.get(sm, "#6366f1") | |
| html += ( | |
| f'<div class="sched-card" style="border-left-color:{c}">' | |
| f'<div class="sched-time">{t.get("start_time")} β {t.get("end_time")}</div>' | |
| f'<div class="sched-title">{t.get("title")}</div>' | |
| f'<div class="sched-meta">{t.get("life_area","β")} Β· {sm} Β· {t.get("duration_minutes")}min</div>' | |
| f'<div class="sched-why">{t.get("scheduling_reason","")}</div>' | |
| f'</div>' | |
| ) | |
| if sched.get("deferred_tasks"): | |
| html += '<div style="margin-top:12px;color:#334155;font-size:11px;font-weight:600;letter-spacing:.6px;text-transform:uppercase">Deferred</div>' | |
| for t in sched["deferred_tasks"]: | |
| html += f'<div style="color:#475569;font-size:12px;padding:3px 0">β {t["title"]} β {t.get("reason","")}</div>' | |
| for w in sched.get("warnings", []): | |
| html += f'<div style="margin-top:8px;color:#f59e0b;font-size:12px">β {w}</div>' | |
| 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(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 "<p style='color:#f87171'>Not logged in.</p>", gr.update(visible=False), [] | |
| tasks = get_tasks(user_id, only_today=True) | |
| if not tasks: | |
| return "<p style='color:#475569;font-size:13px'>No tasks for today.</p>", 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'<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid #1e293b">' | |
| f'<span style="font-family:JetBrains Mono,monospace;color:#334155;font-size:11px;min-width:30px">#{t["id"]}</span>' | |
| f'<span style="font-size:16px">{status}</span>' | |
| f'<span style="flex:1;font-size:13px;color:#cbd5e1">{t["title"]}</span>' | |
| f'<span style="font-size:11px;color:#475569">{t["life_area"] or "β"}</span>' | |
| f'<span style="font-size:11px;color:#334155">{t["time_estimate"]}m{actual}</span>' | |
| f'</div>' | |
| ) | |
| 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'<div style="font-size:12px">{rows}</div>', 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 = [(None, 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) + [(answer, None)] | |
| 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[-1] = (answer, "β 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[-1] = (answer, 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"<li style='margin:3px 0'>{n}</li>" for n in notes[-3:]) if notes else "<li>No new patterns yet</li>" | |
| return ( | |
| chat, | |
| _ok(f"Insights saved! {done}/{total} tasks ({rate}%) completed.") | |
| + f'<ul style="color:#94a3b8;font-size:12px;margin-top:6px;padding-left:16px">{note_html}</ul>' | |
| ) | |
| 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'<span class="chip" ' | |
| f'style="background:{a["color"]}22;color:{a["color"]};border:1px solid {a["color"]}44">' | |
| f'{a["name"]}</span> ' | |
| ) | |
| html = f'<div style="margin:4px 0">{chips}</div>' if chips else '<p style="color:#334155;font-size:13px">No areas yet.</p>' | |
| 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'<span style="color:{"#4ade80" if ok else "#f87171"};font-size:13px">{msg}</span>', | |
| 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'<span style="color:{"#4ade80" if ok else "#f87171"};font-size:13px">{msg}</span>', | |
| 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 "<p style='color:#334155'>Not logged in.</p>" | |
| ctx = load_user_context(user_id) | |
| if not ctx: | |
| return "<p style='color:#334155;font-size:13px'>No AI context yet. Complete a journaling session to start building it.</p>" | |
| lp = ctx.get("learned_patterns", {}) | |
| sf = ctx.get("scheduling_feedback", {}) | |
| pref = ctx.get("preferences", {}) | |
| def row(label, val): | |
| return ( | |
| f'<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #0d1117">' | |
| f'<span style="color:#334155;font-size:12px">{label}</span>' | |
| f'<span style="color:#94a3b8;font-size:12px;font-family:JetBrains Mono,monospace">{val}</span>' | |
| f'</div>' | |
| ) | |
| def lst(v): return ", ".join(v) if v else "β" | |
| html = ( | |
| f'<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">' | |
| f'<div class="panel">' | |
| f'<div class="sec-label">Preferences</div>' | |
| 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'</div>' | |
| f'<div class="panel">' | |
| f'<div class="sec-label">Scheduling Stats</div>' | |
| 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'</div>' | |
| f'</div>' | |
| f'<div class="panel" style="margin-top:14px">' | |
| f'<div class="sec-label">Learned Patterns</div>' | |
| 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'</div>' | |
| ) | |
| notes = lp.get("notes", []) | |
| if notes: | |
| note_rows = "".join( | |
| f'<div style="padding:5px 0;border-bottom:1px solid #0d1117;color:#64748b;font-size:12px">β’ {n}</div>' | |
| for n in notes[-5:] | |
| ) | |
| html += f'<div class="panel" style="margin-top:14px"><div class="sec-label">AI Notes</div>{note_rows}</div>' | |
| history = ctx.get("history_summary", []) | |
| if history: | |
| hist_rows = "".join( | |
| f'<div style="padding:4px 0;color:#475569;font-size:12px">β’ {h}</div>' | |
| for h in history[-5:] | |
| ) | |
| html += f'<div class="panel" style="margin-top:14px"><div class="sec-label">Recent History</div>{hist_rows}</div>' | |
| return html | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # UI BUILD | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Blocks(css=CSS, 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) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # AUTH SECTION | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Column(visible=True, elem_id="auth-card") as auth_section: | |
| gr.HTML('<div id="brand-logo">π§ Second Brain</div>') | |
| gr.HTML('<div id="brand-sub">Your intelligent productivity companion</div>') | |
| 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="β’β’β’β’β’β’β’β’") | |
| with gr.Row(): | |
| reg_wake = gr.Textbox(label="Wake time", value="07:30", scale=1) | |
| reg_sleep = gr.Textbox(label="Sleep time", value="23:00", scale=1) | |
| reg_focus = gr.Dropdown(label="Peak focus", | |
| choices=["Morning","Afternoon","Evening","Night"], | |
| value="Morning", scale=1) | |
| 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('<div id="top-header-brand">π§ Second Brain</div><div id="top-header-user">β</div>') | |
| 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('<div class="sec-label">Describe Your Task</div>') | |
| 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("") | |
| gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>') | |
| 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="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("β 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('<div class="sec-label">Record or Upload Audio</div>') | |
| gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:12px">Whisper transcribes it, then AI classifies the task automatically.</p>') | |
| 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('<div class="sec-label">Plan My Day</div>') | |
| gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:12px">The AI reads your tasks, context, and goals to build the optimal schedule.</p>') | |
| plan_prompt = gr.Textbox( | |
| 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("β¨ Generate Schedule", elem_classes="btn-primary") | |
| btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary") | |
| schedule_html = gr.HTML("") | |
| # ββ Today's task table ββββββββββββββββββββββββββββββββββββ | |
| gr.HTML('<div class="sec-label" style="margin-top:20px">Today\'s Tasks</div>') | |
| 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: JOURNAL | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Journal"): | |
| gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:16px">Mark today\'s tasks complete, then reflect. The AI updates your profile to improve tomorrow\'s schedule.</p>') | |
| # Step 1 β Mark tasks | |
| with gr.Column(elem_classes="panel"): | |
| gr.HTML('<div class="sec-label">Step 1 β Mark Tasks Complete</div>') | |
| journal_task_list = gr.HTML('<p style="color:#334155;font-size:13px">Click "Load Tasks" to begin.</p>') | |
| 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('<div class="sec-label">Step 2 β Reflect</div>') | |
| journal_chatbot = gr.Chatbot(label="", height=380, bubble_full_width=False) | |
| 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('<div class="sec-label">Your Life Areas</div>') | |
| 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('<div class="sec-label" style="margin-top:24px">Remove Area</div>') | |
| 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('<hr>') | |
| gr.HTML('<div class="sec-label">Big-Picture Goals</div>') | |
| gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:10px">The AI uses these when scheduling and prioritising your tasks.</p>') | |
| 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('<div class="sec-label">Scheduling Preferences</div>') | |
| 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('<hr>') | |
| gr.HTML('<div class="sec-label">AI Memory</div>') | |
| gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:10px">Everything the AI has learned about you. Updates after each journaling session.</p>') | |
| 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'<div id="top-header-brand">π§ Second Brain</div><div id="top-header-user">π€ {uname}</div>', | |
| [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( | |
| lambda uid, uname: f'<div id="top-header-brand">π§ Second Brain</div><div id="top-header-user">π€ {uname}</div>', | |
| [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] | |
| ) | |
| # ββ 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] | |
| ) | |
| 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, parsed_panel] | |
| ) | |
| btn_gen_sched.click( | |
| handle_generate_schedule, | |
| [user_id_state, plan_prompt], | |
| [schedule_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] | |
| ) | |
| btn_today_del.click( | |
| handle_delete_today, | |
| [today_del_id, user_id_state], | |
| [today_df, stat_total, stat_done, stat_remain, today_action_msg] | |
| ) | |
| # ββ 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=[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) | |