eraz3r commited on
Commit
15f33db
Β·
verified Β·
1 Parent(s): 93725db

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +457 -293
app.py CHANGED
@@ -1,8 +1,9 @@
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
 
@@ -23,7 +24,6 @@ from core.styles import CSS
23
  init_db()
24
  init_groq()
25
 
26
- # Deadline added to headers
27
  TASK_HEADERS = ["ID", "Done", "Title", "Area", "Urgency", "Importance", "Mind", "Min", "Deadline", "Scheduled"]
28
  _whisper_model = None
29
 
@@ -44,18 +44,13 @@ def _fmt_tasks(tasks):
44
  for t in tasks:
45
  deadline = t.get("deadline_date") or ""
46
  if deadline:
47
- # Highlight imminent deadlines
48
  try:
49
  dl = date.fromisoformat(deadline)
50
  days_left = (dl - date.today()).days
51
- if days_left < 0:
52
- deadline = f"\u274c {deadline}"
53
- elif days_left == 0:
54
- deadline = f"\U0001f6a8 TODAY"
55
- elif days_left == 1:
56
- deadline = f"\u26a0\ufe0f tmrw"
57
- elif days_left <= 3:
58
- deadline = f"\u23f0 {deadline}"
59
  except ValueError:
60
  pass
61
  rows.append([
@@ -91,8 +86,7 @@ def _transcribe(audio_path):
91
  return " ".join(seg.text for seg in segments).strip()
92
 
93
  def _build_clar_html(clarifications):
94
- if not clarifications:
95
- return ""
96
  items = "".join(f"<li style='margin:4px 0'>{q}</li>" for q in clarifications)
97
  return (
98
  f'<div style="margin:10px 0;padding:12px;background:#0c0f1a;'
@@ -113,14 +107,12 @@ def _run_parse(task_text, user_id):
113
  area_choices = [area_val] + area_choices
114
  return result, _build_clar_html(clars), bool(clars), area_val, clars
115
 
116
- # Parse result -> UI outputs
117
- # Outputs: parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance,
118
- # f_state, f_time, f_deadline, f_is_habit, f_interval, parse_status,
119
- # clar_reply_row, clar_questions_state, original_task_state
120
- # (15 values β€” date field removed, deadline added)
121
  def _parse_result_to_outputs(result, clar_html, has_clar, area_val, clars, task_text, status=""):
122
  if not status:
123
  status = _ok("AI classified your task \u2014 answer the questions above." if has_clar else "AI classified your task \u2713")
 
 
 
124
  return (
125
  gr.update(visible=True),
126
  result.get("title", task_text),
@@ -129,7 +121,7 @@ def _parse_result_to_outputs(result, clar_html, has_clar, area_val, clars, task_
129
  result.get("importance", "Important"),
130
  result.get("state_of_mind", "Easy"),
131
  int(result.get("time_estimate") or 30),
132
- "", # deadline_date β€” left empty for user to fill if needed
133
  False, "Daily",
134
  status,
135
  gr.update(visible=has_clar),
@@ -159,8 +151,7 @@ def handle_register(username, password, wake, sleep, focus, goals_text):
159
  if not uid:
160
  return None, "", _err(msg), gr.update(visible=True), gr.update(visible=False)
161
  create_default_life_areas(uid)
162
- if goals_text.strip():
163
- save_goals(uid, goals_text)
164
  ctx = create_blank_context(uid, {"wake_time": wake or "07:30", "sleep_time": sleep or "23:00", "focus_peak": focus or "Morning"})
165
  save_user_context(uid, ctx)
166
  spawn_due_habits(uid)
@@ -171,7 +162,7 @@ def handle_logout(user_id):
171
 
172
 
173
  # =============================================================================
174
- # TODAY TAB
175
  # =============================================================================
176
 
177
  def refresh_today(user_id):
@@ -183,9 +174,7 @@ def refresh_today(user_id):
183
  return _fmt_tasks(tasks), _stat(s["total"],"Today","#a78bfa"), _stat(s["done"],"Done","#4ade80"), _stat(s["remaining"],"Left",c)
184
 
185
  def refresh_all_tasks_auto(user_id):
186
- """Refresh the All Tasks tab with all tasks regardless of date."""
187
- if not user_id:
188
- return []
189
  return _fmt_tasks(get_tasks(user_id))
190
 
191
  def show_text_panel(): return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
@@ -193,11 +182,11 @@ def show_voice_panel(): return gr.update(visible=False), gr.update(visible=True)
193
  def show_plan_panel(): return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
194
  def hide_panels(): return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
195
 
 
196
  # ── Text parse ─────────────────────────────────────────────────────────────────
197
 
198
  def handle_parse_text(task_text, user_id):
199
- if not task_text.strip():
200
- return _EMPTY_PARSE
201
  return _parse_result_to_outputs(*_run_parse(task_text, user_id), task_text)
202
 
203
  def handle_clarification_reply(user_reply, original_task, clarifications, user_id):
@@ -208,12 +197,13 @@ def handle_clarification_reply(user_reply, original_task, clarifications, user_i
208
  result, clar_html_val, still_has, area_val, remaining = _run_parse(enriched, user_id)
209
  if not still_has:
210
  clar_html_val = '<span style="color:#4ade80;font-size:13px">\u2713 Classification updated!</span>'
 
 
211
  return (
212
  result.get("title", original_task), clar_html_val, area_val,
213
  result.get("urgency","Not Urgent"), result.get("importance","Important"),
214
  result.get("state_of_mind","Easy"), int(result.get("time_estimate") or 30),
215
- "", # deadline stays empty
216
- gr.update(visible=still_has), remaining,
217
  )
218
 
219
  def handle_clar_voice(audio_path, original_task, clarifications, user_id):
@@ -225,8 +215,6 @@ def handle_clar_voice(audio_path, original_task, clarifications, user_id):
225
  except Exception:
226
  return (gr.update(),)*8 + (gr.update(visible=True), clarifications)
227
 
228
- # ── Voice parse ────────────────────────────────────────────────────────────────
229
-
230
  def handle_voice_parse(audio_path, user_id):
231
  if audio_path is None:
232
  return _EMPTY_PARSE[:11] + (_err("No audio recorded."),) + _EMPTY_PARSE[12:]
@@ -241,8 +229,6 @@ def handle_voice_parse(audio_path, user_id):
241
  status = _ok(f'Heard: "{preview}" \u2014 classified \u2713' + (" Please answer questions." if has_clar else ""))
242
  return _parse_result_to_outputs(result, clar_html, has_clar, area_val, clars, text, status)
243
 
244
- # ── Confirm task β€” saves WITHOUT a scheduled date ──────────────────────────────
245
-
246
  def handle_confirm_task(user_id, title, area, urgency, importance, state,
247
  time_est, deadline, is_habit, habit_interval):
248
  if not user_id:
@@ -254,146 +240,166 @@ def handle_confirm_task(user_id, title, area, urgency, importance, state,
254
  "state_of_mind": state, "time_estimate": int(time_est or 30),
255
  "deadline_date": deadline.strip() if deadline else "",
256
  "is_habit": is_habit, "habit_interval": habit_interval if is_habit else "",
257
- }, scheduled_date="") # <- no date assigned; scheduler does this
258
  rows, s1, s2, s3 = refresh_today(user_id)
259
  all_rows = refresh_all_tasks_auto(user_id)
260
  return _ok("Task saved! Use \u2018Plan My Tasks\u2019 to schedule it."), rows, s1, s2, s3, all_rows, gr.update(visible=False)
261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
  # =============================================================================
264
- # SMART TASK SCHEDULER (Plan My Tasks)
265
  # =============================================================================
266
 
267
  def handle_smart_schedule(user_id, prompt):
268
- """
269
- RAG-style scheduler: reads all unscheduled tasks + user context/goals/patterns,
270
- then assigns each task to the best future date based on the user's request.
271
- Updates the DB and shows results. Never touches past dates.
272
- """
273
  if not user_id:
274
- return _err("Not logged in."), ""
275
 
276
- # Get ALL unscheduled tasks (across all dates)
277
  unscheduled = get_tasks(user_id, only_unscheduled=True, include_completed=False)
278
  if not unscheduled:
279
- # Also check if user wants to reschedule today or future tasks
280
  all_tasks = get_tasks(user_id, include_completed=False)
281
  if not all_tasks:
282
- return _err("You have no tasks yet. Add some tasks first."), ""
283
- # If everything is already scheduled, offer to reschedule
284
  unscheduled = all_tasks
285
- prompt = prompt + " (Note: all tasks already have dates; feel free to reassign them)"
286
 
287
  ctx = _ensure_context(user_id)
288
  goals = get_goals(user_id)
289
  now = datetime.now()
290
 
291
  result = smart_schedule_tasks(
292
- tasks = unscheduled,
293
- user_context = ctx,
294
- user_goals = goals,
295
- scheduling_prompt= prompt or "Schedule my tasks intelligently across the next week.",
296
- current_dt = now,
297
  )
298
 
299
- # Apply assignments to DB
300
- assigned_count = 0
301
- for a in result.get("assignments", []):
302
- try:
303
- task_id = int(a["task_id"])
304
- assigned_date = a["assigned_date"]
305
- assign_task_date(task_id, user_id, assigned_date)
306
- assigned_count += 1
307
- except (ValueError, KeyError, TypeError):
308
- pass
309
-
310
- # Build result HTML
311
- COLOR = {"Flow": "#0ea5e9", "Easy": "#4ade80", "Quick": "#a78bfa", "Personal": "#f87171",
312
- "Urgent": "#f87171", "Not Urgent": "#4ade80", "Habit": "#a78bfa"}
313
 
314
- # Summary header
315
  html = (
316
- f'<div style="margin-bottom:14px;padding:12px;background:#0c0f1a;border-radius:10px;">'
317
  f'<div style="color:#a78bfa;font-size:12px;font-weight:600;margin-bottom:6px">'
318
- f'\U0001f9e0 AI Scheduling Complete \u2014 {assigned_count} task(s) assigned</div>'
319
- f'<p style="color:#94a3b8;font-size:13px;margin:0">{result.get("summary", "")}</p>'
 
320
  f'</div>'
321
  )
322
 
323
- # Assignments grouped by date
324
  by_date = {}
325
- assignments = result.get("assignments", [])
326
- # Build a quick lookup from task_id to task data
327
- task_lookup = {t["id"]: t for t in unscheduled}
328
-
329
  for a in assignments:
330
- d = a.get("assigned_date", "?")
331
- by_date.setdefault(d, []).append(a)
332
 
333
  for d in sorted(by_date.keys()):
334
  try:
335
  dl = date.fromisoformat(d)
336
- days_from_now = (dl - date.today()).days
337
- if days_from_now == 0: label = f"{d} (Today)"
338
- elif days_from_now == 1: label = f"{d} (Tomorrow)"
339
- else: label = f"{d} ({dl.strftime('%A')})"
340
  except ValueError:
341
- label = d
342
-
343
- html += f'<div style="margin:10px 0 4px;color:#60a5fa;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.6px">\U0001f4c5 {label}</div>'
344
  for a in by_date[d]:
345
- tid = a.get("task_id")
346
- task = task_lookup.get(int(tid) if tid else -1, {})
347
- sm = task.get("state_of_mind","Easy")
348
- c = COLOR.get(sm, "#6366f1")
349
- deadline_str = ""
 
 
 
350
  if task.get("deadline_date"):
351
- deadline_str = f' | \u23f3 deadline {task["deadline_date"]}'
352
  html += (
353
  f'<div class="sched-card" style="border-left-color:{c};margin-bottom:6px">'
354
- f'<div class="sched-title">{a.get("title","")}</div>'
355
- f'<div class="sched-meta">{task.get("life_area") or "β€”"} Β· {sm} Β· {task.get("time_estimate") or "?"}min{deadline_str}</div>'
 
356
  f'<div class="sched-why">\U0001f4a1 {a.get("reasoning","")}</div>'
357
  f'</div>'
358
  )
359
 
360
- # Skipped tasks
361
- skipped = result.get("skipped", [])
362
  if skipped:
363
- html += '<div style="margin-top:12px;color:#334155;font-size:11px;font-weight:600;text-transform:uppercase">Not scheduled</div>'
364
  for s in skipped:
365
- html += f'<div style="color:#475569;font-size:12px;padding:3px 0">\u2717 {s.get("title","")} \u2014 {s.get("reason","")}</div>'
366
-
367
- # All Tasks board (update after scheduling)
368
- all_rows = refresh_all_tasks_auto(user_id)
369
- return html, all_rows
370
-
371
- def handle_toggle_today(task_id, user_id):
372
- if task_id and user_id: toggle_task_complete(int(task_id), user_id)
373
- rows, s1, s2, s3 = refresh_today(user_id)
374
- return rows, s1, s2, s3, _ok("Updated")
375
-
376
- def handle_delete_today(task_id, user_id):
377
- if task_id and user_id: delete_task(int(task_id), user_id)
378
- rows, s1, s2, s3 = refresh_today(user_id)
379
- return rows, s1, s2, s3, _ok("Deleted")
380
 
 
 
381
 
382
- # =============================================================================
383
- # ALL TASKS TAB
384
- # =============================================================================
385
 
386
- def refresh_all_tasks(user_id, filter_area="All", only_today=False):
387
- if not user_id: return []
388
- return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today))
 
 
 
 
 
389
 
390
- def handle_toggle_all(task_id, user_id, filter_area, only_today):
391
- if task_id and user_id: toggle_task_complete(int(task_id), user_id)
392
- return refresh_all_tasks(user_id, filter_area, only_today), _ok("Updated")
 
 
 
 
393
 
394
- def handle_delete_all(task_id, user_id, filter_area, only_today):
395
- if task_id and user_id: delete_task(int(task_id), user_id)
396
- return refresh_all_tasks(user_id, filter_area, only_today), _ok("Deleted")
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
 
399
  # =============================================================================
@@ -414,14 +420,22 @@ def _parse_ids(text, valid_ids):
414
  import re as _re
415
  return [int(n) for n in _re.findall(r'\b(\d+)\b', text) if int(n) in valid_ids]
416
 
 
 
 
 
 
 
 
417
  def handle_start_journal(user_id):
418
- blank = ([], [], False, gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), [], "marking", "")
 
 
419
  if not user_id:
420
- return ([(None, "\u26a0 Please sign in first.")],) + blank[1:]
421
  tasks = get_tasks(user_id, only_today=True)
422
  if not tasks:
423
- msg = "No tasks logged for today. Add and schedule some tasks first, then come back to reflect!"
424
- return ([(None, msg)],) + blank[1:]
425
 
426
  tasks_state = [{
427
  "task_id": t["id"], "title": t["title"], "life_area": t.get("life_area",""),
@@ -436,20 +450,20 @@ def handle_start_journal(user_id):
436
  if completed == total and total > 0:
437
  opener = f"\U0001f389 You knocked out all **{total}** tasks today \u2014 great work!\n\n{task_md}\n\nDid the day feel productive or were you grinding through it?"
438
  elif completed == 0:
439
- opener = (f"Here\'s what was on your plate today:\n\n{task_md}\n\n"
440
  f"None are marked complete yet. Tell me which ones you finished "
441
  f"(e.g. *\"I did #1 and #3\"*), or say **skip** to start reflecting.")
442
  else:
443
  incomplete = [t for t in tasks_state if not t["completed"]]
444
  inc_str = ", ".join(f"#{t['task_id']}" for t in incomplete[:4])
445
- opener = (f"Here\'s today\'s list:\n\n{task_md}\n\n"
446
  f"Done: **{completed}/{total}**. Still open: {inc_str}. "
447
  f"Tell me if you finished any more, or say **skip** to reflect.")
448
 
449
- first_msg = (None, opener)
450
  hist = [{"role": "assistant", "content": opener, "focus": "task_review"}]
451
  return (
452
- [first_msg], hist, True,
453
  gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True),
454
  tasks_state, "marking", "",
455
  )
@@ -461,14 +475,14 @@ def handle_journal_message(user_id, user_text, audio_path, chat, hist, tasks_sta
461
  try:
462
  transcribed = _transcribe(audio_path)
463
  if transcribed:
464
- user_text = transcribed
465
  voice_used = True
466
  except Exception as e:
467
- err = (None, f"\u26a0 Couldn\'t transcribe audio: {e}. Please type instead.")
468
- return list(chat) + [err], hist, "", None, tasks_state, active, phase, ""
469
 
470
  if not active or not user_text.strip():
471
- return chat, hist, "", None, tasks_state, active, phase, ""
472
 
473
  display_text = f"\U0001f3a4 *{user_text}*" if voice_used else user_text
474
  chat = list(chat) + [(display_text, None)]
@@ -487,7 +501,7 @@ def handle_journal_message(user_id, user_text, audio_path, chat, hist, tasks_sta
487
 
488
  titles_str = ", ".join(updated)
489
  confirm = (f"Got it \u2014 marked **{titles_str}** as complete \u2705\n\n" if updated else "")
490
- skip_words = {"done","skip","reflect","none","nothing","no more","that\'s it","let\'s go","start","go","next","continue"}
491
  wants_skip = any(w in user_text.lower() for w in skip_words)
492
  done_count = sum(1 for t in tasks_state if t["completed"])
493
 
@@ -495,16 +509,16 @@ def handle_journal_message(user_id, user_text, audio_path, chat, hist, tasks_sta
495
  ctx = _ensure_context(user_id)
496
  opening = build_opening_question(ctx, tasks_state)
497
  bridge = confirm + opening["question"]
498
- ai_msg = (None, bridge)
499
  hist.append({"role": "assistant", "content": bridge, "focus": opening["question_focus"]})
500
- return list(chat) + [ai_msg], hist, "", None, tasks_state, active, "reflecting", ""
501
  else:
502
  remaining = [t for t in tasks_state if not t["completed"]]
503
  rem_str = " | ".join(f"#{t['task_id']} {t['title']}" for t in remaining[:5])
504
  follow = confirm + f"Still pending: {rem_str}\n\nAnything else done? Or say **skip** to start reflecting."
505
- ai_msg = (None, follow)
506
  hist.append({"role": "assistant", "content": follow, "focus": "task_marking"})
507
- return list(chat) + [ai_msg], hist, "", None, tasks_state, active, "marking", ""
508
 
509
  # Phase: reflecting
510
  if phase == "reflecting":
@@ -512,23 +526,23 @@ def handle_journal_message(user_id, user_text, audio_path, chat, hist, tasks_sta
512
  ctx = _ensure_context(user_id)
513
  result = get_next_journal_question(ctx, tasks_state, hist)
514
  except Exception as e:
515
- err_msg = (None, f"\u26a0 AI error: {e}. Try sending again.")
516
- return list(chat) + [err_msg], hist, "", None, tasks_state, active, phase, ""
517
 
518
  if result.get("session_complete"):
519
- closing = "That\'s a solid reflection \u2014 I have plenty to work with. \U0001f9e0 Hit **Finish & Save** to lock in your insights!"
520
- ai_msg = (None, closing)
521
  hist.append({"role": "assistant", "content": closing, "focus": "complete"})
522
- return list(chat) + [ai_msg], hist, "", None, tasks_state, active, "done", _ok("Session complete \u2014 click Finish & Save")
523
 
524
- next_q = result.get("question") or "Anything else you\'d like to reflect on?"
525
- ai_msg = (None, next_q)
526
  hist.append({"role": "assistant", "content": next_q, "focus": result.get("question_focus","")})
527
- return list(chat) + [ai_msg], hist, "", None, tasks_state, active, "reflecting", ""
528
 
529
  # Phase: done
530
- nudge = (None, "Session complete! Click **Finish & Save** to save your insights.")
531
- return list(chat) + [nudge], hist, "", None, tasks_state, active, "done", ""
532
 
533
 
534
  def handle_finish_journal(user_id, chat, hist, tasks_state):
@@ -540,55 +554,111 @@ def handle_finish_journal(user_id, chat, hist, tasks_state):
540
  save_user_context(user_id, updated)
541
  except Exception as e:
542
  return list(chat) + [(None, f"\u26a0 Save failed: {e}")], _err("Save failed")
543
-
544
- total = len(tasks_state)
545
- done = sum(1 for t in tasks_state if t.get("completed"))
546
- rate = round(done / total * 100) if total else 0
547
- notes = updated.get("learned_patterns",{}).get("notes",[])
548
  notes_md = "\n".join(f"\u2022 {n}" for n in notes[-3:]) if notes else "\u2022 Keep reflecting to build patterns"
549
- summary = (
550
- f"\u2705 **Insights saved!** {done}/{total} tasks ({rate}%) complete today.\n\n"
551
- f"**What I learned:**\n{notes_md}\n\n"
552
- f"I\'ll use this to make tomorrow\'s scheduling smarter. Great work today! \U0001f319"
553
- )
554
  return list(chat) + [(None, summary)], _ok(f"Saved! {done}/{total} tasks ({rate}%)")
555
 
556
-
557
  def handle_restart_journal():
558
  return [], [], False, gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), [], "marking", ""
559
 
560
 
561
  # =============================================================================
562
- # LIFE AREAS & GOALS
563
  # =============================================================================
564
 
565
- def render_areas(user_id):
566
- if not user_id: return "", [], []
567
  areas = get_life_areas(user_id)
568
- chips = "".join(
569
- f'<span class="chip" style="background:{a["color"]}22;color:{a["color"]};border:1px solid {a["color"]}44">{a["name"]}</span> '
570
- for a in areas
571
- )
572
- html = f'<div style="margin:4px 0">{chips}</div>' if chips else '<p style="color:#334155;font-size:13px">No areas yet.</p>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  names = [a["name"] for a in areas]
574
- return html, names, names
575
 
576
  def handle_add_area(user_id, name, color):
577
  ok, msg = add_life_area(user_id, name, color)
578
- html, names, _ = render_areas(user_id)
579
  ct = "#4ade80" if ok else "#f87171"
580
  return f'<span style="color:{ct};font-size:13px">{msg}</span>', html, gr.update(choices=names, value=None), gr.update(value="")
581
 
582
  def handle_del_area(user_id, name):
583
- if not name: return _err("Select an area first."), "", []
584
- ok, msg = delete_life_area(user_id, name)
585
- html, names, _ = render_areas(user_id)
586
- ct = "#4ade80" if ok else "#f87171"
587
- return f'<span style="color:{ct};font-size:13px">{msg}</span>', html, gr.update(choices=names, value=None)
588
 
589
  def handle_save_goals(user_id, goals_text):
590
  if not user_id: return _err("Not logged in.")
591
- save_goals(user_id, goals_text)
592
  return _ok("Goals saved!")
593
 
594
  def load_goals_txt(user_id):
@@ -596,7 +666,7 @@ def load_goals_txt(user_id):
596
 
597
 
598
  # =============================================================================
599
- # PREFERENCES
600
  # =============================================================================
601
 
602
  def load_prefs(user_id):
@@ -608,7 +678,8 @@ def load_prefs(user_id):
608
  def handle_save_prefs(user_id, wake, sleep, focus, brk, flow_max):
609
  if not user_id: return _err("Not logged in.")
610
  ctx = _ensure_context(user_id)
611
- 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)})
 
612
  save_user_context(user_id, ctx)
613
  return _ok("Preferences saved!")
614
 
@@ -616,21 +687,95 @@ def render_context_html(user_id):
616
  if not user_id: return "<p style='color:#334155'>Not logged in.</p>"
617
  ctx = load_user_context(user_id)
618
  if not ctx: return "<p style='color:#334155;font-size:13px'>No AI context yet. Complete a journal session to build it.</p>"
619
- lp = ctx.get("learned_patterns",{}); sf = ctx.get("scheduling_feedback",{}); pref = ctx.get("preferences",{})
620
- 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>'
621
- 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>'
622
- html = "<div style='font-size:13px'>" + _hdr("Preferences")
623
- html += _row("Wake",pref.get("wake_time","\u2014"))+_row("Sleep",pref.get("sleep_time","\u2014"))+_row("Peak Focus",pref.get("focus_peak","\u2014"))
624
- html += _hdr("Learned Patterns")
625
- html += _row("Avg overrun",f'{lp.get("avg_task_overrun_pct",0)}%')+_row("Flow batching",str(lp.get("flow_batch_capable","Unknown")))
626
- html += _row("Days tracked",str(sf.get("total_days_scheduled",0)))+_row("Avg completion",f'{round(sf.get("avg_completion_rate",0)*100)}%')
627
- notes = lp.get("notes",[])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
628
  if notes:
629
- html += _hdr("AI Notes")
630
- html += "".join(f'<div style="color:#94a3b8;font-size:12px;padding:3px 0">\u2022 {n}</div>' for n in notes[-5:])
631
- _ver = ctx.get("version", 1)
632
- _upd = (ctx.get("last_updated") or "β€”")[:10]
633
- html += f'<div style="color:#334155;font-size:11px;margin-top:12px">v{_ver} Β· {_upd}</div></div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
  return html
635
 
636
 
@@ -653,8 +798,13 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
653
  # Clarification β€” voice panel
654
  vclar_questions_state = gr.State([])
655
  voriginal_task_state = gr.State("")
 
 
 
 
 
656
 
657
- # AUTH
658
  with gr.Column(visible=True, elem_id="auth-card") as auth_section:
659
  gr.HTML('<div id="brand-logo">\U0001f9e0 Second Brain</div>')
660
  gr.HTML('<div id="brand-sub">Your intelligent productivity companion</div>')
@@ -674,46 +824,44 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
674
  reg_btn = gr.Button("Create Account", elem_classes="btn-primary")
675
  reg_msg = gr.HTML("")
676
 
677
- # MAIN APP
678
  with gr.Column(visible=False) as app_section:
679
-
680
  with gr.Row(elem_id="top-header"):
681
  header_html = gr.HTML('<div id="top-header-brand">\U0001f9e0 Second Brain</div><div id="top-header-user">\u2014</div>')
682
  logout_btn = gr.Button("Sign Out", elem_classes="btn-secondary", scale=0)
683
 
684
  with gr.Tabs():
685
 
686
- # TAB 1: TODAY
687
  with gr.Tab("\U0001f4c5 Today"):
688
  with gr.Row():
689
  stat_total = gr.HTML(_stat("\u2014","Today","#a78bfa"))
690
  stat_done = gr.HTML(_stat("\u2014","Done","#4ade80"))
691
  stat_remain = gr.HTML(_stat("\u2014","Left","#f87171"))
692
  with gr.Row():
693
- btn_add_text = gr.Button("\u270f\ufe0f Add Task (Text)", elem_classes="btn-primary", scale=3)
694
  btn_add_voice = gr.Button("\U0001f399\ufe0f Add Task (Voice)", elem_classes="btn-accent", scale=3)
695
- btn_plan_day = gr.Button("\U0001f9e0 Plan My Tasks", elem_classes="btn-secondary", scale=3)
696
- btn_refresh = gr.Button("\u21bb", elem_classes="btn-secondary", scale=1)
697
 
698
- # Text panel
699
  with gr.Column(visible=False, elem_classes="panel") as text_panel:
700
  gr.HTML('<div class="sec-label">Describe Your Task</div>')
701
- task_input = gr.Textbox(label="", placeholder="e.g. Finish the client proposal, it\'s really important", lines=2)
702
  with gr.Row():
703
  btn_parse = gr.Button("\U0001f916 Parse with AI", elem_classes="btn-primary")
704
  btn_cancel_text = gr.Button("Cancel", elem_classes="btn-secondary")
705
-
706
  with gr.Column(visible=False) as parsed_panel:
707
  parse_status = gr.HTML("")
708
  clar_html = gr.HTML("")
709
  with gr.Column(visible=False) as clar_reply_row:
710
  gr.HTML('<div class="sec-label" style="margin-top:8px">Answer the questions above</div>')
711
  with gr.Row():
712
- clar_text_input = gr.Textbox(label="Type your answer", lines=2, scale=4, placeholder="e.g. It\'s urgent, about 2 hours, for my manager")
713
- clar_audio_input = gr.Audio(sources=["microphone"], type="filepath", label="\U0001f3a4 Or speak", scale=1)
714
  btn_clar_submit = gr.Button("\U0001f504 Re-classify", elem_classes="btn-accent")
715
  gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>')
716
- gr.HTML('<p style="color:#475569;font-size:12px;margin:0 0 10px">Date will be assigned by the AI scheduler \u2014 just save and use \u201cPlan My Tasks\u201d.</p>')
717
  with gr.Row():
718
  f_title = gr.Textbox(label="Title", scale=3)
719
  f_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1)
@@ -723,7 +871,7 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
723
  f_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy")
724
  with gr.Row():
725
  f_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
726
- f_deadline = gr.Textbox(label="\U0001f3af Deadline (YYYY-MM-DD, optional)", placeholder="2026-03-01", scale=2)
727
  f_is_habit = gr.Checkbox(label="\u267b\ufe0f Habit", scale=1)
728
  f_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1)
729
  with gr.Row():
@@ -731,16 +879,15 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
731
  btn_discard = gr.Button("\u2717 Discard", elem_classes="btn-danger")
732
  save_msg = gr.HTML("")
733
 
734
- # Voice panel
735
  with gr.Column(visible=False, elem_classes="panel") as voice_panel:
736
  gr.HTML('<div class="sec-label">Add Task by Voice</div>')
737
- gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">Record your task \u2014 Whisper transcribes it, AI classifies it. Same flow as text.</p>')
738
  voice_audio_input = gr.Audio(sources=["microphone","upload"], type="filepath", label="")
739
  with gr.Row():
740
  btn_voice_parse = gr.Button("\U0001f916 Transcribe & Classify", elem_classes="btn-primary")
741
  btn_cancel_voice = gr.Button("Cancel", elem_classes="btn-secondary")
742
  voice_status = gr.HTML("")
743
-
744
  with gr.Column(visible=False) as voice_parsed_panel:
745
  voice_transcribed_txt = gr.Textbox(label="Transcribed text (editable)", lines=1)
746
  voice_clar_html = gr.HTML("")
@@ -748,10 +895,9 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
748
  gr.HTML('<div class="sec-label" style="margin-top:8px">Answer the questions above</div>')
749
  with gr.Row():
750
  vclar_text = gr.Textbox(label="Type your answer", lines=2, scale=4)
751
- vclar_audio = gr.Audio(sources=["microphone"], type="filepath", label="\U0001f3a4 Or speak", scale=1)
752
  btn_vclar_submit = gr.Button("\U0001f504 Re-classify", elem_classes="btn-accent")
753
  gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>')
754
- gr.HTML('<p style="color:#475569;font-size:12px;margin:0 0 10px">Date assigned by AI scheduler after saving.</p>')
755
  with gr.Row():
756
  vf_title = gr.Textbox(label="Title", scale=3)
757
  vf_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1)
