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 json as _json | |
| import gradio as gr | |
| from datetime import date, datetime | |
| # ββ Core modules ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| from core.database import ( | |
| init_db, register_user, login_user, get_username, | |
| create_default_life_areas, save_goals, get_goals, | |
| get_life_areas, get_life_area_names, add_life_area, delete_life_area, | |
| get_tasks, save_task, assign_task_date, toggle_task_complete, delete_task, get_today_stats, | |
| load_user_context, save_user_context, spawn_due_habits, | |
| ) | |
| from core.ai_engine import ( | |
| init_groq, create_blank_context, | |
| parse_task_with_groq, generate_schedule, | |
| build_opening_question, get_next_journal_question, synthesize_journal, | |
| ) | |
| from core.styles import CSS | |
| # ββ Boot ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| init_db() | |
| init_groq() # reads GROQ_API_KEY from env | |
| TASK_HEADERS = ["ID", "Done", "Title", "Area", "Urgency", "Importance", "Mind", "Min", "Deadline", "Scheduled"] | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SHARED HELPERS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _ok(msg): return f'<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: | |
| _today = date.today() | |
| rows = [] | |
| for t in tasks: | |
| dl = (t.get("deadline_date") or "").strip() | |
| if dl and dl.lower() not in ("null", "none", ""): | |
| try: | |
| diff = (date.fromisoformat(dl) - _today).days | |
| if diff < 0: dl = f"β {dl}" | |
| elif diff == 0: dl = "π¨ TODAY" | |
| elif diff == 1: dl = "β οΈ tomorrow" | |
| elif diff <= 3: dl = f"β° {dl}" | |
| except ValueError: | |
| pass | |
| else: | |
| dl = "β" | |
| sched = (t.get("scheduled_date") or "").strip() | |
| rows.append([ | |
| t["id"], | |
| "β " if t["is_completed"] else "β¬", | |
| ("π " if t["is_habit"] else "") + t["title"], | |
| t["life_area"] or "β", | |
| t["urgency"] or "β", | |
| t["importance"] or "β", | |
| t["state_of_mind"] or "β", | |
| str(t["time_estimate"]) + "m" if t["time_estimate"] else "β", | |
| dl, | |
| sched if sched else "β¬ unscheduled", | |
| ]) | |
| return rows | |
| def _area_choices(user_id): | |
| if not user_id: | |
| return ["All"] | |
| return ["All"] + get_life_area_names(user_id) | |
| def _ensure_context(user_id): | |
| """Load or create blank AI context for a user.""" | |
| ctx = load_user_context(user_id) | |
| if not ctx: | |
| ctx = create_blank_context(user_id) | |
| save_user_context(user_id, ctx) | |
| return ctx | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # AUTH HANDLERS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def handle_login(username, password): | |
| uid, msg = login_user(username, password) | |
| if not uid: | |
| return None, "", _err(msg), gr.update(visible=True), gr.update(visible=False) | |
| spawn_due_habits(uid) | |
| uname = get_username(uid) | |
| return uid, uname, _ok(msg), gr.update(visible=False), gr.update(visible=True) | |
| def handle_register(username, password, wake, sleep, focus, goals_text): | |
| uid, msg = register_user(username, password) | |
| if not uid: | |
| return None, "", _err(msg), gr.update(visible=True), gr.update(visible=False) | |
| create_default_life_areas(uid) | |
| if goals_text.strip(): | |
| save_goals(uid, goals_text) | |
| prefs = {"wake_time": wake or "07:30", "sleep_time": sleep or "23:00", "focus_peak": focus or "Morning"} | |
| ctx = create_blank_context(uid, prefs) | |
| save_user_context(uid, ctx) | |
| spawn_due_habits(uid) | |
| uname = get_username(uid) | |
| return uid, uname, _ok(msg + " Logged in!"), gr.update(visible=False), gr.update(visible=True) | |
| def handle_logout(user_id): | |
| return None, "", gr.update(visible=True), gr.update(visible=False) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TODAY TAB HANDLERS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def refresh_today(user_id): | |
| if not user_id: | |
| return [], _stat("β","Today","#a78bfa"), _stat("β","Done","#4ade80"), _stat("β","Left","#f87171") | |
| tasks = get_tasks(user_id, only_today=True) | |
| s = get_today_stats(user_id) | |
| c = "#4ade80" if s["remaining"] == 0 and s["total"] > 0 else "#f87171" | |
| return ( | |
| _fmt_tasks(tasks), | |
| _stat(s["total"], "Today", "#a78bfa"), | |
| _stat(s["done"], "Done", "#4ade80"), | |
| _stat(s["remaining"], "Left", c), | |
| ) | |
| def show_text_panel(): | |
| return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False) | |
| def show_voice_panel(): | |
| return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False) | |
| def show_plan_panel(): | |
| return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True) | |
| def hide_panels(): | |
| return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) | |
| def handle_parse_text(task_text, user_id): | |
| """Call Groq to classify a typed task, return pre-filled fields.""" | |
| if not task_text.strip(): | |
| return (gr.update(visible=False), | |
| "", "", "Work", "Not Urgent", "Important", "Easy", 30, str(date.today()), False, "Daily", "", | |
| gr.update(visible=False), [], "") | |
| ctx = _ensure_context(user_id) | |
| goals = get_goals(user_id) | |
| areas = get_life_area_names(user_id) if user_id else [] | |
| result = parse_task_with_groq(task_text, ctx, goals, areas) | |
| clarifications = result.get("clarifications_needed", []) | |
| clar_html = "" | |
| has_clarifications = bool(clarifications) | |
| if clarifications: | |
| items = "".join(f"<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 | |
| raw_dl = result.get("deadline_date") or "" | |
| dl_val = "" if str(raw_dl).lower() in ("null", "none", "n/a", "") else str(raw_dl) | |
| return ( | |
| gr.update(visible=True), | |
| result.get("title", task_text), | |
| clar_html, | |
| area_val, | |
| result.get("urgency", "Not Urgent"), | |
| result.get("importance", "Important"), | |
| result.get("state_of_mind", "Easy"), | |
| int(result.get("time_estimate") or 30), | |
| dl_val, | |
| False, | |
| "Daily", | |
| _ok("AI classified β β answer questions above." if has_clarifications else "AI classified β"), | |
| gr.update(visible=has_clarifications), | |
| clarifications, | |
| task_text, | |
| ) | |
| def handle_clarification_reply(user_reply, original_task, clarifications, user_id): | |
| """Re-run Groq classification with the original task + clarification Q&A appended.""" | |
| if not user_reply.strip(): | |
| return (gr.update(), gr.update(), gr.update(), gr.update(), | |
| gr.update(), gr.update(), gr.update(), gr.update(visible=True), "") | |
| ctx = _ensure_context(user_id) | |
| goals = get_goals(user_id) | |
| areas = get_life_area_names(user_id) if user_id else [] | |
| # Build enriched task description: original + each clarification + the user's answer | |
| q_block = "\n".join(f"Q: {q}" for q in clarifications) | |
| enriched = ( | |
| f"{original_task}\n\n" | |
| f"Additional context from user (answers to clarification questions):\n" | |
| f"{q_block}\n" | |
| f"A: {user_reply}" | |
| ) | |
| result = parse_task_with_groq(enriched, ctx, goals, areas) | |
| # If AI still has clarifications, show them; otherwise hide the clarification row | |
| remaining = result.get("clarifications_needed", []) | |
| if remaining: | |
| items = "".join(f"<li style='margin:4px 0'>{q}</li>" for q in remaining) | |
| clar_html_val = ( | |
| 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>' | |
| ) | |
| clar_row_visible = True | |
| else: | |
| clar_html_val = '<span style="color:#4ade80;font-size:13px">β Clarified β classification updated!</span>' | |
| clar_row_visible = False | |
| area_choices = areas if areas else ["Work","Health","Finance","Learning","Personal","Family","Other"] | |
| area_val = result.get("life_area") or (area_choices[0] if area_choices else "Work") | |
| return ( | |
| result.get("title", original_task), | |
| clar_html_val, | |
| area_val, | |
| result.get("urgency", "Not Urgent"), | |
| result.get("importance", "Important"), | |
| result.get("state_of_mind", "Easy"), | |
| int(result.get("time_estimate") or 30), | |
| gr.update(visible=clar_row_visible), | |
| remaining, | |
| ) | |
| def handle_transcribe_voice(audio_path, user_id): | |
| """Whisper transcription then Groq parse β same pipeline as notebook.""" | |
| if audio_path is None: | |
| return gr.update(visible=False), _err("No audio recorded.") | |
| try: | |
| from faster_whisper import WhisperModel | |
| global _whisper_model | |
| if _whisper_model is None: | |
| _whisper_model = WhisperModel("small", device="cpu", compute_type="int8") | |
| segments, _ = _whisper_model.transcribe(audio_path) | |
| text = " ".join(seg.text for seg in segments).strip() | |
| return gr.update(visible=True, value=text), _ok(f'Transcribed: "{text[:60]}..."') | |
| except Exception as e: | |
| return gr.update(visible=False), _err(f"Transcription error: {e}") | |
| _whisper_model = None | |
| def handle_confirm_task(user_id, title, area, urgency, importance, state, | |
| time_est, deadline, is_habit, habit_interval): | |
| if not user_id: | |
| return _err("Not logged in."), [], _stat("β","Today","#a78bfa"), _stat("β","Done","#4ade80"), _stat("β","Left","#f87171"), [], gr.update(visible=False) | |
| if not title.strip(): | |
| return _err("Title cannot be empty."), [], _stat("β","Today","#a78bfa"), _stat("β","Done","#4ade80"), _stat("β","Left","#f87171"), [], gr.update(visible=True) | |
| task = { | |
| "title": title, "life_area": area, | |
| "urgency": urgency, "importance": importance, "state_of_mind": state, | |
| "time_estimate": int(time_est or 30), | |
| "deadline_date": (deadline or "").strip(), | |
| "is_habit": is_habit, | |
| "habit_interval": habit_interval if is_habit else "", | |
| } | |
| save_task(user_id, task, scheduled_date="") # saved unscheduled β planner assigns date | |
| rows, s1, s2, s3 = refresh_today(user_id) | |
| all_rows = _fmt_tasks(get_tasks(user_id)) | |
| return _ok("β Task saved! Use 'Plan My Day' to schedule it."), rows, s1, s2, s3, all_rows, gr.update(visible=False) | |
| def _render_sched_html(sched): | |
| COLOR = {"Flow": "#0ea5e9", "Easy": "#4ade80", "Quick": "#a78bfa", "Personal": "#f87171"} | |
| html = ( | |
| f'<div style="padding:12px;background:#0c1a0c;border-radius:8px;margin-bottom:12px">' | |
| f'<div style="color:#4ade80;font-size:12px;font-weight:700">π {sched.get("schedule_date","Today")}</div>' | |
| f'<div style="color:#94a3b8;font-size:13px;margin-top:4px">{sched.get("day_summary","")}</div>' | |
| 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};margin-bottom:6px">' | |
| 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="color:#475569;font-size:11px;font-weight:600;margin:10px 0 4px;text-transform:uppercase">Deferred</div>' | |
| for t in sched["deferred_tasks"]: | |
| html += f'<div style="color:#475569;font-size:12px;padding:2px 0">β {t["title"]} β {t.get("reason","")}</div>' | |
| for w in sched.get("warnings", []): | |
| html += f'<div style="color:#f59e0b;font-size:12px;margin-top:6px">β {w}</div>' | |
| return html | |
| def handle_generate_schedule(user_id, prompt): | |
| """Step 1: generate proposal only. Nothing written to DB.""" | |
| if not user_id: | |
| return _err("Not logged in."), "", gr.update(visible=False) | |
| tasks = get_tasks(user_id, include_completed=False) | |
| if not tasks: | |
| return _err("No tasks found. Add some tasks first."), "", gr.update(visible=False) | |
| ctx = _ensure_context(user_id) | |
| goals = get_goals(user_id) | |
| try: | |
| sched = generate_schedule(ctx, tasks, prompt or "Schedule all my tasks intelligently across upcoming days.", goals) | |
| except Exception as e: | |
| return _err(f"Scheduling error: {e}"), "", gr.update(visible=False) | |
| if "error" in sched and "scheduled_tasks" not in sched: | |
| return _err(f"Scheduling failed: {sched.get('error')}"), "", gr.update(visible=False) | |
| html = _render_sched_html(sched) | |
| return html, _json.dumps(sched), gr.update(visible=True) | |
| def handle_confirm_schedule(user_id, pending_json): | |
| """Step 2a: apply the proposed schedule to the database.""" | |
| if not user_id or not pending_json: | |
| return _err("Nothing to confirm."), gr.update(visible=False), [], [] | |
| try: | |
| sched = _json.loads(pending_json) | |
| except Exception: | |
| return _err("Could not read proposal."), gr.update(visible=False), [], [] | |
| count = 0 | |
| sched_date = sched.get("schedule_date", str(date.today())) | |
| for t in sched.get("scheduled_tasks", []): | |
| tid = t.get("task_id") or t.get("id") | |
| if tid: | |
| try: | |
| assign_task_date(int(tid), user_id, sched_date) | |
| count += 1 | |
| except Exception: | |
| pass | |
| all_rows = _fmt_tasks(get_tasks(user_id)) | |
| today_rows = _fmt_tasks(get_tasks(user_id, only_today=True)) | |
| return _ok(f"β {count} tasks scheduled for {sched_date}!"), gr.update(visible=False), all_rows, today_rows | |
| def handle_refine_schedule(user_id, refinement, pending_json): | |
| """Step 2b: re-run with a refinement instruction.""" | |
| if not user_id or not refinement.strip(): | |
| return gr.update(), gr.update(), gr.update() | |
| hint = "" | |
| if pending_json: | |
| try: | |
| old = _json.loads(pending_json) | |
| hint = f"\n\nPrevious plan summary: {old.get('day_summary', '')}" | |
| except Exception: | |
| pass | |
| return handle_generate_schedule(user_id, refinement + hint) | |
| def handle_manual_assign(task_id, task_date, user_id): | |
| """Manually assign a date to a specific task.""" | |
| if not task_id or not task_date or not user_id: | |
| return _err("Enter task ID and date.") | |
| try: | |
| assign_task_date(int(task_id), user_id, task_date.strip()) | |
| return _ok(f"Task #{int(task_id)} assigned to {task_date.strip()}") | |
| except Exception as e: | |
| return _err(f"Error: {e}") | |
| def handle_toggle_today(task_id, user_id): | |
| if task_id and user_id: | |
| toggle_task_complete(int(task_id), user_id) | |
| rows, s1, s2, s3 = refresh_today(user_id) | |
| return rows, s1, s2, s3, _ok("Updated") | |
| def handle_delete_today(task_id, user_id): | |
| if task_id and user_id: | |
| delete_task(int(task_id), user_id) | |
| rows, s1, s2, s3 = refresh_today(user_id) | |
| return rows, s1, s2, s3, _ok("Deleted") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ALL TASKS TAB HANDLERS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def refresh_all_tasks(user_id, filter_area="All", only_today=False): | |
| if not user_id: | |
| return [] | |
| return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today)) | |
| def handle_toggle_all(task_id, user_id, filter_area, only_today): | |
| if task_id and user_id: | |
| toggle_task_complete(int(task_id), user_id) | |
| return refresh_all_tasks(user_id, filter_area, only_today), _ok("Updated") | |
| def handle_delete_all(task_id, user_id, filter_area, only_today): | |
| if task_id and user_id: | |
| delete_task(int(task_id), user_id) | |
| return refresh_all_tasks(user_id, filter_area, only_today), _ok("Deleted") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # JOURNAL TAB HANDLERS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def handle_load_journal_tasks(user_id): | |
| if not user_id: | |
| return "<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 = [{"role": "assistant", "content": opening["question"]}] | |
| hist = [{"role": "assistant", "content": opening["question"], "focus": opening["question_focus"]}] | |
| return ( | |
| chat, hist, True, | |
| gr.update(interactive=True), | |
| gr.update(interactive=True), | |
| _ok("Session started β type your answer below"), | |
| ) | |
| def handle_send_answer(user_id, answer, chat, hist, tasks_state, active): | |
| if not active or not answer.strip(): | |
| return chat, hist, "", "" | |
| chat = list(chat) + [{"role": "user", "content": answer}] | |
| hist = list(hist) + [{"role": "user", "content": answer}] | |
| ctx = _ensure_context(user_id) | |
| result = get_next_journal_question(ctx, tasks_state, hist) | |
| if result.get("session_complete"): | |
| chat = list(chat) + [{"role": "assistant", "content": "β That's enough for today. Click **Finish & Save** to update your profile."}] | |
| return chat, hist, "", _ok("Session complete β save when ready") | |
| next_q = result.get("question", "") | |
| hist.append({"role": "assistant", "content": next_q, "focus": result.get("question_focus", "")}) | |
| chat = list(chat) + [{"role": "assistant", "content": next_q}] | |
| return chat, hist, "", "" | |
| def handle_finish_journal(user_id, chat, hist, tasks_state): | |
| if not user_id or not hist: | |
| return chat, _err("Nothing to save.") | |
| ctx = _ensure_context(user_id) | |
| updated = synthesize_journal(ctx, tasks_state, hist) | |
| save_user_context(user_id, updated) | |
| total = len(tasks_state) | |
| done = sum(1 for t in tasks_state if t.get("completed")) | |
| rate = round(done / total * 100) if total else 0 | |
| notes = updated.get("learned_patterns", {}).get("notes", []) | |
| note_html = "".join(f"<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 | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CALENDAR VIEW | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def render_calendar(user_id): | |
| if not user_id: | |
| return "<p style='color:#475569;font-size:13px'>Sign in to view calendar.</p>" | |
| tasks = get_tasks(user_id) | |
| if not tasks: | |
| return "<p style='color:#475569;font-size:13px'>No tasks yet.</p>" | |
| _today = date.today() | |
| COLOR = {"Flow":"#0ea5e9","Easy":"#4ade80","Quick":"#a78bfa","Personal":"#f87171"} | |
| by_date, unscheduled = {}, [] | |
| for t in tasks: | |
| sd = (t.get("scheduled_date") or "").strip() | |
| if sd: | |
| by_date.setdefault(sd, []).append(t) | |
| else: | |
| unscheduled.append(t) | |
| html = "" | |
| for d in sorted(by_date.keys()): | |
| try: | |
| dt = date.fromisoformat(d) | |
| diff = (dt - _today).days | |
| if diff == 0: label = f"TODAY β {dt.strftime('%a, %b %d')}" | |
| elif diff == 1: label = f"TOMORROW β {dt.strftime('%a, %b %d')}" | |
| elif diff < 0: label = dt.strftime('%a, %b %d') + " (past)" | |
| else: label = dt.strftime('%a, %b %d') | |
| hdr = "#a78bfa" if diff == 0 else "#60a5fa" if diff > 0 else "#334155" | |
| except ValueError: | |
| label, hdr = d, "#60a5fa" | |
| html += (f'<div style="margin-bottom:20px">' | |
| f'<div style="color:{hdr};font-size:11px;font-weight:700;letter-spacing:1px;' | |
| f'text-transform:uppercase;padding:6px 0;border-bottom:1px solid #1e293b;margin-bottom:8px">' | |
| f'π {label}</div>') | |
| for t in by_date[d]: | |
| sm = t.get("state_of_mind","Easy") | |
| c = COLOR.get(sm,"#6366f1") | |
| done = t.get("is_completed") | |
| dl = (t.get("deadline_date") or "").strip() | |
| dl_badge = (f'<span style="background:#f8714422;color:#f87171;font-size:10px;' | |
| f'padding:1px 5px;border-radius:4px;margin-left:6px">due {dl}</span>' | |
| ) if dl and dl.lower() not in ("null","none","") else "" | |
| html += (f'<div style="padding:8px 10px;margin-bottom:4px;background:#0c0f1a;border-radius:8px;' | |
| f'border-left:3px solid {c};opacity:{"0.4" if done else "1"}">' | |
| f'<div style="color:#cbd5e1;font-size:13px">{"β " if done else "β¬"} ' | |
| f'<b>#{t["id"]}</b> {t["title"]}{dl_badge}</div>' | |
| f'<div style="color:#475569;font-size:11px;margin-top:2px">' | |
| f'{t.get("life_area","β")} Β· {sm} Β· {t.get("time_estimate","?")}min</div></div>') | |
| html += '</div>' | |
| if unscheduled: | |
| html += ('<div style="margin-top:10px"><div style="color:#334155;font-size:11px;font-weight:700;' | |
| 'letter-spacing:1px;text-transform:uppercase;padding:6px 0;border-bottom:1px solid #1e293b;margin-bottom:8px">' | |
| 'β¬ UNSCHEDULED β Use Plan My Day to assign dates</div>') | |
| for t in unscheduled: | |
| sm = t.get("state_of_mind","Easy") | |
| c = COLOR.get(sm,"#6366f1") | |
| dl = (t.get("deadline_date") or "").strip() | |
| dl_badge = (f'<span style="background:#f8714422;color:#f87171;font-size:10px;' | |
| f'padding:1px 5px;border-radius:4px;margin-left:6px">due {dl}</span>' | |
| ) if dl and dl.lower() not in ("null","none","") else "" | |
| html += (f'<div style="padding:8px 10px;margin-bottom:4px;background:#0c0f1a;border-radius:8px;' | |
| f'border-left:3px solid {c}">' | |
| f'<div style="color:#94a3b8;font-size:13px">β¬ <b>#{t["id"]}</b> {t["title"]}{dl_badge}</div>' | |
| f'<div style="color:#475569;font-size:11px;margin-top:2px">' | |
| f'{t.get("life_area","β")} Β· {sm} Β· {t.get("time_estimate","?")}min</div></div>') | |
| html += '</div>' | |
| return html or "<p style='color:#475569'>No tasks.</p>" | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # UI BUILD | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Blocks(title="π§ Second Brain") as demo: | |
| # ββ Session state ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| user_id_state = gr.State(None) | |
| username_state = gr.State("") | |
| # Journal states | |
| journal_chat_state = gr.State([]) | |
| journal_hist_state = gr.State([]) | |
| journal_tasks_state = gr.State([]) | |
| journal_active = gr.State(False) | |
| # Clarification states | |
| clar_questions_state = gr.State([]) | |
| original_task_state = gr.State("") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # AUTH SECTION | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Column(visible=True, elem_id="auth-card") as auth_section: | |
| gr.HTML('<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="β’β’β’β’β’β’β’β’") | |
| 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('<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("") | |
| # ββ Clarification reply row (shown only when AI has questions) ββ | |
| with gr.Column(visible=False) as clar_reply_row: | |
| gr.HTML('<div class="sec-label" style="margin-top:8px">Your Answer</div>') | |
| 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('<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="Deadline (YYYY-MM-DD, optional)", placeholder="e.g. 2026-03-15", value="", scale=1) | |
| f_is_habit = gr.Checkbox(label="β»οΈ This is a habit", scale=1) | |
| f_interval = gr.Dropdown(label="Recurs", | |
| choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], | |
| value="Daily", visible=False, scale=1) | |
| with gr.Row(): | |
| btn_confirm = gr.Button("β Save Task", elem_classes="btn-success") | |
| btn_discard = gr.Button("β Discard", elem_classes="btn-danger") | |
| save_msg = gr.HTML("") | |
| # ββ Voice add panel βββββββββββββββββββββββββββββββββββββββ | |
| with gr.Column(visible=False, elem_classes="panel") as voice_panel: | |
| gr.HTML('<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:#94a3b8;font-size:13px;margin-bottom:10px">AI schedules ALL your unscheduled tasks using your patterns and goals. Review before applying.</p>') | |
| plan_prompt = gr.Textbox( | |
| label="Instructions (optional)", | |
| placeholder="e.g. 'Schedule everything for this week' or 'I have a meeting at 2pm, focus on Work tasks'", | |
| lines=2 | |
| ) | |
| with gr.Row(): | |
| btn_gen_sched = gr.Button("π§ Generate Plan", elem_classes="btn-primary") | |
| btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary") | |
| schedule_html = gr.HTML("") | |
| sched_pending = gr.State("") | |
| with gr.Column(visible=False) as sched_confirm_row: | |
| gr.HTML('<div style="padding:10px;background:#0c1a0c;border-radius:8px;margin:8px 0">' | |
| '<span style="color:#4ade80;font-weight:600;font-size:13px">π Review the plan above, then:</span></div>') | |
| with gr.Row(): | |
| btn_sched_ok = gr.Button("β Confirm & Apply", elem_classes="btn-success", scale=2) | |
| btn_sched_discard = gr.Button("β Discard", elem_classes="btn-danger", scale=1) | |
| gr.HTML('<div style="color:#475569;font-size:12px;margin:10px 0 4px">π¬ Or refine it:</div>') | |
| with gr.Row(): | |
| sched_refine_txt = gr.Textbox(label="", placeholder="e.g. Move Flow tasks to morning, skip Fridays", scale=4) | |
| btn_sched_refine = gr.Button("π Re-plan", elem_classes="btn-accent", scale=1) | |
| gr.HTML('<div style="color:#475569;font-size:12px;margin:10px 0 4px">π Or manually assign a task:</div>') | |
| with gr.Row(): | |
| manual_tid = gr.Number(label="Task ID", precision=0, scale=1) | |
| manual_date = gr.Textbox(label="Date (YYYY-MM-DD)", placeholder=str(date.today()), scale=2) | |
| btn_manual = gr.Button("Assign Date", elem_classes="btn-secondary", scale=1) | |
| manual_msg = gr.HTML("") | |
| sched_status = gr.HTML("") | |
| # ββ Today's task table ββββββββββββββββββββββββββββββββββββ | |
| gr.HTML('<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: CALENDAR | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Calendar"): | |
| gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">All tasks grouped by scheduled date. Unscheduled tasks appear at the bottom β use Plan My Day to assign them.</p>') | |
| with gr.Row(): | |
| btn_cal_refresh = gr.Button("β» Refresh", elem_classes="btn-secondary", scale=1) | |
| calendar_html = gr.HTML("") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 4: JOURNAL | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Journal"): | |
| gr.HTML('<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) | |
| 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: _fmt_tasks(get_tasks(uid)) if uid else [], | |
| [user_id_state], [all_df] | |
| ).then( | |
| render_calendar, [user_id_state], [calendar_html] | |
| ).then( | |
| lambda uid: render_areas(uid)[0:2], | |
| [user_id_state], [areas_display, del_area_dd] | |
| ).then( | |
| load_goals_txt, [user_id_state], [goals_input] | |
| ).then( | |
| lambda uid: load_prefs(uid), | |
| [user_id_state], [pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max] | |
| ) | |
| reg_btn.click( | |
| handle_register, | |
| [reg_user_in, reg_pass_in, reg_wake, reg_sleep, reg_focus, reg_goals], | |
| [user_id_state, username_state, reg_msg, auth_section, app_section] | |
| ).then( | |
| lambda uid, uname: f'<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: _fmt_tasks(get_tasks(uid)) if uid else [], | |
| [user_id_state], [all_df] | |
| ).then( | |
| render_calendar, [user_id_state], [calendar_html] | |
| ).then( | |
| lambda uid: render_areas(uid)[0:2], | |
| [user_id_state], [areas_display, del_area_dd] | |
| ) | |
| logout_btn.click( | |
| handle_logout, | |
| [user_id_state], | |
| [user_id_state, username_state, auth_section, app_section] | |
| ) | |
| # ββ Today tab βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| btn_add_text.click(show_text_panel, outputs=[text_panel, voice_panel, plan_panel]) | |
| btn_add_voice.click(show_voice_panel, outputs=[text_panel, voice_panel, plan_panel]) | |
| btn_plan_day.click(show_plan_panel, outputs=[text_panel, voice_panel, plan_panel]) | |
| btn_cancel_text.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel]) | |
| btn_cancel_voice.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel]) | |
| btn_cancel_plan.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel]) | |
| btn_refresh.click( | |
| refresh_today, [user_id_state], | |
| [today_df, stat_total, stat_done, stat_remain] | |
| ) | |
| # Show habit interval dropdown only when habit checked | |
| f_is_habit.change( | |
| lambda v: gr.update(visible=v), | |
| [f_is_habit], [f_interval] | |
| ) | |
| btn_parse.click( | |
| handle_parse_text, | |
| [task_input, user_id_state], | |
| [parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance, | |
| f_state, f_time, f_date, f_is_habit, f_interval, parse_status, | |
| clar_reply_row, clar_questions_state, original_task_state] | |
| ) | |
| btn_clar_submit.click( | |
| handle_clarification_reply, | |
| [clar_reply_input, original_task_state, clar_questions_state, user_id_state], | |
| [f_title, clar_html, f_area, f_urgency, f_importance, | |
| f_state, f_time, clar_reply_row, clar_questions_state] | |
| ).then( | |
| lambda: "", | |
| outputs=[clar_reply_input] | |
| ) | |
| btn_transcribe.click( | |
| handle_transcribe_voice, | |
| [audio_input, user_id_state], | |
| [transcribed, voice_msg] | |
| ) | |
| btn_discard.click( | |
| lambda: gr.update(visible=False), | |
| outputs=[parsed_panel] | |
| ) | |
| btn_confirm.click( | |
| handle_confirm_task, | |
| [user_id_state, f_title, f_area, f_urgency, f_importance, f_state, | |
| f_time, f_date, f_is_habit, f_interval], | |
| [save_msg, today_df, stat_total, stat_done, stat_remain, all_df, parsed_panel] | |
| ).then(render_calendar, [user_id_state], [calendar_html]) | |
| btn_gen_sched.click( | |
| handle_generate_schedule, | |
| [user_id_state, plan_prompt], | |
| [schedule_html, sched_pending, sched_confirm_row] | |
| ) | |
| btn_sched_ok.click( | |
| handle_confirm_schedule, | |
| [user_id_state, sched_pending], | |
| [sched_status, sched_confirm_row, all_df, today_df] | |
| ).then(render_calendar, [user_id_state], [calendar_html]) | |
| btn_sched_discard.click( | |
| lambda: ("", gr.update(visible=False)), | |
| outputs=[schedule_html, sched_confirm_row] | |
| ) | |
| btn_sched_refine.click( | |
| handle_refine_schedule, | |
| [user_id_state, sched_refine_txt, sched_pending], | |
| [schedule_html, sched_pending, sched_confirm_row] | |
| ).then(lambda: "", outputs=[sched_refine_txt]) | |
| btn_manual.click( | |
| handle_manual_assign, | |
| [manual_tid, manual_date, user_id_state], | |
| [manual_msg] | |
| ).then( | |
| lambda uid: _fmt_tasks(get_tasks(uid)) if uid else [], | |
| [user_id_state], [all_df] | |
| ).then(render_calendar, [user_id_state], [calendar_html]) | |
| btn_today_done.click( | |
| handle_toggle_today, | |
| [today_done_id, user_id_state], | |
| [today_df, stat_total, stat_done, stat_remain, today_action_msg] | |
| ).then( | |
| lambda uid: _fmt_tasks(get_tasks(uid)) if uid else [], | |
| [user_id_state], [all_df] | |
| ).then(render_calendar, [user_id_state], [calendar_html]) | |
| btn_today_del.click( | |
| handle_delete_today, | |
| [today_del_id, user_id_state], | |
| [today_df, stat_total, stat_done, stat_remain, today_action_msg] | |
| ).then( | |
| lambda uid: _fmt_tasks(get_tasks(uid)) if uid else [], | |
| [user_id_state], [all_df] | |
| ).then(render_calendar, [user_id_state], [calendar_html]) | |
| # ββ Calendar tab ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| btn_cal_refresh.click(render_calendar, [user_id_state], [calendar_html]) | |
| # ββ All Tasks tab ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| btn_all_ref.click( | |
| refresh_all_tasks, | |
| [user_id_state, all_filter, all_today_chk], | |
| [all_df] | |
| ) | |
| all_filter.change( | |
| refresh_all_tasks, | |
| [user_id_state, all_filter, all_today_chk], | |
| [all_df] | |
| ) | |
| all_today_chk.change( | |
| refresh_all_tasks, | |
| [user_id_state, all_filter, all_today_chk], | |
| [all_df] | |
| ) | |
| btn_all_done.click( | |
| handle_toggle_all, | |
| [all_done_id, user_id_state, all_filter, all_today_chk], | |
| [all_df, all_action_msg] | |
| ).then(render_calendar, [user_id_state], [calendar_html]) | |
| btn_all_del.click( | |
| handle_delete_all, | |
| [all_del_id, user_id_state, all_filter, all_today_chk], | |
| [all_df, all_action_msg] | |
| ).then(render_calendar, [user_id_state], [calendar_html]) | |
| # ββ Journal tab ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| btn_load_tasks.click( | |
| handle_load_journal_tasks, | |
| [user_id_state], | |
| [journal_task_list, journal_mark_row, journal_tasks_state] | |
| ) | |
| btn_j_done.click( | |
| lambda tid, mins, uid, st: handle_journal_mark(tid, mins, uid, st, True), | |
| [j_task_id, j_actual_min, user_id_state, journal_tasks_state], | |
| [journal_tasks_state, journal_mark_msg] | |
| ).then( | |
| handle_load_journal_tasks, | |
| [user_id_state], | |
| [journal_task_list, journal_mark_row, journal_tasks_state] | |
| ) | |
| btn_j_undone.click( | |
| lambda tid, mins, uid, st: handle_journal_mark(tid, mins, uid, st, False), | |
| [j_task_id, j_actual_min, user_id_state, journal_tasks_state], | |
| [journal_tasks_state, journal_mark_msg] | |
| ).then( | |
| handle_load_journal_tasks, | |
| [user_id_state], | |
| [journal_task_list, journal_mark_row, journal_tasks_state] | |
| ) | |
| btn_j_start.click( | |
| handle_start_journal, | |
| [user_id_state, journal_tasks_state], | |
| [journal_chatbot, journal_hist_state, journal_active, | |
| j_answer, btn_j_send, journal_msg] | |
| ) | |
| btn_j_send.click( | |
| handle_send_answer, | |
| [user_id_state, j_answer, journal_chatbot, journal_hist_state, | |
| journal_tasks_state, journal_active], | |
| [journal_chatbot, journal_hist_state, j_answer, journal_msg] | |
| ) | |
| j_answer.submit( | |
| handle_send_answer, | |
| [user_id_state, j_answer, journal_chatbot, journal_hist_state, | |
| journal_tasks_state, journal_active], | |
| [journal_chatbot, journal_hist_state, j_answer, journal_msg] | |
| ) | |
| btn_j_finish.click( | |
| handle_finish_journal, | |
| [user_id_state, journal_chatbot, journal_hist_state, journal_tasks_state], | |
| [journal_chatbot, journal_msg] | |
| ) | |
| btn_j_restart.click( | |
| handle_restart_journal, | |
| outputs=[journal_chatbot, journal_hist_state, journal_active, | |
| j_answer, btn_j_send, journal_msg] | |
| ) | |
| # ββ Areas & Goals tab βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| btn_add_area.click( | |
| handle_add_area, | |
| [user_id_state, new_area_name, new_area_color], | |
| [area_msg, areas_display, del_area_dd, new_area_name] | |
| ).then( | |
| lambda uid: gr.update(choices=_area_choices(uid), value="All"), | |
| [user_id_state], [all_filter] | |
| ) | |
| btn_del_area.click( | |
| handle_del_area, | |
| [user_id_state, del_area_dd], | |
| [del_area_msg, areas_display, del_area_dd] | |
| ).then( | |
| lambda uid: gr.update(choices=_area_choices(uid), value="All"), | |
| [user_id_state], [all_filter] | |
| ) | |
| btn_save_goals.click( | |
| handle_save_goals, | |
| [user_id_state, goals_input], | |
| [goals_msg] | |
| ) | |
| # ββ Preferences tab βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| btn_save_prefs.click( | |
| handle_save_prefs, | |
| [user_id_state, pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max], | |
| [prefs_msg] | |
| ) | |
| btn_ref_ctx.click( | |
| render_context_html, | |
| [user_id_state], | |
| [ctx_display] | |
| ) | |
| # Refresh AI context display whenever the refresh button is clicked | |
| # (Gradio Tabs.select doesn't support conditional output, so we use the button) | |
| # ββ Launch βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860, css=CSS) |