eraz3r commited on
Commit
72b5f8d
·
verified ·
1 Parent(s): 3c3057f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +495 -924
app.py CHANGED
@@ -1,24 +1,11 @@
1
  """
2
  app.py — Second Brain Gradio Application
3
- ==========================================
4
- Modular structure:
5
- core/database.py — SQLite persistence (users, tasks, life areas, goals, AI context)
6
- core/ai_engine.py — All Groq API calls (task parsing, scheduling, journaling)
7
- core/styles.py — CSS
8
-
9
- Run locally:
10
- pip install -r requirements.txt
11
- GROQ_API_KEY=gsk_... python app.py
12
-
13
- HuggingFace Spaces:
14
- Add GROQ_API_KEY in Space Settings → Repository Secrets
15
  """
16
 
17
  import os
18
  import gradio as gr
19
  from datetime import date, datetime
20
 
21
- # ── Core modules ──────────────────────────────────────────────────────────────
22
  from core.database import (
23
  init_db, register_user, login_user, get_username,
24
  create_default_life_areas, save_goals, get_goals,
@@ -33,66 +20,53 @@ from core.ai_engine import (
33
  )
34
  from core.styles import CSS
35
 
36
- # ── Boot ──────────────────────────────────────────────────────────────────────
37
  init_db()
38
- init_groq() # reads GROQ_API_KEY from env
39
 
40
  TASK_HEADERS = ["ID", "Done", "Title", "Area", "Urgency", "Importance", "Mind", "Min", "Date"]
 
41
 
42
-
43
- # ═══════════════════════════════════════════════════════════════════════════════
44
- # SHARED HELPERS
45
- # ═══════════════════════════════════════════════════════════════════════════════
46
-
47
  def _ok(msg): return f'<span style="color:#4ade80;font-size:13px">✓ {msg}</span>'
48
  def _err(msg): return f'<span style="color:#f87171;font-size:13px">⚠ {msg}</span>'
49
- def _info(msg): return f'<span style="color:#60a5fa;font-size:13px">ℹ {msg}</span>'
50
 
51
  def _stat(val, label, color):
52
- return f'<div class="stat-card"><div class="stat-num" style="color:{color}">{val}</div><div class="stat-label">{label}</div></div>'
53
-
54
- def _fmt_tasks(tasks: list) -> list:
55
- rows = []
56
- for t in tasks:
57
- rows.append([
58
- t["id"],
59
- "✅" if t["is_completed"] else "",
60
- ("🔁 " if t["is_habit"] else "") + t["title"],
61
- t["life_area"] or "—",
62
- t["urgency"] or "—",
63
- t["importance"] or "—",
64
- t["state_of_mind"] or "—",
65
- str(t["time_estimate"]) + "m" if t["time_estimate"] else "—",
66
- t["scheduled_date"] or "—",
67
- ])
68
- return rows
69
-
70
- def _area_choices(user_id):
71
- if not user_id:
72
- return ["All"]
73
- return ["All"] + get_life_area_names(user_id)
74
-
75
- def _ensure_context(user_id):
76
- """Load or create blank AI context for a user."""
77
- ctx = load_user_context(user_id)
78
  if not ctx:
79
- ctx = create_blank_context(user_id)
80
- save_user_context(user_id, ctx)
81
  return ctx
82
 
 
 
 
 
 
 
 
83
 
84
- # ═══════════════════════════════════════════════════════════════════════════════
85
- # AUTH HANDLERS
86
- # ═══════════════════════════════════════════════════════════════════════════════
87
-
88
  def handle_login(username, password):
89
  uid, msg = login_user(username, password)
90
  if not uid:
91
  return None, "", _err(msg), gr.update(visible=True), gr.update(visible=False)
92
  spawn_due_habits(uid)
93
- uname = get_username(uid)
94
- return uid, uname, _ok(msg), gr.update(visible=False), gr.update(visible=True)
95
-
96
 
97
  def handle_register(username, password, wake, sleep, focus, goals_text):
98
  uid, msg = register_user(username, password)
@@ -101,716 +75,463 @@ def handle_register(username, password, wake, sleep, focus, goals_text):
101
  create_default_life_areas(uid)
102
  if goals_text.strip():
103
  save_goals(uid, goals_text)
104
- prefs = {"wake_time": wake or "07:30", "sleep_time": sleep or "23:00", "focus_peak": focus or "Morning"}
105
- ctx = create_blank_context(uid, prefs)
 
106
  save_user_context(uid, ctx)
107
  spawn_due_habits(uid)
108
- uname = get_username(uid)
109
- return uid, uname, _ok(msg + " Logged in!"), gr.update(visible=False), gr.update(visible=True)
110
 
111
-
112
- def handle_logout(user_id):
113
  return None, "", gr.update(visible=True), gr.update(visible=False)
114
 
115
-
116
- # ═══════════════════════════════════════════════════════════════════════════════
117
- # TODAY TAB HANDLERS
118
- # ═══════════════════════════════════════════════════════════════════════════════
119
-
120
- def refresh_today(user_id):
121
- if not user_id:
122
  return [], _stat("—","Today","#a78bfa"), _stat("—","Done","#4ade80"), _stat("—","Left","#f87171")
123
- tasks = get_tasks(user_id, only_today=True)
124
- s = get_today_stats(user_id)
125
- c = "#4ade80" if s["remaining"] == 0 and s["total"] > 0 else "#f87171"
126
- return (
127
- _fmt_tasks(tasks),
128
- _stat(s["total"], "Today", "#a78bfa"),
129
- _stat(s["done"], "Done", "#4ade80"),
130
- _stat(s["remaining"], "Left", c),
131
- )
132
-
133
-
134
- def show_text_panel():
135
- return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
136
-
137
- def show_voice_panel():
138
- return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
139
-
140
- def show_plan_panel():
141
- return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
142
-
143
- def hide_panels():
144
- return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
145
-
146
-
147
- def handle_parse_text(task_text, user_id):
148
- """Call Groq to classify a typed task, return pre-filled fields."""
149
- if not task_text.strip():
150
- return (gr.update(visible=False),
151
- "", "", "Work", "Not Urgent", "Important", "Easy", 30, str(date.today()), False, "Daily", "",
152
- gr.update(visible=False), [], "")
153
-
154
- ctx = _ensure_context(user_id)
155
- goals = get_goals(user_id)
156
- areas = get_life_area_names(user_id) if user_id else []
157
-
158
- result = parse_task_with_groq(task_text, ctx, goals, areas)
159
-
160
- clarifications = result.get("clarifications_needed", [])
161
  clar_html = ""
162
- has_clarifications = bool(clarifications)
163
- if clarifications:
164
- items = "".join(f"<li style='margin:4px 0'>{q}</li>" for q in clarifications)
165
- clar_html = (
166
- f'<div style="margin:10px 0;padding:12px;background:#0c0f1a;'
167
- f'border-left:3px solid #a78bfa;border-radius:8px">'
168
- f'<div style="color:#a78bfa;font-size:11px;font-weight:600;margin-bottom:6px">⚠ Please clarify:</div>'
169
- f'<ul style="color:#94a3b8;font-size:13px;margin:0;padding-left:16px">{items}</ul></div>'
170
- )
171
-
172
- area_choices = areas if areas else ["Work", "Health", "Finance", "Learning", "Personal", "Family", "Other"]
173
- area_val = result.get("life_area") or (area_choices[0] if area_choices else "Work")
174
- if area_val not in area_choices:
175
- area_choices = [area_val] + area_choices
176
-
177
- return (
178
- gr.update(visible=True),
179
- result.get("title", task_text),
180
- clar_html,
181
- area_val,
182
- result.get("urgency", "Not Urgent"),
183
- result.get("importance", "Important"),
184
- result.get("state_of_mind", "Easy"),
185
- int(result.get("time_estimate") or 30),
186
- str(date.today()),
187
- False,
188
- "Daily",
189
- _ok("AI classified your task — please answer the clarification questions above." if has_clarifications else "AI classified your task"),
190
- gr.update(visible=has_clarifications), # clar_reply_row
191
- clarifications, # clar_questions_state
192
- task_text, # original_task_state
193
- )
194
-
195
-
196
- def handle_clarification_reply(user_reply, original_task, clarifications, user_id):
197
- """Re-run Groq classification with the original task + clarification Q&A appended."""
198
- if not user_reply.strip():
199
- return (gr.update(), gr.update(), gr.update(), gr.update(),
200
- gr.update(), gr.update(), gr.update(), gr.update(visible=True), "")
201
-
202
- ctx = _ensure_context(user_id)
203
- goals = get_goals(user_id)
204
- areas = get_life_area_names(user_id) if user_id else []
205
-
206
- # Build enriched task description: original + each clarification + the user's answer
207
- q_block = "\n".join(f"Q: {q}" for q in clarifications)
208
- enriched = (
209
- f"{original_task}\n\n"
210
- f"Additional context from user (answers to clarification questions):\n"
211
- f"{q_block}\n"
212
- f"A: {user_reply}"
213
- )
214
-
215
- result = parse_task_with_groq(enriched, ctx, goals, areas)
216
-
217
- # If AI still has clarifications, show them; otherwise hide the clarification row
218
- remaining = result.get("clarifications_needed", [])
219
- if remaining:
220
- items = "".join(f"<li style='margin:4px 0'>{q}</li>" for q in remaining)
221
- clar_html_val = (
222
- f'<div style="margin:10px 0;padding:12px;background:#0c0f1a;'
223
- f'border-left:3px solid #a78bfa;border-radius:8px">'
224
- f'<div style="color:#a78bfa;font-size:11px;font-weight:600;margin-bottom:6px">⚠ Please clarify:</div>'
225
- f'<ul style="color:#94a3b8;font-size:13px;margin:0;padding-left:16px">{items}</ul></div>'
226
- )
227
- clar_row_visible = True
228
- else:
229
- clar_html_val = '<span style="color:#4ade80;font-size:13px">✓ Clarified — classification updated!</span>'
230
- clar_row_visible = False
231
-
232
- area_choices = areas if areas else ["Work","Health","Finance","Learning","Personal","Family","Other"]
233
- area_val = result.get("life_area") or (area_choices[0] if area_choices else "Work")
234
-
235
- return (
236
- result.get("title", original_task),
237
- clar_html_val,
238
- area_val,
239
- result.get("urgency", "Not Urgent"),
240
- result.get("importance", "Important"),
241
- result.get("state_of_mind", "Easy"),
242
- int(result.get("time_estimate") or 30),
243
- gr.update(visible=clar_row_visible),
244
- remaining,
245
- )
246
-
247
-
248
- def handle_transcribe_voice(audio_path, user_id):
249
- """Whisper transcription then Groq parse — same pipeline as notebook."""
250
  if audio_path is None:
251
- return gr.update(visible=False), _err("No audio recorded.")
252
  try:
253
- from faster_whisper import WhisperModel
254
- global _whisper_model
255
- if _whisper_model is None:
256
- _whisper_model = WhisperModel("small", device="cpu", compute_type="int8")
257
- segments, _ = _whisper_model.transcribe(audio_path)
258
- text = " ".join(seg.text for seg in segments).strip()
259
- return gr.update(visible=True, value=text), _ok(f'Transcribed: "{text[:60]}..."')
260
  except Exception as e:
261
- return gr.update(visible=False), _err(f"Transcription error: {e}")
262
-
263
- _whisper_model = None
264
-
265
-
266
- def handle_confirm_task(user_id, title, area, urgency, importance, state,
267
- time_est, sched_date, is_habit, habit_interval):
268
- if not user_id:
269
- return _err("Not logged in."), [], _stat("—","Today","#a78bfa"), _stat("—","Done","#4ade80"), _stat("—","Left","#f87171"), gr.update(visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  if not title.strip():
271
- return _err("Title cannot be empty."), [], _stat("—","Today","#a78bfa"), _stat("—","Done","#4ade80"), _stat("—","Left","#f87171"), gr.update(visible=True)
272
-
273
- task = {
274
- "title": title, "life_area": area,
275
- "urgency": urgency, "importance": importance, "state_of_mind": state,
276
- "time_estimate": int(time_est or 30),
277
- "is_habit": is_habit,
278
- "habit_interval": habit_interval if is_habit else "",
279
- }
280
- save_task(user_id, task, sched_date)
281
- rows, s1, s2, s3 = refresh_today(user_id)
282
- return _ok("Task saved!"), rows, s1, s2, s3, gr.update(visible=False)
283
-
284
-
285
- def handle_generate_schedule(user_id, prompt):
286
- if not user_id:
287
- return _err("Not logged in.")
288
- tasks = get_tasks(user_id, only_today=True, include_completed=False)
289
- if not tasks:
290
- return _err("No incomplete tasks today to schedule.")
291
-
292
- ctx = _ensure_context(user_id)
293
- goals = get_goals(user_id)
294
- sched = generate_schedule(ctx, tasks, prompt or "Schedule my day sensibly.", goals)
295
-
296
  if "error" in sched and "scheduled_tasks" not in sched:
297
  return _err(f"Scheduling failed: {sched.get('error')}")
298
-
299
- COLOR = {"Flow": "#0ea5e9", "Easy": "#4ade80", "Quick": "#a78bfa", "Personal": "#f87171"}
300
- html = (
301
- f'<div style="margin-bottom:14px">'
302
- f'<span style="color:#a78bfa;font-size:12px;font-weight:600">📅 {sched.get("schedule_date","Today")}</span>'
303
- f'<p style="color:#475569;font-size:13px;margin:6px 0 0">{sched.get("day_summary","")}</p>'
304
- f'</div>'
305
- )
306
-
307
  for t in sched.get("scheduled_tasks", []):
308
- sm = t.get("state_of_mind", "Easy")
309
- c = COLOR.get(sm, "#6366f1")
310
- html += (
311
- f'<div class="sched-card" style="border-left-color:{c}">'
312
- f'<div class="sched-time">{t.get("start_time")} {t.get("end_time")}</div>'
313
- f'<div class="sched-title">{t.get("title")}</div>'
314
- f'<div class="sched-meta">{t.get("life_area","—")} · {sm} · {t.get("duration_minutes")}min</div>'
315
- f'<div class="sched-why">{t.get("scheduling_reason","")}</div>'
316
- f'</div>'
317
- )
318
-
319
  if sched.get("deferred_tasks"):
320
- html += '<div style="margin-top:12px;color:#334155;font-size:11px;font-weight:600;letter-spacing:.6px;text-transform:uppercase">Deferred</div>'
321
  for t in sched["deferred_tasks"]:
322
  html += f'<div style="color:#475569;font-size:12px;padding:3px 0">✗ {t["title"]} — {t.get("reason","")}</div>'
323
-
324
- for w in sched.get("warnings", []):
325
- html += f'<div style="margin-top:8px;color:#f59e0b;font-size:12px">⚠ {w}</div>'
326
-
327
  return html
328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
 
330
- def handle_toggle_today(task_id, user_id):
331
- if task_id and user_id:
332
- toggle_task_complete(int(task_id), user_id)
333
- rows, s1, s2, s3 = refresh_today(user_id)
334
- return rows, s1, s2, s3, _ok("Updated")
335
-
336
-
337
- def handle_delete_today(task_id, user_id):
338
- if task_id and user_id:
339
- delete_task(int(task_id), user_id)
340
- rows, s1, s2, s3 = refresh_today(user_id)
341
- return rows, s1, s2, s3, _ok("Deleted")
342
-
343
-
344
- # ═══════════════════════════════════════════════════════════════════════════════
345
- # ALL TASKS TAB HANDLERS
346
- # ═══════════════════════════════════════════════════════════════════════════════
347
-
348
- def refresh_all_tasks(user_id, filter_area="All", only_today=False):
349
- if not user_id:
350
- return []
351
- return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today))
352
-
353
-
354
- def handle_toggle_all(task_id, user_id, filter_area, only_today):
355
- if task_id and user_id:
356
- toggle_task_complete(int(task_id), user_id)
357
- return refresh_all_tasks(user_id, filter_area, only_today), _ok("Updated")
358
-
359
-
360
- def handle_delete_all(task_id, user_id, filter_area, only_today):
361
- if task_id and user_id:
362
- delete_task(int(task_id), user_id)
363
- return refresh_all_tasks(user_id, filter_area, only_today), _ok("Deleted")
364
-
365
-
366
- # ═══════════════════════════════════════════════════════════════════════════════
367
- # JOURNAL TAB HANDLERS
368
- # ═══════════════════════════════════════════════════════════════════════════════
369
-
370
- def handle_load_journal_tasks(user_id):
371
- if not user_id:
372
- return "<p style='color:#f87171'>Not logged in.</p>", gr.update(visible=False), []
373
-
374
- tasks = get_tasks(user_id, only_today=True)
375
  if not tasks:
