eraz3r commited on
Commit
af687af
Β·
verified Β·
1 Parent(s): 506bf08

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +529 -798
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,11 +20,11 @@ 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
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -51,38 +38,84 @@ def _info(msg): return f'<span style="color:#60a5fa;font-size:13px">β„Ή {msg}</s
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):
@@ -90,9 +123,7 @@ def handle_login(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,20 +132,17 @@ 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):
@@ -123,694 +151,527 @@ def refresh_today(user_id):
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 +680,217 @@ 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
 
30
  # ═══════════════════════════════════════════════════════════════════════════════
 
38
  def _stat(val, label, color):
39
  return f'<div class="stat-card"><div class="stat-num" style="color:{color}">{val}</div><div class="stat-label">{label}</div></div>'
40
 
41
+ def _fmt_tasks(tasks):
42
+ return [[
43
+ t["id"], "βœ…" if t["is_completed"] else "⬜",
44
+ ("πŸ” " if t["is_habit"] else "") + t["title"],
45
+ t["life_area"] or "β€”", t["urgency"] or "β€”", t["importance"] or "β€”",
46
+ t["state_of_mind"] or "β€”",
47
+ str(t["time_estimate"]) + "m" if t["time_estimate"] else "β€”",
48
+ t["scheduled_date"] or "β€”",
49
+ ] for t in tasks]
 
 
 
 
 
 
50
 
51
  def _area_choices(user_id):
52
+ return ["All"] + (get_life_area_names(user_id) if user_id else [])
 
 
53
 
54
  def _ensure_context(user_id):
 
55
  ctx = load_user_context(user_id)
56
  if not ctx:
57
  ctx = create_blank_context(user_id)
58
  save_user_context(user_id, ctx)
59
  return ctx
60
 
61
+ def _transcribe(audio_path):
62
+ global _whisper_model
63
+ from faster_whisper import WhisperModel
64
+ if _whisper_model is None:
65
+ _whisper_model = WhisperModel("small", device="cpu", compute_type="int8")
66
+ segments, _ = _whisper_model.transcribe(audio_path)
67
+ return " ".join(seg.text for seg in segments).strip()
68
+
69
+ def _build_clar_html(clarifications):
70
+ if not clarifications:
71
+ return ""
72
+ items = "".join(f"<li style='margin:4px 0'>{q}</li>" for q in clarifications)
73
+ return (
74
+ f'<div style="margin:10px 0;padding:12px;background:#0c0f1a;'
75
+ f'border-left:3px solid #a78bfa;border-radius:8px">'
76
+ f'<div style="color:#a78bfa;font-size:11px;font-weight:600;margin-bottom:6px">⚠ Please clarify:</div>'
77
+ f'<ul style="color:#94a3b8;font-size:13px;margin:0;padding-left:16px">{items}</ul></div>'
78
+ )
79
+
80
+ def _run_parse(task_text, user_id):
81
+ ctx = _ensure_context(user_id)
82
+ goals = get_goals(user_id)
83
+ areas = get_life_area_names(user_id) if user_id else []
84
+ result = parse_task_with_groq(task_text, ctx, goals, areas)
85
+ clars = result.get("clarifications_needed", [])
86
+ area_choices = areas or ["Work","Health","Finance","Learning","Personal","Family","Other"]
87
+ area_val = result.get("life_area") or area_choices[0]
88
+ if area_val not in area_choices:
89
+ area_choices = [area_val] + area_choices
90
+ return result, _build_clar_html(clars), bool(clars), area_val, clars
91
+
92
+ def _parse_result_to_outputs(result, clar_html, has_clar, area_val, clars, task_text, status=""):
93
+ """Convert a parse result into the standard 15-value tuple for UI outputs."""
94
+ if not status:
95
+ status = _ok("AI classified your task οΏ½οΏ½ answer the questions above." if has_clar else "AI classified your task βœ“")
96
+ return (
97
+ gr.update(visible=True),
98
+ result.get("title", task_text),
99
+ clar_html, area_val,
100
+ result.get("urgency", "Not Urgent"),
101
+ result.get("importance", "Important"),
102
+ result.get("state_of_mind", "Easy"),
103
+ int(result.get("time_estimate") or 30),
104
+ str(date.today()), False, "Daily",
105
+ status,
106
+ gr.update(visible=has_clar),
107
+ clars, task_text,
108
+ )
109
+
110
+ _EMPTY_PARSE = (
111
+ gr.update(visible=False), "", "", "Work", "Not Urgent", "Important",
112
+ "Easy", 30, str(date.today()), False, "Daily", "",
113
+ gr.update(visible=False), [], "",
114
+ )
115
+
116
 
117
  # ═══════════════════════════════════════════════════════════════════════════════
118
+ # AUTH
119
  # ═══════════════════════════════════════════════════════════════════════════════
120
 
121
  def handle_login(username, password):
 
123
  if not uid:
124
  return None, "", _err(msg), gr.update(visible=True), gr.update(visible=False)
125
  spawn_due_habits(uid)
126
+ return uid, get_username(uid), _ok(msg), gr.update(visible=False), gr.update(visible=True)
 
 
127
 
128
  def handle_register(username, password, wake, sleep, focus, goals_text):
129
  uid, msg = register_user(username, password)
 
132
  create_default_life_areas(uid)
133
  if goals_text.strip():
134
  save_goals(uid, goals_text)
135
+ ctx = create_blank_context(uid, {"wake_time": wake or "07:30", "sleep_time": sleep or "23:00", "focus_peak": focus or "Morning"})
 
136
  save_user_context(uid, ctx)
137
  spawn_due_habits(uid)
138
+ return uid, get_username(uid), _ok(msg + " Logged in!"), gr.update(visible=False), gr.update(visible=True)
 
 
139
 
140
  def handle_logout(user_id):
141
  return None, "", gr.update(visible=True), gr.update(visible=False)
142
 
143
 
144
  # ═══════════════════════════════════════════════════════════════════════════════
145
+ # TODAY TAB
146
  # ═══════════════════════════════════════════════════════════════════════════════
147
 
148
  def refresh_today(user_id):
 
151
  tasks = get_tasks(user_id, only_today=True)
152
  s = get_today_stats(user_id)
153
  c = "#4ade80" if s["remaining"] == 0 and s["total"] > 0 else "#f87171"
154
+ return _fmt_tasks(tasks), _stat(s["total"],"Today","#a78bfa"), _stat(s["done"],"Done","#4ade80"), _stat(s["remaining"],"Left",c)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
+ def show_text_panel(): return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
157
+ def show_voice_panel(): return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
158
+ def show_plan_panel(): return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
159
+ def hide_panels(): return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
160
 
161
+ # ── Text parse ─────────────────────────────────────────────────────────────────
162
 
163
  def handle_parse_text(task_text, user_id):
 
164
  if not task_text.strip():
