eraz3r commited on
Commit
75efc02
·
verified ·
1 Parent(s): 82b5c8f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +422 -270
app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- app.py Second Brain Gradio Application
3
  """
4
 
5
  import os
@@ -10,12 +10,12 @@ from core.database import (
10
  init_db, register_user, login_user, get_username,
11
  create_default_life_areas, save_goals, get_goals,
12
  get_life_areas, get_life_area_names, add_life_area, delete_life_area,
13
- get_tasks, save_task, toggle_task_complete, delete_task, get_today_stats,
14
- load_user_context, save_user_context, spawn_due_habits,
15
  )
16
  from core.ai_engine import (
17
  init_groq, create_blank_context,
18
- parse_task_with_groq, generate_schedule,
19
  build_opening_question, get_next_journal_question, synthesize_journal,
20
  )
21
  from core.styles import CSS
@@ -23,30 +23,54 @@ from core.styles import CSS
23
  init_db()
24
  init_groq()
25
 
26
- TASK_HEADERS = ["ID", "Done", "Title", "Area", "Urgency", "Importance", "Mind", "Min", "Date"]
 
27
  _whisper_model = None
28
 
29
 
30
- # ═══════════════════════════════════════════════════════════════════════════════
31
  # SHARED HELPERS
32
- # ═══════════════════════════════════════════════════════════════════════════════
33
 
34
- def _ok(msg): return f'<span style="color:#4ade80;font-size:13px"> {msg}</span>'
35
- def _err(msg): return f'<span style="color:#f87171;font-size:13px"> {msg}</span>'
36
- def _info(msg): return f'<span style="color:#60a5fa;font-size:13px"> {msg}</span>'
37
 
38
  def _stat(val, label, color):
39
  return f'<div class="stat-card"><div class="stat-num" style="color:{color}">{val}</div><div class="stat-label">{label}</div></div>'
40
 
41
  def _fmt_tasks(tasks):
42
- return [[
43
- t["id"], "✅" if t["is_completed"] else "⬜",
44
- ("🔁 " if t["is_habit"] else "") + t["title"],
45
- t["life_area"] or "—", t["urgency"] or "—", t["importance"] or "—",
46
- t["state_of_mind"] or "—",
47
- str(t["time_estimate"]) + "m" if t["time_estimate"] else "—",
48
- t["scheduled_date"] or "—",
49
- ] for t in tasks]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
  def _area_choices(user_id):
52
  return ["All"] + (get_life_area_names(user_id) if user_id else [])
@@ -73,7 +97,7 @@ def _build_clar_html(clarifications):
73
  return (
74
  f'<div style="margin:10px 0;padding:12px;background:#0c0f1a;'
75
  f'border-left:3px solid #a78bfa;border-radius:8px">'
76
- f'<div style="color:#a78bfa;font-size:11px;font-weight:600;margin-bottom:6px"> Please clarify:</div>'
77
  f'<ul style="color:#94a3b8;font-size:13px;margin:0;padding-left:16px">{items}</ul></div>'
78
  )
79
 
@@ -89,10 +113,14 @@ def _run_parse(task_text, user_id):
89
  area_choices = [area_val] + area_choices
90
  return result, _build_clar_html(clars), bool(clars), area_val, clars
91
 
 
 
 
 
 
92
  def _parse_result_to_outputs(result, clar_html, has_clar, area_val, clars, task_text, status=""):
93
- """Convert a parse result into the standard 15-value tuple for UI outputs."""
94
  if not status:
95
- status = _ok("AI classified your task answer the questions above." if has_clar else "AI classified your task ")
96
  return (
97
  gr.update(visible=True),
98
  result.get("title", task_text),
@@ -101,7 +129,8 @@ def _parse_result_to_outputs(result, clar_html, has_clar, area_val, clars, task_
101
  result.get("importance", "Important"),
102
  result.get("state_of_mind", "Easy"),
103
  int(result.get("time_estimate") or 30),
104
- str(date.today()), False, "Daily",
 
105
  status,
106
  gr.update(visible=has_clar),
107
  clars, task_text,
@@ -109,14 +138,14 @@ def _parse_result_to_outputs(result, clar_html, has_clar, area_val, clars, task_
109
 
110
  _EMPTY_PARSE = (
111
  gr.update(visible=False), "", "", "Work", "Not Urgent", "Important",
112
- "Easy", 30, str(date.today()), False, "Daily", "",
113
  gr.update(visible=False), [], "",
114
  )
115
 
116
 
117
- # ═══════════════════════════════════════════════════════════════════════════════
118
  # AUTH
119
- # ═══════════════════════════════════════════════════════════════════════════════
120
 
121
  def handle_login(username, password):
122
  uid, msg = login_user(username, password)
@@ -141,18 +170,24 @@ def handle_logout(user_id):
141
  return None, "", gr.update(visible=True), gr.update(visible=False)
142
 
143
 
144
- # ═══════════════════════════════════════════════════════════════════════════════
145
  # TODAY TAB
146
- # ═══════════════════════════════════════════════════════════════════════════════
147
 
148
  def refresh_today(user_id):
149
  if not user_id:
150
- return [], _stat("","Today","#a78bfa"), _stat("","Done","#4ade80"), _stat("","Left","#f87171")
151
  tasks = get_tasks(user_id, only_today=True)
152
  s = get_today_stats(user_id)
153
  c = "#4ade80" if s["remaining"] == 0 and s["total"] > 0 else "#f87171"
154
  return _fmt_tasks(tasks), _stat(s["total"],"Today","#a78bfa"), _stat(s["done"],"Done","#4ade80"), _stat(s["remaining"],"Left",c)
155
 
 
 
 
 
 
 
156
  def show_text_panel(): return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
157
  def show_voice_panel(): return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
158
  def show_plan_panel(): return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
@@ -167,29 +202,30 @@ def handle_parse_text(task_text, user_id):
167
 
168
  def handle_clarification_reply(user_reply, original_task, clarifications, user_id):
169
  if not user_reply.strip():
170
- return (gr.update(),)*7 + (gr.update(visible=True), [])
171
  q_block = "\n".join(f"Q: {q}" for q in clarifications)
172
  enriched = f"{original_task}\n\nUser clarification:\n{q_block}\nA: {user_reply}"
173
  result, clar_html_val, still_has, area_val, remaining = _run_parse(enriched, user_id)
174
  if not still_has:
175
- clar_html_val = '<span style="color:#4ade80;font-size:13px"> Classification updated!</span>'
176
  return (
177
  result.get("title", original_task), clar_html_val, area_val,
178
  result.get("urgency","Not Urgent"), result.get("importance","Important"),
179
  result.get("state_of_mind","Easy"), int(result.get("time_estimate") or 30),
 
180
  gr.update(visible=still_has), remaining,
181
  )
182
 
183
  def handle_clar_voice(audio_path, original_task, clarifications, user_id):
184
  if not audio_path:
185
- return (gr.update(),)*7 + (gr.update(visible=True), [])
186
  try:
187
  reply = _transcribe(audio_path)
188
  return handle_clarification_reply(reply, original_task, clarifications, user_id)
189
- except Exception as e:
190
- return (gr.update(),)*7 + (gr.update(visible=True), clarifications)
191
 
192
- # ── Voice parse — full pipeline ────────────────────────────────────────────────
193
 
194
  def handle_voice_parse(audio_path, user_id):
195
  if audio_path is None:
@@ -201,47 +237,136 @@ def handle_voice_parse(audio_path, user_id):
201
  if not text:
202
  return _EMPTY_PARSE[:-2] + (_err("Could not hear anything. Try again."), gr.update(visible=False), [], "")
203
  result, clar_html, has_clar, area_val, clars = _run_parse(text, user_id)
204
- preview = text[:50] + ("" if len(text) > 50 else "")
205
- status = _ok(f'Heard: "{preview}" classified ' + (" Please answer questions." if has_clar else ""))
206
  return _parse_result_to_outputs(result, clar_html, has_clar, area_val, clars, text, status)
207
 
208
- # ── Confirm task ───────────────────────────────────────────────────────────────
209
 
210
- def handle_confirm_task(user_id, title, area, urgency, importance, state, time_est, sched_date, is_habit, habit_interval):
 
211
  if not user_id:
212
- return _err("Not logged in."), [], _stat("","Today","#a78bfa"), _stat("","Done","#4ade80"), _stat("","Left","#f87171"), gr.update(visible=False)
213
  if not title.strip():
214
- return _err("Title cannot be empty."), [], _stat("","Today","#a78bfa"), _stat("","Done","#4ade80"), _stat("","Left","#f87171"), gr.update(visible=True)
215
  save_task(user_id, {
216
  "title": title, "life_area": area, "urgency": urgency, "importance": importance,
217
  "state_of_mind": state, "time_estimate": int(time_est or 30),
 
218
  "is_habit": is_habit, "habit_interval": habit_interval if is_habit else "",
219
- }, sched_date)
220
  rows, s1, s2, s3 = refresh_today(user_id)
221
- return _ok("Task saved!"), rows, s1, s2, s3, gr.update(visible=False)
 
222
 
223
- def handle_generate_schedule(user_id, prompt):
224
- if not user_id: return _err("Not logged in.")
225
- tasks = get_tasks(user_id, only_today=True, include_completed=False)
226
- if not tasks: return _err("No incomplete tasks today to schedule.")
227
- sched = generate_schedule(_ensure_context(user_id), tasks, prompt or "Schedule my day sensibly.", get_goals(user_id))
228
- if "error" in sched and "scheduled_tasks" not in sched:
229
- return _err(f"Scheduling failed: {sched.get('error')}")
230
- COLOR = {"Flow":"#0ea5e9","Easy":"#4ade80","Quick":"#a78bfa","Personal":"#f87171"}
231
- html = (f'<div style="margin-bottom:14px"><span style="color:#a78bfa;font-size:12px;font-weight:600">📅 {sched.get("schedule_date","Today")}</span>'
232
- f'<p style="color:#475569;font-size:13px;margin:6px 0 0">{sched.get("day_summary","")}</p></div>')
233
- for t in sched.get("scheduled_tasks", []):
234
- sm = t.get("state_of_mind","Easy"); c = COLOR.get(sm,"#6366f1")
235
- html += (f'<div class="sched-card" style="border-left-color:{c}"><div class="sched-time">{t.get("start_time")} – {t.get("end_time")}</div>'
236
- f'<div class="sched-title">{t.get("title")}</div><div class="sched-meta">{t.get("life_area","—")} · {sm} · {t.get("duration_minutes")}min</div>'
237
- f'<div class="sched-why">{t.get("scheduling_reason","")}</div></div>')
238
- if sched.get("deferred_tasks"):
239
- html += '<div style="margin-top:12px;color:#334155;font-size:11px;font-weight:600;text-transform:uppercase">Deferred</div>'
240
- for t in sched["deferred_tasks"]:
241
- html += f'<div style="color:#475569;font-size:12px;padding:3px 0">✗ {t["title"]} — {t.get("reason","")}</div>'
242
- for w in sched.get("warnings", []):
243
- html += f'<div style="margin-top:8px;color:#f59e0b;font-size:12px">⚠ {w}</div>'
244
- return html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
  def handle_toggle_today(task_id, user_id):
247
  if task_id and user_id: toggle_task_complete(int(task_id), user_id)
@@ -254,12 +379,13 @@ def handle_delete_today(task_id, user_id):
254
  return rows, s1, s2, s3, _ok("Deleted")
255
 
256
 
257
- # ═══════════════════════════════════════════════════════════════════════════════
258
  # ALL TASKS TAB
259
- # ═══════════════════════════════════════════════════════════════════════════════
260
 
261
  def refresh_all_tasks(user_id, filter_area="All", only_today=False):
262
- return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today)) if user_id else []
 
263
 
264
  def handle_toggle_all(task_id, user_id, filter_area, only_today):
265
  if task_id and user_id: toggle_task_complete(int(task_id), user_id)
@@ -270,31 +396,31 @@ def handle_delete_all(task_id, user_id, filter_area, only_today):
270
  return refresh_all_tasks(user_id, filter_area, only_today), _ok("Deleted")
271
 
272
 
273
- # ═══════════════════════════════════════════════════════════════════════════════
274
- # JOURNAL — CHAT-FIRST, VOICE-AWARE
275
- # ═══════════════════════════════════════════════════════════════════════════════
276
 
277
  def _task_list_md(tasks):
278
  if not tasks: return "*No tasks found.*"
279
  lines = []
280
  for t in tasks:
281
  tid = t.get("task_id") or t.get("id")
282
- status = "" if (t.get("is_completed") or t.get("completed")) else ""
283
- dur = f" · {t['actual_duration']}m actual" if t.get("actual_duration") else ""
284
- lines.append(f"{status} **#{tid}** {t['title']} _{t.get('life_area','')} · {t.get('time_estimate','')}m{dur}_")
285
  return "\n".join(lines)
286
 
287
  def _parse_ids(text, valid_ids):
288
- import re
289
- return [int(n) for n in re.findall(r'\b(\d+)\b', text) if int(n) in valid_ids]
290
 
291
  def handle_start_journal(user_id):
292
  blank = ([], [], False, gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), [], "marking", "")
293
  if not user_id:
294
- return ([(None, " Please sign in first.")],) + blank[1:]
295
  tasks = get_tasks(user_id, only_today=True)
296
  if not tasks:
297
- msg = "You don't have any tasks logged for today. Add some tasks first, then come back to reflect!"
298
  return ([(None, msg)],) + blank[1:]
299
 
300
  tasks_state = [{
@@ -308,16 +434,16 @@ def handle_start_journal(user_id):
308
  task_md = _task_list_md(tasks_state)
309
 
310
  if completed == total and total > 0:
311
- opener = f"🎉 You knocked out all **{total}** tasks today great work!\n\n{task_md}\n\nDid the day feel productive, or were you grinding through it?"
312
  elif completed == 0:
313
- opener = (f"Here's what was on your plate today:\n\n{task_md}\n\n"
314
  f"None are marked complete yet. Tell me which ones you finished "
315
- f"(e.g. *\"I did #1 and #3\"*), or say **skip** to go straight to reflecting.")
316
  else:
317
  incomplete = [t for t in tasks_state if not t["completed"]]
318
  inc_str = ", ".join(f"#{t['task_id']}" for t in incomplete[:4])
319
- opener = (f"Here's today's list:\n\n{task_md}\n\n"
320
- f"You've done **{completed}/{total}** so far. Still open: {inc_str}. "
321
  f"Tell me if you finished any more, or say **skip** to reflect.")
322
 
323
  first_msg = (None, opener)
@@ -330,8 +456,6 @@ def handle_start_journal(user_id):
330
 
331
 
332
  def handle_journal_message(user_id, user_text, audio_path, chat, hist, tasks_state, active, phase):
333
- """Unified handler for text + voice journal messages."""
334
- # Resolve voice input
335
  voice_used = False
336
  if audio_path is not None:
337
  try:
@@ -340,19 +464,17 @@ def handle_journal_message(user_id, user_text, audio_path, chat, hist, tasks_sta
340
  user_text = transcribed
341
  voice_used = True
342
  except Exception as e:
343
- err = (None, f" Couldn't transcribe audio: {e}. Please type instead.")
344
  return list(chat) + [err], hist, "", None, tasks_state, active, phase, ""
345
 
346
  if not active or not user_text.strip():
347
  return chat, hist, "", None, tasks_state, active, phase, ""
348
 
349
- # Show voice transcription as label if voice was used
350
- display_text = f"🎙 *{user_text}*" if voice_used else user_text
351
- user_msg = (display_text, None)
352
- chat = list(chat) + [user_msg]
353
  hist = list(hist) + [{"role": "user", "content": user_text}]
354
 
355
- # ── Phase: marking ────────────────────────────────────────────────────────
356
  if phase == "marking":
357
  valid_ids = {t["task_id"] for t in tasks_state}
358
  mentioned = _parse_ids(user_text, valid_ids)
@@ -363,72 +485,71 @@ def handle_journal_message(user_id, user_text, audio_path, chat, hist, tasks_sta
363
  t["completed"] = True
364
  updated.append(t["title"])
365
 
366
- confirm = (f"Got it — marked **{', '.join(updated)}** as complete ✅\n\n" if updated else "")
367
- skip_words = {"done","skip","reflect","none","nothing","no more","that's it","let's go","start","go","next","continue"}
368
- wants_skip = any(w in user_text.lower() for w in skip_words)
369
- total = len(tasks_state)
370
- done_count = sum(1 for t in tasks_state if t["completed"])
371
 
372
- if wants_skip or done_count == total or (updated and done_count > 0):
373
- # Move to reflection
374
  ctx = _ensure_context(user_id)
375
  opening = build_opening_question(ctx, tasks_state)
376
  bridge = confirm + opening["question"]
377
  ai_msg = (None, bridge)
378
  hist.append({"role": "assistant", "content": bridge, "focus": opening["question_focus"]})
379
- return chat + [ai_msg], hist, "", None, tasks_state, active, "reflecting", ""
380
  else:
381
- remaining = [t for t in tasks_state if not t["completed"]]
382
- rem_str = " | ".join(f"#{t['task_id']} {t['title']}" for t in remaining[:5])
383
- follow = confirm + f"Still pending: {rem_str}\n\nAnything else you finished? Or say **skip** to start reflecting."
384
- ai_msg = (None, follow)
385
  hist.append({"role": "assistant", "content": follow, "focus": "task_marking"})
386
- return chat + [ai_msg], hist, "", None, tasks_state, active, "marking", ""
387
 
388
- # ── Phase: reflecting ─────────────────────────────────────────────────────
389
  if phase == "reflecting":
390
  try:
391
  ctx = _ensure_context(user_id)
392
  result = get_next_journal_question(ctx, tasks_state, hist)
393
  except Exception as e:
394
- err_msg = (None, f" AI error: {e}. Try sending again.")
395
- return chat + [err_msg], hist, "", None, tasks_state, active, phase, ""
396
 
397
  if result.get("session_complete"):
398
- closing = "That's a solid reflection you've given me plenty to work with. 🧠 Hit **Finish & Save** to lock in your insights!"
399
  ai_msg = (None, closing)
400
  hist.append({"role": "assistant", "content": closing, "focus": "complete"})
401
- return chat + [ai_msg], hist, "", None, tasks_state, active, "done", _ok("Session complete click Finish & Save")
402
 
403
- next_q = result.get("question") or "Anything else you'd like to add about today?"
404
  ai_msg = (None, next_q)
405
  hist.append({"role": "assistant", "content": next_q, "focus": result.get("question_focus","")})
406
- return chat + [ai_msg], hist, "", None, tasks_state, active, "reflecting", ""
407
 
408
- # ── Phase: done ───────────────────────────────────────────────────────────
409
- nudge = (None, "Session is complete! Click **Finish & Save** to save your insights.")
410
- return chat + [nudge], hist, "", None, tasks_state, active, "done", ""
411
 
412
 
413
  def handle_finish_journal(user_id, chat, hist, tasks_state):
414
  if not user_id or not hist:
415
- return list(chat) + [(None, " Nothing to save yet.")], ""
416
  try:
417
  ctx = _ensure_context(user_id)
418
  updated = synthesize_journal(ctx, tasks_state, hist)
419
  save_user_context(user_id, updated)
420
  except Exception as e:
421
- return list(chat) + [(None, f" Save failed: {e}")], _err("Save failed")
422
 
423
  total = len(tasks_state)
424
  done = sum(1 for t in tasks_state if t.get("completed"))
425
  rate = round(done / total * 100) if total else 0
426
  notes = updated.get("learned_patterns",{}).get("notes",[])
427
- notes_md = "\n".join(f" {n}" for n in notes[-3:]) if notes else " Keep reflecting to build patterns"
428
  summary = (
429
- f" **Insights saved!** {done}/{total} tasks ({rate}%) complete today.\n\n"
430
  f"**What I learned:**\n{notes_md}\n\n"
431
- f"I'll use this to make tomorrow's schedule smarter. Great work today! 🌙"
432
  )
433
  return list(chat) + [(None, summary)], _ok(f"Saved! {done}/{total} tasks ({rate}%)")
434
 
@@ -437,9 +558,9 @@ def handle_restart_journal():
437
  return [], [], False, gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), [], "marking", ""
438
 
439
 
440
- # ═══════════════════════════════════════════════════════════════════════════════
441
- # AREAS & GOALS
442
- # ═══════════════════════════════════════════════════════════════════════════════
443
 
444
  def render_areas(user_id):
445
  if not user_id: return "", [], []
@@ -455,15 +576,15 @@ def render_areas(user_id):
455
  def handle_add_area(user_id, name, color):
456
  ok, msg = add_life_area(user_id, name, color)
457
  html, names, _ = render_areas(user_id)
458
- color_txt = "#4ade80" if ok else "#f87171"
459
- return f'<span style="color:{color_txt};font-size:13px">{msg}</span>', html, gr.update(choices=names, value=None), gr.update(value="")
460
 
461
  def handle_del_area(user_id, name):
462
  if not name: return _err("Select an area first."), "", []
463
  ok, msg = delete_life_area(user_id, name)
464
  html, names, _ = render_areas(user_id)
465
- color_txt = "#4ade80" if ok else "#f87171"
466
- return f'<span style="color:{color_txt};font-size:13px">{msg}</span>', html, gr.update(choices=names, value=None)
467
 
468
  def handle_save_goals(user_id, goals_text):
469
  if not user_id: return _err("Not logged in.")
@@ -474,9 +595,9 @@ def load_goals_txt(user_id):
474
  return "\n".join(get_goals(user_id)) if user_id else ""
475
 
476
 
477
- # ═══════════════════════════════════════════════════════════════════════════════
478
  # PREFERENCES
479
- # ═══════════════════════════════════════════════════════════════════════════════
480
 
481
  def load_prefs(user_id):
482
  ctx = load_user_context(user_id) if user_id else None
@@ -499,23 +620,23 @@ def render_context_html(user_id):
499
  def _row(l,v): return f'<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid #1e293b"><span style="color:#475569;font-size:12px">{l}</span><span style="color:#cbd5e1;font-size:12px">{v}</span></div>'
500
  def _hdr(t): return f'<div style="color:#a78bfa;font-size:10px;font-weight:600;letter-spacing:1px;text-transform:uppercase;margin:14px 0 8px">{t}</div>'
501
  html = "<div style='font-size:13px'>" + _hdr("Preferences")
502
- html += _row("Wake",pref.get("wake_time",""))+_row("Sleep",pref.get("sleep_time",""))+_row("Peak Focus",pref.get("focus_peak",""))
503
  html += _hdr("Learned Patterns")
504
  html += _row("Avg overrun",f'{lp.get("avg_task_overrun_pct",0)}%')+_row("Flow batching",str(lp.get("flow_batch_capable","Unknown")))
505
  html += _row("Days tracked",str(sf.get("total_days_scheduled",0)))+_row("Avg completion",f'{round(sf.get("avg_completion_rate",0)*100)}%')
506
  notes = lp.get("notes",[])
507
  if notes:
508
  html += _hdr("AI Notes")
509
- html += "".join(f'<div style="color:#94a3b8;font-size:12px;padding:3px 0"> {n}</div>' for n in notes[-5:])
510
- html += f'<div style="color:#334155;font-size:11px;margin-top:12px">v{ctx.get("version",1)} · {ctx.get("last_updated","")[:10]}</div></div>'
511
  return html
512
 
513
 
514
- # ═══════════════════════════════════════════════════════════════════════════════
515
  # UI
516
- # ═══════════════════════════════════════════════════════════════════════════════
517
 
518
- with gr.Blocks(title="🧠 Second Brain") as demo:
519
 
520
  user_id_state = gr.State(None)
521
  username_state = gr.State("")
@@ -531,54 +652,54 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
531
  vclar_questions_state = gr.State([])
532
  voriginal_task_state = gr.State("")
533
 
534
- # ── AUTH ──────────────────────────────────────────────────────────────────
535
  with gr.Column(visible=True, elem_id="auth-card") as auth_section:
536
- gr.HTML('<div id="brand-logo">🧠 Second Brain</div>')
537
  gr.HTML('<div id="brand-sub">Your intelligent productivity companion</div>')
538
  with gr.Tabs():
539
  with gr.Tab("Sign In"):
540
  login_user_in = gr.Textbox(label="Username", placeholder="your username")
541
- login_pass_in = gr.Textbox(label="Password", type="password", placeholder="••••••••")
542
  login_btn = gr.Button("Sign In", elem_classes="btn-primary")
543
  login_msg = gr.HTML("")
544
  with gr.Tab("Create Account"):
545
  reg_user_in = gr.Textbox(label="Username", placeholder="choose a username")
546
- reg_pass_in = gr.Textbox(label="Password (min 6 chars)", type="password", placeholder="••••••••")
547
  reg_wake = gr.Textbox(label="Wake time", value="07:30")
548
  reg_sleep = gr.Textbox(label="Sleep time", value="23:00")
549
  reg_focus = gr.Dropdown(label="Peak focus", choices=["Morning","Afternoon","Evening","Night"], value="Morning")
550
- reg_goals = gr.Textbox(label="Big-picture goals (one per line, optional)", lines=3, placeholder="Launch my startup\nGet fit\nLearn ML")
551
  reg_btn = gr.Button("Create Account", elem_classes="btn-primary")
552
  reg_msg = gr.HTML("")
553
 
554
- # ── MAIN APP ──────────────────────────────────────────────────────────────
555
  with gr.Column(visible=False) as app_section:
556
 
557
  with gr.Row(elem_id="top-header"):
558
- header_html = gr.HTML('<div id="top-header-brand">🧠 Second Brain</div><div id="top-header-user"></div>')
559
  logout_btn = gr.Button("Sign Out", elem_classes="btn-secondary", scale=0)
560
 
561
  with gr.Tabs():
562
 
563
- # ── TAB 1: TODAY ─────────────────────────────────────────────────
564
- with gr.Tab("📅 Today"):
565
  with gr.Row():
566
- stat_total = gr.HTML(_stat("","Today","#a78bfa"))
567
- stat_done = gr.HTML(_stat("","Done","#4ade80"))
568
- stat_remain = gr.HTML(_stat("","Left","#f87171"))
569
  with gr.Row():
570
- btn_add_text = gr.Button("✏️ Add Task (Text)", elem_classes="btn-primary", scale=3)
571
- btn_add_voice = gr.Button("🎙️ Add Task (Voice)", elem_classes="btn-accent", scale=3)
572
- btn_plan_day = gr.Button("🗓 Plan My Day", elem_classes="btn-secondary", scale=3)
573
- btn_refresh = gr.Button("", elem_classes="btn-secondary", scale=1)
574
 
575
- # ── Text panel ────────────────────────────────────────────────
576
  with gr.Column(visible=False, elem_classes="panel") as text_panel:
577
  gr.HTML('<div class="sec-label">Describe Your Task</div>')
578
- task_input = gr.Textbox(label="", placeholder="e.g. Finish the client proposal by tomorrow, it's really important", lines=2)
579
  with gr.Row():
580
- btn_parse = gr.Button("🤖 Parse with AI", elem_classes="btn-primary")
581
- btn_cancel_text = gr.Button("Cancel", elem_classes="btn-secondary")
582
 
583
  with gr.Column(visible=False) as parsed_panel:
584
  parse_status = gr.HTML("")
@@ -586,12 +707,11 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
586
  with gr.Column(visible=False) as clar_reply_row:
587
  gr.HTML('<div class="sec-label" style="margin-top:8px">Answer the questions above</div>')
588
  with gr.Row():
589
- clar_text_input = gr.Textbox(label="Type your answer", lines=2, scale=4,
590
- placeholder="e.g. It's urgent, about 2 hours, for my manager")
591
- clar_audio_input = gr.Audio(sources=["microphone"], type="filepath",
592
- label="🎙 Or speak", scale=1)
593
- btn_clar_submit = gr.Button("🔄 Re-classify", elem_classes="btn-accent")
594
  gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>')
 
595
  with gr.Row():
596
  f_title = gr.Textbox(label="Title", scale=3)
597
  f_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1)
@@ -601,22 +721,22 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
601
  f_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy")
602
  with gr.Row():
603
  f_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
604
- f_date = gr.Textbox(label="Scheduled Date", value=str(date.today()), scale=1)
605
- f_is_habit = gr.Checkbox(label="♻️ This is a habit", scale=1)
606
  f_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1)
607
  with gr.Row():
608
- btn_confirm = gr.Button(" Save Task", elem_classes="btn-success")
609
- btn_discard = gr.Button(" Discard", elem_classes="btn-danger")
610
  save_msg = gr.HTML("")
611
 
612
- # ── Voice panel ───────────────────────────────────────────────
613
  with gr.Column(visible=False, elem_classes="panel") as voice_panel:
614
  gr.HTML('<div class="sec-label">Add Task by Voice</div>')
615
- gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">Record your task Whisper transcribes it, then AI classifies it automatically. Same flow as text.</p>')
616
  voice_audio_input = gr.Audio(sources=["microphone","upload"], type="filepath", label="")
617
  with gr.Row():
618
- btn_voice_parse = gr.Button("🤖 Transcribe & Classify", elem_classes="btn-primary")
619
- btn_cancel_voice = gr.Button("Cancel", elem_classes="btn-secondary")
620
  voice_status = gr.HTML("")
621
 
622
  with gr.Column(visible=False) as voice_parsed_panel:
@@ -625,11 +745,11 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
625
  with gr.Column(visible=False) as voice_clar_row:
626
  gr.HTML('<div class="sec-label" style="margin-top:8px">Answer the questions above</div>')
627
  with gr.Row():
628
- vclar_text = gr.Textbox(label="Type your answer", lines=2, scale=4,
629
- placeholder="e.g. It's urgent, about 90 minutes, very high priority")
630
- vclar_audio = gr.Audio(sources=["microphone"], type="filepath", label="🎙 Or speak", scale=1)
631
- btn_vclar_submit = gr.Button("🔄 Re-classify", elem_classes="btn-accent")
632
  gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>')
 
633
  with gr.Row():
634
  vf_title = gr.Textbox(label="Title", scale=3)
635
  vf_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1)
@@ -639,65 +759,72 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
639
  vf_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy")
640
  with gr.Row():
641
  vf_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
642
- vf_date = gr.Textbox(label="Scheduled Date", value=str(date.today()), scale=1)
643
- vf_is_habit = gr.Checkbox(label="♻️ This is a habit", scale=1)
644
  vf_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1)
645
  with gr.Row():
646
- btn_voice_confirm = gr.Button(" Save Task", elem_classes="btn-success")
647
- btn_voice_discard = gr.Button(" Discard", elem_classes="btn-danger")
648
  voice_save_msg = gr.HTML("")
649
 
650
- # ── Plan My Day panel ─────────────────────────────────────────
651
  with gr.Column(visible=False, elem_classes="panel") as plan_panel:
652
- gr.HTML('<div class="sec-label">Plan My Day</div>')
653
- gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">AI reads your tasks, context, and goals to build the optimal time-blocked schedule.</p>')
654
- plan_prompt = gr.Textbox(label="Any fixed events or notes?", placeholder="e.g. Meeting at 10am, deep work morning, done by 7pm", lines=3)
 
 
 
 
 
 
 
655
  with gr.Row():
656
- btn_gen_sched = gr.Button(" Generate Schedule", elem_classes="btn-primary")
657
- btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary")
658
  schedule_html = gr.HTML("")
659
 
660
  gr.HTML('<div class="sec-label" style="margin-top:20px">Today\'s Tasks</div>')
661
  today_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
662
  with gr.Row():
663
- today_done_id = gr.Number(label="Task ID toggle done", precision=0, scale=2)
664
- btn_today_done = gr.Button(" Mark Done", elem_classes="btn-success", scale=2)
665
- today_del_id = gr.Number(label="Task ID delete", precision=0, scale=2)
666
- btn_today_del = gr.Button("🗑 Delete", elem_classes="btn-danger", scale=2)
667
  today_action_msg = gr.HTML("")
668
 
669
- # ── TAB 2: ALL TASKS ─────────────────────────────────────────────
670
- with gr.Tab("📋 All Tasks"):
 
671
  with gr.Row():
672
- all_filter = gr.Dropdown(label="Filter by Area", choices=["All"], value="All", scale=4)
673
  all_today_chk = gr.Checkbox(label="Today only", value=False, scale=1)
674
- btn_all_ref = gr.Button(" Refresh", elem_classes="btn-secondary", scale=1)
 
675
  all_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
676
  with gr.Row():
677
- all_done_id = gr.Number(label="Task ID toggle done", precision=0, scale=2)
678
- btn_all_done = gr.Button(" Done", elem_classes="btn-success", scale=2)
679
- all_del_id = gr.Number(label="Task ID delete", precision=0, scale=2)
680
- btn_all_del = gr.Button("🗑 Delete", elem_classes="btn-danger", scale=2)
681
  all_action_msg = gr.HTML("")
682
 
683
- # ── TAB 3: JOURNAL ───────────────────────────────────────────────
684
- with gr.Tab("📓 Journal"):
685
- gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">End-of-day reflection. Chat naturally the AI marks tasks, asks questions, and updates your profile.</p>')
686
  journal_chatbot = gr.Chatbot(label="", height=440)
687
  with gr.Row():
688
- j_answer = gr.Textbox(label="", placeholder="Type or use the mic", lines=1,
689
- scale=5, interactive=False, show_label=False)
690
- j_audio_in = gr.Audio(sources=["microphone"], type="filepath",
691
- label="🎙", scale=1, interactive=False)
692
- btn_j_send = gr.Button("Send →", elem_classes="btn-accent", scale=1, interactive=False)
693
  with gr.Row():
694
- btn_j_start = gr.Button(" Start Session", elem_classes="btn-primary", scale=3)
695
- btn_j_finish = gr.Button(" Finish & Save", elem_classes="btn-success", scale=3)
696
- btn_j_restart = gr.Button(" Reset", elem_classes="btn-secondary", scale=1)
697
  journal_msg = gr.HTML("")
698
 
699
- # ── TAB 4: AREAS & GOALS ─────────────────────────────────────────
700
- with gr.Tab("🗂 Areas & Goals"):
701
  gr.HTML('<div class="sec-label">Your Life Areas</div>')
702
  areas_display = gr.HTML("")
703
  with gr.Row():
@@ -717,8 +844,8 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
717
  btn_save_goals = gr.Button("Save Goals", elem_classes="btn-primary")
718
  goals_msg = gr.HTML("")
719
 
720
- # ── TAB 5: PREFERENCES ───────────────────────────────────────────
721
- with gr.Tab("⚙️ Preferences"):
722
  gr.HTML('<div class="sec-label">Scheduling Preferences</div>')
723
  with gr.Row():
724
  pref_wake = gr.Textbox(label="Wake time", placeholder="07:30", scale=1)
@@ -731,22 +858,24 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
731
  prefs_msg = gr.HTML("")
732
  gr.HTML('<hr>')
733
  gr.HTML('<div class="sec-label">AI Memory</div>')
734
- gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">Everything the AI has learned about you. Updates after each journal session.</p>')
735
  ctx_display = gr.HTML("")
736
- btn_ref_ctx = gr.Button(" Refresh", elem_classes="btn-secondary")
 
737
 
738
- # ═════════════════════════════════════════════════════════════════════════
739
  # EVENT WIRING
740
- # ═════════════════════════════════════════════════════════════════════════
741
 
742
- # ── Auth ──────────────────────────────────────────────────────────────────
743
  def _header_html(uid, uname):
744
- return f'<div id="top-header-brand">🧠 Second Brain</div><div id="top-header-user">👤 {uname}</div>'
745
 
 
746
  login_btn.click(handle_login, [login_user_in, login_pass_in],
747
  [user_id_state, username_state, login_msg, auth_section, app_section]
748
  ).then(_header_html, [user_id_state, username_state], [header_html]
749
  ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
 
750
  ).then(lambda uid: render_areas(uid)[:2], [user_id_state], [areas_display, del_area_dd]
751
  ).then(load_prefs, [user_id_state], [pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max]
752
  ).then(load_goals_txt, [user_id_state], [goals_input])
@@ -755,12 +884,13 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
755
  [user_id_state, username_state, reg_msg, auth_section, app_section]
756
  ).then(_header_html, [user_id_state, username_state], [header_html]
757
  ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
 
758
  ).then(lambda uid: render_areas(uid)[:2], [user_id_state], [areas_display, del_area_dd])
759
 
760
  logout_btn.click(handle_logout, [user_id_state], [user_id_state, username_state, auth_section, app_section])
761
 
762
- # ── Panels ──────────────────────────────────────────────────���─────────────
763
- btn_add_text.click(show_text_panel, outputs=[text_panel, voice_panel, plan_panel])
764
  btn_add_voice.click(show_voice_panel, outputs=[text_panel, voice_panel, plan_panel])
765
  btn_plan_day.click(show_plan_panel, outputs=[text_panel, voice_panel, plan_panel])
766
  btn_cancel_text.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
@@ -771,103 +901,125 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
771
  f_is_habit.change(lambda v: gr.update(visible=v), [f_is_habit], [f_interval])
772
  vf_is_habit.change(lambda v: gr.update(visible=v), [vf_is_habit], [vf_interval])
773
 
774
- # ── Text task flow ─────────────────────────────────────────────────────────
775
- _TEXT_PARSE_OUTPUTS = [parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance,
776
- f_state, f_time, f_date, f_is_habit, f_interval, parse_status,
777
- clar_reply_row, clar_questions_state, original_task_state]
778
- _TEXT_CLAR_OUTPUTS = [f_title, clar_html, f_area, f_urgency, f_importance,
779
- f_state, f_time, clar_reply_row, clar_questions_state]
 
 
 
 
 
780
 
781
- btn_parse.click(handle_parse_text, [task_input, user_id_state], _TEXT_PARSE_OUTPUTS)
782
 
783
  btn_clar_submit.click(
784
  handle_clarification_reply,
785
  [clar_text_input, original_task_state, clar_questions_state, user_id_state],
786
- _TEXT_CLAR_OUTPUTS
787
  ).then(lambda: ("", None), outputs=[clar_text_input, clar_audio_input])
788
 
789
  clar_audio_input.change(
790
  handle_clar_voice,
791
  [clar_audio_input, original_task_state, clar_questions_state, user_id_state],
792
- _TEXT_CLAR_OUTPUTS
793
  ).then(lambda: None, outputs=[clar_audio_input])
794
 
795
  btn_discard.click(lambda: gr.update(visible=False), outputs=[parsed_panel])
 
796
  btn_confirm.click(
797
  handle_confirm_task,
798
- [user_id_state, f_title, f_area, f_urgency, f_importance, f_state, f_time, f_date, f_is_habit, f_interval],
799
- [save_msg, today_df, stat_total, stat_done, stat_remain, parsed_panel]
800
  )
801
 
802
- # ── Voice task flow ────────────────────────────────────────────────────────
803
- _VOICE_PARSE_OUTPUTS = [voice_parsed_panel, voice_transcribed_txt,
804
- voice_clar_html, vf_area, vf_urgency, vf_importance,
805
- vf_state, vf_time, vf_date, vf_is_habit, vf_interval,
806
- voice_status, voice_clar_row,
807
- vclar_questions_state, voriginal_task_state]
808
- _VOICE_CLAR_OUTPUTS = [vf_title, voice_clar_html, vf_area, vf_urgency, vf_importance,
809
- vf_state, vf_time, voice_clar_row, vclar_questions_state]
810
 
811
- btn_voice_parse.click(handle_voice_parse, [voice_audio_input, user_id_state], _VOICE_PARSE_OUTPUTS)
812
 
813
  btn_vclar_submit.click(
814
  handle_clarification_reply,
815
  [vclar_text, voriginal_task_state, vclar_questions_state, user_id_state],
816
- _VOICE_CLAR_OUTPUTS
817
  ).then(lambda: ("", None), outputs=[vclar_text, vclar_audio])
818
 
819
  vclar_audio.change(
820
  handle_clar_voice,
821
  [vclar_audio, voriginal_task_state, vclar_questions_state, user_id_state],
822
- _VOICE_CLAR_OUTPUTS
823
  ).then(lambda: None, outputs=[vclar_audio])
824
 
825
  btn_voice_discard.click(lambda: gr.update(visible=False), outputs=[voice_parsed_panel])
826
  btn_voice_confirm.click(
827
  handle_confirm_task,
828
- [user_id_state, vf_title, vf_area, vf_urgency, vf_importance, vf_state, vf_time, vf_date, vf_is_habit, vf_interval],
829
- [voice_save_msg, today_df, stat_total, stat_done, stat_remain, voice_parsed_panel]
830
  )
831
 
832
- # ── Schedule ───────────────────────────────────────────────────────────────
833
- btn_gen_sched.click(handle_generate_schedule, [user_id_state, plan_prompt], [schedule_html])
 
 
 
 
834
  btn_today_done.click(handle_toggle_today, [today_done_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg])
835
  btn_today_del.click(handle_delete_today, [today_del_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg])
836
 
837
- # ── All Tasks ──────────────────────────────────────────────────────────────
838
- btn_all_ref.click(refresh_all_tasks, [user_id_state, all_filter, all_today_chk], [all_df])
839
- all_filter.change(refresh_all_tasks, [user_id_state, all_filter, all_today_chk], [all_df])
840
- all_today_chk.change(refresh_all_tasks,[user_id_state, all_filter, all_today_chk], [all_df])
841
- btn_all_done.click(handle_toggle_all, [all_done_id, user_id_state, all_filter, all_today_chk], [all_df, all_action_msg])
842
- btn_all_del.click(handle_delete_all, [all_del_id, user_id_state, all_filter, all_today_chk], [all_df, all_action_msg])
843
-
844
- # ── Journal ────────────────────────────────────────────────────────────────
845
- _J_START_OUTPUTS = [journal_chatbot, journal_hist_state, journal_active,
846
- j_answer, btn_j_send, j_audio_in, journal_tasks_state,
847
- journal_phase_state, journal_msg]
848
- _J_MSG_INPUTS = [user_id_state, j_answer, j_audio_in, journal_chatbot,
849
- journal_hist_state, journal_tasks_state, journal_active, journal_phase_state]
850
- _J_MSG_OUTPUTS = [journal_chatbot, journal_hist_state, j_answer, j_audio_in,
851
- journal_tasks_state, journal_active, journal_phase_state, journal_msg]
852
-
853
- btn_j_start.click(handle_start_journal, [user_id_state], _J_START_OUTPUTS)
854
- btn_j_send.click(handle_journal_message, _J_MSG_INPUTS, _J_MSG_OUTPUTS)
855
- j_answer.submit(handle_journal_message, _J_MSG_INPUTS, _J_MSG_OUTPUTS)
856
- j_audio_in.change(handle_journal_message, _J_MSG_INPUTS, _J_MSG_OUTPUTS)
 
 
 
 
 
 
 
 
 
 
 
 
 
857
 
858
  btn_j_finish.click(
859
  handle_finish_journal,
860
  [user_id_state, journal_chatbot, journal_hist_state, journal_tasks_state],
861
  [journal_chatbot, journal_msg]
862
  )
863
- btn_j_restart.click(
864
- handle_restart_journal,
865
- outputs=[journal_chatbot, journal_hist_state, journal_active,
866
- j_answer, btn_j_send, j_audio_in, journal_tasks_state,
867
- journal_phase_state, journal_msg]
868
- )
869
 
870
- # ── Areas & Goals ──────────────────────────────────────────────────────────
871
  btn_add_area.click(
872
  handle_add_area, [user_id_state, new_area_name, new_area_color],
873
  [area_msg, areas_display, del_area_dd, new_area_name]
@@ -880,7 +1032,7 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
880
 
881
  btn_save_goals.click(handle_save_goals, [user_id_state, goals_input], [goals_msg])
882
 
883
- # ── Preferences ────────────────────────────────────────────────────────────
884
  btn_save_prefs.click(
885
  handle_save_prefs,
886
  [user_id_state, pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max],
@@ -888,6 +1040,6 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
888
  )
889
  btn_ref_ctx.click(render_context_html, [user_id_state], [ctx_display])
890
 
891
- # ── Launch ─────────────────────────────────────────────────────────────────────
892
  if __name__ == "__main__":
893
  demo.launch(server_name="0.0.0.0", server_port=7860, css=CSS)
 
1
  """