@@ -761,7 +907,7 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
761
  vf_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy")
762
  with gr.Row():
763
  vf_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
764
- vf_deadline = gr.Textbox(label="\U0001f3af Deadline (optional)", placeholder="2026-03-01", scale=2)
765
  vf_is_habit = gr.Checkbox(label="\u267b\ufe0f Habit", scale=1)
766
  vf_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1)
767
  with gr.Row():
@@ -769,49 +915,58 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
769
  btn_voice_discard = gr.Button("\u2717 Discard", elem_classes="btn-danger")
770
  voice_save_msg = gr.HTML("")
771
 
772
- # Plan My Tasks panel
773
  with gr.Column(visible=False, elem_classes="panel") as plan_panel:
774
  gr.HTML('<div class="sec-label">Plan My Tasks</div>')
775
- gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">'
776
- 'The AI reads your unscheduled tasks, your learned patterns, goals, and current time \u2014 '
777
- 'then assigns each task to the best future date. It never places tasks in the past.'
778
- '</p>')
779
  plan_prompt = gr.Textbox(
780
  label="Tell the AI what you want",
781
- placeholder="e.g. \'Clear today, schedule everything from tomorrow\' or \'Focus on Work tasks this week\' or \'I have a free morning Thursday\'",
782
- lines=3
783
  )
