second-brain / app.py
eraz3r's picture
Update app.py
cc4e015 verified
Raw
History Blame Contribute Delete
70.5 kB
"""
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)