165
+ return _EMPTY_PARSE
166
+ return _parse_result_to_outputs(*_run_parse(task_text, user_id), task_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
  def handle_clarification_reply(user_reply, original_task, clarifications, user_id):
 
169
  if not user_reply.strip():
170
+ return (gr.update(),)*7 + (gr.update(visible=True), [])
171
+ q_block = "\n".join(f"Q: {q}" for q in clarifications)
172
+ enriched = f"{original_task}\n\nUser clarification:\n{q_block}\nA: {user_reply}"
173
+ result, clar_html_val, still_has, area_val, remaining = _run_parse(enriched, user_id)
174
+ if not still_has:
175
+ clar_html_val = '<span style="color:#4ade80;font-size:13px">βœ“ Classification updated!</span>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  return (
177
+ result.get("title", original_task), clar_html_val, area_val,
178
+ result.get("urgency","Not Urgent"), result.get("importance","Important"),
179
+ result.get("state_of_mind","Easy"), int(result.get("time_estimate") or 30),
180
+ gr.update(visible=still_has), remaining,
 
 
 
 
 
181
  )
182
 
183
+ def handle_clar_voice(audio_path, original_task, clarifications, user_id):
184
+ if not audio_path:
185
+ return (gr.update(),)*7 + (gr.update(visible=True), [])
186
+ try:
187
+ reply = _transcribe(audio_path)
188
+ return handle_clarification_reply(reply, original_task, clarifications, user_id)
189
+ except Exception as e:
190
+ return (gr.update(),)*7 + (gr.update(visible=True), clarifications)
191
 
192
+ # ── Voice parse β€” full pipeline ────────────────────────────────────────────────
193
+
194
+ def handle_voice_parse(audio_path, user_id):
195
  if audio_path is None:
196
+ return _EMPTY_PARSE[:-2] + (_err("No audio recorded."), gr.update(visible=False), [], "")
197
  try:
198
+ text = _transcribe(audio_path)
 
 
 
 
 
 
199
  except Exception as e:
200
+ return _EMPTY_PARSE[:-2] + (_err(f"Transcription error: {e}"), gr.update(visible=False), [], "")
201
+ if not text:
202
+ return _EMPTY_PARSE[:-2] + (_err("Could not hear anything. Try again."), gr.update(visible=False), [], "")
203
+ result, clar_html, has_clar, area_val, clars = _run_parse(text, user_id)
204
+ preview = text[:50] + ("…" if len(text) > 50 else "")
205
+ status = _ok(f'Heard: "{preview}" β€” classified βœ“' + (" Please answer questions." if has_clar else ""))
206
+ return _parse_result_to_outputs(result, clar_html, has_clar, area_val, clars, text, status)
207
 
208
+ # ── Confirm task ───────────────────────────────────────────────────────────────
209
 
210
+ def handle_confirm_task(user_id, title, area, urgency, importance, state, time_est, sched_date, is_habit, habit_interval):
 
 
211
  if not user_id:
212
  return _err("Not logged in."), [], _stat("β€”","Today","#a78bfa"), _stat("β€”","Done","#4ade80"), _stat("β€”","Left","#f87171"), gr.update(visible=False)
213
  if not title.strip():
214
  return _err("Title cannot be empty."), [], _stat("β€”","Today","#a78bfa"), _stat("β€”","Done","#4ade80"), _stat("β€”","Left","#f87171"), gr.update(visible=True)
215
+ save_task(user_id, {
216
+ "title": title, "life_area": area, "urgency": urgency, "importance": importance,
217
+ "state_of_mind": state, "time_estimate": int(time_est or 30),
218
+ "is_habit": is_habit, "habit_interval": habit_interval if is_habit else "",
219
+ }, sched_date)
 
 
 
 
220
  rows, s1, s2, s3 = refresh_today(user_id)
221
  return _ok("Task saved!"), rows, s1, s2, s3, gr.update(visible=False)
222
 
 
223
  def handle_generate_schedule(user_id, prompt):
224
+ if not user_id: return _err("Not logged in.")
 
225
  tasks = get_tasks(user_id, only_today=True, include_completed=False)
226
+ if not tasks: return _err("No incomplete tasks today to schedule.")
227
+ sched = generate_schedule(_ensure_context(user_id), tasks, prompt or "Schedule my day sensibly.", get_goals(user_id))
 
 
 
 
 
228
  if "error" in sched and "scheduled_tasks" not in sched:
229
  return _err(f"Scheduling failed: {sched.get('error')}")
230
+ COLOR = {"Flow":"#0ea5e9","Easy":"#4ade80","Quick":"#a78bfa","Personal":"#f87171"}
231
+ html = (f'<div style="margin-bottom:14px"><span style="color:#a78bfa;font-size:12px;font-weight:600">πŸ“… {sched.get("schedule_date","Today")}</span>'
232
+ f'<p style="color:#475569;font-size:13px;margin:6px 0 0">{sched.get("day_summary","")}</p></div>')
 
 
 
 
 
 
233
  for t in sched.get("scheduled_tasks", []):
234
+ sm = t.get("state_of_mind","Easy"); c = COLOR.get(sm,"#6366f1")
235
+ html += (f'<div class="sched-card" style="border-left-color:{c}"><div class="sched-time">{t.get("start_time")} – {t.get("end_time")}</div>'
236
+ f'<div class="sched-title">{t.get("title")}</div><div class="sched-meta">{t.get("life_area","β€”")} Β· {sm} Β· {t.get("duration_minutes")}min</div>'
237
+ f'<div class="sched-why">{t.get("scheduling_reason","")}</div></div>')
 
 
 
 
 
 
 
238
  if sched.get("deferred_tasks"):
239
+ html += '<div style="margin-top:12px;color:#334155;font-size:11px;font-weight:600;text-transform:uppercase">Deferred</div>'
240
  for t in sched["deferred_tasks"]:
241
  html += f'<div style="color:#475569;font-size:12px;padding:3px 0">βœ— {t["title"]} β€” {t.get("reason","")}</div>'
 
242
  for w in sched.get("warnings", []):
243
  html += f'<div style="margin-top:8px;color:#f59e0b;font-size:12px">⚠ {w}</div>'
 
244
  return html
245
 
 
246
  def handle_toggle_today(task_id, user_id):
247
+ if task_id and user_id: toggle_task_complete(int(task_id), user_id)
 
248
  rows, s1, s2, s3 = refresh_today(user_id)
249
  return rows, s1, s2, s3, _ok("Updated")
250
 
 
251
  def handle_delete_today(task_id, user_id):
252
+ if task_id and user_id: delete_task(int(task_id), user_id)
 
253
  rows, s1, s2, s3 = refresh_today(user_id)
254
  return rows, s1, s2, s3, _ok("Deleted")
255
 
256
 
257
  # ═══════════════════════════════════════════════════════════════════════════════
258
+ # ALL TASKS TAB
259
  # ═══════════════════════════════════════════════════════════════════════════════