784
  with gr.Row():
785
- btn_gen_sched = gr.Button("\U0001f9e0 Run AI Scheduler", elem_classes="btn-primary")
786
  btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary")
787
  schedule_html = gr.HTML("")
788
 
 
 
 
 
 
 
 
 
 
 
 
789
  gr.HTML('<div class="sec-label" style="margin-top:20px">Today\'s Tasks</div>')
790
  today_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
 
791
  with gr.Row():
792
- today_done_id = gr.Number(label="Task ID \u2014 toggle done", precision=0, scale=2)
793
  btn_today_done = gr.Button("\u2705 Mark Done", elem_classes="btn-success", scale=2)
794
- today_del_id = gr.Number(label="Task ID \u2014 delete", precision=0, scale=2)
795
- btn_today_del = gr.Button("\U0001f5d1 Delete", elem_classes="btn-danger", scale=2)
796
  today_action_msg = gr.HTML("")
797
 
798
- # TAB 2: ALL TASKS
799
  with gr.Tab("\U0001f4cb All Tasks"):
800
- gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">All tasks including unscheduled ones. Filter and manage here.</p>')
801
  with gr.Row():
802
  all_filter = gr.Dropdown(label="Filter by Area", choices=["All"], value="All", scale=3)
803
- all_today_chk = gr.Checkbox(label="Today only", value=False, scale=1)
804
  all_unsched = gr.Checkbox(label="Unscheduled only", value=False, scale=1)
805
  btn_all_ref = gr.Button("\u21bb Refresh", elem_classes="btn-secondary", scale=1)
806
  all_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
807
  with gr.Row():
808
- all_done_id = gr.Number(label="Task ID \u2014 toggle done", precision=0, scale=2)
809
  btn_all_done = gr.Button("\u2705 Done", elem_classes="btn-success", scale=2)
810
- all_del_id = gr.Number(label="Task ID \u2014 delete", precision=0, scale=2)
811
  btn_all_del = gr.Button("\U0001f5d1 Delete", elem_classes="btn-danger", scale=2)
812
  all_action_msg = gr.HTML("")
813
 
814
- # TAB 3: JOURNAL
815
  with gr.Tab("\U0001f4d3 Journal"):
816
  gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">End-of-day reflection. Chat naturally \u2014 AI marks tasks, asks questions, updates your profile.</p>')
817
  journal_chatbot = gr.Chatbot(label="", height=440)
@@ -825,28 +980,29 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
825
  btn_j_restart = gr.Button("\u21ba Reset", elem_classes="btn-secondary", scale=1)
826
  journal_msg = gr.HTML("")
827
 
828
- # TAB 4: AREAS & GOALS
829
  with gr.Tab("\U0001f5c2 Areas & Goals"):
830
  gr.HTML('<div class="sec-label">Your Life Areas</div>')
831
  areas_display = gr.HTML("")
 
832
  with gr.Row():
833
- new_area_name = gr.Textbox(label="New area name", placeholder="e.g. Side Project", scale=3)
 
 
 
 
 
834
  new_area_color = gr.ColorPicker(label="Color", value="#6366f1", scale=1)
835
- btn_add_area = gr.Button("Add", elem_classes="btn-primary", scale=1)
836
  area_msg = gr.HTML("")
837
- gr.HTML('<div class="sec-label" style="margin-top:24px">Remove Area</div>')
838
- with gr.Row():
839
- del_area_dd = gr.Dropdown(label="Select area to remove", choices=[], scale=4)
840
- btn_del_area = gr.Button("Remove", elem_classes="btn-danger", scale=1)
841
- del_area_msg = gr.HTML("")
842
  gr.HTML('<hr>')
843
  gr.HTML('<div class="sec-label">Big-Picture Goals</div>')
844
- gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">The AI factors these in when scheduling and prioritising.</p>')
845
  goals_input = gr.Textbox(label="One goal per line", lines=5, placeholder="Launch my SaaS\nRun 5K\nLearn ML")
846
  btn_save_goals = gr.Button("Save Goals", elem_classes="btn-primary")
847
  goals_msg = gr.HTML("")
848
 
849
- # TAB 5: PREFERENCES
850
  with gr.Tab("\u2699\ufe0f Preferences"):
851
  gr.HTML('<div class="sec-label">Scheduling Preferences</div>')
852
  with gr.Row():
@@ -860,7 +1016,7 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
860
  prefs_msg = gr.HTML("")
861
  gr.HTML('<hr>')
862
  gr.HTML('<div class="sec-label">AI Memory</div>')
863
- gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">What the AI has learned about you. Updates after each journal session.</p>')
864
  ctx_display = gr.HTML("")
865
  btn_ref_ctx = gr.Button("\u21bb Refresh", elem_classes="btn-secondary")
866
 
@@ -878,7 +1034,7 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
878
  ).then(_header_html, [user_id_state, username_state], [header_html]
879
  ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
880
  ).then(refresh_all_tasks_auto, [user_id_state], [all_df]
881
- ).then(lambda uid: render_areas(uid)[:2], [user_id_state], [areas_display, del_area_dd]
882
  ).then(load_prefs, [user_id_state], [pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max]
883
  ).then(load_goals_txt, [user_id_state], [goals_input])
884
 
@@ -887,7 +1043,7 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
887
  ).then(_header_html, [user_id_state, username_state], [header_html]
888
  ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
889
  ).then(refresh_all_tasks_auto, [user_id_state], [all_df]
890
- ).then(lambda uid: render_areas(uid)[:2], [user_id_state], [areas_display, del_area_dd])
891
 
892
  logout_btn.click(handle_logout, [user_id_state], [user_id_state, username_state, auth_section, app_section])
893
 
@@ -903,63 +1059,50 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
903
  f_is_habit.change(lambda v: gr.update(visible=v), [f_is_habit], [f_interval])