376
- return "<p style='color:#475569;font-size:13px'>No tasks for today.</p>", gr.update(visible=False), []
377
-
378
- rows = ""
379
- state = []
380
- for t in tasks:
381
- status = "✅" if t["is_completed"] else "⬜"
382
- actual = f" ({t['actual_duration']}m actual)" if t["actual_duration"] else ""
383
- rows += (
384
- f'<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid #1e293b">'
385
- f'<span style="font-family:JetBrains Mono,monospace;color:#334155;font-size:11px;min-width:30px">#{t["id"]}</span>'
386
- f'<span style="font-size:16px">{status}</span>'
387
- f'<span style="flex:1;font-size:13px;color:#cbd5e1">{t["title"]}</span>'
388
- f'<span style="font-size:11px;color:#475569">{t["life_area"] or "—"}</span>'
389
- f'<span style="font-size:11px;color:#334155">{t["time_estimate"]}m{actual}</span>'
390
- f'</div>'
391
- )
392
- state.append({
393
- "task_id": t["id"], "title": t["title"],
394
- "state_of_mind": t["state_of_mind"],
395
- "start_time": "", "end_time": "",
396
- "time_estimate": t["time_estimate"],
397
- "completed": bool(t["is_completed"]),
398
- "actual_duration": t["actual_duration"],
399
- })
400
-
401
- return f'<div style="font-size:12px">{rows}</div>', gr.update(visible=True), state
402
-
403
-
404
- def handle_journal_mark(task_id, actual_mins, user_id, tasks_state, mark_done: bool):
405
- if not task_id or not user_id:
406
- return tasks_state, _err("Enter a task ID.")
407
- tid = int(task_id)
408
- if mark_done:
409
- toggle_task_complete(tid, user_id, int(actual_mins) if actual_mins else None)
410
- else:
411
- toggle_task_complete(tid, user_id)
412
- for t in tasks_state:
413
  if t["task_id"] == tid:
414
  t["completed"] = mark_done
415
- if mark_done and actual_mins:
416
- t["actual_duration"] = int(actual_mins)
417
- return tasks_state, _ok("Marked" if mark_done else "Unmarked")
418
-
419
-
420
- def handle_start_journal(user_id, tasks_state):
421
- if not user_id:
422
- return [], [], False, gr.update(interactive=False), gr.update(interactive=False), _err("Not logged in.")
423
- if not tasks_state:
424
- return [], [], False, gr.update(interactive=False), gr.update(interactive=False), _err("Load today's tasks first.")
425
-
426
- ctx = _ensure_context(user_id)
427
- opening = build_opening_question(ctx, tasks_state)
428
- chat = [(None, opening["question"])]
429
- hist = [{"role": "assistant", "content": opening["question"], "focus": opening["question_focus"]}]
430
-
431
- return (
432
- chat, hist, True,
433
- gr.update(interactive=True),
434
- gr.update(interactive=True),
435
- _ok("Session started — type your answer below"),
436
- )
437
-
438
-
439
- def handle_send_answer(user_id, answer, chat, hist, tasks_state, active):
440
- if not active or not answer.strip():
441
- return chat, hist, "", ""
442
-
443
- chat = list(chat) + [(answer, None)]
444
- hist = list(hist) + [{"role": "user", "content": answer}]
445
-
446
- ctx = _ensure_context(user_id)
447
- result = get_next_journal_question(ctx, tasks_state, hist)
448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  if result.get("session_complete"):
450
- chat[-1] = (answer, "That's enough for today. Click **Finish & Save** to update your profile.")
451
- return chat, hist, "", _ok("Session complete save when ready")
452
-
 
 
 
453
  next_q = result.get("question", "")
454
- hist.append({"role": "assistant", "content": next_q, "focus": result.get("question_focus", "")})
455
- chat[-1] = (answer, next_q)
456
- return chat, hist, "", ""
457
-
458
-
459
- def handle_finish_journal(user_id, chat, hist, tasks_state):
460
- if not user_id or not hist:
461
- return chat, _err("Nothing to save.")
462
-
463
- ctx = _ensure_context(user_id)
464
- updated = synthesize_journal(ctx, tasks_state, hist)
465
- save_user_context(user_id, updated)
466
-
467
- total = len(tasks_state)
468
- done = sum(1 for t in tasks_state if t.get("completed"))
469
- rate = round(done / total * 100) if total else 0
470
- notes = updated.get("learned_patterns", {}).get("notes", [])
471
- 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>"
472
-
473
- return (
474
- chat,
475
- _ok(f"Insights saved! {done}/{total} tasks ({rate}%) completed.")
476
- + f'<ul style="color:#94a3b8;font-size:12px;margin-top:6px;padding-left:16px">{note_html}</ul>'
477
- )
478
-
479
 
480
  def handle_restart_journal():
481
- return [], [], False, gr.update(interactive=False), gr.update(interactive=False), ""
482
-
483
-
484
- # ═══════════════════════════════════════════════════════════════════════════════
485
- # LIFE AREAS & GOALS HANDLERS
486
- # ═══════════════════════════════════════════════════════════════════════════════
487
-
488
- def render_areas(user_id):
489
- if not user_id:
490
- return "", [], []
491
- areas = get_life_areas(user_id)
492
- chips = ""
493
- for a in areas:
494
- chips += (
495
- f'<span class="chip" '
496
- f'style="background:{a["color"]}22;color:{a["color"]};border:1px solid {a["color"]}44">'
497
- f'{a["name"]}</span> '
498
- )
499
- html = f'<div style="margin:4px 0">{chips}</div>' if chips else '<p style="color:#334155;font-size:13px">No areas yet.</p>'
500
- names = [a["name"] for a in areas]
501
  return html, names, names
502
 
503
-
504
- def handle_add_area(user_id, name, color):
505
- ok, msg = add_life_area(user_id, name, color)
506
- css = "ok" if ok else "err"
507
- html, names, _ = render_areas(user_id)
508
- return (
509
- f'<span style="color:{"#4ade80" if ok else "#f87171"};font-size:13px">{msg}</span>',
510
- html, gr.update(choices=names, value=None), gr.update(value=""),
511
- )
512
-
513
-
514
- def handle_del_area(user_id, name):
515
- if not name:
516
- return _err("Select an area first."), "", []
517
- ok, msg = delete_life_area(user_id, name)
518
- html, names, _ = render_areas(user_id)
519
- return (
520
- f'<span style="color:{"#4ade80" if ok else "#f87171"};font-size:13px">{msg}</span>',
521
- html, gr.update(choices=names, value=None),
522
- )
523
-
524
-
525
- def handle_save_goals(user_id, goals_text):
526
- if not user_id:
527
- return _err("Not logged in.")
528
- save_goals(user_id, goals_text)
529
- return _ok("Goals saved!")
530
-
531
-
532
- def load_goals_txt(user_id):
533
- if not user_id:
534
- return ""
535
- return "\n".join(get_goals(user_id))
536
-
537
-
538
- # ���══════════════════════════════════════════════════════════════════════════════
539
- # PREFERENCES HANDLERS
540
- # ═══════════════════════════════════════════════════════════════════════════════
541
-
542
- def load_prefs(user_id):
543
- ctx = load_user_context(user_id) if user_id else None
544
- if not ctx:
545
- return "07:30", "23:00", "Morning", 10, 90
546
- p = ctx.get("preferences", {})
547
- return (
548
- p.get("wake_time", "07:30"),
549
- p.get("sleep_time", "23:00"),
550
- p.get("focus_peak", "Morning"),
551
- p.get("break_duration_minutes", 10),
552
- p.get("max_flow_block_minutes", 90),
553
- )
554
-
555
-
556
- def handle_save_prefs(user_id, wake, sleep, focus, brk, flow_max):
557
- if not user_id:
558
- return _err("Not logged in.")
559
- ctx = _ensure_context(user_id)
560
- ctx["preferences"].update({
561
- "wake_time": wake or "07:30",
562
- "sleep_time": sleep or "23:00",
563
- "focus_peak": focus or "Morning",
564
- "break_duration_minutes": int(brk or 10),
565
- "max_flow_block_minutes": int(flow_max or 90),
566
- })
567
- save_user_context(user_id, ctx)
568
- return _ok("Preferences saved!")
569
-
570
-
571
- def render_context_html(user_id):
572
- if not user_id:
573
- return "<p style='color:#334155'>Not logged in.</p>"
574
- ctx = load_user_context(user_id)
575
- if not ctx:
576
- return "<p style='color:#334155;font-size:13px'>No AI context yet. Complete a journaling session to start building it.</p>"
577
-
578
- lp = ctx.get("learned_patterns", {})
579
- sf = ctx.get("scheduling_feedback", {})
580
- pref = ctx.get("preferences", {})
581
-
582
- def row(label, val):
583
- return (
584
- f'<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #0d1117">'
585
- f'<span style="color:#334155;font-size:12px">{label}</span>'
586
- f'<span style="color:#94a3b8;font-size:12px;font-family:JetBrains Mono,monospace">{val}</span>'
587
- f'</div>'
588
- )
589
  def lst(v): return ", ".join(v) if v else "—"