260
 
261
  def refresh_all_tasks(user_id, filter_area="All", only_today=False):
262
+ return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today)) if user_id else []
 
 
 
263
 
264
  def handle_toggle_all(task_id, user_id, filter_area, only_today):
265
+ if task_id and user_id: toggle_task_complete(int(task_id), user_id)
 
266
  return refresh_all_tasks(user_id, filter_area, only_today), _ok("Updated")
267
 
 
268
  def handle_delete_all(task_id, user_id, filter_area, only_today):
269
+ if task_id and user_id: delete_task(int(task_id), user_id)
 
270
  return refresh_all_tasks(user_id, filter_area, only_today), _ok("Deleted")
271
 
272
 
273
  # ═══════════════════════════════════════════════════════════════════════════════
274
+ # JOURNAL β€” CHAT-FIRST, VOICE-AWARE
275
  # ═══════════════════════════════════════════════════════════════════════════════
276
 
277
+ def _task_list_md(tasks):
278
+ if not tasks: return "*No tasks found.*"
279
+ lines = []
280
+ for t in tasks:
281
+ tid = t.get("task_id") or t.get("id")
282
+ status = "βœ…" if (t.get("is_completed") or t.get("completed")) else "⬜"
283
+ dur = f" Β· {t['actual_duration']}m actual" if t.get("actual_duration") else ""
284
+ lines.append(f"{status} **#{tid}** {t['title']} _{t.get('life_area','')} Β· {t.get('time_estimate','')}m{dur}_")
285
+ return "\n".join(lines)
286
+
287
+ def _parse_ids(text, valid_ids):
288
+ import re
289
+ return [int(n) for n in re.findall(r'\b(\d+)\b', text) if int(n) in valid_ids]
290
+
291
+ def handle_start_journal(user_id):
292
+ blank = ([], [], False, gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), [], "marking", "")
293
  if not user_id:
294
+ return ([{"role":"assistant","content":"⚠ Please sign in first."}],) + blank[1:]
 
295
  tasks = get_tasks(user_id, only_today=True)
296
  if not tasks:
297
+ msg = "You don't have any tasks logged for today. Add some tasks first, then come back to reflect!"
298
+ return ([{"role":"assistant","content":msg}],) + blank[1:]
299
+
300
+ tasks_state = [{
301
+ "task_id": t["id"], "title": t["title"], "life_area": t.get("life_area",""),
302
+ "state_of_mind": t.get("state_of_mind",""), "time_estimate": t.get("time_estimate",30),
303
+ "completed": bool(t["is_completed"]), "actual_duration": t.get("actual_duration"),
304
+ } for t in tasks]
305
+
306
+ total = len(tasks_state)
307
+ completed = sum(1 for t in tasks_state if t["completed"])
308
+ task_md = _task_list_md(tasks_state)
309
+
310
+ if completed == total and total > 0:
311
+ opener = f"πŸŽ‰ You knocked out all **{total}** tasks today β€” great work!\n\n{task_md}\n\nDid the day feel productive, or were you grinding through it?"
312
+ elif completed == 0:
313
+ opener = (f"Here's what was on your plate today:\n\n{task_md}\n\n"
314
+ f"None are marked complete yet. Tell me which ones you finished "
315
+ f"(e.g. *\"I did #1 and #3\"*), or say **skip** to go straight to reflecting.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  else:
317
+ incomplete = [t for t in tasks_state if not t["completed"]]
318
+ inc_str = ", ".join(f"#{t['task_id']}" for t in incomplete[:4])
319
+ opener = (f"Here's today's list:\n\n{task_md}\n\n"
320
+ f"You've done **{completed}/{total}** so far. Still open: {inc_str}. "
321
+ f"Tell me if you finished any more, or say **skip** to reflect.")
322
+
323
+ first_msg = {"role": "assistant", "content": opener}
324
+ hist = [{"role": "assistant", "content": opener, "focus": "task_review"}]
 
 
 
 
 
 
 
 
 
 
 
 
325
  return (
326
+ [first_msg], hist, True,
327
+ gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True),
328
+ tasks_state, "marking", "",
329
+ )
330
+
331
+
332
+ def handle_journal_message(user_id, user_text, audio_path, chat, hist, tasks_state, active, phase):
333
+ """Unified handler for text + voice journal messages."""
334
+ # Resolve voice input
335
+ voice_used = False
336
+ if audio_path is not None:
337
+ try:
338
+ transcribed = _transcribe(audio_path)
339
+ if transcribed:
340
+ user_text = transcribed
341
+ voice_used = True
342
+ except Exception as e:
343
+ err = {"role": "assistant", "content": f"⚠ Couldn't transcribe audio: {e}. Please type instead."}
344
+ return list(chat) + [err], hist, "", None, tasks_state, active, phase, ""
345
+
346
+ if not active or not user_text.strip():
347
+ return chat, hist, "", None, tasks_state, active, phase, ""
348
+
349
+ # Show voice transcription as label if voice was used
350
+ display_text = f"πŸŽ™ *{user_text}*" if voice_used else user_text
351
+ user_msg = {"role": "user", "content": display_text}
352
+ chat = list(chat) + [user_msg]
353
+ hist = list(hist) + [{"role": "user", "content": user_text}]
354
+
355
+ # ── Phase: marking ────────────────────────────────────────────────────────
356
+ if phase == "marking":
357
+ valid_ids = {t["task_id"] for t in tasks_state}
358
+ mentioned = _parse_ids(user_text, valid_ids)
359
+ updated = []
360
+ for t in tasks_state:
361
+ if t["task_id"] in mentioned and not t["completed"]:
362
+ toggle_task_complete(t["task_id"], user_id)
363
+ t["completed"] = True
364
+ updated.append(t["title"])
365
+
366
+ confirm = (f"Got it β€” marked **{', '.join(updated)}** as complete βœ…\n\n" if updated else "")
367
+ skip_words = {"done","skip","reflect","none","nothing","no more","that's it","let's go","start","go","next","continue"}
368
+ wants_skip = any(w in user_text.lower() for w in skip_words)
369
+ total = len(tasks_state)
370
+ done_count = sum(1 for t in tasks_state if t["completed"])
371
+
372
+ if wants_skip or done_count == total or (updated and done_count > 0):
373
+ # Move to reflection
374
+ ctx = _ensure_context(user_id)
375
+ opening = build_opening_question(ctx, tasks_state)
376
+ bridge = confirm + opening["question"]
377
+ ai_msg = {"role": "assistant", "content": bridge}
378
+ hist.append({"role": "assistant", "content": bridge, "focus": opening["question_focus"]})
379
+ return chat + [ai_msg], hist, "", None, tasks_state, active, "reflecting", ""
380
+ else:
381
+ remaining = [t for t in tasks_state if not t["completed"]]
382
+ rem_str = " | ".join(f"#{t['task_id']} {t['title']}" for t in remaining[:5])
383
+ follow = confirm + f"Still pending: {rem_str}\n\nAnything else you finished? Or say **skip** to start reflecting."
384
+ ai_msg = {"role": "assistant", "content": follow}
385
+ hist.append({"role": "assistant", "content": follow, "focus": "task_marking"})
386
+ return chat + [ai_msg], hist, "", None, tasks_state, active, "marking", ""
387
+
388
+ # ── Phase: reflecting ─────────────────────────────────────────────────────
389
+ if phase == "reflecting":
390
+ try:
391
+ ctx = _ensure_context(user_id)
392
+ result = get_next_journal_question(ctx, tasks_state, hist)
393
+ except Exception as e:
394
+ err_msg = {"role": "assistant", "content": f"⚠ AI error: {e}. Try sending again."}
395
+ return chat + [err_msg], hist, "", None, tasks_state, active, phase, ""
396
+
397
+ if result.get("session_complete"):
398
+ closing = "That's a solid reflection β€” you've given me plenty to work with. 🧠 Hit **Finish & Save** to lock in your insights!"
399
+ ai_msg = {"role": "assistant", "content": closing}
400
+ hist.append({"role": "assistant", "content": closing, "focus": "complete"})
401
+ return chat + [ai_msg], hist, "", None, tasks_state, active, "done", _ok("Session complete β€” click Finish & Save")
402
+
403
+ next_q = result.get("question") or "Anything else you'd like to add about today?"
404
+ ai_msg = {"role": "assistant", "content": next_q}
405
+ hist.append({"role": "assistant", "content": next_q, "focus": result.get("question_focus","")})
406
+ return chat + [ai_msg], hist, "", None, tasks_state, active, "reflecting", ""
407
+
408
+ # ── Phase: done ───────────────────────────────────────────────────────────
409
+ nudge = {"role": "assistant", "content": "Session is complete! Click **Finish & Save** to save your insights."}
410
+ return chat + [nudge], hist, "", None, tasks_state, active, "done", ""
411
 