904
  vf_is_habit.change(lambda v: gr.update(visible=v), [vf_is_habit], [vf_interval])
905
 
906
- # Text task flow
907
- # Outputs: parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance,
908
- # f_state, f_time, f_deadline, f_is_habit, f_interval, parse_status,
909
- # clar_reply_row, clar_questions_state, original_task_state
910
  _TEXT_PARSE_OUT = [parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance,
911
  f_state, f_time, f_deadline, f_is_habit, f_interval, parse_status,
912
  clar_reply_row, clar_questions_state, original_task_state]
913
- # clar reply outputs: f_title, clar_html, f_area, f_urgency, f_importance,
914
- # f_state, f_time, f_deadline, clar_reply_row, clar_questions_state
915
  _TEXT_CLAR_OUT = [f_title, clar_html, f_area, f_urgency, f_importance,
916
  f_state, f_time, f_deadline, clar_reply_row, clar_questions_state]
917
 
918
  btn_parse.click(handle_parse_text, [task_input, user_id_state], _TEXT_PARSE_OUT)
919
-
920
  btn_clar_submit.click(
921
  handle_clarification_reply,
922
  [clar_text_input, original_task_state, clar_questions_state, user_id_state],
923
  _TEXT_CLAR_OUT
924
  ).then(lambda: ("", None), outputs=[clar_text_input, clar_audio_input])
925
-
926
  clar_audio_input.change(
927
  handle_clar_voice,
928
  [clar_audio_input, original_task_state, clar_questions_state, user_id_state],
929
  _TEXT_CLAR_OUT
930
  ).then(lambda: None, outputs=[clar_audio_input])
931
-
932
  btn_discard.click(lambda: gr.update(visible=False), outputs=[parsed_panel])
933
- # Confirm saves task + refreshes BOTH today and all_tasks
934
  btn_confirm.click(
935
  handle_confirm_task,
936
  [user_id_state, f_title, f_area, f_urgency, f_importance, f_state, f_time, f_deadline, f_is_habit, f_interval],
937
  [save_msg, today_df, stat_total, stat_done, stat_remain, all_df, parsed_panel]
938
  )
939
 
940
- # Voice task flow
941
  _VOICE_PARSE_OUT = [voice_parsed_panel, voice_transcribed_txt,
942
  voice_clar_html, vf_area, vf_urgency, vf_importance,
943
  vf_state, vf_time, vf_deadline, vf_is_habit, vf_interval,
944
- voice_status, voice_clar_row,
945
- vclar_questions_state, voriginal_task_state]
946
  _VOICE_CLAR_OUT = [vf_title, voice_clar_html, vf_area, vf_urgency, vf_importance,
947
  vf_state, vf_time, vf_deadline, voice_clar_row, vclar_questions_state]
948
 
949
  btn_voice_parse.click(handle_voice_parse, [voice_audio_input, user_id_state], _VOICE_PARSE_OUT)
950
-
951
  btn_vclar_submit.click(
952
  handle_clarification_reply,
953
  [vclar_text, voriginal_task_state, vclar_questions_state, user_id_state],
954
  _VOICE_CLAR_OUT
955
  ).then(lambda: ("", None), outputs=[vclar_text, vclar_audio])
956
-
957
  vclar_audio.change(
958
  handle_clar_voice,
959
  [vclar_audio, voriginal_task_state, vclar_questions_state, user_id_state],
960
  _VOICE_CLAR_OUT
961
  ).then(lambda: None, outputs=[vclar_audio])
962
-
963
  btn_voice_discard.click(lambda: gr.update(visible=False), outputs=[voice_parsed_panel])
964
  btn_voice_confirm.click(
965
  handle_confirm_task,
@@ -967,61 +1110,81 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
967
  [voice_save_msg, today_df, stat_total, stat_done, stat_remain, all_df, voice_parsed_panel]
968
  )
969
 
970
- # Smart scheduler β€” updates both schedule_html and all_df
 
 
 
971
  btn_gen_sched.click(
972
- handle_smart_schedule, [user_id_state, plan_prompt],
973
- [schedule_html, all_df]
 
 
 
 
 
 
 
 
974
  ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain])
975
 
976
- btn_today_done.click(handle_toggle_today, [today_done_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg])
977
- btn_today_del.click(handle_delete_today, [today_del_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978
 
979
- # All Tasks
980
- def refresh_all_tasks_filtered(user_id, filter_area, only_today, only_unsched):
981
  if not user_id: return []
982
  return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today, only_unscheduled=only_unsched))
983
 