2
+ app.py - Second Brain Gradio Application
3
  """
4
 
5
  import os
 
10
  init_db, register_user, login_user, get_username,
11
  create_default_life_areas, save_goals, get_goals,
12
  get_life_areas, get_life_area_names, add_life_area, delete_life_area,
13
+ get_tasks, save_task, assign_task_date, toggle_task_complete, delete_task,
14
+ get_today_stats, load_user_context, save_user_context, spawn_due_habits,
15
  )
16
  from core.ai_engine import (
17
  init_groq, create_blank_context,
18
+ parse_task_with_groq, smart_schedule_tasks,
19
  build_opening_question, get_next_journal_question, synthesize_journal,
20
  )
21
  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
 
30
 
31
+ # =============================================================================
32
  # SHARED HELPERS
33
+ # =============================================================================
34
 
35
+ def _ok(msg): return f'<span style="color:#4ade80;font-size:13px">\u2713 {msg}</span>'
36
+ def _err(msg): return f'<span style="color:#f87171;font-size:13px">\u26a0 {msg}</span>'
37
+ def _info(msg): return f'<span style="color:#60a5fa;font-size:13px">\u2139 {msg}</span>'
38
 
39
  def _stat(val, label, color):
40
  return f'<div class="stat-card"><div class="stat-num" style="color:{color}">{val}</div><div class="stat-label">{label}</div></div>'
41
 
42
  def _fmt_tasks(tasks):
43
+ rows = []
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([
62
+ t["id"],
63
+ "\u2705" if t["is_completed"] else "\u2b1c",
64
+ ("\U0001f501 " if t["is_habit"] else "") + t["title"],
65
+ t["life_area"] or "\u2014",
66
+ t["urgency"] or "\u2014",
67
+ t["importance"] or "\u2014",
68
+ t["state_of_mind"] or "\u2014",
69
+ str(t["time_estimate"]) + "m" if t["time_estimate"] else "\u2014",
70
+ deadline or "\u2014",
71
+ t["scheduled_date"] or "\u2014 unscheduled",
72
+ ])
73
+ return rows
74
 
75
  def _area_choices(user_id):
76
  return ["All"] + (get_life_area_names(user_id) if user_id else [])
 
97
  return (
98
  f'<div style="margin:10px 0;padding:12px;background:#0c0f1a;'
99
  f'border-left:3px solid #a78bfa;border-radius:8px">'
100
+ f'<div style="color:#a78bfa;font-size:11px;font-weight:600;margin-bottom:6px">\u26a0 Please clarify:</div>'
101
  f'<ul style="color:#94a3b8;font-size:13px;margin:0;padding-left:16px">{items}</ul></div>'
102
  )
103
 
 
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
  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),
136
  clars, task_text,
 
138
 
139
  _EMPTY_PARSE = (
140
  gr.update(visible=False), "", "", "Work", "Not Urgent", "Important",
141
+ "Easy", 30, "", False, "Daily", "",
142
  gr.update(visible=False), [], "",
143
  )
144
 
145
 
146
+ # =============================================================================
147
  # AUTH
148
+ # =============================================================================
149
 
150
  def handle_login(username, password):
151
  uid, msg = login_user(username, password)
 
170
  return None, "", gr.update(visible=True), gr.update(visible=False)
171
 
172
 
173
+ # =============================================================================
174
  # TODAY TAB
175
+ # =============================================================================
176
 
177
  def refresh_today(user_id):
178
  if not user_id:
179
+ return [], _stat("\u2014","Today","#a78bfa"), _stat("\u2014","Done","#4ade80"), _stat("\u2014","Left","#f87171")
180
  tasks = get_tasks(user_id, only_today=True)
181
  s = get_today_stats(user_id)
182
  c = "#4ade80" if s["remaining"] == 0 and s["total"] > 0 else "#f87171"
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)
192
  def show_voice_panel(): return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
193
  def show_plan_panel(): return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
 
202
 
203
  def handle_clarification_reply(user_reply, original_task, clarifications, user_id):
204
  if not user_reply.strip():
205
+ return (gr.update(),)*8 + (gr.update(visible=True), [])
206
  q_block = "\n".join(f"Q: {q}" for q in clarifications)
207
  enriched = f"{original_task}\n\nUser clarification:\n{q_block}\nA: {user_reply}"
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):
220
  if not audio_path:
221
+ return (gr.update(),)*8 + (gr.update(visible=True), [])
222
  try:
223
  reply = _transcribe(audio_path)
224
  return handle_clarification_reply(reply, 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:
 
237
  if not text:
238
  return _EMPTY_PARSE[:-2] + (_err("Could not hear anything. Try again."), gr.update(visible=False), [], "")
239
  result, clar_html, has_clar, area_val, clars = _run_parse(text, user_id)
240
+ preview = text[:50] + ("\u2026" if len(text) > 50 else "")
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:
249
+ return _err("Not logged in."), [], _stat("\u2014","Today","#a78bfa"), _stat("\u2014","Done","#4ade80"), _stat("\u2014","Left","#f87171"), [], gr.update(visible=False)
250
  if not title.strip():
251
+ return _err("Title cannot be empty."), [], _stat("\u2014","Today","#a78bfa"), _stat("\u2014","Done","#4ade80"), _stat("\u2014","Left","#f87171"), [], gr.update(visible=True)
252
  save_task(user_id, {
253
  "title": title, "life_area": area, "urgency": urgency, "importance": importance,
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","\u2014")} \u00b7 {sm} \u00b7 {task.get("time_estimate","?")}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)
 
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)
 
396
  return refresh_all_tasks(user_id, filter_area, only_today), _ok("Deleted")
397
 
398
 
399
+ # =============================================================================
400
+ # JOURNAL
401
+ # =============================================================================
402
 
403
  def _task_list_md(tasks):
404
  if not tasks: return "*No tasks found.*"
405
  lines = []
406
  for t in tasks:
407
  tid = t.get("task_id") or t.get("id")
408
+ status = "\u2705" if (t.get("is_completed") or t.get("completed")) else "\u2b1c"
409
+ dur = f" \u00b7 {t['actual_duration']}m actual" if t.get("actual_duration") else ""
410
+ lines.append(f"{status} **#{tid}** {t['title']} _{t.get('life_area','')} \u00b7 {t.get('time_estimate','')}m{dur}_")
411
  return "\n".join(lines)
412
 
413
  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 = [{
 
434
  task_md = _task_list_md(tasks_state)
435
 
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)
 
456
 
457
 
458
  def handle_journal_message(user_id, user_text, audio_path, chat, hist, tasks_state, active, phase):
 
 
459
  voice_used = False
460
  if audio_path is not None:
461
  try:
 
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)]
 
 
475
  hist = list(hist) + [{"role": "user", "content": user_text}]
476
 
477
+ # Phase: marking tasks
478
  if phase == "marking":
479
  valid_ids = {t["task_id"] for t in tasks_state}
480
  mentioned = _parse_ids(user_text, valid_ids)
 
485
  t["completed"] = True
486
  updated.append(t["title"])
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
 
494
+ if wants_skip or done_count == len(tasks_state) or (updated and done_count > 0):
 
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":
511
  try:
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):
535
  if not user_id or not hist:
536
+ return list(chat) + [(None, "\u26a0 Nothing to save yet.")], ""
537
  try:
538
  ctx = _ensure_context(user_id)
539
  updated = synthesize_journal(ctx, tasks_state, hist)
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
 
 
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 "", [], []
 
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.")
 
595
  return "\n".join(get_goals(user_id)) if user_id else ""
596
 
597
 
598
+ # =============================================================================
599
  # PREFERENCES
600
+ # =============================================================================
601
 
602
  def load_prefs(user_id):
603
  ctx = load_user_context(user_id) if user_id else None
 
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
+ html += f'<div style="color:#334155;font-size:11px;margin-top:12px">v{ctx.get("version",1)} \u00b7 {ctx.get("last_updated","\u2014")[:10]}</div></div>'
632
  return html
633
 
634
 
635
+ # =============================================================================
636
  # UI
637
+ # =============================================================================
638
 
639
+ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
640
 
641
  user_id_state = gr.State(None)
642
  username_state = gr.State("")
 
652
  vclar_questions_state = gr.State([])
653
  voriginal_task_state = gr.State("")
654
 
655
+ # AUTH
656
  with gr.Column(visible=True, elem_id="auth-card") as auth_section:
657
+ gr.HTML('<div id="brand-logo">\U0001f9e0 Second Brain</div>')
658
  gr.HTML('<div id="brand-sub">Your intelligent productivity companion</div>')
659
  with gr.Tabs():
660
  with gr.Tab("Sign In"):
661
  login_user_in = gr.Textbox(label="Username", placeholder="your username")
662
+ login_pass_in = gr.Textbox(label="Password", type="password", placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022")
663
  login_btn = gr.Button("Sign In", elem_classes="btn-primary")
664
  login_msg = gr.HTML("")
665
  with gr.Tab("Create Account"):
666
  reg_user_in = gr.Textbox(label="Username", placeholder="choose a username")
667
+ reg_pass_in = gr.Textbox(label="Password (min 6 chars)", type="password")
668
  reg_wake = gr.Textbox(label="Wake time", value="07:30")
669
  reg_sleep = gr.Textbox(label="Sleep time", value="23:00")
670
  reg_focus = gr.Dropdown(label="Peak focus", choices=["Morning","Afternoon","Evening","Night"], value="Morning")
671
+ reg_goals = gr.Textbox(label="Big-picture goals (optional)", lines=3, placeholder="Launch my startup\nGet fit\nLearn ML")
672
  reg_btn = gr.Button("Create Account", elem_classes="btn-primary")
673
  reg_msg = gr.HTML("")
674
 
675
+ # MAIN APP
676
  with gr.Column(visible=False) as app_section:
677
 
678
  with gr.Row(elem_id="top-header"):
679
+ header_html = gr.HTML('<div id="top-header-brand">\U0001f9e0 Second Brain</div><div id="top-header-user">\u2014</div>')
680
  logout_btn = gr.Button("Sign Out", elem_classes="btn-secondary", scale=0)
681
 
682
  with gr.Tabs():
683
 
684
+ # TAB 1: TODAY
685
+ with gr.Tab("\U0001f4c5 Today"):
686
  with gr.Row():
687
+ stat_total = gr.HTML(_stat("\u2014","Today","#a78bfa"))
688
+ stat_done = gr.HTML(_stat("\u2014","Done","#4ade80"))
689
+ stat_remain = gr.HTML(_stat("\u2014","Left","#f87171"))
690
  with gr.Row():
691
+ btn_add_text = gr.Button("\u270f\ufe0f Add Task (Text)", elem_classes="btn-primary", scale=3)
692
+ btn_add_voice = gr.Button("\U0001f399\ufe0f Add Task (Voice)", elem_classes="btn-accent", scale=3)
693
+ btn_plan_day = gr.Button("\U0001f9e0 Plan My Tasks", elem_classes="btn-secondary", scale=3)
694
+ btn_refresh = gr.Button("\u21bb", elem_classes="btn-secondary", scale=1)
695
 
696
+ # Text panel
697
  with gr.Column(visible=False, elem_classes="panel") as text_panel:
698
  gr.HTML('<div class="sec-label">Describe Your Task</div>')
699
+ task_input = gr.Textbox(label="", placeholder="e.g. Finish the client proposal, it\'s really important", lines=2)
700
  with gr.Row():
701
+ btn_parse = gr.Button("\U0001f916 Parse with AI", elem_classes="btn-primary")
702
+ btn_cancel_text = gr.Button("Cancel", elem_classes="btn-secondary")
703
 
704
  with gr.Column(visible=False) as parsed_panel:
705
  parse_status = gr.HTML("")
 
707
  with gr.Column(visible=False) as clar_reply_row:
708
  gr.HTML('<div class="sec-label" style="margin-top:8px">Answer the questions above</div>')
709
  with gr.Row():
710
+ 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")
711
+ clar_audio_input = gr.Audio(sources=["microphone"], type="filepath", label="\U0001f3a4 Or speak", scale=1)
712
+ btn_clar_submit = gr.Button("\U0001f504 Re-classify", elem_classes="btn-accent")
 
 
713
  gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>')
714
+ 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>')
715
  with gr.Row():
716
  f_title = gr.Textbox(label="Title", scale=3)
717
  f_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1)
 
721
  f_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy")
722
  with gr.Row():
723
  f_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
724
+ f_deadline = gr.Textbox(label="\U0001f3af Deadline (YYYY-MM-DD, optional)", placeholder="2026-03-01", scale=2)
725
+ f_is_habit = gr.Checkbox(label="\u267b\ufe0f Habit", scale=1)
726
  f_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1)
727
  with gr.Row():
728
+ btn_confirm = gr.Button("\u2713 Save Task", elem_classes="btn-success")
729
+ btn_discard = gr.Button("\u2717 Discard", elem_classes="btn-danger")
730
  save_msg = gr.HTML("")
731
 
732
+ # Voice panel
733
  with gr.Column(visible=False, elem_classes="panel") as voice_panel:
734
  gr.HTML('<div class="sec-label">Add Task by Voice</div>')
735
+ 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>')
736
  voice_audio_input = gr.Audio(sources=["microphone","upload"], type="filepath", label="")
737
  with gr.Row():
738
+ btn_voice_parse = gr.Button("\U0001f916 Transcribe & Classify", elem_classes="btn-primary")
739
+ btn_cancel_voice = gr.Button("Cancel", elem_classes="btn-secondary")
740
  voice_status = gr.HTML("")
741
 
742
  with gr.Column(visible=False) as voice_parsed_panel:
 
745
  with gr.Column(visible=False) as voice_clar_row:
746
  gr.HTML('<div class="sec-label" style="margin-top:8px">Answer the questions above</div>')
747
  with gr.Row():
748
+ vclar_text = gr.Textbox(label="Type your answer", lines=2, scale=4)
749
+ vclar_audio = gr.Audio(sources=["microphone"], type="filepath", label="\U0001f3a4 Or speak", scale=1)
750
+ btn_vclar_submit = gr.Button("\U0001f504 Re-classify", elem_classes="btn-accent")
 
751
  gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>')
752
+ gr.HTML('<p style="color:#475569;font-size:12px;margin:0 0 10px">Date assigned by AI scheduler after saving.</p>')
753
  with gr.Row():
754
  vf_title = gr.Textbox(label="Title", scale=3)
755
  vf_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1)
 
759
  vf_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy")
760
  with gr.Row():
761
  vf_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
762
+ vf_deadline = gr.Textbox(label="\U0001f3af Deadline (optional)", placeholder="2026-03-01", scale=2)
763
+ vf_is_habit = gr.Checkbox(label="\u267b\ufe0f Habit", scale=1)
764
  vf_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1)
765
  with gr.Row():
766
+ btn_voice_confirm = gr.Button("\u2713 Save Task", elem_classes="btn-success")
767
+ btn_voice_discard = gr.Button("\u2717 Discard", elem_classes="btn-danger")
768
  voice_save_msg = gr.HTML("")
769
 
770
+ # Plan My Tasks panel
771
  with gr.Column(visible=False, elem_classes="panel") as plan_panel:
772
+ gr.HTML('<div class="sec-label">Plan My Tasks</div>')
773
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">'
774
+ 'The AI reads your unscheduled tasks, your learned patterns, goals, and current time \u2014 '
775
+ 'then assigns each task to the best future date. It never places tasks in the past.'
776
+ '</p>')
777
+ plan_prompt = gr.Textbox(
778
+ label="Tell the AI what you want",
779
+ placeholder="e.g. \'Clear today, schedule everything from tomorrow\' or \'Focus on Work tasks this week\' or \'I have a free morning Thursday\'",
780
+ lines=3
781
+ )
782
  with gr.Row():
783
+ btn_gen_sched = gr.Button("\U0001f9e0 Run AI Scheduler", elem_classes="btn-primary")
784
+ btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary")
785
  schedule_html = gr.HTML("")
786
 
787
  gr.HTML('<div class="sec-label" style="margin-top:20px">Today\'s Tasks</div>')
788
  today_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
789
  with gr.Row():
790
+ today_done_id = gr.Number(label="Task ID \u2014 toggle done", precision=0, scale=2)
791
+ btn_today_done = gr.Button("\u2705 Mark Done", elem_classes="btn-success", scale=2)
792
+ today_del_id = gr.Number(label="Task ID \u2014 delete", precision=0, scale=2)
793
+ btn_today_del = gr.Button("\U0001f5d1 Delete", elem_classes="btn-danger", scale=2)
794
  today_action_msg = gr.HTML("")
795
 
796
+ # TAB 2: ALL TASKS
797
+ with gr.Tab("\U0001f4cb All Tasks"):
798
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">All tasks including unscheduled ones. Filter and manage here.</p>')
799
  with gr.Row():
800
+ all_filter = gr.Dropdown(label="Filter by Area", choices=["All"], value="All", scale=3)
801
  all_today_chk = gr.Checkbox(label="Today only", value=False, scale=1)
802
+ all_unsched = gr.Checkbox(label="Unscheduled only", value=False, scale=1)
803
+ btn_all_ref = gr.Button("\u21bb Refresh", elem_classes="btn-secondary", scale=1)
804
  all_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
805
  with gr.Row():
806
+ all_done_id = gr.Number(label="Task ID \u2014 toggle done", precision=0, scale=2)
807
+ btn_all_done = gr.Button("\u2705 Done", elem_classes="btn-success", scale=2)
808
+ all_del_id = gr.Number(label="Task ID \u2014 delete", precision=0, scale=2)
809
+ btn_all_del = gr.Button("\U0001f5d1 Delete", elem_classes="btn-danger", scale=2)
810
  all_action_msg = gr.HTML("")
811
 
812
+ # TAB 3: JOURNAL
813
+ with gr.Tab("\U0001f4d3 Journal"):
814
+ 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>')
815
  journal_chatbot = gr.Chatbot(label="", height=440)
816
  with gr.Row():
817
+ j_answer = gr.Textbox(label="", placeholder="Type or use the mic\u2026", lines=1, scale=5, interactive=False, show_label=False)
818
+ j_audio_in = gr.Audio(sources=["microphone"], type="filepath", label="\U0001f3a4", scale=1, interactive=False)
819
+ btn_j_send = gr.Button("Send \u2192", elem_classes="btn-accent", scale=1, interactive=False)
 
 
820
  with gr.Row():
821
+ btn_j_start = gr.Button("\u25b6 Start Session", elem_classes="btn-primary", scale=3)
822
+ btn_j_finish = gr.Button("\u2713 Finish & Save", elem_classes="btn-success", scale=3)
823
+ btn_j_restart = gr.Button("\u21ba Reset", elem_classes="btn-secondary", scale=1)
824
  journal_msg = gr.HTML("")
825
 
826
+ # TAB 4: AREAS & GOALS
827
+ with gr.Tab("\U0001f5c2 Areas & Goals"):
828
  gr.HTML('<div class="sec-label">Your Life Areas</div>')
829
  areas_display = gr.HTML("")
830
  with gr.Row():
 
844
  btn_save_goals = gr.Button("Save Goals", elem_classes="btn-primary")
845
  goals_msg = gr.HTML("")
846
 
847
+ # TAB 5: PREFERENCES
848
+ with gr.Tab("\u2699\ufe0f Preferences"):
849
  gr.HTML('<div class="sec-label">Scheduling Preferences</div>')
850
  with gr.Row():
851
  pref_wake = gr.Textbox(label="Wake time", placeholder="07:30", scale=1)
 
858
  prefs_msg = gr.HTML("")
859
  gr.HTML('<hr>')
860
  gr.HTML('<div class="sec-label">AI Memory</div>')
861
+ 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>')
862
  ctx_display = gr.HTML("")
863
+ btn_ref_ctx = gr.Button("\u21bb Refresh", elem_classes="btn-secondary")
864
+
865
 
866
+ # =========================================================================
867
  # EVENT WIRING
868
+ # =========================================================================
869
 
 
870
  def _header_html(uid, uname):
871
+ return f'<div id="top-header-brand">\U0001f9e0 Second Brain</div><div id="top-header-user">\U0001f464 {uname}</div>'
872
 
873
+ # Auth
874
  login_btn.click(handle_login, [login_user_in, login_pass_in],
875
  [user_id_state, username_state, login_msg, auth_section, app_section]
876
  ).then(_header_html, [user_id_state, username_state], [header_html]
877
  ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
878
+ ).then(refresh_all_tasks_auto, [user_id_state], [all_df]
879
  ).then(lambda uid: render_areas(uid)[:2], [user_id_state], [areas_display, del_area_dd]
880
  ).then(load_prefs, [user_id_state], [pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max]
881
  ).then(load_goals_txt, [user_id_state], [goals_input])
 
884
  [user_id_state, username_state, reg_msg, auth_section, app_section]
885
  ).then(_header_html, [user_id_state, username_state], [header_html]
886
  ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
887
+ ).then(refresh_all_tasks_auto, [user_id_state], [all_df]
888
  ).then(lambda uid: render_areas(uid)[:2], [user_id_state], [areas_display, del_area_dd])
889
 
890
  logout_btn.click(handle_logout, [user_id_state], [user_id_state, username_state, auth_section, app_section])
891
 
892
+ # Panels
893
+ btn_add_text.click(show_text_panel, outputs=[text_panel, voice_panel, plan_panel])
894
  btn_add_voice.click(show_voice_panel, outputs=[text_panel, voice_panel, plan_panel])
895
  btn_plan_day.click(show_plan_panel, outputs=[text_panel, voice_panel, plan_panel])
896
  btn_cancel_text.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
 
901
  f_is_habit.change(lambda v: gr.update(visible=v), [f_is_habit], [f_interval])
902
  vf_is_habit.change(lambda v: gr.update(visible=v), [vf_is_habit], [vf_interval])
903
 
904
+ # Text task flow
905
+ # Outputs: parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance,
906
+ # f_state, f_time, f_deadline, f_is_habit, f_interval, parse_status,
907
+ # clar_reply_row, clar_questions_state, original_task_state
908
+ _TEXT_PARSE_OUT = [parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance,
909
+ f_state, f_time, f_deadline, f_is_habit, f_interval, parse_status,
910
+ clar_reply_row, clar_questions_state, original_task_state]
911
+ # clar reply outputs: f_title, clar_html, f_area, f_urgency, f_importance,
912
+ # f_state, f_time, f_deadline, clar_reply_row, clar_questions_state
913
+ _TEXT_CLAR_OUT = [f_title, clar_html, f_area, f_urgency, f_importance,
914
+ f_state, f_time, f_deadline, clar_reply_row, clar_questions_state]
915
 
916
+ btn_parse.click(handle_parse_text, [task_input, user_id_state], _TEXT_PARSE_OUT)
917
 
918
  btn_clar_submit.click(
919
  handle_clarification_reply,
920
  [clar_text_input, original_task_state, clar_questions_state, user_id_state],
921
+ _TEXT_CLAR_OUT
922
  ).then(lambda: ("", None), outputs=[clar_text_input, clar_audio_input])
923
 
924
  clar_audio_input.change(
925
  handle_clar_voice,
926
  [clar_audio_input, original_task_state, clar_questions_state, user_id_state],
927
+ _TEXT_CLAR_OUT
928
  ).then(lambda: None, outputs=[clar_audio_input])
929
 
930
  btn_discard.click(lambda: gr.update(visible=False), outputs=[parsed_panel])
931
+ # Confirm saves task + refreshes BOTH today and all_tasks
932
  btn_confirm.click(
933
  handle_confirm_task,
934
+ [user_id_state, f_title, f_area, f_urgency, f_importance, f_state, f_time, f_deadline, f_is_habit, f_interval],
935
+ [save_msg, today_df, stat_total, stat_done, stat_remain, all_df, parsed_panel]
936
  )
937
 
938
+ # Voice task flow
939
+ _VOICE_PARSE_OUT = [voice_parsed_panel, voice_transcribed_txt,
940
+ voice_clar_html, vf_area, vf_urgency, vf_importance,
941
+ vf_state, vf_time, vf_deadline, vf_is_habit, vf_interval,
942
+ voice_status, voice_clar_row,
943
+ vclar_questions_state, voriginal_task_state]
944
+ _VOICE_CLAR_OUT = [vf_title, voice_clar_html, vf_area, vf_urgency, vf_importance,
945
+ vf_state, vf_time, vf_deadline, voice_clar_row, vclar_questions_state]
946
 
947
+ btn_voice_parse.click(handle_voice_parse, [voice_audio_input, user_id_state], _VOICE_PARSE_OUT)
948
 
949
  btn_vclar_submit.click(
950
  handle_clarification_reply,
951
  [vclar_text, voriginal_task_state, vclar_questions_state, user_id_state],
952
+ _VOICE_CLAR_OUT
953
  ).then(lambda: ("", None), outputs=[vclar_text, vclar_audio])
954
 
955
  vclar_audio.change(
956
  handle_clar_voice,
957
  [vclar_audio, voriginal_task_state, vclar_questions_state, user_id_state],
958
+ _VOICE_CLAR_OUT
959
  ).then(lambda: None, outputs=[vclar_audio])
960
 
961
  btn_voice_discard.click(lambda: gr.update(visible=False), outputs=[voice_parsed_panel])
962
  btn_voice_confirm.click(
963
  handle_confirm_task,
964
+ [user_id_state, vf_title, vf_area, vf_urgency, vf_importance, vf_state, vf_time, vf_deadline, vf_is_habit, vf_interval],
965
+ [voice_save_msg, today_df, stat_total, stat_done, stat_remain, all_df, voice_parsed_panel]
966
  )
967
 
968
+ # Smart scheduler — updates both schedule_html and all_df
969
+ btn_gen_sched.click(
970
+ handle_smart_schedule, [user_id_state, plan_prompt],
971
+ [schedule_html, all_df]
972
+ ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain])
973
+
974
  btn_today_done.click(handle_toggle_today, [today_done_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg])
975
  btn_today_del.click(handle_delete_today, [today_del_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg])
976
 
977
+ # All Tasks
978
+ def refresh_all_tasks_filtered(user_id, filter_area, only_today, only_unsched):
979
+ if not user_id: return []
980
+ return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today, only_unscheduled=only_unsched))
981
+
982
+ btn_all_ref.click(refresh_all_tasks_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
983
+ all_filter.change(refresh_all_tasks_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
984
+ all_today_chk.change(refresh_all_tasks_filtered,[user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
985
+ all_unsched.change(refresh_all_tasks_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
986
+
987
+ def handle_toggle_all_f(task_id, user_id, filter_area, only_today, only_unsched):
988
+ if task_id and user_id: toggle_task_complete(int(task_id), user_id)
989
+ return refresh_all_tasks_filtered(user_id, filter_area, only_today, only_unsched), _ok("Updated")
990
+ def handle_delete_all_f(task_id, user_id, filter_area, only_today, only_unsched):
991
+ if task_id and user_id: delete_task(int(task_id), user_id)
992
+ return refresh_all_tasks_filtered(user_id, filter_area, only_today, only_unsched), _ok("Deleted")
993
+
994
+ 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])
995
+ 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])
996
+
997
+ # Journal
998
+ _J_START_OUT = [journal_chatbot, journal_hist_state, journal_active,
999
+ j_answer, btn_j_send, j_audio_in, journal_tasks_state,
1000
+ journal_phase_state, journal_msg]
1001
+ _J_MSG_IN = [user_id_state, j_answer, j_audio_in, journal_chatbot,
1002
+ journal_hist_state, journal_tasks_state, journal_active, journal_phase_state]
1003
+ _J_MSG_OUT = [journal_chatbot, journal_hist_state, j_answer, j_audio_in,
1004
+ journal_tasks_state, journal_active, journal_phase_state, journal_msg]
1005
+
1006
+ btn_j_start.click(handle_start_journal, [user_id_state], _J_START_OUT)
1007
+ btn_j_send.click(handle_journal_message, _J_MSG_IN, _J_MSG_OUT)
1008
+ j_answer.submit(handle_journal_message, _J_MSG_IN, _J_MSG_OUT)
1009
+ j_audio_in.change(handle_journal_message, _J_MSG_IN, _J_MSG_OUT)
1010
 
1011
  btn_j_finish.click(
1012
  handle_finish_journal,
1013
  [user_id_state, journal_chatbot, journal_hist_state, journal_tasks_state],
1014
  [journal_chatbot, journal_msg]
1015
  )
1016
+ btn_j_restart.click(handle_restart_journal, outputs=[
1017
+ journal_chatbot, journal_hist_state, journal_active,
1018
+ j_answer, btn_j_send, j_audio_in, journal_tasks_state,
1019
+ journal_phase_state, journal_msg
1020
+ ])
 
1021
 
1022
+ # Areas & Goals
1023
  btn_add_area.click(
1024
  handle_add_area, [user_id_state, new_area_name, new_area_color],
1025
  [area_msg, areas_display, del_area_dd, new_area_name]
 
1032
 
1033
  btn_save_goals.click(handle_save_goals, [user_id_state, goals_input], [goals_msg])
1034
 
1035
+ # Preferences
1036
  btn_save_prefs.click(
1037
  handle_save_prefs,
1038
  [user_id_state, pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max],
 
1040
  )
1041
  btn_ref_ctx.click(render_context_html, [user_id_state], [ctx_display])
1042
 
1043
+
1044
  if __name__ == "__main__":
1045
  demo.launch(server_name="0.0.0.0", server_port=7860, css=CSS)