590
-
591
- html = (
592
- f'<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">'
593
- f'<div class="panel">'
594
- f'<div class="sec-label">Preferences</div>'
595
- f'{row("Wake", pref.get("wake_time","—"))}'
596
- f'{row("Sleep", pref.get("sleep_time","—"))}'
597
- f'{row("Peak focus", pref.get("focus_peak",""))}'
598
- f'{row("Break", str(pref.get("break_duration_minutes","—")) + "min")}'
599
- f'{row("Max flow block", str(pref.get("max_flow_block_minutes","—")) + "min")}'
600
- f'</div>'
601
- f'<div class="panel">'
602
- f'<div class="sec-label">Scheduling Stats</div>'
603
- f'{row("Days scheduled", sf.get("total_days_scheduled",0))}'
604
- f'{row("Avg completion", str(round(sf.get("avg_completion_rate",0)*100)) + "%")}'
605
- f'{row("Context version", ctx.get("version",1))}'
606
- f'{row("Last updated", (ctx.get("last_updated","—") or "—")[:16])}'
607
- f'</div>'
608
- f'</div>'
609
- f'<div class="panel" style="margin-top:14px">'
610
- f'<div class="sec-label">Learned Patterns</div>'
611
- f'{row("Productive times", lst(lp.get("productive_times",[])))}'
612
- f'{row("Low energy times", lst(lp.get("low_energy_times",[])))}'
613
- f'{row("Avg task overrun", str(lp.get("avg_task_overrun_pct",0)) + "%")}'
614
- f'{row("Flow batchable", str(lp.get("flow_batch_capable","Unknown")))}'
615
- f'{row("Best morning areas", lst(lp.get("best_life_areas_morning",[])))}'
616
- f'{row("Skipped types", lst(lp.get("common_skipped_task_types",[])))}'
617
- f'</div>'
618
- )
619
-
620
- notes = lp.get("notes", [])
621
  if notes:
622
- note_rows = "".join(
623
- f'<div style="padding:5px 0;border-bottom:1px solid #0d1117;color:#64748b;font-size:12px"> {n}</div>'
624
- for n in notes[-5:]
625
- )
626
- html += f'<div class="panel" style="margin-top:14px"><div class="sec-label">AI Notes</div>{note_rows}</div>'
627
-
628
- history = ctx.get("history_summary", [])
629
  if history:
630
- hist_rows = "".join(
631
- f'<div style="padding:4px 0;color:#475569;font-size:12px"> {h}</div>'
632
- for h in history[-5:]
633
- )
634
- html += f'<div class="panel" style="margin-top:14px"><div class="sec-label">Recent History</div>{hist_rows}</div>'
635
-
636
  return html
637
 
638
-
639
  # ═══════════════════════════════════════════════════════════════════════════════
640
- # UI BUILD
641
  # ═══════════════════════════════════════════════════════════════════════════════
 
642
 
643
- with gr.Blocks(title="🧠 Second Brain") as demo:
644
-
645
- # ── Session state ──────────────────────────────────────────────────────────
646
- user_id_state = gr.State(None)
647
- username_state = gr.State("")
648
- # Journal states
649
- journal_chat_state = gr.State([])
650
  journal_hist_state = gr.State([])
651
  journal_tasks_state = gr.State([])
652
  journal_active = gr.State(False)
653
- # Clarification states
654
  clar_questions_state = gr.State([])
655
  original_task_state = gr.State("")
656
 
657
- # ═════════════════════════════════════════════════════════════════════════
658
- # AUTH SECTION
659
- # ═════════════════════════════════════════════════════════════════════════
660
  with gr.Column(visible=True, elem_id="auth-card") as auth_section:
661
  gr.HTML('<div id="brand-logo">🧠 Second Brain</div>')
662
  gr.HTML('<div id="brand-sub">Your intelligent productivity companion</div>')
663
-
664
  with gr.Tabs():
665
- # ── Sign In ────────────────────────────────────────────────────
666
  with gr.Tab("Sign In"):
667
  login_user_in = gr.Textbox(label="Username", placeholder="your username")
668
  login_pass_in = gr.Textbox(label="Password", type="password", placeholder="••••••••")
669
  login_btn = gr.Button("Sign In", elem_classes="btn-primary")
670
  login_msg = gr.HTML("")
671
-
672
- # ── Create Account ─────────────────────────────────────────────
673
  with gr.Tab("Create Account"):
674
- reg_user_in = gr.Textbox(label="Username", placeholder="choose a username")
675
- reg_pass_in = gr.Textbox(label="Password (min 6 chars)", type="password", placeholder="••••••••")
676
- reg_wake = gr.Textbox(label="Wake time", value="07:30")
677
- reg_sleep = gr.Textbox(label="Sleep time", value="23:00")
678
- reg_focus = gr.Dropdown(label="Peak focus",
679
- choices=["Morning","Afternoon","Evening","Night"],
680
- value="Morning")
681
- reg_goals = gr.Textbox(
682
- label="Big-picture goals (one per line, optional)",
683
- lines=3,
684
- placeholder="Launch my startup\nGet fit\nLearn machine learning"
685
- )
686
- reg_btn = gr.Button("Create Account", elem_classes="btn-primary")
687
- reg_msg = gr.HTML("")
688
-
689
- # ═════════════════════════════════════════════════════════════════════════
690
- # MAIN APP SECTION
691
- # ═════════════════════════════════════════════════════════════════════════
692
  with gr.Column(visible=False) as app_section:
693
-
694
- # ── Top header ─────────────────────────────────────────────────────
695
  with gr.Row(elem_id="top-header"):
696
  header_html = gr.HTML('<div id="top-header-brand">🧠 Second Brain</div><div id="top-header-user">—</div>')
697
  logout_btn = gr.Button("Sign Out", elem_classes="btn-secondary", scale=0)
698
 
699
- # ── Tabs ────────────────────────────────────────────────────────────
700
- with gr.Tabs() as main_tabs:
701
 
702
- # ───────────────────────────────────────────��─────────────────
703
- # TAB 1: TODAY
704
- # ─────────────────────────────────────────────────────────────
705
  with gr.Tab("📅 Today"):
706
- # Stats row
707
  with gr.Row():
708
  stat_total = gr.HTML(_stat("—","Today","#a78bfa"))
709
  stat_done = gr.HTML(_stat("—","Done","#4ade80"))
710
  stat_remain = gr.HTML(_stat("—","Left","#f87171"))
711
-
712
- # Action buttons
713
  with gr.Row():
714
  btn_add_text = gr.Button("✏️ Add Task (Text)", elem_classes="btn-primary", scale=3)
715
  btn_add_voice = gr.Button("🎙️ Add Task (Voice)", elem_classes="btn-accent", scale=3)
716
  btn_plan_day = gr.Button("🗓 Plan My Day", elem_classes="btn-secondary", scale=3)
717
  btn_refresh = gr.Button("↻", elem_classes="btn-secondary", scale=1)
718
 
719
- # ── Text add panel ────────────────────────────────────────
720
  with gr.Column(visible=False, elem_classes="panel") as text_panel:
721
  gr.HTML('<div class="sec-label">Describe Your Task</div>')
722
- task_input = gr.Textbox(
723
- label="",
724
- placeholder="e.g. Finish the client proposal by tomorrow, it's really important",
725
- lines=2
726
- )
727
  with gr.Row():
728
- btn_parse = gr.Button("🤖 Parse with AI", elem_classes="btn-primary")
729
- btn_cancel_text = gr.Button("Cancel", elem_classes="btn-secondary")
730
 
731
  with gr.Column(visible=False) as parsed_panel:
732
  parse_status = gr.HTML("")
733
  clar_html = gr.HTML("")
734
- # ── Clarification reply row (shown only when AI has questions) ──
735
  with gr.Column(visible=False) as clar_reply_row:
736
- gr.HTML('<div class="sec-label" style="margin-top:8px">Your Answer</div>')
737
- clar_reply_input = gr.Textbox(
738
- label="",
739
- placeholder="e.g. It's for a client deadline, very high priority, should take about 2 hours",
740
- lines=2
741
- )
742
- btn_clar_submit = gr.Button("🔄 Re-classify with my answer", elem_classes="btn-accent")
743
- gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>')
744
  with gr.Row():
745
  f_title = gr.Textbox(label="Title", scale=3)
746
- f_area = gr.Dropdown(label="Life Area",
747
- choices=["Work","Health","Finance","Learning","Personal","Family","Other"],
748
- scale=1)
749
  with gr.Row():
750
- f_urgency = gr.Dropdown(label="Urgency",
751
- choices=["Urgent","Not Urgent","Habit"], value="Not Urgent")
752
- f_importance = gr.Dropdown(label="Importance",
753
- choices=["Move the Needle","Important","Not Important"],
754
- value="Important")
755
- f_state = gr.Dropdown(label="State of Mind",
756
- choices=["Flow","Easy","Quick","Personal"], value="Easy")
757
  with gr.Row():
758
- f_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
759
- f_date = gr.Textbox(label="Scheduled Date", value=str(date.today()), scale=1)
760
- f_is_habit = gr.Checkbox(label="♻️ This is a habit", scale=1)
761
- f_interval = gr.Dropdown(label="Recurs",
762
- choices=["Daily","Weekly","Weekdays","Weekends","Monthly"],
763
- value="Daily", visible=False, scale=1)
764
  with gr.Row():
765
  btn_confirm = gr.Button("✓ Save Task", elem_classes="btn-success")
766
  btn_discard = gr.Button("✗ Discard", elem_classes="btn-danger")
767
  save_msg = gr.HTML("")
768
 
769
- # ── Voice add panel ───────────────────────────────────────
770
  with gr.Column(visible=False, elem_classes="panel") as voice_panel:
771
  gr.HTML('<div class="sec-label">Record or Upload Audio</div>')
772
- gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:12px">Whisper transcribes it, then AI classifies the task automatically.</p>')
773
- audio_input = gr.Audio(sources=["microphone","upload"], type="filepath", label="")
774
- transcribed = gr.Textbox(label="Transcribed text (editable before parsing)",
775
- visible=False, lines=2)
776
  with gr.Row():
777
- btn_transcribe = gr.Button("🤖 Transcribe & Parse", elem_classes="btn-primary")
778
- btn_cancel_voice = gr.Button("Cancel", elem_classes="btn-secondary")
779
- voice_msg = gr.HTML("")
780
 
781
- # ── Plan My Day panel ─────────────────────────────────────
782
  with gr.Column(visible=False, elem_classes="panel") as plan_panel:
783
  gr.HTML('<div class="sec-label">Plan My Day</div>')
784
- gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:12px">The AI reads your tasks, context, and goals to build the optimal schedule.</p>')
785
- plan_prompt = gr.Textbox(
786
- label="Any fixed events or preferences today?",
787
- placeholder="e.g. I have a meeting at 10am, want deep work in the morning, done by 7pm",
788
- lines=3
789
- )
790
  with gr.Row():
791
- btn_gen_sched = gr.Button("✨ Generate Schedule", elem_classes="btn-primary")
792
- btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary")
793
  schedule_html = gr.HTML("")
794
 
795
- # ── Today's task table ────────────────────────────────────
796
  gr.HTML('<div class="sec-label" style="margin-top:20px">Today\'s Tasks</div>')
797
  today_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
798
  with gr.Row():
799
- today_done_id = gr.Number(label="Task ID — toggle done", precision=0, scale=2)
800
  btn_today_done = gr.Button("✅ Mark Done", elem_classes="btn-success", scale=2)
801
  today_del_id = gr.Number(label="Task ID — delete", precision=0, scale=2)
802
  btn_today_del = gr.Button("🗑 Delete", elem_classes="btn-danger", scale=2)
803
  today_action_msg = gr.HTML("")
804
 
805
- # ─────────────────────────────────────────────────────────────
806
- # TAB 2: ALL TASKS
807
- # ─────────────────────────────────────────────────────────────
808
  with gr.Tab("📋 All Tasks"):
809
  with gr.Row():
810
  all_filter = gr.Dropdown(label="Filter by Area", choices=["All"], value="All", scale=4)