984
- btn_all_ref.click(refresh_all_tasks_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
985
- all_filter.change(refresh_all_tasks_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
986
- all_today_chk.change(refresh_all_tasks_filtered,[user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
987
- all_unsched.change(refresh_all_tasks_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
988
-
989
- def handle_toggle_all_f(task_id, user_id, filter_area, only_today, only_unsched):
990
- if task_id and user_id: toggle_task_complete(int(task_id), user_id)
991
- return refresh_all_tasks_filtered(user_id, filter_area, only_today, only_unsched), _ok("Updated")
992
- def handle_delete_all_f(task_id, user_id, filter_area, only_today, only_unsched):
993
- if task_id and user_id: delete_task(int(task_id), user_id)
994
- return refresh_all_tasks_filtered(user_id, filter_area, only_today, only_unsched), _ok("Deleted")
995
 
996
- btn_all_done.click(handle_toggle_all_f, [all_done_id, user_id_state, all_filter, all_today_chk, all_unsched], [all_df, all_action_msg])
997
- btn_all_del.click(handle_delete_all_f, [all_del_id, user_id_state, all_filter, all_today_chk, all_unsched], [all_df, all_action_msg])
998
 
999
- # Journal
1000
  _J_START_OUT = [journal_chatbot, journal_hist_state, journal_active,
1001
  j_answer, btn_j_send, j_audio_in, journal_tasks_state,
1002
  journal_phase_state, journal_msg]
1003
- _J_MSG_IN = [user_id_state, j_answer, j_audio_in, journal_chatbot,
1004
- journal_hist_state, journal_tasks_state, journal_active, journal_phase_state]
1005
- _J_MSG_OUT = [journal_chatbot, journal_hist_state, j_answer, j_audio_in,
1006
- journal_tasks_state, journal_active, journal_phase_state, journal_msg]
1007
 
1008
  btn_j_start.click(handle_start_journal, [user_id_state], _J_START_OUT)
1009
- btn_j_send.click(handle_journal_message, _J_MSG_IN, _J_MSG_OUT)
1010
- j_answer.submit(handle_journal_message, _J_MSG_IN, _J_MSG_OUT)
1011
- j_audio_in.change(handle_journal_message, _J_MSG_IN, _J_MSG_OUT)
1012
 
1013
  btn_j_finish.click(
1014
  handle_finish_journal,
1015
  [user_id_state, journal_chatbot, journal_hist_state, journal_tasks_state],
1016
  [journal_chatbot, journal_msg]
1017
  )
1018
- btn_j_restart.click(handle_restart_journal, outputs=[
1019
- journal_chatbot, journal_hist_state, journal_active,
1020
- j_answer, btn_j_send, j_audio_in, journal_tasks_state,
1021
- journal_phase_state, journal_msg
1022
- ])
1023
 
1024
- # Areas & Goals
1025
  btn_add_area.click(
1026
  handle_add_area, [user_id_state, new_area_name, new_area_color],
1027
  [area_msg, areas_display, del_area_dd, new_area_name]
@@ -1029,12 +1192,13 @@ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
1029
 
1030
  btn_del_area.click(
1031
  handle_del_area, [user_id_state, del_area_dd],
1032
- [del_area_msg, areas_display, del_area_dd]
 
1033
  ).then(lambda uid: gr.update(choices=_area_choices(uid), value="All"), [user_id_state], [all_filter])
1034
 
1035
  btn_save_goals.click(handle_save_goals, [user_id_state, goals_input], [goals_msg])
1036
 
1037
- # Preferences
1038
  btn_save_prefs.click(
1039
  handle_save_prefs,
1040
  [user_id_state, pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max],
 
1
  """
2
+ app.py β€” Second Brain Gradio Application
3
  """
4
 
5
  import os
6
+ import json
7
  import gradio as gr
8
  from datetime import date, datetime
9
 
 
24
  init_db()
25
  init_groq()
26
 
 
27
  TASK_HEADERS = ["ID", "Done", "Title", "Area", "Urgency", "Importance", "Mind", "Min", "Deadline", "Scheduled"]
28
  _whisper_model = None
29
 
 
44
  for t in tasks:
45
  deadline = t.get("deadline_date") or ""
46
  if deadline:
 
47
  try:
48
  dl = date.fromisoformat(deadline)
49
  days_left = (dl - date.today()).days
50
+ if days_left < 0: deadline = f"\u274c {deadline}"
51
+ elif days_left == 0: deadline = "\U0001f6a8 TODAY"
52
+ elif days_left == 1: deadline = "\u26a0\ufe0f tmrw"
53
+ elif days_left <= 3: deadline = f"\u23f0 {deadline}"
 
 
 
 
54
  except ValueError:
55
  pass
56
  rows.append([
 
86
  return " ".join(seg.text for seg in segments).strip()
87
 
88
  def _build_clar_html(clarifications):
89
+ if not clarifications: return ""
 
90
  items = "".join(f"<li style='margin:4px 0'>{q}</li>" for q in clarifications)
91
  return (
92
  f'<div style="margin:10px 0;padding:12px;background:#0c0f1a;'
 
107
  area_choices = [area_val] + area_choices
108
  return result, _build_clar_html(clars), bool(clars), area_val, clars
109
 
 
 
 
 
 
110
  def _parse_result_to_outputs(result, clar_html, has_clar, area_val, clars, task_text, status=""):
111
  if not status:
112
  status = _ok("AI classified your task \u2014 answer the questions above." if has_clar else "AI classified your task \u2713")
113
+ # Extract deadline from AI result
114
+ deadline = result.get("deadline_date") or ""
115
+ if deadline and deadline.lower() in ("null","none","n/a",""): deadline = ""
116
  return (
117
  gr.update(visible=True),
118
  result.get("title", task_text),
 
121
  result.get("importance", "Important"),
122
  result.get("state_of_mind", "Easy"),
123
  int(result.get("time_estimate") or 30),
124
+ deadline,
125
  False, "Daily",
126
  status,
127
  gr.update(visible=has_clar),
 
151
  if not uid:
152
  return None, "", _err(msg), gr.update(visible=True), gr.update(visible=False)
153
  create_default_life_areas(uid)
154
+ if goals_text.strip(): save_goals(uid, goals_text)
 
155
  ctx = create_blank_context(uid, {"wake_time": wake or "07:30", "sleep_time": sleep or "23:00", "focus_peak": focus or "Morning"})
156
  save_user_context(uid, ctx)
157
  spawn_due_habits(uid)
 
162
 
163
 
164
  # =============================================================================
165
+ # TODAY / ALL TASKS
166
  # =============================================================================
167
 
168
  def refresh_today(user_id):
 
174
  return _fmt_tasks(tasks), _stat(s["total"],"Today","#a78bfa"), _stat(s["done"],"Done","#4ade80"), _stat(s["remaining"],"Left",c)
175
 
176
  def refresh_all_tasks_auto(user_id):
177
+ if not user_id: return []
 
 
178
  return _fmt_tasks(get_tasks(user_id))
179
 
180
  def show_text_panel(): return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
 
182
  def show_plan_panel(): return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
183
  def hide_panels(): return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
184
 
185
+
186
  # ── Text parse ─────────────────────────────────────────────────────────────────
187
 
188
  def handle_parse_text(task_text, user_id):
189
+ if not task_text.strip(): return _EMPTY_PARSE
 
190
  return _parse_result_to_outputs(*_run_parse(task_text, user_id), task_text)
191
 
192
  def handle_clarification_reply(user_reply, original_task, clarifications, user_id):
 
197
  result, clar_html_val, still_has, area_val, remaining = _run_parse(enriched, user_id)
198
  if not still_has:
199
  clar_html_val = '<span style="color:#4ade80;font-size:13px">\u2713 Classification updated!</span>'
200
+ deadline = result.get("deadline_date") or ""
201
+ if deadline and deadline.lower() in ("null","none","n/a",""): deadline = ""
202
  return (
203
  result.get("title", original_task), clar_html_val, area_val,
204
  result.get("urgency","Not Urgent"), result.get("importance","Important"),
205
  result.get("state_of_mind","Easy"), int(result.get("time_estimate") or 30),
206
+ deadline, gr.update(visible=still_has), remaining,
 
207
  )
208
 
209
  def handle_clar_voice(audio_path, original_task, clarifications, user_id):
 
215
  except Exception:
216
  return (gr.update(),)*8 + (gr.update(visible=True), clarifications)
217
 
 
 
218
  def handle_voice_parse(audio_path, user_id):
219
  if audio_path is None:
220
  return _EMPTY_PARSE[:11] + (_err("No audio recorded."),) + _EMPTY_PARSE[12:]
 
229
  status = _ok(f'Heard: "{preview}" \u2014 classified \u2713' + (" Please answer questions." if has_clar else ""))
230
  return _parse_result_to_outputs(result, clar_html, has_clar, area_val, clars, text, status)
231
 
 
 
232
  def handle_confirm_task(user_id, title, area, urgency, importance, state,
233
  time_est, deadline, is_habit, habit_interval):
234
  if not user_id:
 
240
  "state_of_mind": state, "time_estimate": int(time_est or 30),
241
  "deadline_date": deadline.strip() if deadline else "",
242
  "is_habit": is_habit, "habit_interval": habit_interval if is_habit else "",
243
+ }, scheduled_date="")
244
  rows, s1, s2, s3 = refresh_today(user_id)
245
  all_rows = refresh_all_tasks_auto(user_id)
246
  return _ok("Task saved! Use \u2018Plan My Tasks\u2019 to schedule it."), rows, s1, s2, s3, all_rows, gr.update(visible=False)
247
 
248
+ # ── Quick toggle/delete from table (uses selected row ID) ─────────────────────
249
+
250
+ def handle_action_today(evt: gr.SelectData, tasks_data, user_id, action):
251
+ """Called when a row is clicked in today_df then action button pressed."""
252
+ if not user_id or tasks_data is None or len(tasks_data) == 0: return refresh_today(user_id) + (_ok(""),)
253
+ # evt.index is [row, col]; grab the ID from col 0
254
+ try:
255
+ row_idx = evt.index[0] if hasattr(evt, 'index') else 0
256
+ task_id = int(tasks_data[row_idx][0])
257
+ except Exception:
258
+ return refresh_today(user_id) + (_ok("Select a row first"),)
259
+ if action == "done": toggle_task_complete(task_id, user_id)
260
+ else: delete_task(task_id, user_id)
261
+ rows, s1, s2, s3 = refresh_today(user_id)
262
+ return rows, s1, s2, s3, _ok("Done" if action=="done" else "Deleted")
263
+
264
+ def handle_toggle_today_id(task_id, user_id):
265
+ if task_id and user_id: toggle_task_complete(int(task_id), user_id)
266
+ rows, s1, s2, s3 = refresh_today(user_id)
267
+ return rows, s1, s2, s3, _ok("Updated")
268
+
269
+ def handle_delete_today_id(task_id, user_id):
270
+ if task_id and user_id: delete_task(int(task_id), user_id)
271
+ rows, s1, s2, s3 = refresh_today(user_id)
272
+ return rows, s1, s2, s3, _ok("Deleted")
273
+
274
+ def handle_toggle_all_id(task_id, user_id):
275
+ if task_id and user_id: toggle_task_complete(int(task_id), user_id)
276
+ return _fmt_tasks(get_tasks(user_id)), _ok("Updated")
277
+
278
+ def handle_delete_all_id(task_id, user_id):
279
+ if task_id and user_id: delete_task(int(task_id), user_id)
280
+ return _fmt_tasks(get_tasks(user_id)), _ok("Deleted")
281
+
282
 
283
  # =============================================================================
284
+ # SMART TASK SCHEDULER β€” with confirm flow
285
  # =============================================================================
286
 
287
  def handle_smart_schedule(user_id, prompt):
288
+ """Step 1: Generate proposals and show them for user review. Don't apply yet."""
 
 
 
 
289
  if not user_id:
290
+ return _err("Not logged in."), "", gr.update(visible=False), gr.update(visible=False), []
291
 
 
292
  unscheduled = get_tasks(user_id, only_unscheduled=True, include_completed=False)
293
  if not unscheduled:
 
294
  all_tasks = get_tasks(user_id, include_completed=False)
295
  if not all_tasks:
296
+ return _err("No tasks yet. Add some tasks first."), "", gr.update(visible=False), gr.update(visible=False), []
 
297
  unscheduled = all_tasks
298
+ prompt = (prompt or "") + " (All tasks already have dates; feel free to reassign.)"
299
 
300
  ctx = _ensure_context(user_id)
301
  goals = get_goals(user_id)
302
  now = datetime.now()
303
 
304
  result = smart_schedule_tasks(
305
+ tasks=unscheduled, user_context=ctx, user_goals=goals,
306
+ scheduling_prompt=prompt or "Schedule my tasks intelligently across the next week.",
307
+ current_dt=now,
 
 
308
  )
309
 
310
+ # Build PREVIEW html β€” do NOT apply to DB yet
311
+ COLOR = {"Flow":"#0ea5e9","Easy":"#4ade80","Quick":"#a78bfa","Personal":"#f87171"}
312
+ task_lookup = {t["id"]: t for t in unscheduled}
313
+ assignments = result.get("assignments", [])
 
 
 
 
 
 
 
 
 
 
314
 
 
315
  html = (
316
+ f'<div style="padding:12px;background:#0c0f1a;border-radius:10px;margin-bottom:12px">'
317
  f'<div style="color:#a78bfa;font-size:12px;font-weight:600;margin-bottom:6px">'
318
+ f'\U0001f9e0 AI Proposal \u2014 {len(assignments)} task(s) scheduled</div>'
319
+ f'<p style="color:#94a3b8;font-size:13px;margin:0 0 8px">{result.get("summary","")}</p>'
320
+ f'<p style="color:#f59e0b;font-size:12px;margin:0">\u26a0 Review below, then <b>Confirm & Apply</b> or refine with a follow-up prompt.</p>'
321
  f'</div>'
322
  )
323
 
 
324
  by_date = {}
 
 
 
 
325
  for a in assignments:
326
+ by_date.setdefault(a.get("assigned_date","?"), []).append(a)
 
327
 
328
  for d in sorted(by_date.keys()):
329
  try:
330
  dl = date.fromisoformat(d)
331
+ dfn = (dl - date.today()).days
332
+ if dfn == 0: lbl = f"{d} (Today)"
333
+ elif dfn == 1: lbl = f"{d} (Tomorrow)"
334
+ else: lbl = f"{d} ({dl.strftime('%A')})"
335
  except ValueError:
336
+ lbl = d
337
+ html += f'<div style="margin:10px 0 4px;color:#60a5fa;font-size:11px;font-weight:600;text-transform:uppercase">\U0001f4c5 {lbl}</div>'
 
338
  for a in by_date[d]:
339
+ tid = a.get("task_id")
340
+ task = task_lookup.get(int(tid) if tid else -1, {})
341
+ sm = task.get("state_of_mind","Easy")
342
+ c = COLOR.get(sm,"#6366f1")
343
+ st = a.get("start_time","")
344
+ et = a.get("end_time","")
345
+ time_block = f'<span style="color:#60a5fa;font-size:11px;font-weight:600">{st}\u2013{et}</span> &nbsp;' if st and et else ""
346
+ dl_str = ""
347
  if task.get("deadline_date"):
348
+ dl_str = f' &nbsp;|\u23f3 due {task["deadline_date"]}'
349
  html += (
350
  f'<div class="sched-card" style="border-left-color:{c};margin-bottom:6px">'
351
+ f'<div style="display:flex;justify-content:space-between;align-items:baseline">'
352
+ f'<div class="sched-title">{a.get("title","")}</div>{time_block}</div>'
353
+ f'<div class="sched-meta">{task.get("life_area") or "β€”"} Β· {sm} Β· {task.get("time_estimate") or "?"}min{dl_str}</div>'
354
  f'<div class="sched-why">\U0001f4a1 {a.get("reasoning","")}</div>'
355
  f'</div>'
356
  )
357
 
358
+ skipped = result.get("skipped",[])
 
359
  if skipped:
360
+ html += '<div style="margin-top:12px;color:#475569;font-size:11px;font-weight:600;text-transform:uppercase">Not scheduled</div>'
361
  for s in skipped:
362
+ html += f'<div style="color:#475569;font-size:12px;padding:2px 0">\u2717 {s.get("title","")} \u2014 {s.get("reason","")}</div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
+ for w in result.get("warnings",[]):
365
+ html += f'<div style="color:#f59e0b;font-size:12px;margin-top:6px">\u26a0 {w}</div>'
366
 
367
+ # Store result in JSON for confirm step
368
+ pending_json = json.dumps(result)
369
+ return html, pending_json, gr.update(visible=True), gr.update(visible=True), []
370
 
371
+ def handle_confirm_schedule(user_id, pending_json, all_df_placeholder):
372
+ """Step 2: User clicked Confirm β€” apply assignments to DB."""
373
+ if not user_id or not pending_json:
374
+ return _err("Nothing to confirm."), [], gr.update(visible=False), gr.update(visible=False)
375
+ try:
376
+ result = json.loads(pending_json)
377
+ except Exception:
378
+ return _err("Could not parse proposal."), [], gr.update(visible=False), gr.update(visible=False)
379
 
380
+ count = 0
381
+ for a in result.get("assignments",[]):
382
+ try:
383
+ assign_task_date(int(a["task_id"]), user_id, a["assigned_date"])
384
+ count += 1
385
+ except Exception:
386
+ pass
387
 
388
+ all_rows = refresh_all_tasks_auto(user_id)
389
+ return _ok(f"\u2713 {count} tasks scheduled!"), all_rows, gr.update(visible=False), gr.update(visible=False)
390
+
391
+ def handle_refine_schedule(user_id, new_prompt, pending_json):
392
+ """Step 2 alt: User typed a refinement β€” re-run scheduler with the old result as context."""
393
+ if not user_id or not new_prompt.strip():
394
+ return gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
395
+ combined = new_prompt
396
+ if pending_json:
397
+ try:
398
+ old = json.loads(pending_json)
399
+ combined = f"{new_prompt}\n\n(Previous proposal summary: {old.get('summary','')})"
400
+ except Exception:
401
+ pass
402
+ return handle_smart_schedule(user_id, combined)
403
 
404
 
405
  # =============================================================================
 
420
  import re as _re
421
  return [int(n) for n in _re.findall(r'\b(\d+)\b', text) if int(n) in valid_ids]
422
 
423
+ def _chat_msg(role, text):
424
+ """Return a (user_msg, assistant_msg) tuple for Gradio Chatbot (non-messages mode)."""
425
+ if role == "assistant":
426
+ return (None, text)
427
+ else:
428
+ return (text, None)
429
+
430
  def handle_start_journal(user_id):
431
+ blank_outputs = ([], [], False,
432
+ gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False),
433
+ [], "marking", "")
434
  if not user_id:
435
+ return ([(None, "\u26a0 Please sign in first.")],) + blank_outputs[1:]
436
  tasks = get_tasks(user_id, only_today=True)
437
  if not tasks:
438
+ return ([(None, "No tasks scheduled for today. Add and schedule tasks first, then come back to reflect!")],) + blank_outputs[1:]
 
439
 
440
  tasks_state = [{
441
  "task_id": t["id"], "title": t["title"], "life_area": t.get("life_area",""),
 
450
  if completed == total and total > 0:
451
  opener = f"\U0001f389 You knocked out all **{total}** tasks today \u2014 great work!\n\n{task_md}\n\nDid the day feel productive or were you grinding through it?"
452
  elif completed == 0:
453
+ opener = (f"Here's what was on your plate today:\n\n{task_md}\n\n"
454
  f"None are marked complete yet. Tell me which ones you finished "
455
  f"(e.g. *\"I did #1 and #3\"*), or say **skip** to start reflecting.")
456
  else:
457
  incomplete = [t for t in tasks_state if not t["completed"]]
458
  inc_str = ", ".join(f"#{t['task_id']}" for t in incomplete[:4])
459
+ opener = (f"Here's today's list:\n\n{task_md}\n\n"
460
  f"Done: **{completed}/{total}**. Still open: {inc_str}. "
461
  f"Tell me if you finished any more, or say **skip** to reflect.")
462
 
463
+ chat = [(None, opener)]
464
  hist = [{"role": "assistant", "content": opener, "focus": "task_review"}]
465
  return (
466
+ chat, hist, True,
467
  gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True),
468
  tasks_state, "marking", "",
469
  )
 
475
  try:
476
  transcribed = _transcribe(audio_path)
477
  if transcribed:
478
+ user_text = transcribed
479
  voice_used = True
480
  except Exception as e:
481
+ chat = list(chat) + [(None, f"\u26a0 Transcription error: {e}. Please type instead.")]
482
+ return chat, hist, "", None, tasks_state, active, phase, ""
483
 
484
  if not active or not user_text.strip():
485
+ return list(chat), hist, "", None, tasks_state, active, phase, ""
486
 
487
  display_text = f"\U0001f3a4 *{user_text}*" if voice_used else user_text
488
  chat = list(chat) + [(display_text, None)]
 
501
 
502
  titles_str = ", ".join(updated)
503
  confirm = (f"Got it \u2014 marked **{titles_str}** as complete \u2705\n\n" if updated else "")
504
+ skip_words = {"done","skip","reflect","none","nothing","no more","start","go","next","continue","all"}
505
  wants_skip = any(w in user_text.lower() for w in skip_words)
506
  done_count = sum(1 for t in tasks_state if t["completed"])
507
 
 
509
  ctx = _ensure_context(user_id)
510
  opening = build_opening_question(ctx, tasks_state)
511
  bridge = confirm + opening["question"]
512
+ chat = list(chat) + [(None, bridge)]
513
  hist.append({"role": "assistant", "content": bridge, "focus": opening["question_focus"]})
514
+ return chat, hist, "", None, tasks_state, active, "reflecting", ""
515
  else:
516
  remaining = [t for t in tasks_state if not t["completed"]]
517
  rem_str = " | ".join(f"#{t['task_id']} {t['title']}" for t in remaining[:5])
518
  follow = confirm + f"Still pending: {rem_str}\n\nAnything else done? Or say **skip** to start reflecting."
519
+ chat = list(chat) + [(None, follow)]
520
  hist.append({"role": "assistant", "content": follow, "focus": "task_marking"})
521
+ return chat, hist, "", None, tasks_state, active, "marking", ""
522
 
523
  # Phase: reflecting
524
  if phase == "reflecting":
 
526
  ctx = _ensure_context(user_id)
527
  result = get_next_journal_question(ctx, tasks_state, hist)
528
  except Exception as e:
529
+ chat = list(chat) + [(None, f"\u26a0 AI error: {e}. Try again.")]
530
+ return chat, hist, "", None, tasks_state, active, phase, ""
531
 
532
  if result.get("session_complete"):
533
+ closing = "That's a solid reflection \u2014 I have plenty to work with. \U0001f9e0 Hit **Finish & Save** to lock in your insights!"
534
+ chat = list(chat) + [(None, closing)]
535
  hist.append({"role": "assistant", "content": closing, "focus": "complete"})
536
+ return chat, hist, "", None, tasks_state, active, "done", _ok("Session complete \u2014 click Finish & Save")
537
 
538
+ next_q = result.get("question") or "Anything else you'd like to reflect on?"
539
+ chat = list(chat) + [(None, next_q)]
540
  hist.append({"role": "assistant", "content": next_q, "focus": result.get("question_focus","")})
541
+ return chat, hist, "", None, tasks_state, active, "reflecting", ""
542
 
543
  # Phase: done
544
+ chat = list(chat) + [(None, "Session complete! Click **Finish & Save** to save your insights.")]
545
+ return chat, hist, "", None, tasks_state, active, "done", ""
546
 
547
 
548
  def handle_finish_journal(user_id, chat, hist, tasks_state):
 
554
  save_user_context(user_id, updated)
555
  except Exception as e:
556
  return list(chat) + [(None, f"\u26a0 Save failed: {e}")], _err("Save failed")
557
+ total = len(tasks_state)
558
+ done = sum(1 for t in tasks_state if t.get("completed"))
559
+ rate = round(done / total * 100) if total else 0
560
+ notes = updated.get("learned_patterns",{}).get("notes",[])
 
561
  notes_md = "\n".join(f"\u2022 {n}" for n in notes[-3:]) if notes else "\u2022 Keep reflecting to build patterns"
562
+ summary = (f"\u2705 **Insights saved!** {done}/{total} tasks ({rate}%) complete today.\n\n"
563
+ f"**What I learned:**\n{notes_md}\n\n"
564
+ f"I'll use this to make tomorrow's scheduling smarter. Great work! \U0001f319")
 
 
565
  return list(chat) + [(None, summary)], _ok(f"Saved! {done}/{total} tasks ({rate}%)")
566
 
 
567
  def handle_restart_journal():
568
  return [], [], False, gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), [], "marking", ""
569
 
570
 
571
  # =============================================================================
572
+ # AREAS & GOALS
573
  # =============================================================================
574
 
575
+ def render_areas_html(user_id):
576
+ if not user_id: return ""
577
  areas = get_life_areas(user_id)
578
+ tasks = get_tasks(user_id)
579
+ # Count tasks per area
580
+ counts = {}
581
+ for t in tasks:
582
+ a = t.get("life_area","")
583
+ if a: counts[a] = counts.get(a,0) + 1
584
+ max_count = max(counts.values()) if counts else 1
585
+ if not areas:
586
+ return '<p style="color:#475569;font-size:13px">No areas yet. Add one below.</p>'
587
+ html = '<div style="display:flex;flex-direction:column;gap:10px">'
588
+ for a in areas:
589
+ name = a["name"]
590
+ color = a["color"]
591
+ cnt = counts.get(name, 0)
592
+ bar_w = max(4, round(cnt / max_count * 100)) if max_count else 4
593
+ html += (
594
+ f'<div style="padding:12px 14px;background:#0c0f1a;border-radius:10px;'
595
+ f'border-left:3px solid {color};display:flex;align-items:center;gap:12px">'
596
+ f'<div style="flex:1">'
597
+ f'<div style="color:{color};font-size:13px;font-weight:600">{name}</div>'
598
+ f'<div style="margin-top:6px;height:4px;background:#1e293b;border-radius:2px">'
599
+ f'<div style="width:{bar_w}%;height:4px;background:{color};border-radius:2px"></div></div>'
600
+ f'<div style="color:#475569;font-size:11px;margin-top:3px">{cnt} task{"s" if cnt!=1 else ""}</div>'
601
+ f'</div>'
602
+ f'<button onclick="this.closest(\'.gradio-container\').dispatchEvent(new CustomEvent(\'del-area\',{{detail:\'{name}\'}})); return false;" '
603
+ f'style="background:none;border:1px solid #334155;color:#475569;padding:4px 8px;'
604
+ f'border-radius:6px;font-size:11px;cursor:pointer" title="Remove {name}">\u2717</button>'
605
+ f'</div>'
606
+ )
607
+ html += '</div>'
608
+ return html
609
+
610
+ def render_areas_list(user_id):
611
+ if not user_id: return "", []
612
+ areas = get_life_areas(user_id)
613
+ tasks_all = get_tasks(user_id)
614
+ counts = {}
615
+ for t in tasks_all:
616
+ a = t.get("life_area","")
617
+ if a: counts[a] = counts.get(a,0) + 1
618
+ max_count = max(counts.values()) if counts else 1
619
+
620
+ if not areas:
621
+ html = '<p style="color:#475569;font-size:13px">No areas yet. Add one below.</p>'
622
+ else:
623
+ html = '<div style="display:flex;flex-direction:column;gap:8px">'
624
+ for a in areas:
625
+ name = a["name"]
626
+ color = a["color"]
627
+ cnt = counts.get(name, 0)
628
+ bar_w = max(4, round(cnt / max_count * 100)) if max_count else 4
629
+ html += (
630
+ f'<div style="padding:12px 14px;background:#0c0f1a;border-radius:10px;'
631
+ f'border-left:3px solid {color};display:flex;align-items:center;gap:12px">'
632
+ f'<div style="flex:1">'
633
+ f'<div style="color:{color};font-size:13px;font-weight:600">{name}</div>'
634
+ f'<div style="margin-top:6px;height:4px;background:#1e293b;border-radius:2px">'
635
+ f'<div style="width:{bar_w}%;height:4px;background:{color};border-radius:2px"></div></div>'
636
+ f'<div style="color:#475569;font-size:11px;margin-top:3px">{cnt} task{"s" if cnt!=1 else ""}</div>'
637
+ f'</div>'
638
+ f'<button style="background:none;border:1px solid #ef444444;color:#f87171;padding:4px 10px;'
639
+ f'border-radius:6px;font-size:11px;cursor:pointer"\n'
640
+ f'onclick="void(0)">\u2717 Remove</button>'
641
+ f'</div>'
642
+ )
643
+ html += '</div>'
644
  names = [a["name"] for a in areas]
645
+ return html, names
646
 
647
  def handle_add_area(user_id, name, color):
648
  ok, msg = add_life_area(user_id, name, color)
649
+ html, names = render_areas_list(user_id)
650
  ct = "#4ade80" if ok else "#f87171"
651
  return f'<span style="color:{ct};font-size:13px">{msg}</span>', html, gr.update(choices=names, value=None), gr.update(value="")
652
 
653
  def handle_del_area(user_id, name):
654
+ if not name: return _err("Select an area first."), ""
655
+ delete_life_area(user_id, name)
656
+ html, names = render_areas_list(user_id)
657
+ return _ok(f'"{name}" removed.'), html
 
658
 
659
  def handle_save_goals(user_id, goals_text):
660
  if not user_id: return _err("Not logged in.")
661
+ save_goals(uid=user_id, goals_text=goals_text)
662
  return _ok("Goals saved!")
663
 
664
  def load_goals_txt(user_id):
 
666
 
667
 
668
  # =============================================================================
669
+ # PREFERENCES & AI MEMORY
670
  # =============================================================================
671
 
672
  def load_prefs(user_id):
 
678
  def handle_save_prefs(user_id, wake, sleep, focus, brk, flow_max):
679
  if not user_id: return _err("Not logged in.")
680
  ctx = _ensure_context(user_id)
681
+ ctx["preferences"].update({"wake_time":wake or "07:30","sleep_time":sleep or "23:00","focus_peak":focus or "Morning",
682
+ "break_duration_minutes":int(brk or 10),"max_flow_block_minutes":int(flow_max or 90)})
683
  save_user_context(user_id, ctx)
684
  return _ok("Preferences saved!")
685
 
 
687
  if not user_id: return "<p style='color:#334155'>Not logged in.</p>"
688
  ctx = load_user_context(user_id)
689
  if not ctx: return "<p style='color:#334155;font-size:13px'>No AI context yet. Complete a journal session to build it.</p>"
690
+ lp = ctx.get("learned_patterns",{})
691
+ sf = ctx.get("scheduling_feedback",{})
692
+ pref = ctx.get("preferences",{})
693
+ _ver = ctx.get("version",1)
694
+ _upd = (ctx.get("last_updated") or "")[:10]
695
+
696
+ # Notes with timestamps (notes are stored as strings; newer ones may have timestamps embedded)
697
+ notes = lp.get("notes",[])
698
+ history = ctx.get("history_summary",[])
699
+ rates = sf.get("last_7_day_completion_rates",[])
700
+ avg_rate = round(sf.get("avg_completion_rate",0)*100)
701
+ overrun = lp.get("avg_task_overrun_pct",0)
702
+ days = sf.get("total_days_scheduled",0)
703
+
704
+ # ── Mini SVG bar chart for last-7-day completion ───────────────────────
705
+ chart_html = ""
706
+ if rates:
707
+ bar_w = 28
708
+ gap = 6
709
+ chart_w = len(rates)*(bar_w+gap)
710
+ chart_h = 60
711
+ bars = ""
712
+ for idx, r in enumerate(rates[-7:]):
713
+ x = idx*(bar_w+gap)
714
+ h = max(4, round(r*chart_h))
715
+ y = chart_h - h
716
+ col = "#4ade80" if r >= 0.7 else "#f59e0b" if r >= 0.4 else "#f87171"
717
+ pct = round(r*100)
718
+ bars += (f'<rect x="{x}" y="{y}" width="{bar_w}" height="{h}" fill="{col}" rx="3"/>'
719
+ f'<text x="{x+bar_w//2}" y="{chart_h+14}" text-anchor="middle" '
720
+ f'fill="#475569" font-size="9">{pct}%</text>')
721
+ chart_html = (
722
+ f'<div style="margin:12px 0 8px">'
723
+ f'<div style="color:#a78bfa;font-size:10px;font-weight:600;letter-spacing:1px;text-transform:uppercase;margin-bottom:8px">Last {len(rates[-7:])} Day Completion</div>'
724
+ f'<svg width="{chart_w}" height="{chart_h+20}" style="overflow:visible">{bars}</svg>'
725
+ f'</div>'
726
+ )
727
+
728
+ # ── Overrun pill ───────────────────────────────────────────────────────
729
+ overrun_col = "#4ade80" if overrun <= 10 else "#f59e0b" if overrun <= 30 else "#f87171"
730
+
731
+ # ── Stats row ─────────────────────────────────────────────────────────
732
+ def _pill(val, label, color):
733
+ return (f'<div style="flex:1;background:#0c0f1a;border-radius:8px;padding:10px 12px;text-align:center">'
734
+ f'<div style="color:{color};font-size:18px;font-weight:700">{val}</div>'
735
+ f'<div style="color:#475569;font-size:10px;margin-top:2px">{label}</div></div>')
736
+
737
+ stats_row = (
738
+ f'<div style="display:flex;gap:8px;margin-bottom:14px">'
739
+ f'{_pill(days,"Days tracked","#a78bfa")}'
740
+ f'{_pill(str(avg_rate)+"%","Avg completion","#4ade80")}'
741
+ f'{_pill(str(overrun)+"%","Task overrun",overrun_col)}'
742
+ f'{_pill(str(lp.get("flow_batch_capable","?")), "Flow batching","#60a5fa")}'
743
+ f'</div>'
744
+ )
745
+
746
+ # ── AI Notes with timestamps ───────────────────────────────────────────
747
+ notes_html = ""
748
  if notes:
749
+ notes_html = '<div style="color:#a78bfa;font-size:10px;font-weight:600;letter-spacing:1px;text-transform:uppercase;margin:14px 0 8px">AI Insights</div>'
750
+ for n in notes[-8:]:
751
+ # Notes may be plain strings or "YYYY-MM-DD: text"
752
+ if ": " in str(n) and str(n)[:10].count("-") == 2:
753
+ ts, txt = n.split(": ",1)
754
+ else:
755
+ ts, txt = _upd, str(n)
756
+ notes_html += (
757
+ f'<div style="display:flex;gap:8px;padding:6px 0;border-bottom:1px solid #1e293b;align-items:flex-start">'
758
+ f'<span style="color:#334155;font-size:10px;white-space:nowrap;padding-top:1px">{ts}</span>'
759
+ f'<span style="color:#94a3b8;font-size:12px">\u2022 {txt}</span>'
760
+ f'</div>'
761
+ )
762
+
763
+ # ── History summary ────────────────────────────────────────────────────
764
+ hist_html = ""
765
+ if history:
766
+ hist_html = '<div style="color:#a78bfa;font-size:10px;font-weight:600;letter-spacing:1px;text-transform:uppercase;margin:14px 0 8px">Session History</div>'
767
+ for h in history[-5:]:
768
+ hist_html += f'<div style="color:#475569;font-size:12px;padding:3px 0">\u2022 {h}</div>'
769
+
770
+ html = (
771
+ f'<div style="font-size:13px">'
772
+ f'<div style="color:#334155;font-size:11px;margin-bottom:12px">v{_ver} \u00b7 last updated {_upd}</div>'
773
+ f'{stats_row}'
774
+ f'{chart_html}'
775
+ f'{notes_html}'
776
+ f'{hist_html}'
777
+ f'</div>'
778
+ )
779
  return html
780
 
781
 
 
798
  # Clarification β€” voice panel
799
  vclar_questions_state = gr.State([])
800
  voriginal_task_state = gr.State("")
801
+ # Scheduler pending state
802
+ sched_pending_state = gr.State("")
803
+ # Selected task ids
804
+ today_sel_state = gr.State(None)
805
+ all_sel_state = gr.State(None)
806
 
807
+ # ── AUTH ─────────────────────────────────────────────────────────────────
808
  with gr.Column(visible=True, elem_id="auth-card") as auth_section:
809
  gr.HTML('<div id="brand-logo">\U0001f9e0 Second Brain</div>')
810
  gr.HTML('<div id="brand-sub">Your intelligent productivity companion</div>')
 
824
  reg_btn = gr.Button("Create Account", elem_classes="btn-primary")
825
  reg_msg = gr.HTML("")
826
 
827
+ # ── MAIN APP ─────────────────────────────────────────────────────────────
828
  with gr.Column(visible=False) as app_section:
 
829
  with gr.Row(elem_id="top-header"):
830
  header_html = gr.HTML('<div id="top-header-brand">\U0001f9e0 Second Brain</div><div id="top-header-user">\u2014</div>')
831
  logout_btn = gr.Button("Sign Out", elem_classes="btn-secondary", scale=0)
832
 
833
  with gr.Tabs():
834
 
835
+ # ── TAB 1: TODAY ────────────────────────────────────────────────
836
  with gr.Tab("\U0001f4c5 Today"):
837
  with gr.Row():
838
  stat_total = gr.HTML(_stat("\u2014","Today","#a78bfa"))
839
  stat_done = gr.HTML(_stat("\u2014","Done","#4ade80"))
840
  stat_remain = gr.HTML(_stat("\u2014","Left","#f87171"))
841
  with gr.Row():
842
+ btn_add_text = gr.Button("\u270f\ufe0f Add Task (Text)", elem_classes="btn-primary", scale=3)
843
  btn_add_voice = gr.Button("\U0001f399\ufe0f Add Task (Voice)", elem_classes="btn-accent", scale=3)
844
+ btn_plan_day = gr.Button("\U0001f9e0 Plan My Tasks", elem_classes="btn-secondary", scale=3)
845
+ btn_refresh = gr.Button("\u21bb", elem_classes="btn-secondary", scale=1)
846
 
847
+ # ── Text capture panel ────────────────────────────────────
848
  with gr.Column(visible=False, elem_classes="panel") as text_panel:
849
  gr.HTML('<div class="sec-label">Describe Your Task</div>')
850
+ task_input = gr.Textbox(label="", placeholder="e.g. Finish the proposal by Friday, it's really urgent", lines=2)
851
  with gr.Row():
852
  btn_parse = gr.Button("\U0001f916 Parse with AI", elem_classes="btn-primary")
853
  btn_cancel_text = gr.Button("Cancel", elem_classes="btn-secondary")
 
854
  with gr.Column(visible=False) as parsed_panel:
855
  parse_status = gr.HTML("")
856
  clar_html = gr.HTML("")
857
  with gr.Column(visible=False) as clar_reply_row:
858
  gr.HTML('<div class="sec-label" style="margin-top:8px">Answer the questions above</div>')
859
  with gr.Row():
860
+ clar_text_input = gr.Textbox(label="Type your answer", lines=2, scale=4)
861
+ clar_audio_input = gr.Audio(sources=["microphone"], type="filepath", label="\U0001f3a4", scale=1)
862
  btn_clar_submit = gr.Button("\U0001f504 Re-classify", elem_classes="btn-accent")
863
  gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>')
864
+ gr.HTML('<p style="color:#475569;font-size:12px;margin:0 0 10px">Deadline is auto-extracted from your text. Date will be assigned by the AI scheduler.</p>')
865
  with gr.Row():
866
  f_title = gr.Textbox(label="Title", scale=3)
867
  f_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1)
 
871
  f_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy")
872
  with gr.Row():
873
  f_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
874
+ f_deadline = gr.Textbox(label="\U0001f3af Deadline (YYYY-MM-DD)", placeholder="auto-filled or type", scale=2)
875
  f_is_habit = gr.Checkbox(label="\u267b\ufe0f Habit", scale=1)
876
  f_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1)
877
  with gr.Row():
 
879
  btn_discard = gr.Button("\u2717 Discard", elem_classes="btn-danger")
880
  save_msg = gr.HTML("")
881
 
882
+ # ── Voice capture panel ───────────────────────────────────
883
  with gr.Column(visible=False, elem_classes="panel") as voice_panel:
884
  gr.HTML('<div class="sec-label">Add Task by Voice</div>')
885
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">Say your task naturally including any deadline \u2014 AI extracts it all.</p>')
886
  voice_audio_input = gr.Audio(sources=["microphone","upload"], type="filepath", label="")
887
  with gr.Row():
888
  btn_voice_parse = gr.Button("\U0001f916 Transcribe & Classify", elem_classes="btn-primary")
889
  btn_cancel_voice = gr.Button("Cancel", elem_classes="btn-secondary")
890
  voice_status = gr.HTML("")
 
891
  with gr.Column(visible=False) as voice_parsed_panel:
892
  voice_transcribed_txt = gr.Textbox(label="Transcribed text (editable)", lines=1)
893
  voice_clar_html = gr.HTML("")
 
895
  gr.HTML('<div class="sec-label" style="margin-top:8px">Answer the questions above</div>')
896
  with gr.Row():
897
  vclar_text = gr.Textbox(label="Type your answer", lines=2, scale=4)
898
+ vclar_audio = gr.Audio(sources=["microphone"], type="filepath", label="\U0001f3a4", scale=1)
899
  btn_vclar_submit = gr.Button("\U0001f504 Re-classify", elem_classes="btn-accent")
900
  gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>')
 
901
  with gr.Row():
902
  vf_title = gr.Textbox(label="Title", scale=3)
903
  vf_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1)
 
907
  vf_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy")
908
  with gr.Row():
909
  vf_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
910
+ vf_deadline = gr.Textbox(label="\U0001f3af Deadline (YYYY-MM-DD)", placeholder="auto-filled or type", scale=2)
911
  vf_is_habit = gr.Checkbox(label="\u267b\ufe0f Habit", scale=1)
912
  vf_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1)
913
  with gr.Row():
 
915
  btn_voice_discard = gr.Button("\u2717 Discard", elem_classes="btn-danger")
916
  voice_save_msg = gr.HTML("")
917
 
918
+ # ── Plan My Tasks panel ───────────────────────────────────
919
  with gr.Column(visible=False, elem_classes="panel") as plan_panel:
920
  gr.HTML('<div class="sec-label">Plan My Tasks</div>')
921
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">'
922
+ 'The AI reads your unscheduled tasks, patterns, goals and current time \u2014 '
923
+ 'proposes a schedule with time blocks. Review before applying.</p>')
 
924
  plan_prompt = gr.Textbox(
925
  label="Tell the AI what you want",
926
+ placeholder="e.g. 'Clear today, schedule from tomorrow' or 'Focus on Work tasks this week, I have Thursday free'",
927
+ lines=2
928
  )
929
  with gr.Row():
930
+ btn_gen_sched = gr.Button("\U0001f9e0 Generate Proposal", elem_classes="btn-primary")
931
  btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary")
932
  schedule_html = gr.HTML("")
933
 
934
+ # Confirm/Refine row β€” hidden until proposal generated
935
+ with gr.Column(visible=False) as sched_confirm_row:
936
+ gr.HTML('<div style="margin:10px 0 6px;color:#f59e0b;font-size:12px">'
937
+ '\u26a0 Review the proposal above. Confirm to apply, or type a refinement.</div>')
938
+ with gr.Row():
939
+ btn_sched_confirm = gr.Button("\u2713 Confirm & Apply", elem_classes="btn-success", scale=2)
940
+ btn_sched_cancel = gr.Button("\u2717 Discard", elem_classes="btn-danger", scale=1)
941
+ refine_prompt = gr.Textbox(label="Or refine (optional)", placeholder="e.g. 'Move the Flow tasks to morning'", lines=1)
942
+ btn_refine = gr.Button("\U0001f504 Re-generate with refinement", elem_classes="btn-accent")
943
+ sched_status = gr.HTML("")
944
+
945
  gr.HTML('<div class="sec-label" style="margin-top:20px">Today\'s Tasks</div>')
946
  today_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
947
+ gr.HTML('<p style="color:#475569;font-size:11px;margin:4px 0 8px">Enter task ID then click action:</p>')
948
  with gr.Row():
949
+ today_task_id = gr.Number(label="Task ID", precision=0, scale=1, minimum=1)
950
  btn_today_done = gr.Button("\u2705 Mark Done", elem_classes="btn-success", scale=2)
951
+ btn_today_del = gr.Button("\U0001f5d1 Delete", elem_classes="btn-danger", scale=2)
 
952
  today_action_msg = gr.HTML("")
953
 
954
+ # ── TAB 2: ALL TASKS ─────────────────────────────────────────
955
  with gr.Tab("\U0001f4cb All Tasks"):
956
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">All tasks including unscheduled. Enter ID then click action.</p>')
957
  with gr.Row():
958
  all_filter = gr.Dropdown(label="Filter by Area", choices=["All"], value="All", scale=3)
959
+ all_today_chk = gr.Checkbox(label="Today only", value=False, scale=1)
960
  all_unsched = gr.Checkbox(label="Unscheduled only", value=False, scale=1)
961
  btn_all_ref = gr.Button("\u21bb Refresh", elem_classes="btn-secondary", scale=1)
962
  all_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
963
  with gr.Row():
964
+ all_task_id = gr.Number(label="Task ID", precision=0, scale=1, minimum=1)
965
  btn_all_done = gr.Button("\u2705 Done", elem_classes="btn-success", scale=2)
 
966
  btn_all_del = gr.Button("\U0001f5d1 Delete", elem_classes="btn-danger", scale=2)
967
  all_action_msg = gr.HTML("")
968
 
969
+ # ── TAB 3: JOURNAL ────────────────────────────────────────────
970
  with gr.Tab("\U0001f4d3 Journal"):
971
  gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">End-of-day reflection. Chat naturally \u2014 AI marks tasks, asks questions, updates your profile.</p>')
972
  journal_chatbot = gr.Chatbot(label="", height=440)
 
980
  btn_j_restart = gr.Button("\u21ba Reset", elem_classes="btn-secondary", scale=1)
981
  journal_msg = gr.HTML("")
982
 
983
+ # ── TAB 4: AREAS & GOALS ──────────────────────────────────────
984
  with gr.Tab("\U0001f5c2 Areas & Goals"):
985
  gr.HTML('<div class="sec-label">Your Life Areas</div>')
986
  areas_display = gr.HTML("")
987
+ gr.HTML('<div style="margin-top:14px;color:#475569;font-size:11px">To remove an area, select it from the dropdown and click Remove:</div>')
988
  with gr.Row():
989
+ del_area_dd = gr.Dropdown(label="Select area to remove", choices=[], scale=4, allow_custom_value=False)
990
+ btn_del_area = gr.Button("\u2717 Remove", elem_classes="btn-danger", scale=1)
991
+ del_area_msg = gr.HTML("")
992
+ gr.HTML('<div class="sec-label" style="margin-top:16px">Add New Area</div>')
993
+ with gr.Row():
994
+ new_area_name = gr.Textbox(label="Name", placeholder="e.g. Side Project", scale=3)
995
  new_area_color = gr.ColorPicker(label="Color", value="#6366f1", scale=1)
996
+ btn_add_area = gr.Button("+ Add", elem_classes="btn-primary", scale=1)
997
  area_msg = gr.HTML("")
 
 
 
 
 
998
  gr.HTML('<hr>')
999
  gr.HTML('<div class="sec-label">Big-Picture Goals</div>')
1000
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">The AI factors these into scheduling and prioritisation.</p>')
1001
  goals_input = gr.Textbox(label="One goal per line", lines=5, placeholder="Launch my SaaS\nRun 5K\nLearn ML")
1002
  btn_save_goals = gr.Button("Save Goals", elem_classes="btn-primary")
1003
  goals_msg = gr.HTML("")
1004
 
1005
+ # ── TAB 5: PREFERENCES ───────────────────────────────────────
1006
  with gr.Tab("\u2699\ufe0f Preferences"):
1007
  gr.HTML('<div class="sec-label">Scheduling Preferences</div>')
1008
  with gr.Row():
 
1016
  prefs_msg = gr.HTML("")
1017
  gr.HTML('<hr>')
1018
  gr.HTML('<div class="sec-label">AI Memory</div>')
1019
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">Stats, charts, and insights the AI has learned about you. Updates after each journal session.</p>')
1020
  ctx_display = gr.HTML("")
1021
  btn_ref_ctx = gr.Button("\u21bb Refresh", elem_classes="btn-secondary")
1022
 
 
1034
  ).then(_header_html, [user_id_state, username_state], [header_html]
1035
  ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
1036
  ).then(refresh_all_tasks_auto, [user_id_state], [all_df]
1037
+ ).then(lambda uid: render_areas_list(uid), [user_id_state], [areas_display, del_area_dd]
1038
  ).then(load_prefs, [user_id_state], [pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max]
1039
  ).then(load_goals_txt, [user_id_state], [goals_input])
1040
 
 
1043
  ).then(_header_html, [user_id_state, username_state], [header_html]
1044
  ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
1045
  ).then(refresh_all_tasks_auto, [user_id_state], [all_df]
1046
+ ).then(lambda uid: render_areas_list(uid), [user_id_state], [areas_display, del_area_dd])
1047
 
1048
  logout_btn.click(handle_logout, [user_id_state], [user_id_state, username_state, auth_section, app_section])
1049
 
 
1059
  f_is_habit.change(lambda v: gr.update(visible=v), [f_is_habit], [f_interval])
1060
  vf_is_habit.change(lambda v: gr.update(visible=v), [vf_is_habit], [vf_interval])
1061
 
1062
+ # ── Text task flow ────────────────────────────────────────────────────
 
 
 
1063
  _TEXT_PARSE_OUT = [parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance,
1064
  f_state, f_time, f_deadline, f_is_habit, f_interval, parse_status,
1065
  clar_reply_row, clar_questions_state, original_task_state]
 
 
1066
  _TEXT_CLAR_OUT = [f_title, clar_html, f_area, f_urgency, f_importance,
1067
  f_state, f_time, f_deadline, clar_reply_row, clar_questions_state]
1068
 
1069
  btn_parse.click(handle_parse_text, [task_input, user_id_state], _TEXT_PARSE_OUT)
 
1070
  btn_clar_submit.click(
1071
  handle_clarification_reply,
1072
  [clar_text_input, original_task_state, clar_questions_state, user_id_state],
1073
  _TEXT_CLAR_OUT
1074
  ).then(lambda: ("", None), outputs=[clar_text_input, clar_audio_input])
 
1075
  clar_audio_input.change(
1076
  handle_clar_voice,
1077
  [clar_audio_input, original_task_state, clar_questions_state, user_id_state],
1078
  _TEXT_CLAR_OUT
1079
  ).then(lambda: None, outputs=[clar_audio_input])
 
1080
  btn_discard.click(lambda: gr.update(visible=False), outputs=[parsed_panel])
 
1081
  btn_confirm.click(
1082
  handle_confirm_task,
1083
  [user_id_state, f_title, f_area, f_urgency, f_importance, f_state, f_time, f_deadline, f_is_habit, f_interval],
1084
  [save_msg, today_df, stat_total, stat_done, stat_remain, all_df, parsed_panel]
1085
  )
1086
 
1087
+ # ── Voice task flow ───────────────────────────────────────────────────
1088
  _VOICE_PARSE_OUT = [voice_parsed_panel, voice_transcribed_txt,
1089
  voice_clar_html, vf_area, vf_urgency, vf_importance,
1090
  vf_state, vf_time, vf_deadline, vf_is_habit, vf_interval,
1091
+ voice_status, voice_clar_row, vclar_questions_state, voriginal_task_state]
 
1092
  _VOICE_CLAR_OUT = [vf_title, voice_clar_html, vf_area, vf_urgency, vf_importance,
1093
  vf_state, vf_time, vf_deadline, voice_clar_row, vclar_questions_state]
1094
 
1095
  btn_voice_parse.click(handle_voice_parse, [voice_audio_input, user_id_state], _VOICE_PARSE_OUT)
 
1096
  btn_vclar_submit.click(
1097
  handle_clarification_reply,
1098
  [vclar_text, voriginal_task_state, vclar_questions_state, user_id_state],
1099
  _VOICE_CLAR_OUT
1100
  ).then(lambda: ("", None), outputs=[vclar_text, vclar_audio])
 
1101
  vclar_audio.change(
1102
  handle_clar_voice,
1103
  [vclar_audio, voriginal_task_state, vclar_questions_state, user_id_state],
1104
  _VOICE_CLAR_OUT
1105
  ).then(lambda: None, outputs=[vclar_audio])
 
1106
  btn_voice_discard.click(lambda: gr.update(visible=False), outputs=[voice_parsed_panel])
1107
  btn_voice_confirm.click(
1108
  handle_confirm_task,
 
1110
  [voice_save_msg, today_df, stat_total, stat_done, stat_remain, all_df, voice_parsed_panel]
1111
  )
1112
 
1113
+ # ── Scheduler with confirm flow ───────────────────────────────────────
1114
+ # Step 1: generate proposal (5 outputs)
1115
+ _SCHED_PROP_OUTS = [schedule_html, sched_pending_state, sched_confirm_row, sched_confirm_row, all_df]
1116
+
1117
  btn_gen_sched.click(
1118
+ handle_smart_schedule,
1119
+ [user_id_state, plan_prompt],
1120
+ [schedule_html, sched_pending_state, sched_confirm_row, btn_sched_confirm, all_df]
1121
+ )
1122
+
1123
+ # Step 2a: confirm β†’ apply
1124
+ btn_sched_confirm.click(
1125
+ handle_confirm_schedule,
1126
+ [user_id_state, sched_pending_state, all_df],
1127
+ [sched_status, all_df, sched_confirm_row, btn_sched_confirm]
1128
  ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain])