412
 
413
  def handle_finish_journal(user_id, chat, hist, tasks_state):
414
  if not user_id or not hist:
415
+ return list(chat) + [{"role":"assistant","content":"⚠ Nothing to save yet."}], ""
416
+ try:
417
+ ctx = _ensure_context(user_id)
418
+ updated = synthesize_journal(ctx, tasks_state, hist)
419
+ save_user_context(user_id, updated)
420
+ except Exception as e:
421
+ return list(chat) + [{"role":"assistant","content":f"⚠ Save failed: {e}"}], _err("Save failed")
 
 
 
 
422
 
423
+ total = len(tasks_state)
424
+ done = sum(1 for t in tasks_state if t.get("completed"))
425
+ rate = round(done / total * 100) if total else 0
426
+ notes = updated.get("learned_patterns",{}).get("notes",[])
427
+ notes_md = "\n".join(f"β€’ {n}" for n in notes[-3:]) if notes else "β€’ Keep reflecting to build patterns"
428
+ summary = (
429
+ f"βœ… **Insights saved!** {done}/{total} tasks ({rate}%) complete today.\n\n"
430
+ f"**What I learned:**\n{notes_md}\n\n"
431
+ f"I'll use this to make tomorrow's schedule smarter. Great work today! πŸŒ™"
432
  )
433
+ return list(chat) + [{"role":"assistant","content":summary}], _ok(f"Saved! {done}/{total} tasks ({rate}%)")
434
 
435
 
436
  def handle_restart_journal():
437
+ return [], [], False, gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), [], "marking", ""
438
 
439
 
440
  # ═══════════════════════════════════════════════════════════════════════════════
441
+ # AREAS & GOALS
442
  # ═══════════════════════════════════════════════════════════════════════════════
443
 
444
  def render_areas(user_id):
445
+ if not user_id: return "", [], []
446
+ areas = get_life_areas(user_id)
447
+ chips = "".join(
448
+ f'<span class="chip" style="background:{a["color"]}22;color:{a["color"]};border:1px solid {a["color"]}44">{a["name"]}</span> '
449
+ for a in areas
450
+ )
451
+ html = f'<div style="margin:4px 0">{chips}</div>' if chips else '<p style="color:#334155;font-size:13px">No areas yet.</p>'
452
+ names = [a["name"] for a in areas]
 
 
 
 
453
  return html, names, names
454
 
 
455
  def handle_add_area(user_id, name, color):
456
  ok, msg = add_life_area(user_id, name, color)
 
457
  html, names, _ = render_areas(user_id)
458
+ color_txt = "#4ade80" if ok else "#f87171"
459
+ return f'<span style="color:{color_txt};font-size:13px">{msg}</span>', html, gr.update(choices=names, value=None), gr.update(value="")
 
 
 
460
 
461
  def handle_del_area(user_id, name):
462
+ if not name: return _err("Select an area first."), "", []
 
463
  ok, msg = delete_life_area(user_id, name)
464
  html, names, _ = render_areas(user_id)
465
+ color_txt = "#4ade80" if ok else "#f87171"
466
+ return f'<span style="color:{color_txt};font-size:13px">{msg}</span>', html, gr.update(choices=names, value=None)
 
 
 
467
 
468
  def handle_save_goals(user_id, goals_text):
469
+ if not user_id: return _err("Not logged in.")
 
470
  save_goals(user_id, goals_text)
471
  return _ok("Goals saved!")
472
 
 
473
  def load_goals_txt(user_id):
474
+ return "\n".join(get_goals(user_id)) if user_id else ""
 
 
475
 
476
 
477
  # ═══════════════════════════════════════════════════════════════════════════════
478
+ # PREFERENCES
479
  # ═══════════════════════════════════════════════════════════════════════════════
480
 
481
  def load_prefs(user_id):
482
  ctx = load_user_context(user_id) if user_id else None
483
+ if not ctx: return "07:30","23:00","Morning",10,90
484
+ p = ctx.get("preferences",{})
485
+ 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)
 
 
 
 
 
 
 
 
486
 
487
  def handle_save_prefs(user_id, wake, sleep, focus, brk, flow_max):
488
+ if not user_id: return _err("Not logged in.")
 
489
  ctx = _ensure_context(user_id)
490
+ 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)})
 
 
 
 
 
 
491
  save_user_context(user_id, ctx)
492
  return _ok("Preferences saved!")
493
 
 
494
  def render_context_html(user_id):
495
+ if not user_id: return "<p style='color:#334155'>Not logged in.</p>"
 
496
  ctx = load_user_context(user_id)