811
  all_today_chk = gr.Checkbox(label="Today only", value=False, scale=1)
812
  btn_all_ref = gr.Button("↻ Refresh", elem_classes="btn-secondary", scale=1)
813
-
814
  all_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
815
  with gr.Row():
816
  all_done_id = gr.Number(label="Task ID — toggle done", precision=0, scale=2)
@@ -819,347 +540,197 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
819
  btn_all_del = gr.Button("🗑 Delete", elem_classes="btn-danger", scale=2)
820
  all_action_msg = gr.HTML("")
821
 
822
- # ─────────────────────────────────────────────────────────────
823
- # TAB 3: JOURNAL
824
- # ─────────────────────────────────────────────────────────────
825
  with gr.Tab("📓 Journal"):
826
- 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>')
827
 
828
- # Step 1 — Mark tasks
829
  with gr.Column(elem_classes="panel"):
830
- gr.HTML('<div class="sec-label">Step 1 — Mark Tasks Complete</div>')
831
- journal_task_list = gr.HTML('<p style="color:#334155;font-size:13px">Click "Load Tasks" to begin.</p>')
832
- btn_load_tasks = gr.Button("Load Today's Tasks", elem_classes="btn-secondary")
 
 
833
  with gr.Row(visible=False) as journal_mark_row:
834
- j_task_id = gr.Number(label="Task ID", precision=0, scale=2)
835
- j_actual_min = gr.Number(label="Actual time (min)", precision=0, value=0, scale=2)
836
- btn_j_done = gr.Button("✅ Mark Complete", elem_classes="btn-success", scale=2)
837
  btn_j_undone = gr.Button("↩ Unmark", elem_classes="btn-secondary", scale=1)
838
  journal_mark_msg = gr.HTML("")
839
 
840
- # Step 2 — Chat
841
  with gr.Column(elem_classes="panel"):
842
- gr.HTML('<div class="sec-label">Step 2 — Reflect</div>')
843
- journal_chatbot = gr.Chatbot(label="", height=380)
844
  with gr.Row():