1129
 
1130
+ # Step 2b: discard
1131
+ btn_sched_cancel.click(
1132
+ lambda: ("", gr.update(visible=False), gr.update(visible=False)),
1133
+ outputs=[schedule_html, sched_confirm_row, btn_sched_confirm]
1134
+ )
1135
+
1136
+ # Step 2c: refine
1137
+ btn_refine.click(
1138
+ handle_refine_schedule,
1139
+ [user_id_state, refine_prompt, sched_pending_state],
1140
+ [schedule_html, sched_pending_state, sched_confirm_row, btn_sched_confirm, all_df]
1141
+ ).then(lambda: "", outputs=[refine_prompt])
1142
+
1143
+ # ── Today task actions ────────────────────────────────────────────────
1144
+ btn_today_done.click(
1145
+ handle_toggle_today_id, [today_task_id, user_id_state],
1146
+ [today_df, stat_total, stat_done, stat_remain, today_action_msg]
1147
+ )
1148
+ btn_today_del.click(
1149
+ handle_delete_today_id, [today_task_id, user_id_state],
1150
+ [today_df, stat_total, stat_done, stat_remain, today_action_msg]
1151
+ )
1152
 
1153
+ # ── All Tasks ─────────────────────────────────────────────────────────
1154
+ def refresh_all_filtered(user_id, filter_area, only_today, only_unsched):
1155
  if not user_id: return []