497
+ if not ctx: return "<p style='color:#334155;font-size:13px'>No AI context yet. Complete a journal session to build it.</p>"
498
+ lp = ctx.get("learned_patterns",{}); sf = ctx.get("scheduling_feedback",{}); pref = ctx.get("preferences",{})
499
+ def _row(l,v): return f'<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid #1e293b"><span style="color:#475569;font-size:12px">{l}</span><span style="color:#cbd5e1;font-size:12px">{v}</span></div>'
500
+ def _hdr(t): return f'<div style="color:#a78bfa;font-size:10px;font-weight:600;letter-spacing:1px;text-transform:uppercase;margin:14px 0 8px">{t}</div>'
501
+ html = "<div style='font-size:13px'>" + _hdr("Preferences")
502
+ html += _row("Wake",pref.get("wake_time","β€”"))+_row("Sleep",pref.get("sleep_time","β€”"))+_row("Peak Focus",pref.get("focus_peak","β€”"))
503
+ html += _hdr("Learned Patterns")
504
+ html += _row("Avg overrun",f'{lp.get("avg_task_overrun_pct",0)}%')+_row("Flow batching",str(lp.get("flow_batch_capable","Unknown")))
505
+ html += _row("Days tracked",str(sf.get("total_days_scheduled",0)))+_row("Avg completion",f'{round(sf.get("avg_completion_rate",0)*100)}%')
506
+ notes = lp.get("notes",[])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  if notes:
508
+ html += _hdr("AI Notes")
509
+ html += "".join(f'<div style="color:#94a3b8;font-size:12px;padding:3px 0">β€’ {n}</div>' for n in notes[-5:])
510
+ html += f'<div style="color:#334155;font-size:11px;margin-top:12px">v{ctx.get("version",1)} Β· {ctx.get("last_updated","β€”")[:10]}</div></div>'
 
 
 
 
 
 
 
 
 
 
 
511
  return html
512
 
513
 
514
  # ═══════════════════════════════════════════════════════════════════════════════
515
+ # UI
516
  # ═══════════════════════════════════════════════════════════════════════════════
517
 
518
  with gr.Blocks(title="🧠 Second Brain") as demo:
519
 
520
+ user_id_state = gr.State(None)
521
+ username_state = gr.State("")
522
+ # Journal
 
 
523
  journal_hist_state = gr.State([])
524
  journal_tasks_state = gr.State([])
525
  journal_active = gr.State(False)
526
+ journal_phase_state = gr.State("marking")
527
+ # Clarification β€” text panel
528
  clar_questions_state = gr.State([])
529
  original_task_state = gr.State("")
530
+ # Clarification β€” voice panel
531
+ vclar_questions_state = gr.State([])
532
+ voriginal_task_state = gr.State("")
533
 
534
+ # ── AUTH ──────────────────────────────────────────────────────────────────
 
 
535
  with gr.Column(visible=True, elem_id="auth-card") as auth_section:
536
  gr.HTML('<div id="brand-logo">🧠 Second Brain</div>')
537
  gr.HTML('<div id="brand-sub">Your intelligent productivity companion</div>')
 
538
  with gr.Tabs():
 
539
  with gr.Tab("Sign In"):
540
  login_user_in = gr.Textbox(label="Username", placeholder="your username")
541
  login_pass_in = gr.Textbox(label="Password", type="password", placeholder="β€’β€’β€’β€’β€’β€’β€’β€’")
542
  login_btn = gr.Button("Sign In", elem_classes="btn-primary")
543
  login_msg = gr.HTML("")
 
 
544
  with gr.Tab("Create Account"):
545
+ reg_user_in = gr.Textbox(label="Username", placeholder="choose a username")
546
+ reg_pass_in = gr.Textbox(label="Password (min 6 chars)", type="password", placeholder="β€’β€’β€’β€’β€’β€’β€’β€’")
547
+ reg_wake = gr.Textbox(label="Wake time", value="07:30")
548
+ reg_sleep = gr.Textbox(label="Sleep time", value="23:00")
549
+ reg_focus = gr.Dropdown(label="Peak focus", choices=["Morning","Afternoon","Evening","Night"], value="Morning")
550
+ reg_goals = gr.Textbox(label="Big-picture goals (one per line, optional)", lines=3, placeholder="Launch my startup\nGet fit\nLearn ML")
551
+ reg_btn = gr.Button("Create Account", elem_classes="btn-primary")
552
+ reg_msg = gr.HTML("")
553
+
554
+ # ── MAIN APP ──────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
555
  with gr.Column(visible=False) as app_section:
556
 
 
557
  with gr.Row(elem_id="top-header"):
558
  header_html = gr.HTML('<div id="top-header-brand">🧠 Second Brain</div><div id="top-header-user">β€”</div>')
559
  logout_btn = gr.Button("Sign Out", elem_classes="btn-secondary", scale=0)
560
 
561
+ with gr.Tabs():
 
562
 
563
+ # ── TAB 1: TODAY ─────────────────────────────────────────────────
 
 
564
  with gr.Tab("πŸ“… Today"):
 
565
  with gr.Row():
566
  stat_total = gr.HTML(_stat("β€”","Today","#a78bfa"))
567
  stat_done = gr.HTML(_stat("β€”","Done","#4ade80"))
568
  stat_remain = gr.HTML(_stat("β€”","Left","#f87171"))
 
 
569
  with gr.Row():
570
  btn_add_text = gr.Button("✏️ Add Task (Text)", elem_classes="btn-primary", scale=3)
571
  btn_add_voice = gr.Button("πŸŽ™οΈ Add Task (Voice)", elem_classes="btn-accent", scale=3)
572
  btn_plan_day = gr.Button("πŸ—“ Plan My Day", elem_classes="btn-secondary", scale=3)
573
  btn_refresh = gr.Button("↻", elem_classes="btn-secondary", scale=1)
574
 
575
+ # ── Text panel ────────────────────────────────────────────────
576
  with gr.Column(visible=False, elem_classes="panel") as text_panel:
577
  gr.HTML('<div class="sec-label">Describe Your Task</div>')
578
+ task_input = gr.Textbox(label="", placeholder="e.g. Finish the client proposal by tomorrow, it's really important", lines=2)
 
 
 
 
579
  with gr.Row():
580
+ btn_parse = gr.Button("πŸ€– Parse with AI", elem_classes="btn-primary")
581
+ btn_cancel_text = gr.Button("Cancel", elem_classes="btn-secondary")
582
 
583
  with gr.Column(visible=False) as parsed_panel:
584
  parse_status = gr.HTML("")
585
  clar_html = gr.HTML("")
 
586
  with gr.Column(visible=False) as clar_reply_row:
587
+ gr.HTML('<div class="sec-label" style="margin-top:8px">Answer the questions above</div>')
588
+ with gr.Row():
589
+ clar_text_input = gr.Textbox(label="Type your answer", lines=2, scale=4,
590
+ placeholder="e.g. It's urgent, about 2 hours, for my manager")
591
+ clar_audio_input = gr.Audio(sources=["microphone"], type="filepath",
592
+ label="πŸŽ™ Or speak", scale=1)
593
+ btn_clar_submit = gr.Button("πŸ”„ Re-classify", elem_classes="btn-accent")
594
  gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>')