845
- btn_j_start = gr.Button("▶ Start Reflection", elem_classes="btn-primary", scale=2)
846
- btn_j_finish = gr.Button("✓ Finish & Save Insights", elem_classes="btn-success", scale=2)
847
- btn_j_restart = gr.Button("↺ Restart", elem_classes="btn-secondary", scale=1)
848
- j_answer = gr.Textbox(label="Your answer", placeholder="Type here…", lines=2, interactive=False)
849
- btn_j_send = gr.Button("Send →", elem_classes="btn-accent", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
850
  journal_msg = gr.HTML("")
851
 
852
- # ─────────────────────────────────────────────────────────────
853
- # TAB 4: LIFE AREAS & GOALS
854
- # ─────────────────────────────────────────────────────────────
855
  with gr.Tab("🗂 Areas & Goals"):
856
  gr.HTML('<div class="sec-label">Your Life Areas</div>')
857
  areas_display = gr.HTML("")
858
-
859
  with gr.Row():
860
  new_area_name = gr.Textbox(label="New area name", placeholder="e.g. Side Project", scale=3)
861
  new_area_color = gr.ColorPicker(label="Color", value="#6366f1", scale=1)
862
  btn_add_area = gr.Button("Add", elem_classes="btn-primary", scale=1)
863
  area_msg = gr.HTML("")
864
-
865
  gr.HTML('<div class="sec-label" style="margin-top:24px">Remove Area</div>')
866
  with gr.Row():
867
  del_area_dd = gr.Dropdown(label="Select area to remove", choices=[], scale=4)
868
  btn_del_area = gr.Button("Remove", elem_classes="btn-danger", scale=1)
869
  del_area_msg = gr.HTML("")
870
-
871
  gr.HTML('<hr>')
872
  gr.HTML('<div class="sec-label">Big-Picture Goals</div>')
873
- gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:10px">The AI uses these when scheduling and prioritising your tasks.</p>')
874
- goals_input = gr.Textbox(label="One goal per line", lines=5,
875
- placeholder="Launch my SaaS\nRun 5K\nLearn ML")
876
- btn_save_goals = gr.Button("Save Goals", elem_classes="btn-primary")
877
- goals_msg = gr.HTML("")
878
-
879
- # ─────────────────────────────────────────────────────────────
880
- # TAB 5: PREFERENCES
881
- # ─────────────────────────────────────────────────────────────
882
  with gr.Tab("⚙️ Preferences"):
883
  gr.HTML('<div class="sec-label">Scheduling Preferences</div>')
884
  with gr.Row():
885
  pref_wake = gr.Textbox(label="Wake time", placeholder="07:30", scale=1)
886
  pref_sleep = gr.Textbox(label="Sleep time", placeholder="23:00", scale=1)
887
- pref_focus = gr.Dropdown(label="Peak focus",
888
- choices=["Morning","Afternoon","Evening","Night"],
889
- value="Morning", scale=1)
890
  with gr.Row():
891
- pref_break = gr.Number(label="Break between tasks (min)", value=10, minimum=0, scale=1)
892
  pref_flow_max = gr.Number(label="Max Flow block (min)", value=90, minimum=30, scale=1)
893
  btn_save_prefs = gr.Button("Save Preferences", elem_classes="btn-primary")
894
  prefs_msg = gr.HTML("")
895
-
896
  gr.HTML('<hr>')
897
  gr.HTML('<div class="sec-label">AI Memory</div>')
898
- 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>')
899
- ctx_display = gr.HTML("")
900
- btn_ref_ctx = gr.Button("↻ Refresh", elem_classes="btn-secondary")
901
-
902
-
903
- # ═══════════════════════════════════════════════════════════════════════
904
- # EVENT WIRING
905
- # ═══════════════════════════════════════════════════════════════════════
906
-
907
- # ── Auth ──────────────────────────────────────────────────────────────
908
-
909
- def _post_login(uid, uname):
910
- if not uid:
911
- return uid, uname
912
- spawn_due_habits(uid)
913
- return uid, uname
914
-
915
- login_btn.click(
916
- handle_login,
917
- [login_user_in, login_pass_in],
918
- [user_id_state, username_state, login_msg, auth_section, app_section]
919
- ).then(
920
- lambda uid, uname: f'<div id="top-header-brand">🧠 Second Brain</div><div id="top-header-user">👤 {uname}</div>',
921
- [user_id_state, username_state], [header_html]
922
- ).then(
923
- refresh_today, [user_id_state],
924
- [today_df, stat_total, stat_done, stat_remain]
925
- ).then(
926
- lambda uid: render_areas(uid)[0:2],
927
- [user_id_state], [areas_display, del_area_dd]
928
- ).then(
929
- load_goals_txt, [user_id_state], [goals_input]
930
- ).then(
931
- lambda uid: load_prefs(uid),
932
- [user_id_state], [pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max]
933
- )
934
 
935
- reg_btn.click(
936
- handle_register,
937
- [reg_user_in, reg_pass_in, reg_wake, reg_sleep, reg_focus, reg_goals],
938
- [user_id_state, username_state, reg_msg, auth_section, app_section]
939
- ).then(
940
- lambda uid, uname: f'<div id="top-header-brand">🧠 Second Brain</div><div id="top-header-user">👤 {uname}</div>',
941
- [user_id_state, username_state], [header_html]
942
- ).then(
943
- refresh_today, [user_id_state],
944
- [today_df, stat_total, stat_done, stat_remain]
945
- ).then(
946
- lambda uid: render_areas(uid)[0:2],
947
- [user_id_state], [areas_display, del_area_dd]
948
- )
949
 
950
- logout_btn.click(
951
- handle_logout,
952
- [user_id_state],
953
- [user_id_state, username_state, auth_section, app_section]
954
- )
 
 
 
955
 
956
- # ── Today tab ─────────────────────────────────────────────────────────
 
 
 
957
 
958
- btn_add_text.click(show_text_panel, outputs=[text_panel, voice_panel, plan_panel])
 
 
 
959
  btn_add_voice.click(show_voice_panel, outputs=[text_panel, voice_panel, plan_panel])
960
  btn_plan_day.click(show_plan_panel, outputs=[text_panel, voice_panel, plan_panel])
961
  btn_cancel_text.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
962
  btn_cancel_voice.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
963
  btn_cancel_plan.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
 
 
964
 
965
- btn_refresh.click(
966
- refresh_today, [user_id_state],
967
- [today_df, stat_total, stat_done, stat_remain]
968
- )
969
 
970
- # Show habit interval dropdown only when habit checked
971
- f_is_habit.change(
972
- lambda v: gr.update(visible=v),
973
- [f_is_habit], [f_interval]
974
- )
975
 
976
- btn_parse.click(
977
- handle_parse_text,
978
- [task_input, user_id_state],
979
- [parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance,
980
- f_state, f_time, f_date, f_is_habit, f_interval, parse_status,
981
- clar_reply_row, clar_questions_state, original_task_state]
982
  )
983
 
984
  btn_clar_submit.click(
985
  handle_clarification_reply,
986
  [clar_reply_input, original_task_state, clar_questions_state, user_id_state],
987
- [f_title, clar_html, f_area, f_urgency, f_importance,
988
- f_state, f_time, clar_reply_row, clar_questions_state]
989
- ).then(
990
- lambda: "",
991
- outputs=[clar_reply_input]
992
- )
993
-
994
- btn_transcribe.click(
995
- handle_transcribe_voice,
996
- [audio_input, user_id_state],
997
- [transcribed, voice_msg]
998
- )
999
-
1000
- btn_discard.click(
1001
- lambda: gr.update(visible=False),
1002
- outputs=[parsed_panel]
1003
- )
1004
-
1005
- btn_confirm.click(
1006
- handle_confirm_task,
1007
- [user_id_state, f_title, f_area, f_urgency, f_importance, f_state,
1008
- f_time, f_date, f_is_habit, f_interval],
1009
- [save_msg, today_df, stat_total, stat_done, stat_remain, parsed_panel]
1010
- )
1011
-
1012
- btn_gen_sched.click(
1013
- handle_generate_schedule,
1014
- [user_id_state, plan_prompt],
1015
- [schedule_html]
1016
- )
1017
-
1018
- btn_today_done.click(
1019
- handle_toggle_today,
1020
- [today_done_id, user_id_state],
1021
- [today_df, stat_total, stat_done, stat_remain, today_action_msg]
1022
- )
1023
- btn_today_del.click(
1024
- handle_delete_today,
1025
- [today_del_id, user_id_state],
1026
- [today_df, stat_total, stat_done, stat_remain, today_action_msg]
1027
- )
1028
-
1029
- # ── All Tasks tab ──────────────────────────────────────────────────────
1030
-
1031
- btn_all_ref.click(
1032
- refresh_all_tasks,
1033
- [user_id_state, all_filter, all_today_chk],
1034
- [all_df]
1035
- )
1036
- all_filter.change(
1037
- refresh_all_tasks,
1038
- [user_id_state, all_filter, all_today_chk],
1039
- [all_df]
1040
- )
1041
- all_today_chk.change(
1042
- refresh_all_tasks,
1043
- [user_id_state, all_filter, all_today_chk],
1044
- [all_df]
1045
- )
1046
- btn_all_done.click(
1047
- handle_toggle_all,
1048
- [all_done_id, user_id_state, all_filter, all_today_chk],
1049
- [all_df, all_action_msg]
1050
- )
1051
- btn_all_del.click(
1052
- handle_delete_all,
1053
- [all_del_id, user_id_state, all_filter, all_today_chk],
1054
- [all_df, all_action_msg]
1055
- )
1056
-
1057
- # ── Journal tab ────────────────────────────────────────────────────────
1058
-
1059
- btn_load_tasks.click(
1060
- handle_load_journal_tasks,
1061
- [user_id_state],
1062
- [journal_task_list, journal_mark_row, journal_tasks_state]
1063
- )
1064
 
1065
  btn_j_done.click(
1066
  lambda tid, mins, uid, st: handle_journal_mark(tid, mins, uid, st, True),
1067
  [j_task_id, j_actual_min, user_id_state, journal_tasks_state],
1068
- [journal_tasks_state, journal_mark_msg]
1069
- ).then(
1070
- handle_load_journal_tasks,
1071
- [user_id_state],
1072
- [journal_task_list, journal_mark_row, journal_tasks_state]
1073
  )
1074
-
1075
  btn_j_undone.click(
1076
  lambda tid, mins, uid, st: handle_journal_mark(tid, mins, uid, st, False),
1077
  [j_task_id, j_actual_min, user_id_state, journal_tasks_state],
1078
- [journal_tasks_state, journal_mark_msg]
1079
- ).then(
1080
- handle_load_journal_tasks,
1081
- [user_id_state],
1082
- [journal_task_list, journal_mark_row, journal_tasks_state]
1083
  )
1084
 
1085
- btn_j_start.click(
1086
- handle_start_journal,
1087
- [user_id_state, journal_tasks_state],
1088
- [journal_chatbot, journal_hist_state, journal_active,
1089
- j_answer, btn_j_send, journal_msg]
1090
- )
1091
-
1092
- btn_j_send.click(
1093
- handle_send_answer,
1094
- [user_id_state, j_answer, journal_chatbot, journal_hist_state,
1095
- journal_tasks_state, journal_active],
1096
- [journal_chatbot, journal_hist_state, j_answer, journal_msg]
1097
- )
1098
 
1099
- j_answer.submit(
1100
- handle_send_answer,
1101
- [user_id_state, j_answer, journal_chatbot, journal_hist_state,
1102
- journal_tasks_state, journal_active],
1103
- [journal_chatbot, journal_hist_state, j_answer, journal_msg]
1104
- )
 
1105
 
1106
- btn_j_finish.click(
1107
- handle_finish_journal,
1108
  [user_id_state, journal_chatbot, journal_hist_state, journal_tasks_state],
1109
- [journal_chatbot, journal_msg]
1110
- )
1111
 
1112
- btn_j_restart.click(
1113
- handle_restart_journal,
1114
- outputs=[journal_chatbot, journal_hist_state, journal_active,
1115
- j_answer, btn_j_send, journal_msg]
1116
- )
1117
 
1118
- # ── Areas & Goals tab ─────────────────────────────────────────────────
1119
-
1120
- btn_add_area.click(
1121
- handle_add_area,
1122
- [user_id_state, new_area_name, new_area_color],
1123
  [area_msg, areas_display, del_area_dd, new_area_name]
1124
- ).then(
1125
- lambda uid: gr.update(choices=_area_choices(uid), value="All"),
1126
- [user_id_state], [all_filter]
1127
- )
1128
 
1129
- btn_del_area.click(
1130
- handle_del_area,
1131
- [user_id_state, del_area_dd],
1132
  [del_area_msg, areas_display, del_area_dd]
1133
- ).then(
1134
- lambda uid: gr.update(choices=_area_choices(uid), value="All"),
1135
- [user_id_state], [all_filter]
1136
- )
1137
-
1138
- btn_save_goals.click(
1139
- handle_save_goals,
1140
- [user_id_state, goals_input],
1141
- [goals_msg]
1142
- )
1143
-
1144
- # ── Preferences tab ───────────────────────────────────────────────────
1145
-
1146
- btn_save_prefs.click(
1147
- handle_save_prefs,
1148
- [user_id_state, pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max],
1149
- [prefs_msg]
1150
- )
1151
-
1152
- btn_ref_ctx.click(
1153
- render_context_html,
1154
- [user_id_state],
1155
- [ctx_display]
1156
- )
1157
-
1158
- # Refresh AI context display whenever the refresh button is clicked
1159
- # (Gradio Tabs.select doesn't support conditional output, so we use the button)
1160
 
 
1161
 
1162
- # ── Launch ─────────────────────────────────────────────────────────────────────
 
 
 
1163
 
1164
  if __name__ == "__main__":
1165
- demo.launch(server_name="0.0.0.0", server_port=7860, css=CSS)
 
1
  """
2
  app.py — Second Brain Gradio Application
 
 
 
 
 
 
 
 
 
 
 
 
3
  """
4
 
5
  import os
6
  import gradio as gr
7
  from datetime import date, datetime
8
 
 
9
  from core.database import (
10
  init_db, register_user, login_user, get_username,
11
  create_default_life_areas, save_goals, get_goals,
 
20
  )
21
  from core.styles import CSS
22
 
 
23
  init_db()
24
+ init_groq()
25
 
26
  TASK_HEADERS = ["ID", "Done", "Title", "Area", "Urgency", "Importance", "Mind", "Min", "Date"]
27
+ _whisper_model = None
28
 
29
+ # ── helpers ───────────────────────────────────────────────────────────────────
 
 
 
 
30
  def _ok(msg): return f'<span style="color:#4ade80;font-size:13px">✓ {msg}</span>'
31
  def _err(msg): return f'<span style="color:#f87171;font-size:13px">⚠ {msg}</span>'
 
32
 
33
  def _stat(val, label, color):
34
+ return (f'<div class="stat-card"><div class="stat-num" style="color:{color}">{val}</div>'
35
+ f'<div class="stat-label">{label}</div></div>')
36
+
37
+ def _fmt_tasks(tasks):
38
+ return [[t["id"], "✅" if t["is_completed"] else "⬜",
39
+ ("🔁 " if t["is_habit"] else "") + t["title"],
40
+ t["life_area"] or "—", t["urgency"] or "—", t["importance"] or "—",
41
+ t["state_of_mind"] or "",
42
+ str(t["time_estimate"]) + "m" if t["time_estimate"] else "—",
43
+ t["scheduled_date"] or "—"] for t in tasks]
44
+
45
+ def _area_choices(uid):
46
+ return ["All"] + (get_life_area_names(uid) if uid else [])
47
+
48
+ def _ensure_context(uid):
49
+ ctx = load_user_context(uid)
 
 
 
 
 
 
 
 
 
 
50
  if not ctx:
51
+ ctx = create_blank_context(uid)
52
+ save_user_context(uid, ctx)
53
  return ctx
54
 
55
+ def _transcribe(audio_path):
56
+ global _whisper_model
57
+ from faster_whisper import WhisperModel
58
+ if _whisper_model is None:
59
+ _whisper_model = WhisperModel("small", device="cpu", compute_type="int8")
60
+ segs, _ = _whisper_model.transcribe(audio_path)
61
+ return " ".join(s.text for s in segs).strip()
62
 
63
+ # ── auth ──────────────────────────────────────────────────────────────────────
 
 
 
64
  def handle_login(username, password):
65
  uid, msg = login_user(username, password)
66
  if not uid:
67
  return None, "", _err(msg), gr.update(visible=True), gr.update(visible=False)
68
  spawn_due_habits(uid)
69
+ return uid, get_username(uid), _ok(msg), gr.update(visible=False), gr.update(visible=True)
 
 
70
 
71
  def handle_register(username, password, wake, sleep, focus, goals_text):
72
  uid, msg = register_user(username, password)
 
75
  create_default_life_areas(uid)
76
  if goals_text.strip():
77
  save_goals(uid, goals_text)
78
+ ctx = create_blank_context(uid, {"wake_time": wake or "07:30",
79
+ "sleep_time": sleep or "23:00",
80
+ "focus_peak": focus or "Morning"})
81
  save_user_context(uid, ctx)
82
  spawn_due_habits(uid)
83
+ return uid, get_username(uid), _ok(msg + " Welcome!"), gr.update(visible=False), gr.update(visible=True)
 
84
 
85
+ def handle_logout(_):
 
86
  return None, "", gr.update(visible=True), gr.update(visible=False)
87
 
88
+ # ── today ─────────────────────────────────────────────────────────────────────
89
+ def refresh_today(uid):
90
+ if not uid:
 
 
 
 
91
  return [], _stat("—","Today","#a78bfa"), _stat("—","Done","#4ade80"), _stat("—","Left","#f87171")
92
+ tasks = get_tasks(uid, only_today=True)
93
+ s = get_today_stats(uid)
94
+ c = "#4ade80" if s["remaining"]==0 and s["total"]>0 else "#f87171"
95
+ return _fmt_tasks(tasks), _stat(s["total"],"Today","#a78bfa"), _stat(s["done"],"Done","#4ade80"), _stat(s["remaining"],"Left",c)
96
+
97
+ def show_text_panel(): return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
98
+ def show_voice_panel(): return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
99
+ def show_plan_panel(): return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
100
+ def hide_panels(): return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
101
+
102
+ def _build_parse_outputs(raw, result, uid, visible=True):
103
+ clars = result.get("clarifications_needed", [])
104
+ has_c = bool(clars)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  clar_html = ""
106
+ if has_c:
107
+ items = "".join(f"<li style='margin:4px 0'>{q}</li>" for q in clars)
108
+ clar_html = (f'<div style="margin:10px 0;padding:12px;background:#0c0f1a;'
109
+ f'border-left:3px solid #a78bfa;border-radius:8px">'
110
+ f'<div style="color:#a78bfa;font-size:11px;font-weight:600;margin-bottom:6px">⚠ Please clarify:</div>'
111
+ f'<ul style="color:#94a3b8;font-size:13px;margin:0;padding-left:16px">{items}</ul></div>')
112
+ areas = get_life_area_names(uid) if uid else ["Work","Health","Finance","Learning","Personal","Family","Other"]
113
+ if not areas: areas = ["Work","Health","Finance","Learning","Personal","Family","Other"]
114
+ av = result.get("life_area") or areas[0]
115
+ if av not in areas: areas = [av] + areas
116
+ status = _ok("AI classified clarify above then re-classify." if has_c else "AI classified ✓")
117
+ return (gr.update(visible=visible), result.get("title", raw), clar_html, av,
118
+ result.get("urgency","Not Urgent"), result.get("importance","Important"),
119
+ result.get("state_of_mind","Easy"), int(result.get("time_estimate") or 30),
120
+ str(date.today()), False, "Daily", status,
121
+ gr.update(visible=has_c), clars, raw)
122
+
123
+ def handle_parse_text(task_text, uid):
124
+ if not task_text.strip():
125
+ return (gr.update(visible=False), "", "", "Work", "Not Urgent", "Important",
126
+ "Easy", 30, str(date.today()), False, "Daily", "", gr.update(visible=False), [], "")
127
+ ctx = _ensure_context(uid)
128
+ result = parse_task_with_groq(task_text, ctx, get_goals(uid), get_life_area_names(uid) if uid else [])
129
+ return _build_parse_outputs(task_text, result, uid)
130
+
131
+ def handle_voice_parse(audio_path, uid):
132
+ EMPTY = _build_parse_outputs("", {}, uid, False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  if audio_path is None:
134
+ return (_err("No audio recorded."), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), *EMPTY)
135
  try:
136
+ text = _transcribe(audio_path)
 
 
 
 
 
 
137
  except Exception as e:
138
+ return (_err(f"Transcription failed: {e}"), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), *EMPTY)
139
+ if not text:
140
+ return (_err("Couldn't hear anything."), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), *EMPTY)
141
+ ctx = _ensure_context(uid)
142
+ result = parse_task_with_groq(text, ctx, get_goals(uid), get_life_area_names(uid) if uid else [])
143
+ parsed = _build_parse_outputs(text, result, uid, True)
144
+ return (_ok(f'Heard: "{text[:70]}{"…" if len(text)>70 else ""}"'),
145
+ gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), *parsed)
146
+
147
+ def handle_clarification_reply(reply, orig, clars, uid):
148
+ if not reply.strip():
149
+ return (gr.update(),)*6 + (gr.update(), gr.update(visible=True), [])
150
+ ctx = _ensure_context(uid)
151
+ q_block = "\n".join(f"Q: {q}" for q in clars)
152
+ enriched = f"{orig}\n\nUser clarification:\n{q_block}\nA: {reply}"
153
+ result = parse_task_with_groq(enriched, ctx, get_goals(uid), get_life_area_names(uid) if uid else [])
154
+ rem = result.get("clarifications_needed", [])
155
+ if rem:
156
+ items = "".join(f"<li style='margin:4px 0'>{q}</li>" for q in rem)
157
+ ch = (f'<div style="margin:10px 0;padding:12px;background:#0c0f1a;border-left:3px solid #a78bfa;border-radius:8px">'
158
+ f'<div style="color:#a78bfa;font-size:11px;font-weight:600;margin-bottom:6px">⚠ Still needs clarification:</div>'
159
+ f'<ul style="color:#94a3b8;font-size:13px;margin:0;padding-left:16px">{items}</ul></div>')
160
+ cv = True
161
+ else:
162
+ ch, cv = _ok("Clarified — classification updated!"), False
163
+ areas = get_life_area_names(uid) if uid else ["Work","Health","Finance","Learning","Personal","Family","Other"]
164
+ av = result.get("life_area") or (areas[0] if areas else "Work")
165
+ return (result.get("title", orig), ch, av,
166
+ result.get("urgency","Not Urgent"), result.get("importance","Important"),
167
+ result.get("state_of_mind","Easy"), int(result.get("time_estimate") or 30),
168
+ gr.update(visible=cv), rem)
169
+
170
+ def handle_confirm_task(uid, title, area, urgency, importance, state, time_est, sched_date, is_habit, interval):
171
+ if not uid:
172
+ r = refresh_today(uid); return _err("Not logged in."), *r, gr.update(visible=True)
173
  if not title.strip():