1156
  return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today, only_unscheduled=only_unsched))
1157
 
1158
+ btn_all_ref.click(refresh_all_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
1159
+ all_filter.change(refresh_all_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
1160
+ all_today_chk.change(refresh_all_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
1161
+ all_unsched.change(refresh_all_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
 
 
 
 
 
 
 
1162
 
1163
+ btn_all_done.click(handle_toggle_all_id, [all_task_id, user_id_state], [all_df, all_action_msg])
1164
+ btn_all_del.click(handle_delete_all_id, [all_task_id, user_id_state], [all_df, all_action_msg])
1165
 
1166
+ # ── Journal ───────────────────────────────────────────────────────────
1167
  _J_START_OUT = [journal_chatbot, journal_hist_state, journal_active,
1168
  j_answer, btn_j_send, j_audio_in, journal_tasks_state,
1169
  journal_phase_state, journal_msg]
1170
+ _J_MSG_IN = [user_id_state, j_answer, j_audio_in, journal_chatbot,
1171
+ journal_hist_state, journal_tasks_state, journal_active, journal_phase_state]
1172
+ _J_MSG_OUT = [journal_chatbot, journal_hist_state, j_answer, j_audio_in,
1173
+ journal_tasks_state, journal_active, journal_phase_state, journal_msg]
1174
 
1175
  btn_j_start.click(handle_start_journal, [user_id_state], _J_START_OUT)
1176
+ btn_j_send.click(handle_journal_message, _J_MSG_IN, _J_MSG_OUT)
1177
+ j_answer.submit(handle_journal_message, _J_MSG_IN, _J_MSG_OUT)
1178
+ j_audio_in.change(handle_journal_message, _J_MSG_IN, _J_MSG_OUT)
1179
 
1180
  btn_j_finish.click(
1181
  handle_finish_journal,
1182
  [user_id_state, journal_chatbot, journal_hist_state, journal_tasks_state],
1183
  [journal_chatbot, journal_msg]
1184
  )
1185
+ btn_j_restart.click(handle_restart_journal, outputs=_J_START_OUT)
 
 
 
 
1186
 
1187
+ # ── Areas & Goals ─────────────────────────────────────────────────────
1188
  btn_add_area.click(
1189
  handle_add_area, [user_id_state, new_area_name, new_area_color],
1190
  [area_msg, areas_display, del_area_dd, new_area_name]
 
1192
 
1193
  btn_del_area.click(
1194
  handle_del_area, [user_id_state, del_area_dd],
1195
+ [del_area_msg, areas_display]
1196
+ ).then(lambda uid: render_areas_list(uid)[1], [user_id_state], [del_area_dd]
1197
  ).then(lambda uid: gr.update(choices=_area_choices(uid), value="All"), [user_id_state], [all_filter])
1198
 
1199
  btn_save_goals.click(handle_save_goals, [user_id_state, goals_input], [goals_msg])
1200
 
1201
+ # ── Preferences ───────────────────────────────────────────────────────
1202
  btn_save_prefs.click(
1203
  handle_save_prefs,
1204
  [user_id_state, pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max],