595
  with gr.Row():
596
  f_title = gr.Textbox(label="Title", scale=3)
597
+ f_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1)
 
 
598
  with gr.Row():
599
+ f_urgency = gr.Dropdown(label="Urgency", choices=["Urgent","Not Urgent","Habit"], value="Not Urgent")
600
+ f_importance = gr.Dropdown(label="Importance", choices=["Move the Needle","Important","Not Important"], value="Important")
601
+ f_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy")
 
 
 
 
602
  with gr.Row():
603
+ f_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
604
+ f_date = gr.Textbox(label="Scheduled Date", value=str(date.today()), scale=1)
605
  f_is_habit = gr.Checkbox(label="♻️ This is a habit", scale=1)
606
+ f_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1)
 
 
607
  with gr.Row():
608
  btn_confirm = gr.Button("βœ“ Save Task", elem_classes="btn-success")
609
  btn_discard = gr.Button("βœ— Discard", elem_classes="btn-danger")
610
  save_msg = gr.HTML("")
611
 
612
+ # ── Voice panel ───────────────────────────────────────────────
613
  with gr.Column(visible=False, elem_classes="panel") as voice_panel:
614
+ gr.HTML('<div class="sec-label">Add Task by Voice</div>')
615
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">Record your task β€” Whisper transcribes it, then AI classifies it automatically. Same flow as text.</p>')
616
+ voice_audio_input = gr.Audio(sources=["microphone","upload"], type="filepath", label="")
 
 
617
  with gr.Row():
618
+ btn_voice_parse = gr.Button("πŸ€– Transcribe & Classify", elem_classes="btn-primary")
619
+ btn_cancel_voice = gr.Button("Cancel", elem_classes="btn-secondary")
620
+ voice_status = gr.HTML("")
621
+
622
+ with gr.Column(visible=False) as voice_parsed_panel:
623
+ voice_transcribed_txt = gr.Textbox(label="Transcribed text (editable)", lines=1)
624
+ voice_clar_html = gr.HTML("")
625
+ with gr.Column(visible=False) as voice_clar_row:
626
+ gr.HTML('<div class="sec-label" style="margin-top:8px">Answer the questions above</div>')
627
+ with gr.Row():
628
+ vclar_text = gr.Textbox(label="Type your answer", lines=2, scale=4,
629
+ placeholder="e.g. It's urgent, about 90 minutes, very high priority")
630
+ vclar_audio = gr.Audio(sources=["microphone"], type="filepath", label="πŸŽ™ Or speak", scale=1)
631
+ btn_vclar_submit = gr.Button("πŸ”„ Re-classify", elem_classes="btn-accent")
632
+ gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>')
633
+ with gr.Row():
634
+ vf_title = gr.Textbox(label="Title", scale=3)
635
+ vf_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1)
636
+ with gr.Row():
637
+ vf_urgency = gr.Dropdown(label="Urgency", choices=["Urgent","Not Urgent","Habit"], value="Not Urgent")
638
+ vf_importance = gr.Dropdown(label="Importance", choices=["Move the Needle","Important","Not Important"], value="Important")
639
+ vf_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy")
640
+ with gr.Row():
641
+ vf_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
642
+ vf_date = gr.Textbox(label="Scheduled Date", value=str(date.today()), scale=1)
643
+ vf_is_habit = gr.Checkbox(label="♻️ This is a habit", scale=1)
644
+ vf_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1)
645
+ with gr.Row():
646
+ btn_voice_confirm = gr.Button("βœ“ Save Task", elem_classes="btn-success")
647
+ btn_voice_discard = gr.Button("βœ— Discard", elem_classes="btn-danger")
648
+ voice_save_msg = gr.HTML("")
649
 
650
+ # ── Plan My Day panel ─────────────────────────────────────────
651
  with gr.Column(visible=False, elem_classes="panel") as plan_panel:
652
  gr.HTML('<div class="sec-label">Plan My Day</div>')
653
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">AI reads your tasks, context, and goals to build the optimal time-blocked schedule.</p>')
654
+ plan_prompt = gr.Textbox(label="Any fixed events or notes?", placeholder="e.g. Meeting at 10am, deep work morning, done by 7pm", lines=3)
 
 
 
 
655
  with gr.Row():
656
+ btn_gen_sched = gr.Button("✨ Generate Schedule", elem_classes="btn-primary")
657
+ btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary")
658
  schedule_html = gr.HTML("")
659
 
 
660
  gr.HTML('<div class="sec-label" style="margin-top:20px">Today\'s Tasks</div>')
661
  today_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
662
  with gr.Row():
663
+ today_done_id = gr.Number(label="Task ID β€” toggle done", precision=0, scale=2)
664
  btn_today_done = gr.Button("βœ… Mark Done", elem_classes="btn-success", scale=2)
665
  today_del_id = gr.Number(label="Task ID β€” delete", precision=0, scale=2)
666
  btn_today_del = gr.Button("πŸ—‘ Delete", elem_classes="btn-danger", scale=2)
667
  today_action_msg = gr.HTML("")
668
 
669
+ # ── TAB 2: ALL TASKS ─────────────────────────────────────────────
 
 
670
  with gr.Tab("πŸ“‹ All Tasks"):
671
  with gr.Row():
672
  all_filter = gr.Dropdown(label="Filter by Area", choices=["All"], value="All", scale=4)
673
  all_today_chk = gr.Checkbox(label="Today only", value=False, scale=1)
674
  btn_all_ref = gr.Button("↻ Refresh", elem_classes="btn-secondary", scale=1)
 
675
  all_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
676
  with gr.Row():
677
  all_done_id = gr.Number(label="Task ID β€” toggle done", precision=0, scale=2)
 
680
  btn_all_del = gr.Button("πŸ—‘ Delete", elem_classes="btn-danger", scale=2)
681
  all_action_msg = gr.HTML("")
682
 
683
+ # ── TAB 3: JOURNAL ──────────────────────────────────────────��────
 
 
684
  with gr.Tab("πŸ““ Journal"):
685
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">End-of-day reflection. Chat naturally β€” the AI marks tasks, asks questions, and updates your profile.</p>')
686
+ journal_chatbot = gr.Chatbot(
687
+ label="", height=440, type="messages",
688
+ placeholder="<p style='color:#334155;text-align:center;padding-top:80px'>Click <b>β–Ά Start Session</b> to begin</p>",
689
+ )
690
+ with gr.Row():
691
+ j_answer = gr.Textbox(label="", placeholder="Type or use the mic…", lines=1,
692
+ scale=5, interactive=False, show_label=False)
693
+ j_audio_in = gr.Audio(sources=["microphone"], type="filepath",
694
+ label="πŸŽ™", scale=1, interactive=False)
695
+ btn_j_send = gr.Button("Send β†’", elem_classes="btn-accent", scale=1, interactive=False)
696
+ with gr.Row():
697
+ btn_j_start = gr.Button("β–Ά Start Session", elem_classes="btn-primary", scale=3)
698
+ btn_j_finish = gr.Button("βœ“ Finish & Save", elem_classes="btn-success", scale=3)
699
+ btn_j_restart = gr.Button("β†Ί Reset", elem_classes="btn-secondary", scale=1)
700
+ journal_msg = gr.HTML("")
701
+
702
+ # ── TAB 4: AREAS & GOALS ─────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
703
  with gr.Tab("πŸ—‚ Areas & Goals"):