174
+ r = refresh_today(uid); return _err("Title cannot be empty."), *r, gr.update(visible=True)
175
+ save_task(uid, {"title": title.strip(), "life_area": area, "urgency": urgency,
176
+ "importance": importance, "state_of_mind": state,
177
+ "time_estimate": int(time_est or 30), "is_habit": is_habit,
178
+ "habit_interval": interval if is_habit else ""}, sched_date)
179
+ rows, s1, s2, s3 = refresh_today(uid)
180
+ return _ok("Task saved! ✓"), rows, s1, s2, s3, gr.update(visible=False)
181
+
182
+ def handle_generate_schedule(uid, prompt):
183
+ if not uid: return _err("Not logged in.")
184
+ tasks = get_tasks(uid, only_today=True, include_completed=False)
185
+ if not tasks: return _err("No incomplete tasks today add some first.")
186
+ ctx = _ensure_context(uid)
187
+ sched = generate_schedule(ctx, tasks, prompt or "Schedule my day sensibly.", get_goals(uid))
 
 
 
 
 
 
 
 
 
 
 
188
  if "error" in sched and "scheduled_tasks" not in sched:
189
  return _err(f"Scheduling failed: {sched.get('error')}")
190
+ COLOR = {"Flow":"#0ea5e9","Easy":"#4ade80","Quick":"#a78bfa","Personal":"#f87171"}
191
+ html = (f'<div style="margin-bottom:14px"><span style="color:#a78bfa;font-size:12px;font-weight:600">'
192
+ f'📅 {sched.get("schedule_date","Today")}</span>'
193
+ f'<p style="color:#475569;font-size:13px;margin:6px 0 0">{sched.get("day_summary","")}</p></div>')
 
 
 
 
 
194
  for t in sched.get("scheduled_tasks", []):
195
+ sm = t.get("state_of_mind","Easy"); c = COLOR.get(sm,"#6366f1")
196
+ html += (f'<div class="sched-card" style="border-left-color:{c}">'
197
+ f'<div class="sched-time">{t.get("start_time","?")} – {t.get("end_time","?")}</div>'
198
+ f'<div class="sched-title">{t.get("title","")}</div>'
199
+ f'<div class="sched-meta">{t.get("life_area","—")} · {sm} · {t.get("duration_minutes","?")}min</div>'
200
+ f'<div class="sched-why">{t.get("scheduling_reason","")}</div></div>')
 
 
 
 
 
201
  if sched.get("deferred_tasks"):
202
+ html += '<div style="margin-top:12px;color:#334155;font-size:11px;font-weight:600;text-transform:uppercase">Deferred</div>'
203
  for t in sched["deferred_tasks"]:
204
  html += f'<div style="color:#475569;font-size:12px;padding:3px 0">✗ {t["title"]} — {t.get("reason","")}</div>'
205
+ for w in sched.get("warnings",[]): html += f'<div style="margin-top:8px;color:#f59e0b;font-size:12px">⚠ {w}</div>'
 
 
 
206
  return html
207
 
208
+ def handle_toggle_today(tid, uid):
209
+ if tid and uid: toggle_task_complete(int(tid), uid)
210
+ r = refresh_today(uid); return *r, _ok("Updated")
211
+
212
+ def handle_delete_today(tid, uid):
213
+ if tid and uid: delete_task(int(tid), uid)
214
+ r = refresh_today(uid); return *r, _ok("Deleted")
215
+
216
+ # ── all tasks ─────────────────────────────────────────────────────────────────
217
+ def refresh_all_tasks(uid, fa="All", ot=False):
218
+ if not uid: return []
219
+ return _fmt_tasks(get_tasks(uid, filter_area=fa, only_today=ot))
220
+
221
+ def handle_toggle_all(tid, uid, fa, ot):
222
+ if tid and uid: toggle_task_complete(int(tid), uid)
223
+ return refresh_all_tasks(uid, fa, ot), _ok("Updated")
224
+
225
+ def handle_delete_all(tid, uid, fa, ot):
226
+ if tid and uid: delete_task(int(tid), uid)
227
+ return refresh_all_tasks(uid, fa, ot), _ok("Deleted")
228
+
229
+ # ── journal ───────────────────────────────────────────────────────────────────
230
+ def _task_row(t):
231
+ s = "✅" if t["is_completed"] else "⬜"
232
+ a = f'<span style="color:#4ade80"> ({t["actual_duration"]}m actual)</span>' if t.get("actual_duration") else ""
233
+ return (f'<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid #1a2035">'
234
+ f'<code style="color:#334155;font-size:11px;min-width:32px">#{t["id"]}</code>'
235
+ f'<span style="font-size:15px">{s}</span>'
236
+ f'<span style="flex:1;font-size:13px;color:#cbd5e1">{t["title"]}</span>'
237
+ f'<span style="font-size:11px;color:#475569">{t.get("life_area") or "—"}</span>'
238
+ f'<span style="font-size:11px;color:#334155">{t["time_estimate"]}m{a}</span></div>')
239
+
240
+ def _tasks_html(uid):
241
+ tasks = get_tasks(uid, only_today=True)
242
+ return f'<div style="font-size:12px">{"".join(_task_row(t) for t in tasks)}</div>' if tasks else ""
243
+
244
+ def _tasks_to_state(uid):
245
+ return [{"task_id": t["id"], "title": t["title"], "state_of_mind": t["state_of_mind"],
246
+ "time_estimate": t["time_estimate"], "completed": bool(t["is_completed"]),
247
+ "actual_duration": t["actual_duration"]} for t in get_tasks(uid, only_today=True)]
248
+
249
+ _J_BLANK = (
250
+ '<p style="color:#334155;font-size:13px">Click "Load & Start" to begin.</p>',
251
+ gr.update(visible=False), [],
252
+ [], [], False,
253
+ gr.update(interactive=False), gr.update(interactive=False), "",
254
+ )
255
 
256
+ def handle_load_journal_tasks(uid):
257
+ if not uid: return ('<p style="color:#f87171">Not logged in.</p>',) + _J_BLANK[1:]
258
+ tasks = get_tasks(uid, only_today=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  if not tasks:
260
+ return ('<p style="color:#475569;font-size:13px">No tasks today — add some first.</p>',) + _J_BLANK[1:]
261
+ html = f'<div style="font-size:12px">{"".join(_task_row(t) for t in tasks)}</div>'
262
+ state = [{"task_id": t["id"], "title": t["title"], "state_of_mind": t["state_of_mind"],
263
+ "time_estimate": t["time_estimate"], "completed": bool(t["is_completed"]),
264
+ "actual_duration": t["actual_duration"]} for t in tasks]
265
+ ctx = _ensure_context(uid)
266
+ opening = build_opening_question(ctx, state)
267
+ msgs = [{"role": "assistant", "content": opening["question"]}]
268
+ return (html, gr.update(visible=True), state,
269
+ msgs, msgs, True,
270
+ gr.update(interactive=True), gr.update(interactive=True),
271
+ _ok("Loaded — reflection started. Answer below."))
272
+
273
+ def handle_journal_mark(tid, mins, uid, state, mark_done):
274
+ if not tid or not uid: return state, "", _err("Enter a task ID.")
275
+ tid = int(tid)
276
+ cur = next((t for t in state if t["task_id"] == tid), None)
277
+ if cur and bool(cur["completed"]) != mark_done:
278
+ toggle_task_complete(tid, uid, int(mins) if mins else None)
279
+ for t in state:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  if t["task_id"] == tid:
281
  t["completed"] = mark_done
282
+ if mark_done and mins: t["actual_duration"] = int(mins)
283
+ html = _tasks_html(uid)
284
+ return state, html, _ok(f"#{tid} {'marked' if mark_done else '↩ unmarked'}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+ def handle_voice_journal(audio_path):
287
+ if audio_path is None: return gr.update(), _err("No audio recorded.")
288
+ try:
289
+ text = _transcribe(audio_path)
290
+ return gr.update(value=text), _ok(f'Heard: "{text[:60]}{"…" if len(text)>60 else ""}"')
291
+ except Exception as e:
292
+ return gr.update(), _err(f"Transcription error: {e}")
293
+
294
+ def handle_send_answer(uid, answer, chat, hist, state, active):
295
+ if not active: return chat, hist, "", _err("Start the session first."), gr.update()
296
+ if not answer.strip(): return chat, hist, "", "", gr.update()
297
+ new_chat = list(chat) + [{"role": "user", "content": answer}]
298
+ new_hist = list(hist) + [{"role": "user", "content": answer}]
299
+ ctx = _ensure_context(uid)
300
+ result = get_next_journal_question(ctx, state, new_hist)
301
  if result.get("session_complete"):
302
+ done_msg = ("That's a solid reflection. 🎯\n\n"
303
+ "Click **✓ Finish & Save Insights** to lock in what you've learned "
304
+ "and update your AI profile for tomorrow.")
305
+ new_chat.append({"role": "assistant", "content": done_msg})
306
+ new_hist.append({"role": "assistant", "content": done_msg})
307
+ return new_chat, new_hist, "", _ok("Session complete — save your insights!"), gr.update(interactive=False)
308
  next_q = result.get("question", "")
309
+ new_chat.append({"role": "assistant", "content": next_q})
310
+ new_hist.append({"role": "assistant", "content": next_q})
311
+ return new_chat, new_hist, "", "", gr.update(interactive=True)
312
+
313
+ def handle_finish_journal(uid, chat, hist, state):
314
+ if not uid or not hist: return chat, _err("Complete a reflection first.")
315
+ ctx = _ensure_context(uid)
316
+ updated = synthesize_journal(ctx, state, hist)
317
+ save_user_context(uid, updated)
318
+ total = len(state); done = sum(1 for t in state if t.get("completed"))
319
+ rate = round(done/total*100) if total else 0
320
+ notes = updated.get("learned_patterns",{}).get("notes",[])
321
+ 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>"
322
+ summary = (f"**Insights saved** ✓ — {done}/{total} tasks completed ({rate}%)\n\n"
323
+ "Your AI profile has been updated. Tomorrow's schedule will reflect what you've shared.")
324
+ return (list(chat) + [{"role":"assistant","content":summary}],
325
+ _ok(f"Saved! {done}/{total} ({rate}%)") +
326
+ f'<ul style="color:#94a3b8;font-size:12px;margin-top:6px;padding-left:16px">{note_html}</ul>')
 
 
 
 
 
 
 
327
 
328
  def handle_restart_journal():
329
+ return ([], [], [], False,
330
+ gr.update(interactive=False), gr.update(interactive=False),
331
+ '<p style="color:#334155;font-size:13px">Click "Load & Start" to begin.</p>',
332
+ gr.update(visible=False), "")
333
+
334
+ # ── areas & goals ─────────────────────────────────────────────────────────────
335
+ def render_areas(uid):
336
+ if not uid: return "", [], []
337
+ areas = get_life_areas(uid)
338
+ chips = "".join(f'<span class="chip" style="background:{a["color"]}22;color:{a["color"]};border:1px solid {a["color"]}44">{a["name"]}</span> ' for a in areas)
339
+ html = f'<div style="margin:4px 0">{chips}</div>' if chips else '<p style="color:#334155;font-size:13px">No areas yet.</p>'
340
+ names = [a["name"] for a in areas]
 
 
 
 
 
 
 
 
341
  return html, names, names
342
 
343
+ def handle_add_area(uid, name, color):
344
+ ok, msg = add_life_area(uid, name, color)
345
+ html, names, _ = render_areas(uid)
346
+ return (f'<span style="color:{"#4ade80" if ok else "#f87171"};font-size:13px">{msg}</span>',
347
+ html, gr.update(choices=names, value=None), gr.update(value=""))
348
+
349
+ def handle_del_area(uid, name):
350
+ if not name: return _err("Select an area first."), "", gr.update()
351
+ ok, msg = delete_life_area(uid, name)
352
+ html, names, _ = render_areas(uid)
353
+ return (f'<span style="color:{"#4ade80" if ok else "#f87171"};font-size:13px">{msg}</span>',
354
+ html, gr.update(choices=names, value=None))
355
+
356
+ def handle_save_goals(uid, txt):
357
+ if not uid: return _err("Not logged in.")
358
+ save_goals(uid, txt); return _ok("Goals saved!")
359
+
360
+ def load_goals_txt(uid):
361
+ return "\n".join(get_goals(uid)) if uid else ""
362
+
363
+ # ── prefs ─────────────────────────────────────────────────────────────────────
364
+ def load_prefs(uid):
365
+ ctx = load_user_context(uid) if uid else None
366
+ if not ctx: return "07:30","23:00","Morning",10,90
367
+ p = ctx.get("preferences",{})
368
+ 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)
369
+
370
+ def handle_save_prefs(uid, wake, sleep, focus, brk, flow_max):
371
+ if not uid: return _err("Not logged in.")
372
+ ctx = _ensure_context(uid)
373
+ ctx["preferences"].update({"wake_time":wake or "07:30","sleep_time":sleep or "23:00",
374
+ "focus_peak":focus or "Morning","break_duration_minutes":int(brk or 10),
375
+ "max_flow_block_minutes":int(flow_max or 90)})
376
+ save_user_context(uid, ctx); return _ok("Preferences saved!")
377
+
378
+ def render_context_html(uid):
379
+ if not uid: return "<p style='color:#334155'>Not logged in.</p>"
380
+ ctx = load_user_context(uid)
381
+ if not ctx: return "<p style='color:#334155;font-size:13px'>No AI context yet. Complete a journaling session.</p>"
382
+ lp=ctx.get("learned_patterns",{}); sf=ctx.get("scheduling_feedback",{}); pref=ctx.get("preferences",{})
383
+ def row(l,v): return (f'<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #0d1117">'
384
+ f'<span style="color:#334155;font-size:12px">{l}</span>'
385
+ f'<span style="color:#94a3b8;font-size:12px;font-family:JetBrains Mono,monospace">{v}</span></div>')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  def lst(v): return ", ".join(v) if v else "—"
387
+ html = (f'<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">'
388
+ f'<div class="panel"><div class="sec-label">Preferences</div>'
389
+ f'{row("Wake",pref.get("wake_time","—"))}{row("Sleep",pref.get("sleep_time","—"))}'
390
+ f'{row("Peak focus",pref.get("focus_peak","—"))}{row("Break",str(pref.get("break_duration_minutes","—"))+"min")}'
391
+ f'{row("Max flow block",str(pref.get("max_flow_block_minutes","—"))+"min")}</div>'
392
+ f'<div class="panel"><div class="sec-label">Scheduling Stats</div>'
393
+ f'{row("Days scheduled",sf.get("total_days_scheduled",0))}'
394
+ f'{row("Avg completion",str(round(sf.get("avg_completion_rate",0)*100))+"%")}'
395
+ f'{row("Context version",ctx.get("version",1))}'
396
+ f'{row("Last updated",(ctx.get("last_updated","—") or "")[:16])}</div></div>'
397
+ f'<div class="panel" style="margin-top:14px"><div class="sec-label">Learned Patterns</div>'
398
+ f'{row("Productive times",lst(lp.get("productive_times",[])))}'
399
+ f'{row("Low energy times",lst(lp.get("low_energy_times",[])))}'
400
+ f'{row("Avg task overrun",str(lp.get("avg_task_overrun_pct",0))+"%")}'
401
+ f'{row("Flow batchable",str(lp.get("flow_batch_capable","Unknown")))}'
402
+ f'{row("Best morning areas",lst(lp.get("best_life_areas_morning",[])))}'
403
+ f'{row("Skipped types",lst(lp.get("common_skipped_task_types",[])))}</div>')
404
+ notes = lp.get("notes",[])
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  if notes:
406
+ nr = "".join(f'<div style="padding:5px 0;border-bottom:1px solid #0d1117;color:#64748b;font-size:12px">• {n}</div>' for n in notes[-5:])
407
+ html += f'<div class="panel" style="margin-top:14px"><div class="sec-label">AI Notes</div>{nr}</div>'
408
+ history = ctx.get("history_summary",[])
 
 
 
 
409
  if history:
410
+ hr = "".join(f'<div style="padding:4px 0;color:#475569;font-size:12px">• {h}</div>' for h in history[-5:])
411
+ html += f'<div class="panel" style="margin-top:14px"><div class="sec-label">Recent History</div>{hr}</div>'
 
 
 
 
412
  return html
413
 
 
414
  # ═══════════════════════════════════════════════════════════════════════════════
415
+ # UI
416
  # ═══════════════════════════════════════════════════════════════════════════════
417
+ with gr.Blocks(css=CSS, title="🧠 Second Brain") as demo:
418
 
419
+ user_id_state = gr.State(None)
420
+ username_state = gr.State("")
 
 
 
 
 
421
  journal_hist_state = gr.State([])
422
  journal_tasks_state = gr.State([])
423
  journal_active = gr.State(False)
 
424
  clar_questions_state = gr.State([])
425
  original_task_state = gr.State("")
426
 
427
+ # AUTH
 
 
428
  with gr.Column(visible=True, elem_id="auth-card") as auth_section:
429
  gr.HTML('<div id="brand-logo">🧠 Second Brain</div>')
430
  gr.HTML('<div id="brand-sub">Your intelligent productivity companion</div>')
 
431
  with gr.Tabs():
 
432
  with gr.Tab("Sign In"):
433
  login_user_in = gr.Textbox(label="Username", placeholder="your username")
434
  login_pass_in = gr.Textbox(label="Password", type="password", placeholder="••••••••")
435
  login_btn = gr.Button("Sign In", elem_classes="btn-primary")
436
  login_msg = gr.HTML("")
 
 
437
  with gr.Tab("Create Account"):
438
+ reg_user_in = gr.Textbox(label="Username", placeholder="choose a username")
439
+ reg_pass_in = gr.Textbox(label="Password (min 6 chars)", type="password", placeholder="••••••••")
440
+ with gr.Row():
441
+ reg_wake = gr.Textbox(label="Wake time", value="07:30", scale=1)
442
+ reg_sleep = gr.Textbox(label="Sleep time", value="23:00", scale=1)
443
+ reg_focus = gr.Dropdown(label="Peak focus", choices=["Morning","Afternoon","Evening","Night"], value="Morning", scale=1)
444
+ reg_goals = gr.Textbox(label="Big-picture goals (optional, one per line)", lines=3, placeholder="Launch my startup\nGet fit")
445
+ reg_btn = gr.Button("Create Account", elem_classes="btn-primary")
446
+ reg_msg = gr.HTML("")
447
+
448
+ # MAIN APP
 
 
 
 
 
 
 
449
  with gr.Column(visible=False) as app_section:
 
 
450
  with gr.Row(elem_id="top-header"):
451
  header_html = gr.HTML('<div id="top-header-brand">🧠 Second Brain</div><div id="top-header-user">—</div>')
452
  logout_btn = gr.Button("Sign Out", elem_classes="btn-secondary", scale=0)
453
 
454
+ with gr.Tabs():
 
455
 
456
+ # TODAY
 
 
457
  with gr.Tab("📅 Today"):
 
458
  with gr.Row():
459
  stat_total = gr.HTML(_stat("—","Today","#a78bfa"))
460
  stat_done = gr.HTML(_stat("—","Done","#4ade80"))
461
  stat_remain = gr.HTML(_stat("—","Left","#f87171"))
 
 
462
  with gr.Row():
463
  btn_add_text = gr.Button("✏️ Add Task (Text)", elem_classes="btn-primary", scale=3)
464
  btn_add_voice = gr.Button("🎙️ Add Task (Voice)", elem_classes="btn-accent", scale=3)
465
  btn_plan_day = gr.Button("🗓 Plan My Day", elem_classes="btn-secondary", scale=3)
466
  btn_refresh = gr.Button("↻", elem_classes="btn-secondary", scale=1)
467
 
468
+ # Text panel
469
  with gr.Column(visible=False, elem_classes="panel") as text_panel:
470
  gr.HTML('<div class="sec-label">Describe Your Task</div>')
471
+ task_input = gr.Textbox(label="", placeholder="e.g. Finish the client proposal by tomorrow, very important", lines=2)
 
 
 
 
472
  with gr.Row():
473
+ btn_parse = gr.Button("🤖 Parse with AI", elem_classes="btn-primary")
474
+ btn_cancel_text = gr.Button("Cancel", elem_classes="btn-secondary")
475
 
476
  with gr.Column(visible=False) as parsed_panel:
477
  parse_status = gr.HTML("")
478
  clar_html = gr.HTML("")
 
479
  with gr.Column(visible=False) as clar_reply_row:
480
+ gr.HTML('<div class="sec-label" style="margin-top:8px">Your Answer to the Clarification</div>')
481
+ clar_reply_input = gr.Textbox(label="", placeholder="e.g. It's a hard client deadline, high priority, ~2 hours", lines=2)
482
+ btn_clar_submit = gr.Button("🔄 Re-classify with my answer", elem_classes="btn-accent")
483
+ gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Edit Before Saving</div>')
 
 
 
 
484
  with gr.Row():
485
  f_title = gr.Textbox(label="Title", scale=3)
486
+ f_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1)
 
 
487
  with gr.Row():
488
+ f_urgency = gr.Dropdown(label="Urgency", choices=["Urgent","Not Urgent","Habit"], value="Not Urgent")
489
+ f_importance = gr.Dropdown(label="Importance", choices=["Move the Needle","Important","Not Important"], value="Important")
490
+ f_state = gr.Dropdown(label="State of Mind",choices=["Flow","Easy","Quick","Personal"], value="Easy")
 
 
 
 
491
  with gr.Row():
492
+ f_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
493
+ f_date = gr.Textbox(label="Scheduled Date", value=str(date.today()), scale=1)
494
+ f_is_habit = gr.Checkbox(label="♻️ Habit", scale=1)
495
+ f_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1)
 
 
496
  with gr.Row():
497
  btn_confirm = gr.Button("✓ Save Task", elem_classes="btn-success")
498
  btn_discard = gr.Button("✗ Discard", elem_classes="btn-danger")
499
  save_msg = gr.HTML("")
500
 
501
+ # Voice panel
502
  with gr.Column(visible=False, elem_classes="panel") as voice_panel:
503
  gr.HTML('<div class="sec-label">Record or Upload Audio</div>')
504
+ gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:12px">Speak your task — Whisper transcribes it, AI classifies it, and the same confirm panel appears below.</p>')
505
+ audio_input = gr.Audio(sources=["microphone","upload"], type="filepath", label="")
506
+ voice_task_msg = gr.HTML("")
 
507
  with gr.Row():
508
+ btn_voice_parse = gr.Button("🤖 Transcribe & Parse", elem_classes="btn-primary")
509
+ btn_cancel_voice = gr.Button("Cancel", elem_classes="btn-secondary")
 
510
 
511
+ # Plan panel
512
  with gr.Column(visible=False, elem_classes="panel") as plan_panel:
513
  gr.HTML('<div class="sec-label">Plan My Day</div>')
514
+ plan_prompt = gr.Textbox(label="Any fixed events or preferences today?", placeholder="e.g. Meeting at 10am, want deep work in the morning, done by 7pm", lines=3)
 
 
 
 
 
515
  with gr.Row():
516
+ btn_gen_sched = gr.Button("✨ Generate Schedule", elem_classes="btn-primary")
517
+ btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary")
518
  schedule_html = gr.HTML("")
519
 
 
520
  gr.HTML('<div class="sec-label" style="margin-top:20px">Today\'s Tasks</div>')
521
  today_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
522
  with gr.Row():
523
+ today_done_id = gr.Number(label="Task ID — toggle done", precision=0, scale=2)
524
  btn_today_done = gr.Button("✅ Mark Done", elem_classes="btn-success", scale=2)
525
  today_del_id = gr.Number(label="Task ID — delete", precision=0, scale=2)
526
  btn_today_del = gr.Button("🗑 Delete", elem_classes="btn-danger", scale=2)
527
  today_action_msg = gr.HTML("")
528
 
529
+ # ALL TASKS
 
 
530
  with gr.Tab("📋 All Tasks"):
531
  with gr.Row():
532
  all_filter = gr.Dropdown(label="Filter by Area", choices=["All"], value="All", scale=4)
533
  all_today_chk = gr.Checkbox(label="Today only", value=False, scale=1)
534
  btn_all_ref = gr.Button("↻ Refresh", elem_classes="btn-secondary", scale=1)
 
535
  all_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
536
  with gr.Row():
537
  all_done_id = gr.Number(label="Task ID — toggle done", precision=0, scale=2)
 
540
  btn_all_del = gr.Button("🗑 Delete", elem_classes="btn-danger", scale=2)
541
  all_action_msg = gr.HTML("")
542
 
543
+ # JOURNAL
 
 
544
  with gr.Tab("📓 Journal"):
545
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:4px">End-of-day check-in. Mark tasks done, then reflect. The AI asks targeted questions and updates your profile.</p>')
546
 
 
547
  with gr.Column(elem_classes="panel"):