704
  gr.HTML('<div class="sec-label">Your Life Areas</div>')
705
  areas_display = gr.HTML("")
 
706
  with gr.Row():
707
  new_area_name = gr.Textbox(label="New area name", placeholder="e.g. Side Project", scale=3)
708
  new_area_color = gr.ColorPicker(label="Color", value="#6366f1", scale=1)
709
  btn_add_area = gr.Button("Add", elem_classes="btn-primary", scale=1)
710
  area_msg = gr.HTML("")
 
711
  gr.HTML('<div class="sec-label" style="margin-top:24px">Remove Area</div>')
712
  with gr.Row():
713
  del_area_dd = gr.Dropdown(label="Select area to remove", choices=[], scale=4)
714
  btn_del_area = gr.Button("Remove", elem_classes="btn-danger", scale=1)
715
  del_area_msg = gr.HTML("")
 
716
  gr.HTML('<hr>')
717
  gr.HTML('<div class="sec-label">Big-Picture Goals</div>')
718
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">The AI factors these in when scheduling and prioritising.</p>')
719
+ goals_input = gr.Textbox(label="One goal per line", lines=5, placeholder="Launch my SaaS\nRun 5K\nLearn ML")
720
+ btn_save_goals = gr.Button("Save Goals", elem_classes="btn-primary")
721
+ goals_msg = gr.HTML("")
722
+
723
+ # ── TAB 5: PREFERENCES ───────────────────────────────────────────
 
 
 
724
  with gr.Tab("βš™οΈ Preferences"):
725
  gr.HTML('<div class="sec-label">Scheduling Preferences</div>')
726
  with gr.Row():
727
  pref_wake = gr.Textbox(label="Wake time", placeholder="07:30", scale=1)
728
  pref_sleep = gr.Textbox(label="Sleep time", placeholder="23:00", scale=1)
729
+ pref_focus = gr.Dropdown(label="Peak focus", choices=["Morning","Afternoon","Evening","Night"], value="Morning", scale=1)
 
 
730
  with gr.Row():
731
  pref_break = gr.Number(label="Break between tasks (min)", value=10, minimum=0, scale=1)
732
  pref_flow_max = gr.Number(label="Max Flow block (min)", value=90, minimum=30, scale=1)
733
  btn_save_prefs = gr.Button("Save Preferences", elem_classes="btn-primary")
734
  prefs_msg = gr.HTML("")
 
735
  gr.HTML('<hr>')
736
  gr.HTML('<div class="sec-label">AI Memory</div>')
737
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">Everything the AI has learned about you. Updates after each journal session.</p>')
738
+ ctx_display = gr.HTML("")
739
+ btn_ref_ctx = gr.Button("↻ Refresh", elem_classes="btn-secondary")
740
 
741
+ # ═════════════════════════════════════════════════════════════════════════
 
742
  # EVENT WIRING
743
+ # ═════════════════════════════════════════════════════════════════════════
 
 
744
 
745
+ # ── Auth ──────────────────────────────────────────────────────────────────
746
+ def _header_html(uid, uname):
747
+ return f'<div id="top-header-brand">🧠 Second Brain</div><div id="top-header-user">πŸ‘€ {uname}</div>'
 
 
748
 
749
+ login_btn.click(handle_login, [login_user_in, login_pass_in],
 
 
750
  [user_id_state, username_state, login_msg, auth_section, app_section]
751
+ ).then(_header_html, [user_id_state, username_state], [header_html]
752
+ ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
753
+ ).then(lambda uid: render_areas(uid)[:2], [user_id_state], [areas_display, del_area_dd]
754
+ ).then(load_prefs, [user_id_state], [pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max]
755
+ ).then(load_goals_txt, [user_id_state], [goals_input])
 
 
 
 
 
 
 
 
 
 
756
 
757
+ reg_btn.click(handle_register, [reg_user_in, reg_pass_in, reg_wake, reg_sleep, reg_focus, reg_goals],
 
 
758
  [user_id_state, username_state, reg_msg, auth_section, app_section]
759
+ ).then(_header_html, [user_id_state, username_state], [header_html]
760
+ ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
761
+ ).then(lambda uid: render_areas(uid)[:2], [user_id_state], [areas_display, del_area_dd])
 
 
 
 
 
 
 
 
 
 
 
 
 
762
 
763
+ logout_btn.click(handle_logout, [user_id_state], [user_id_state, username_state, auth_section, app_section])
764
 
765
+ # ── Panels ────────────────────────────────────────────────────────────────
766
  btn_add_text.click(show_text_panel, outputs=[text_panel, voice_panel, plan_panel])
767
  btn_add_voice.click(show_voice_panel, outputs=[text_panel, voice_panel, plan_panel])
768
  btn_plan_day.click(show_plan_panel, outputs=[text_panel, voice_panel, plan_panel])
769
  btn_cancel_text.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
770
  btn_cancel_voice.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
771
  btn_cancel_plan.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
772
+ btn_refresh.click(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain])
773
 
774
+ f_is_habit.change(lambda v: gr.update(visible=v), [f_is_habit], [f_interval])
775
+ vf_is_habit.change(lambda v: gr.update(visible=v), [vf_is_habit], [vf_interval])
 
 
776
 
777
+ # ── Text task flow ─────────────────────────────────────────────────────────
778
+ _TEXT_PARSE_OUTPUTS = [parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance,
779
+ f_state, f_time, f_date, f_is_habit, f_interval, parse_status,
780
+ clar_reply_row, clar_questions_state, original_task_state]
781
+ _TEXT_CLAR_OUTPUTS = [f_title, clar_html, f_area, f_urgency, f_importance,
782
+ f_state, f_time, clar_reply_row, clar_questions_state]
783
 
784
+ btn_parse.click(handle_parse_text, [task_input, user_id_state], _TEXT_PARSE_OUTPUTS)
 
 
 
 
 
 
785
 
786
  btn_clar_submit.click(
787
  handle_clarification_reply,
788
+ [clar_text_input, original_task_state, clar_questions_state, user_id_state],
789
+ _TEXT_CLAR_OUTPUTS
790
+ ).then(lambda: ("", None), outputs=[clar_text_input, clar_audio_input])
 
 
 
 
 
 
 
 
 
 
791
 