548
+ with gr.Row():
549
+ gr.HTML('<div class="sec-label" style="margin:0;line-height:34px">Today\'s Tasks</div>')
550
+ btn_load_tasks = gr.Button("Load & Start", elem_classes="btn-primary", scale=0)
551
+ btn_j_restart = gr.Button("↺ Reset", elem_classes="btn-secondary", scale=0)
552
+ journal_task_list = gr.HTML('<p style="color:#334155;font-size:13px">Click "Load & Start" to begin.</p>')
553
  with gr.Row(visible=False) as journal_mark_row:
554
+ j_task_id = gr.Number(label="Task ID", precision=0, scale=2)
555
+ j_actual_min = gr.Number(label="Actual time (min)", precision=0, value=None, scale=2)
556
+ btn_j_done = gr.Button("✅ Mark Complete", elem_classes="btn-success", scale=2)
557
  btn_j_undone = gr.Button("↩ Unmark", elem_classes="btn-secondary", scale=1)
558
  journal_mark_msg = gr.HTML("")
559
 
 
560
  with gr.Column(elem_classes="panel"):
 
 
561
  with gr.Row():
562
+ gr.HTML('<div class="sec-label" style="margin:0;line-height:34px">Reflection</div>')
563
+ btn_j_finish = gr.Button("✓ Finish & Save Insights", elem_classes="btn-success", scale=0)
564
+ journal_chatbot = gr.Chatbot(
565
+ label="", height=400, type="messages",
566
+ show_label=False, bubble_full_width=False, render_markdown=True,
567
+ placeholder="*Click \"Load & Start\" to begin your reflection.*",
568
+ )
569
+ # Text answer
570
+ with gr.Row():
571
+ j_answer = gr.Textbox(label="", placeholder="Type your answer… (or record below)", lines=2, scale=5, interactive=False, show_label=False, container=False)
572
+ btn_j_send = gr.Button("Send →", elem_classes="btn-accent", scale=1, interactive=False)
573
+ # Voice answer
574
+ with gr.Row():
575
+ j_voice_input = gr.Audio(sources=["microphone"], type="filepath", label="🎤 Voice answer (record, then click Fill)", scale=3)
576
+ with gr.Column(scale=2):
577
+ btn_j_voice = gr.Button("🎤 Fill Answer from Voice", elem_classes="btn-secondary")
578
+ j_voice_msg = gr.HTML("")
579
  journal_msg = gr.HTML("")
580
 
581
+ # AREAS & GOALS
 
 
582
  with gr.Tab("🗂 Areas & Goals"):
583
  gr.HTML('<div class="sec-label">Your Life Areas</div>')
584
  areas_display = gr.HTML("")
 
585
  with gr.Row():
586
  new_area_name = gr.Textbox(label="New area name", placeholder="e.g. Side Project", scale=3)
587
  new_area_color = gr.ColorPicker(label="Color", value="#6366f1", scale=1)
588
  btn_add_area = gr.Button("Add", elem_classes="btn-primary", scale=1)
589
  area_msg = gr.HTML("")
 
590
  gr.HTML('<div class="sec-label" style="margin-top:24px">Remove Area</div>')
591
  with gr.Row():
592
  del_area_dd = gr.Dropdown(label="Select area to remove", choices=[], scale=4)
593
  btn_del_area = gr.Button("Remove", elem_classes="btn-danger", scale=1)
594
  del_area_msg = gr.HTML("")
 
595
  gr.HTML('<hr>')
596
  gr.HTML('<div class="sec-label">Big-Picture Goals</div>')
597
+ gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:10px">The AI uses these when scheduling and prioritising.</p>')
598
+ goals_input = gr.Textbox(label="One goal per line", lines=5, placeholder="Launch my SaaS\nRun 5K\nLearn ML")
599
+ btn_save_goals = gr.Button("Save Goals", elem_classes="btn-primary")
600
+ goals_msg = gr.HTML("")
601
+
602
+ # PREFS
 
 
 
603
  with gr.Tab("⚙️ Preferences"):
604
  gr.HTML('<div class="sec-label">Scheduling Preferences</div>')
605
  with gr.Row():
606
  pref_wake = gr.Textbox(label="Wake time", placeholder="07:30", scale=1)
607
  pref_sleep = gr.Textbox(label="Sleep time", placeholder="23:00", scale=1)
608
+ pref_focus = gr.Dropdown(label="Peak focus", choices=["Morning","Afternoon","Evening","Night"], value="Morning", scale=1)
 
 
609
  with gr.Row():
610
+ pref_break = gr.Number(label="Break between tasks (min)", value=10, minimum=0, scale=1)
611
  pref_flow_max = gr.Number(label="Max Flow block (min)", value=90, minimum=30, scale=1)
612
  btn_save_prefs = gr.Button("Save Preferences", elem_classes="btn-primary")
613
  prefs_msg = gr.HTML("")
 
614
  gr.HTML('<hr>')
615
  gr.HTML('<div class="sec-label">AI Memory</div>')
616
+ gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:10px">Everything the AI has learned about you.</p>')
617
+ ctx_display = gr.HTML("")
618
+ btn_ref_ctx = gr.Button("↻ Refresh", elem_classes="btn-secondary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
 
620
+ # ── EVENT WIRING ──────────────────────────────────────────────────────────
621
+ def _hdr(uid, uname):
622
+ return f'<div id="top-header-brand">🧠 Second Brain</div><div id="top-header-user">👤 {uname}</div>'
 
 
 
 
 
 
 
 
 
 
 
623
 
624
+ _LOGIN_CHAIN = [user_id_state, username_state, login_msg, auth_section, app_section]
625
+
626
+ login_btn.click(handle_login, [login_user_in, login_pass_in], _LOGIN_CHAIN
627
+ ).then(_hdr, [user_id_state, username_state], [header_html]
628
+ ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
629
+ ).then(lambda u: render_areas(u)[:2], [user_id_state], [areas_display, del_area_dd]
630
+ ).then(load_goals_txt, [user_id_state], [goals_input]
631
+ ).then(load_prefs, [user_id_state], [pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max])
632
 
633
+ reg_btn.click(handle_register, [reg_user_in, reg_pass_in, reg_wake, reg_sleep, reg_focus, reg_goals], _LOGIN_CHAIN
634
+ ).then(_hdr, [user_id_state, username_state], [header_html]
635
+ ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
636
+ ).then(lambda u: render_areas(u)[:2], [user_id_state], [areas_display, del_area_dd])
637
 
638
+ logout_btn.click(handle_logout, [user_id_state], [user_id_state, username_state, auth_section, app_section])
639
+
640
+ # Today panels
641
+ btn_add_text.click(show_text_panel, outputs=[text_panel, voice_panel, plan_panel])
642
  btn_add_voice.click(show_voice_panel, outputs=[text_panel, voice_panel, plan_panel])
643
  btn_plan_day.click(show_plan_panel, outputs=[text_panel, voice_panel, plan_panel])
644
  btn_cancel_text.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
645
  btn_cancel_voice.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
646
  btn_cancel_plan.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
647
+ btn_refresh.click(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain])
648
+ f_is_habit.change(lambda v: gr.update(visible=v), [f_is_habit], [f_interval])
649
 
650
+ _PARSE_OUTS = [parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance,
651
+ f_state, f_time, f_date, f_is_habit, f_interval, parse_status,
652
+ clar_reply_row, clar_questions_state, original_task_state]
 
653
 
654
+ btn_parse.click(handle_parse_text, [task_input, user_id_state], _PARSE_OUTS)
 
 
 
 
655
 
656
+ # Voice parse → same confirm panel, switches to text panel view
657
+ btn_voice_parse.click(
658
+ handle_voice_parse, [audio_input, user_id_state],
659
+ [voice_task_msg, text_panel, voice_panel, plan_panel, *_PARSE_OUTS]
 
 
660
  )
661
 
662
  btn_clar_submit.click(
663
  handle_clarification_reply,
664
  [clar_reply_input, original_task_state, clar_questions_state, user_id_state],
665
+ [f_title, clar_html, f_area, f_urgency, f_importance, f_state, f_time, clar_reply_row, clar_questions_state]
666
+ ).then(lambda: "", outputs=[clar_reply_input])
667
+
668
+ btn_confirm.click(handle_confirm_task,
669
+ [user_id_state, f_title, f_area, f_urgency, f_importance, f_state, f_time, f_date, f_is_habit, f_interval],
670
+ [save_msg, today_df, stat_total, stat_done, stat_remain, parsed_panel])
671
+ btn_discard.click(lambda: gr.update(visible=False), outputs=[parsed_panel])
672
+ btn_gen_sched.click(handle_generate_schedule, [user_id_state, plan_prompt], [schedule_html])
673
+ btn_today_done.click(handle_toggle_today, [today_done_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg])
674
+ btn_today_del.click(handle_delete_today, [today_del_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg])
675
+
676
+ # All tasks
677
+ btn_all_ref.click(refresh_all_tasks, [user_id_state, all_filter, all_today_chk], [all_df])
678
+ all_filter.change(refresh_all_tasks, [user_id_state, all_filter, all_today_chk], [all_df])
679
+ all_today_chk.change(refresh_all_tasks,[user_id_state, all_filter, all_today_chk], [all_df])
680
+ btn_all_done.click(handle_toggle_all, [all_done_id, user_id_state, all_filter, all_today_chk], [all_df, all_action_msg])
681
+ btn_all_del.click(handle_delete_all, [all_del_id, user_id_state, all_filter, all_today_chk], [all_df, all_action_msg])
682
+
683
+ # Journal
684
+ _J_LOAD_OUTS = [journal_task_list, journal_mark_row, journal_tasks_state,
685
+ journal_chatbot, journal_hist_state, journal_active,
686
+ j_answer, btn_j_send, journal_msg]
687
+
688
+ btn_load_tasks.click(handle_load_journal_tasks, [user_id_state], _J_LOAD_OUTS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
 
690
  btn_j_done.click(
691
  lambda tid, mins, uid, st: handle_journal_mark(tid, mins, uid, st, True),
692
  [j_task_id, j_actual_min, user_id_state, journal_tasks_state],
693
+ [journal_tasks_state, journal_task_list, journal_mark_msg]
 
 
 
 
694
  )
 
695
  btn_j_undone.click(
696
  lambda tid, mins, uid, st: handle_journal_mark(tid, mins, uid, st, False),
697
  [j_task_id, j_actual_min, user_id_state, journal_tasks_state],
698
+ [journal_tasks_state, journal_task_list, journal_mark_msg]
 
 
 
 
699
  )
700
 
701
+ btn_j_voice.click(handle_voice_journal, [j_voice_input], [j_answer, j_voice_msg])
 
 
 
 
 
 
 
 
 
 
 
 
702
 
703
+ _SEND_OUTS = [journal_chatbot, journal_hist_state, j_answer, journal_msg, btn_j_send]
704
+ btn_j_send.click(handle_send_answer,
705
+ [user_id_state, j_answer, journal_chatbot, journal_hist_state, journal_tasks_state, journal_active],
706
+ _SEND_OUTS)
707
+ j_answer.submit(handle_send_answer,
708
+ [user_id_state, j_answer, journal_chatbot, journal_hist_state, journal_tasks_state, journal_active],
709
+ _SEND_OUTS)
710
 
711
+ btn_j_finish.click(handle_finish_journal,
 
712
  [user_id_state, journal_chatbot, journal_hist_state, journal_tasks_state],
713
+ [journal_chatbot, journal_msg])
 
714
 
715
+ btn_j_restart.click(handle_restart_journal, outputs=[
716
+ journal_chatbot, journal_hist_state, journal_tasks_state, journal_active,
717
+ j_answer, btn_j_send, journal_task_list, journal_mark_row, journal_msg])
 
 
718
 
719
+ # Areas & Goals
720
+ btn_add_area.click(handle_add_area, [user_id_state, new_area_name, new_area_color],
 
 
 
721
  [area_msg, areas_display, del_area_dd, new_area_name]
722
+ ).then(lambda u: gr.update(choices=_area_choices(u), value="All"), [user_id_state], [all_filter])
 
 
 
723
 
724
+ btn_del_area.click(handle_del_area, [user_id_state, del_area_dd],
 
 
725
  [del_area_msg, areas_display, del_area_dd]
726
+ ).then(lambda u: gr.update(choices=_area_choices(u), value="All"), [user_id_state], [all_filter])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
727
 
728
+ btn_save_goals.click(handle_save_goals, [user_id_state, goals_input], [goals_msg])
729
 
730
+ # Prefs
731
+ btn_save_prefs.click(handle_save_prefs,
732
+ [user_id_state, pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max], [prefs_msg])
733
+ btn_ref_ctx.click(render_context_html, [user_id_state], [ctx_display])
734
 
735
  if __name__ == "__main__":
736
+ demo.launch(server_name="0.0.0.0", server_port=7860)