792
+ clar_audio_input.change(
793
+ handle_clar_voice,
794
+ [clar_audio_input, original_task_state, clar_questions_state, user_id_state],
795
+ _TEXT_CLAR_OUTPUTS
796
+ ).then(lambda: None, outputs=[clar_audio_input])
797
 
798
+ btn_discard.click(lambda: gr.update(visible=False), outputs=[parsed_panel])
799
  btn_confirm.click(
800
  handle_confirm_task,
801
+ [user_id_state, f_title, f_area, f_urgency, f_importance, f_state, f_time, f_date, f_is_habit, f_interval],
 
802
  [save_msg, today_df, stat_total, stat_done, stat_remain, parsed_panel]
803
  )
804
 
805
+ # ── Voice task flow ────────────────────────────────────────────────────────
806
+ _VOICE_PARSE_OUTPUTS = [voice_parsed_panel, voice_transcribed_txt,
807
+ voice_clar_html, vf_area, vf_urgency, vf_importance,
808
+ vf_state, vf_time, vf_date, vf_is_habit, vf_interval,
809
+ voice_status, voice_clar_row,
810
+ vclar_questions_state, voriginal_task_state]
811
+ _VOICE_CLAR_OUTPUTS = [vf_title, voice_clar_html, vf_area, vf_urgency, vf_importance,
812
+ vf_state, vf_time, voice_clar_row, vclar_questions_state]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
813
 
814
+ btn_voice_parse.click(handle_voice_parse, [voice_audio_input, user_id_state], _VOICE_PARSE_OUTPUTS)
 
 
 
 
815
 
816
+ btn_vclar_submit.click(
817
+ handle_clarification_reply,
818
+ [vclar_text, voriginal_task_state, vclar_questions_state, user_id_state],
819
+ _VOICE_CLAR_OUTPUTS
820
+ ).then(lambda: ("", None), outputs=[vclar_text, vclar_audio])
821
+
822
+ vclar_audio.change(
823
+ handle_clar_voice,
824
+ [vclar_audio, voriginal_task_state, vclar_questions_state, user_id_state],
825
+ _VOICE_CLAR_OUTPUTS
826
+ ).then(lambda: None, outputs=[vclar_audio])
827
+
828
+ btn_voice_discard.click(lambda: gr.update(visible=False), outputs=[voice_parsed_panel])
829
+ btn_voice_confirm.click(
830
+ handle_confirm_task,
831
+ [user_id_state, vf_title, vf_area, vf_urgency, vf_importance, vf_state, vf_time, vf_date, vf_is_habit, vf_interval],
832
+ [voice_save_msg, today_df, stat_total, stat_done, stat_remain, voice_parsed_panel]
833
+ )
834
+
835
+ # ── Schedule ───────────────────────────────────────────────────────────────
836
+ btn_gen_sched.click(handle_generate_schedule, [user_id_state, plan_prompt], [schedule_html])
837
+ btn_today_done.click(handle_toggle_today, [today_done_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg])
838
+ btn_today_del.click(handle_delete_today, [today_del_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg])
839
+
840
+ # ── All Tasks ──────────────────────────────────────────────────────────────
841
+ btn_all_ref.click(refresh_all_tasks, [user_id_state, all_filter, all_today_chk], [all_df])
842
+ all_filter.change(refresh_all_tasks, [user_id_state, all_filter, all_today_chk], [all_df])
843
+ all_today_chk.change(refresh_all_tasks,[user_id_state, all_filter, all_today_chk], [all_df])
844
+ btn_all_done.click(handle_toggle_all, [all_done_id, user_id_state, all_filter, all_today_chk], [all_df, all_action_msg])
845
+ btn_all_del.click(handle_delete_all, [all_del_id, user_id_state, all_filter, all_today_chk], [all_df, all_action_msg])
846
+
847
+ # ── Journal ────────────────────────────────────────────────────────────────
848
+ _J_START_OUTPUTS = [journal_chatbot, journal_hist_state, journal_active,
849
+ j_answer, btn_j_send, j_audio_in, journal_tasks_state,
850
+ journal_phase_state, journal_msg]
851
+ _J_MSG_INPUTS = [user_id_state, j_answer, j_audio_in, journal_chatbot,
852
+ journal_hist_state, journal_tasks_state, journal_active, journal_phase_state]
853
+ _J_MSG_OUTPUTS = [journal_chatbot, journal_hist_state, j_answer, j_audio_in,
854
+ journal_tasks_state, journal_active, journal_phase_state, journal_msg]
855
+
856
+ btn_j_start.click(handle_start_journal, [user_id_state], _J_START_OUTPUTS)
857
+ btn_j_send.click(handle_journal_message, _J_MSG_INPUTS, _J_MSG_OUTPUTS)
858
+ j_answer.submit(handle_journal_message, _J_MSG_INPUTS, _J_MSG_OUTPUTS)
859
+ j_audio_in.change(handle_journal_message, _J_MSG_INPUTS, _J_MSG_OUTPUTS)
860
 
861
  btn_j_finish.click(
862
  handle_finish_journal,
863
  [user_id_state, journal_chatbot, journal_hist_state, journal_tasks_state],
864
  [journal_chatbot, journal_msg]
865
  )
 
866
  btn_j_restart.click(
867
  handle_restart_journal,
868
  outputs=[journal_chatbot, journal_hist_state, journal_active,
869
+ j_answer, btn_j_send, j_audio_in, journal_tasks_state,
870
+ journal_phase_state, journal_msg]
871
  )
872
 
873
+ # ── Areas & Goals ──────────────────────────────────────────────────────────
 
874
  btn_add_area.click(
875
+ handle_add_area, [user_id_state, new_area_name, new_area_color],
 
876
  [area_msg, areas_display, del_area_dd, new_area_name]
877
+ ).then(lambda uid: gr.update(choices=_area_choices(uid), value="All"), [user_id_state], [all_filter])
 
 
 
878
 
879
  btn_del_area.click(
880
+ handle_del_area, [user_id_state, del_area_dd],
 
881
  [del_area_msg, areas_display, del_area_dd]
882
+ ).then(lambda uid: gr.update(choices=_area_choices(uid), value="All"), [user_id_state], [all_filter])
 
 
 
883
 
884
+ btn_save_goals.click(handle_save_goals, [user_id_state, goals_input], [goals_msg])
 
 
 
 
 
 
885
 
886
+ # ── Preferences ────────────────────────────────────────────────────────────
887
  btn_save_prefs.click(
888
  handle_save_prefs,
889
  [user_id_state, pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max],
890
  [prefs_msg]
891
  )
892
+ btn_ref_ctx.click(render_context_html, [user_id_state], [ctx_display])
 
 
 
 
 
 
 
 
 
893
 
894
  # ── Launch ─────────────────────────────────────────────────────────────────────
 
895
  if __name__ == "__main__":
896
  demo.launch(server_name="0.0.0.0", server_port=7860, css=CSS)