eraz3r commited on
Commit
cc4e015
Β·
verified Β·
1 Parent(s): 162afa2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +279 -88
app.py CHANGED
@@ -15,6 +15,7 @@ HuggingFace Spaces:
15
  """
16
 
17
  import os
 
18
  import gradio as gr
19
  from datetime import date, datetime
20
 
@@ -23,7 +24,7 @@ from core.database import (
23
  init_db, register_user, login_user, get_username,
24
  create_default_life_areas, save_goals, get_goals,
25
  get_life_areas, get_life_area_names, add_life_area, delete_life_area,
26
- get_tasks, save_task, toggle_task_complete, delete_task, get_today_stats,
27
  load_user_context, save_user_context, spawn_due_habits,
28
  )
29
  from core.ai_engine import (
@@ -37,7 +38,7 @@ from core.styles import CSS
37
  init_db()
38
  init_groq() # reads GROQ_API_KEY from env
39
 
40
- TASK_HEADERS = ["ID", "Done", "Title", "Area", "Urgency", "Importance", "Mind", "Min", "Date"]
41
 
42
 
43
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -52,18 +53,33 @@ def _stat(val, label, color):
52
  return f'<div class="stat-card"><div class="stat-num" style="color:{color}">{val}</div><div class="stat-label">{label}</div></div>'
53
 
54
  def _fmt_tasks(tasks: list) -> list:
 
55
  rows = []
56
  for t in tasks:
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  rows.append([
58
  t["id"],
59
  "βœ…" if t["is_completed"] else "⬜",
60
  ("πŸ” " if t["is_habit"] else "") + t["title"],
61
- t["life_area"] or "β€”",
62
- t["urgency"] or "β€”",
63
- t["importance"] or "β€”",
64
  t["state_of_mind"] or "β€”",
65
  str(t["time_estimate"]) + "m" if t["time_estimate"] else "β€”",
66
- t["scheduled_date"] or "β€”",
 
67
  ])
68
  return rows
69
 
@@ -174,6 +190,9 @@ def handle_parse_text(task_text, user_id):
174
  if area_val not in area_choices:
175
  area_choices = [area_val] + area_choices
176
 
 
 
 
177
  return (
178
  gr.update(visible=True),
179
  result.get("title", task_text),
@@ -183,13 +202,13 @@ def handle_parse_text(task_text, user_id):
183
  result.get("importance", "Important"),
184
  result.get("state_of_mind", "Easy"),
185
  int(result.get("time_estimate") or 30),
186
- str(date.today()),
187
  False,
188
  "Daily",
189
- _ok("AI classified your task β€” please answer the clarification questions above." if has_clarifications else "AI classified your task"),
190
- gr.update(visible=has_clarifications), # clar_reply_row
191
- clarifications, # clar_questions_state
192
- task_text, # original_task_state
193
  )
194
 
195
 
@@ -264,69 +283,121 @@ _whisper_model = None
264
 
265
 
266
  def handle_confirm_task(user_id, title, area, urgency, importance, state,
267
- time_est, sched_date, is_habit, habit_interval):
268
  if not user_id:
269
- return _err("Not logged in."), [], _stat("β€”","Today","#a78bfa"), _stat("β€”","Done","#4ade80"), _stat("β€”","Left","#f87171"), gr.update(visible=False)
270
  if not title.strip():
271
- return _err("Title cannot be empty."), [], _stat("β€”","Today","#a78bfa"), _stat("β€”","Done","#4ade80"), _stat("β€”","Left","#f87171"), gr.update(visible=True)
272
 
273
  task = {
274
  "title": title, "life_area": area,
275
  "urgency": urgency, "importance": importance, "state_of_mind": state,
276
  "time_estimate": int(time_est or 30),
 
277
  "is_habit": is_habit,
278
  "habit_interval": habit_interval if is_habit else "",
279
  }
280
- save_task(user_id, task, sched_date)
281
  rows, s1, s2, s3 = refresh_today(user_id)
282
- return _ok("Task saved!"), rows, s1, s2, s3, gr.update(visible=False)
283
-
284
-
285
- def handle_generate_schedule(user_id, prompt):
286
- if not user_id:
287
- return _err("Not logged in.")
288
- tasks = get_tasks(user_id, only_today=True, include_completed=False)
289
- if not tasks:
290
- return _err("No incomplete tasks today to schedule.")
291
-
292
- ctx = _ensure_context(user_id)
293
- goals = get_goals(user_id)
294
- sched = generate_schedule(ctx, tasks, prompt or "Schedule my day sensibly.", goals)
295
 
296
- if "error" in sched and "scheduled_tasks" not in sched:
297
- return _err(f"Scheduling failed: {sched.get('error')}")
298
 
 
299
  COLOR = {"Flow": "#0ea5e9", "Easy": "#4ade80", "Quick": "#a78bfa", "Personal": "#f87171"}
300
- html = (
301
- f'<div style="margin-bottom:14px">'
302
- f'<span style="color:#a78bfa;font-size:12px;font-weight:600">πŸ“… {sched.get("schedule_date","Today")}</span>'
303
- f'<p style="color:#475569;font-size:13px;margin:6px 0 0">{sched.get("day_summary","")}</p>'
304
  f'</div>'
305
  )
306
-
307
  for t in sched.get("scheduled_tasks", []):
308
  sm = t.get("state_of_mind", "Easy")
309
  c = COLOR.get(sm, "#6366f1")
310
  html += (
311
- f'<div class="sched-card" style="border-left-color:{c}">'
312
- f'<div class="sched-time">{t.get("start_time")} – {t.get("end_time")}</div>'
313
- f'<div class="sched-title">{t.get("title")}</div>'
314
- f'<div class="sched-meta">{t.get("life_area","β€”")} Β· {sm} Β· {t.get("duration_minutes")}min</div>'
315
  f'<div class="sched-why">{t.get("scheduling_reason","")}</div>'
316
  f'</div>'
317
  )
318
-
319
  if sched.get("deferred_tasks"):
320
- html += '<div style="margin-top:12px;color:#334155;font-size:11px;font-weight:600;letter-spacing:.6px;text-transform:uppercase">Deferred</div>'
321
  for t in sched["deferred_tasks"]:
322
- html += f'<div style="color:#475569;font-size:12px;padding:3px 0">βœ— {t["title"]} β€” {t.get("reason","")}</div>'
323
-
324
  for w in sched.get("warnings", []):
325
- html += f'<div style="margin-top:8px;color:#f59e0b;font-size:12px">⚠ {w}</div>'
326
-
327
  return html
328
 
329
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  def handle_toggle_today(task_id, user_id):
331
  if task_id and user_id:
332
  toggle_task_complete(int(task_id), user_id)
@@ -423,16 +494,8 @@ def handle_start_journal(user_id, tasks_state):
423
  if not tasks_state:
424
  return [], [], False, gr.update(interactive=False), gr.update(interactive=False), _err("Load today's tasks first.")
425
 
426
- try:
427
- ctx = _ensure_context(user_id)
428
- opening = build_opening_question(ctx, tasks_state)
429
- except Exception as e:
430
- return (
431
- [{"role": "assistant", "content": f"⚠ Could not start session: {e}"}],
432
- [], False,
433
- gr.update(interactive=False), gr.update(interactive=False),
434
- _err(f"Start failed: {e}")
435
- )
436
  chat = [{"role": "assistant", "content": opening["question"]}]
437
  hist = [{"role": "assistant", "content": opening["question"], "focus": opening["question_focus"]}]
438
 
@@ -448,24 +511,17 @@ def handle_send_answer(user_id, answer, chat, hist, tasks_state, active):
448
  if not active or not answer.strip():
449
  return chat, hist, "", ""
450
 
451
- # Append user message as dict (Gradio 4.44 requires dict format for Chatbot)
452
  chat = list(chat) + [{"role": "user", "content": answer}]
453
  hist = list(hist) + [{"role": "user", "content": answer}]
454
 
455
- ctx = _ensure_context(user_id)
456
- try:
457
- result = get_next_journal_question(ctx, tasks_state, hist)
458
- except Exception as e:
459
- chat = list(chat) + [{"role": "assistant", "content": f"⚠ AI error: {e}. Please try again."}]
460
- return chat, hist, "", _err(f"AI error: {e}")
461
 
462
  if result.get("session_complete"):
463
- bot_reply = "βœ… That's enough for today. Click **Finish & Save** to update your profile."
464
- hist.append({"role": "assistant", "content": bot_reply})
465
- chat = list(chat) + [{"role": "assistant", "content": bot_reply}]
466
  return chat, hist, "", _ok("Session complete β€” save when ready")
467
 
468
- next_q = result.get("question", "Anything else you'd like to reflect on?")
469
  hist.append({"role": "assistant", "content": next_q, "focus": result.get("question_focus", "")})
470
  chat = list(chat) + [{"role": "assistant", "content": next_q}]
471
  return chat, hist, "", ""
@@ -475,12 +531,9 @@ def handle_finish_journal(user_id, chat, hist, tasks_state):
475
  if not user_id or not hist:
476
  return chat, _err("Nothing to save.")
477
 
478
- try:
479
- ctx = _ensure_context(user_id)
480
- updated = synthesize_journal(ctx, tasks_state, hist)
481
- save_user_context(user_id, updated)
482
- except Exception as e:
483
- return chat, _err(f"Save failed: {e}")
484
 
485
  total = len(tasks_state)
486
  done = sum(1 for t in tasks_state if t.get("completed"))
@@ -654,6 +707,76 @@ def render_context_html(user_id):
654
  return html
655
 
656
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
  # ═══════════════════════════════════════════════════════════════════════════════
658
  # UI BUILD
659
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -763,7 +886,7 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
763
  f_title = gr.Textbox(label="Title", scale=3)
764
  f_area = gr.Dropdown(label="Life Area",
765
  choices=["Work","Health","Finance","Learning","Personal","Family","Other"],
766
- scale=1, allow_custom_value=True)
767
  with gr.Row():
768
  f_urgency = gr.Dropdown(label="Urgency",
769
  choices=["Urgent","Not Urgent","Habit"], value="Not Urgent")
@@ -774,7 +897,7 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
774
  choices=["Flow","Easy","Quick","Personal"], value="Easy")
775
  with gr.Row():
776
  f_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
777
- f_date = gr.Textbox(label="Scheduled Date", value=str(date.today()), scale=1)
778
  f_is_habit = gr.Checkbox(label="♻️ This is a habit", scale=1)
779
  f_interval = gr.Dropdown(label="Recurs",
780
  choices=["Daily","Weekly","Weekdays","Weekends","Monthly"],
@@ -799,16 +922,34 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
799
  # ── Plan My Day panel ─────────────────────────────────────
800
  with gr.Column(visible=False, elem_classes="panel") as plan_panel:
801
  gr.HTML('<div class="sec-label">Plan My Day</div>')
802
- gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:12px">The AI reads your tasks, context, and goals to build the optimal schedule.</p>')
803
  plan_prompt = gr.Textbox(
804
- label="Any fixed events or preferences today?",
805
- placeholder="e.g. I have a meeting at 10am, want deep work in the morning, done by 7pm",
806
- lines=3
807
  )
808
  with gr.Row():
809
- btn_gen_sched = gr.Button("✨ Generate Schedule", elem_classes="btn-primary")
810
  btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary")
811
- schedule_html = gr.HTML("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
812
 
813
  # ── Today's task table ────────────────────────────────────
814
  gr.HTML('<div class="sec-label" style="margin-top:20px">Today\'s Tasks</div>')
@@ -838,7 +979,16 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
838
  all_action_msg = gr.HTML("")
839
 
840
  # ─────────────────────────────────────────────────────────────
841
- # TAB 3: JOURNAL
 
 
 
 
 
 
 
 
 
842
  # ─────────────────────────────────────────────────────────────
843
  with gr.Tab("πŸ““ Journal"):
844
  gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:16px">Mark today\'s tasks complete, then reflect. The AI updates your profile to improve tomorrow\'s schedule.</p>')
@@ -940,6 +1090,11 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
940
  ).then(
941
  refresh_today, [user_id_state],
942
  [today_df, stat_total, stat_done, stat_remain]
 
 
 
 
 
943
  ).then(
944
  lambda uid: render_areas(uid)[0:2],
945
  [user_id_state], [areas_display, del_area_dd]
@@ -960,6 +1115,11 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
960
  ).then(
961
  refresh_today, [user_id_state],
962
  [today_df, stat_total, stat_done, stat_remain]
 
 
 
 
 
963
  ).then(
964
  lambda uid: render_areas(uid)[0:2],
965
  [user_id_state], [areas_display, del_area_dd]
@@ -1024,25 +1184,56 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
1024
  handle_confirm_task,
1025
  [user_id_state, f_title, f_area, f_urgency, f_importance, f_state,
1026
  f_time, f_date, f_is_habit, f_interval],
1027
- [save_msg, today_df, stat_total, stat_done, stat_remain, parsed_panel]
1028
- )
1029
 
1030
  btn_gen_sched.click(
1031
  handle_generate_schedule,
1032
  [user_id_state, plan_prompt],
1033
- [schedule_html]
 
 
 
 
 
 
 
 
 
1034
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
1035
 
1036
  btn_today_done.click(
1037
  handle_toggle_today,
1038
  [today_done_id, user_id_state],
1039
  [today_df, stat_total, stat_done, stat_remain, today_action_msg]
1040
- )
 
 
 
1041
  btn_today_del.click(
1042
  handle_delete_today,
1043
  [today_del_id, user_id_state],
1044
  [today_df, stat_total, stat_done, stat_remain, today_action_msg]
1045
- )
 
 
 
 
 
 
1046
 
1047
  # ── All Tasks tab ──────────────────────────────────────────────────────
1048
 
@@ -1065,12 +1256,12 @@ with gr.Blocks(title="🧠 Second Brain") as demo:
1065
  handle_toggle_all,
1066
  [all_done_id, user_id_state, all_filter, all_today_chk],
1067
  [all_df, all_action_msg]
1068
- )
1069
  btn_all_del.click(
1070
  handle_delete_all,
1071
  [all_del_id, user_id_state, all_filter, all_today_chk],
1072
  [all_df, all_action_msg]
1073
- )
1074
 
1075
  # ── Journal tab ────────────────────────────────────────────────────────
1076
 
 
15
  """
16
 
17
  import os
18
+ import json as _json
19
  import gradio as gr
20
  from datetime import date, datetime
21
 
 
24
  init_db, register_user, login_user, get_username,
25
  create_default_life_areas, save_goals, get_goals,
26
  get_life_areas, get_life_area_names, add_life_area, delete_life_area,
27
+ get_tasks, save_task, assign_task_date, toggle_task_complete, delete_task, get_today_stats,
28
  load_user_context, save_user_context, spawn_due_habits,
29
  )
30
  from core.ai_engine import (
 
38
  init_db()
39
  init_groq() # reads GROQ_API_KEY from env
40
 
41
+ TASK_HEADERS = ["ID", "Done", "Title", "Area", "Urgency", "Importance", "Mind", "Min", "Deadline", "Scheduled"]
42
 
43
 
44
  # ═══════════════════════════════════════════════════════════════════════════════
 
53
  return f'<div class="stat-card"><div class="stat-num" style="color:{color}">{val}</div><div class="stat-label">{label}</div></div>'
54
 
55
  def _fmt_tasks(tasks: list) -> list:
56
+ _today = date.today()
57
  rows = []
58
  for t in tasks:
59
+ dl = (t.get("deadline_date") or "").strip()
60
+ if dl and dl.lower() not in ("null", "none", ""):
61
+ try:
62
+ diff = (date.fromisoformat(dl) - _today).days
63
+ if diff < 0: dl = f"❌ {dl}"
64
+ elif diff == 0: dl = "🚨 TODAY"
65
+ elif diff == 1: dl = "⚠️ tomorrow"
66
+ elif diff <= 3: dl = f"⏰ {dl}"
67
+ except ValueError:
68
+ pass
69
+ else:
70
+ dl = "β€”"
71
+ sched = (t.get("scheduled_date") or "").strip()
72
  rows.append([
73
  t["id"],
74
  "βœ…" if t["is_completed"] else "⬜",
75
  ("πŸ” " if t["is_habit"] else "") + t["title"],
76
+ t["life_area"] or "β€”",
77
+ t["urgency"] or "β€”",
78
+ t["importance"] or "β€”",
79
  t["state_of_mind"] or "β€”",
80
  str(t["time_estimate"]) + "m" if t["time_estimate"] else "β€”",
81
+ dl,
82
+ sched if sched else "⬜ unscheduled",
83
  ])
84
  return rows
85
 
 
190
  if area_val not in area_choices:
191
  area_choices = [area_val] + area_choices
192
 
193
+ raw_dl = result.get("deadline_date") or ""
194
+ dl_val = "" if str(raw_dl).lower() in ("null", "none", "n/a", "") else str(raw_dl)
195
+
196
  return (
197
  gr.update(visible=True),
198
  result.get("title", task_text),
 
202
  result.get("importance", "Important"),
203
  result.get("state_of_mind", "Easy"),
204
  int(result.get("time_estimate") or 30),
205
+ dl_val,
206
  False,
207
  "Daily",
208
+ _ok("AI classified βœ“ β€” answer questions above." if has_clarifications else "AI classified βœ“"),
209
+ gr.update(visible=has_clarifications),
210
+ clarifications,
211
+ task_text,
212
  )
213
 
214
 
 
283
 
284
 
285
  def handle_confirm_task(user_id, title, area, urgency, importance, state,
286
+ time_est, deadline, is_habit, habit_interval):
287
  if not user_id:
288
+ return _err("Not logged in."), [], _stat("β€”","Today","#a78bfa"), _stat("β€”","Done","#4ade80"), _stat("β€”","Left","#f87171"), [], gr.update(visible=False)
289
  if not title.strip():
290
+ return _err("Title cannot be empty."), [], _stat("β€”","Today","#a78bfa"), _stat("β€”","Done","#4ade80"), _stat("β€”","Left","#f87171"), [], gr.update(visible=True)
291
 
292
  task = {
293
  "title": title, "life_area": area,
294
  "urgency": urgency, "importance": importance, "state_of_mind": state,
295
  "time_estimate": int(time_est or 30),
296
+ "deadline_date": (deadline or "").strip(),
297
  "is_habit": is_habit,
298
  "habit_interval": habit_interval if is_habit else "",
299
  }
300
+ save_task(user_id, task, scheduled_date="") # saved unscheduled β€” planner assigns date
301
  rows, s1, s2, s3 = refresh_today(user_id)
302
+ all_rows = _fmt_tasks(get_tasks(user_id))
303
+ return _ok("βœ“ Task saved! Use 'Plan My Day' to schedule it."), rows, s1, s2, s3, all_rows, gr.update(visible=False)
 
 
 
 
 
 
 
 
 
 
 
304
 
 
 
305
 
306
+ def _render_sched_html(sched):
307
  COLOR = {"Flow": "#0ea5e9", "Easy": "#4ade80", "Quick": "#a78bfa", "Personal": "#f87171"}
308
+ html = (
309
+ f'<div style="padding:12px;background:#0c1a0c;border-radius:8px;margin-bottom:12px">'
310
+ f'<div style="color:#4ade80;font-size:12px;font-weight:700">πŸ“… {sched.get("schedule_date","Today")}</div>'
311
+ f'<div style="color:#94a3b8;font-size:13px;margin-top:4px">{sched.get("day_summary","")}</div>'
312
  f'</div>'
313
  )
 
314
  for t in sched.get("scheduled_tasks", []):
315
  sm = t.get("state_of_mind", "Easy")
316
  c = COLOR.get(sm, "#6366f1")
317
  html += (
318
+ f'<div class="sched-card" style="border-left-color:{c};margin-bottom:6px">'
319
+ f'<div class="sched-time">{t.get("start_time","?")} – {t.get("end_time","?")}</div>'
320
+ f'<div class="sched-title">{t.get("title","")}</div>'
321
+ f'<div class="sched-meta">{t.get("life_area","β€”")} Β· {sm} Β· {t.get("duration_minutes","?")}min</div>'
322
  f'<div class="sched-why">{t.get("scheduling_reason","")}</div>'
323
  f'</div>'
324
  )
 
325
  if sched.get("deferred_tasks"):
326
+ html += '<div style="color:#475569;font-size:11px;font-weight:600;margin:10px 0 4px;text-transform:uppercase">Deferred</div>'
327
  for t in sched["deferred_tasks"]:
328
+ html += f'<div style="color:#475569;font-size:12px;padding:2px 0">βœ— {t["title"]} β€” {t.get("reason","")}</div>'
 
329
  for w in sched.get("warnings", []):
330
+ html += f'<div style="color:#f59e0b;font-size:12px;margin-top:6px">⚠ {w}</div>'
 
331
  return html
332
 
333
 
334
+ def handle_generate_schedule(user_id, prompt):
335
+ """Step 1: generate proposal only. Nothing written to DB."""
336
+ if not user_id:
337
+ return _err("Not logged in."), "", gr.update(visible=False)
338
+ tasks = get_tasks(user_id, include_completed=False)
339
+ if not tasks:
340
+ return _err("No tasks found. Add some tasks first."), "", gr.update(visible=False)
341
+ ctx = _ensure_context(user_id)
342
+ goals = get_goals(user_id)
343
+ try:
344
+ sched = generate_schedule(ctx, tasks, prompt or "Schedule all my tasks intelligently across upcoming days.", goals)
345
+ except Exception as e:
346
+ return _err(f"Scheduling error: {e}"), "", gr.update(visible=False)
347
+ if "error" in sched and "scheduled_tasks" not in sched:
348
+ return _err(f"Scheduling failed: {sched.get('error')}"), "", gr.update(visible=False)
349
+ html = _render_sched_html(sched)
350
+ return html, _json.dumps(sched), gr.update(visible=True)
351
+
352
+
353
+ def handle_confirm_schedule(user_id, pending_json):
354
+ """Step 2a: apply the proposed schedule to the database."""
355
+ if not user_id or not pending_json:
356
+ return _err("Nothing to confirm."), gr.update(visible=False), [], []
357
+ try:
358
+ sched = _json.loads(pending_json)
359
+ except Exception:
360
+ return _err("Could not read proposal."), gr.update(visible=False), [], []
361
+ count = 0
362
+ sched_date = sched.get("schedule_date", str(date.today()))
363
+ for t in sched.get("scheduled_tasks", []):
364
+ tid = t.get("task_id") or t.get("id")
365
+ if tid:
366
+ try:
367
+ assign_task_date(int(tid), user_id, sched_date)
368
+ count += 1
369
+ except Exception:
370
+ pass
371
+ all_rows = _fmt_tasks(get_tasks(user_id))
372
+ today_rows = _fmt_tasks(get_tasks(user_id, only_today=True))
373
+ return _ok(f"βœ“ {count} tasks scheduled for {sched_date}!"), gr.update(visible=False), all_rows, today_rows
374
+
375
+
376
+ def handle_refine_schedule(user_id, refinement, pending_json):
377
+ """Step 2b: re-run with a refinement instruction."""
378
+ if not user_id or not refinement.strip():
379
+ return gr.update(), gr.update(), gr.update()
380
+ hint = ""
381
+ if pending_json:
382
+ try:
383
+ old = _json.loads(pending_json)
384
+ hint = f"\n\nPrevious plan summary: {old.get('day_summary', '')}"
385
+ except Exception:
386
+ pass
387
+ return handle_generate_schedule(user_id, refinement + hint)
388
+
389
+
390
+ def handle_manual_assign(task_id, task_date, user_id):
391
+ """Manually assign a date to a specific task."""
392
+ if not task_id or not task_date or not user_id:
393
+ return _err("Enter task ID and date.")
394
+ try:
395
+ assign_task_date(int(task_id), user_id, task_date.strip())
396
+ return _ok(f"Task #{int(task_id)} assigned to {task_date.strip()}")
397
+ except Exception as e:
398
+ return _err(f"Error: {e}")
399
+
400
+
401
  def handle_toggle_today(task_id, user_id):
402
  if task_id and user_id:
403
  toggle_task_complete(int(task_id), user_id)
 
494
  if not tasks_state:
495
  return [], [], False, gr.update(interactive=False), gr.update(interactive=False), _err("Load today's tasks first.")
496
 
497
+ ctx = _ensure_context(user_id)
498
+ opening = build_opening_question(ctx, tasks_state)
 
 
 
 
 
 
 
 
499
  chat = [{"role": "assistant", "content": opening["question"]}]
500
  hist = [{"role": "assistant", "content": opening["question"], "focus": opening["question_focus"]}]
501
 
 
511
  if not active or not answer.strip():
512
  return chat, hist, "", ""
513
 
 
514
  chat = list(chat) + [{"role": "user", "content": answer}]
515
  hist = list(hist) + [{"role": "user", "content": answer}]
516
 
517
+ ctx = _ensure_context(user_id)
518
+ result = get_next_journal_question(ctx, tasks_state, hist)
 
 
 
 
519
 
520
  if result.get("session_complete"):
521
+ chat = list(chat) + [{"role": "assistant", "content": "βœ… That's enough for today. Click **Finish & Save** to update your profile."}]
 
 
522
  return chat, hist, "", _ok("Session complete β€” save when ready")
523
 
524
+ next_q = result.get("question", "")
525
  hist.append({"role": "assistant", "content": next_q, "focus": result.get("question_focus", "")})
526
  chat = list(chat) + [{"role": "assistant", "content": next_q}]
527
  return chat, hist, "", ""
 
531
  if not user_id or not hist:
532
  return chat, _err("Nothing to save.")
533
 
534
+ ctx = _ensure_context(user_id)
535
+ updated = synthesize_journal(ctx, tasks_state, hist)
536
+ save_user_context(user_id, updated)
 
 
 
537
 
538
  total = len(tasks_state)
539
  done = sum(1 for t in tasks_state if t.get("completed"))
 
707
  return html
708
 
709
 
710
+ # ═════════════════════════════════════════════════════════════════════════════��═
711
+ # CALENDAR VIEW
712
+ # ═══════════════════════════════════════════════════════════════════════════════
713
+
714
+ def render_calendar(user_id):
715
+ if not user_id:
716
+ return "<p style='color:#475569;font-size:13px'>Sign in to view calendar.</p>"
717
+ tasks = get_tasks(user_id)
718
+ if not tasks:
719
+ return "<p style='color:#475569;font-size:13px'>No tasks yet.</p>"
720
+ _today = date.today()
721
+ COLOR = {"Flow":"#0ea5e9","Easy":"#4ade80","Quick":"#a78bfa","Personal":"#f87171"}
722
+ by_date, unscheduled = {}, []
723
+ for t in tasks:
724
+ sd = (t.get("scheduled_date") or "").strip()
725
+ if sd:
726
+ by_date.setdefault(sd, []).append(t)
727
+ else:
728
+ unscheduled.append(t)
729
+ html = ""
730
+ for d in sorted(by_date.keys()):
731
+ try:
732
+ dt = date.fromisoformat(d)
733
+ diff = (dt - _today).days
734
+ if diff == 0: label = f"TODAY β€” {dt.strftime('%a, %b %d')}"
735
+ elif diff == 1: label = f"TOMORROW β€” {dt.strftime('%a, %b %d')}"
736
+ elif diff < 0: label = dt.strftime('%a, %b %d') + " (past)"
737
+ else: label = dt.strftime('%a, %b %d')
738
+ hdr = "#a78bfa" if diff == 0 else "#60a5fa" if diff > 0 else "#334155"
739
+ except ValueError:
740
+ label, hdr = d, "#60a5fa"
741
+ html += (f'<div style="margin-bottom:20px">'
742
+ f'<div style="color:{hdr};font-size:11px;font-weight:700;letter-spacing:1px;'
743
+ f'text-transform:uppercase;padding:6px 0;border-bottom:1px solid #1e293b;margin-bottom:8px">'
744
+ f'πŸ“… {label}</div>')
745
+ for t in by_date[d]:
746
+ sm = t.get("state_of_mind","Easy")
747
+ c = COLOR.get(sm,"#6366f1")
748
+ done = t.get("is_completed")
749
+ dl = (t.get("deadline_date") or "").strip()
750
+ dl_badge = (f'<span style="background:#f8714422;color:#f87171;font-size:10px;'
751
+ f'padding:1px 5px;border-radius:4px;margin-left:6px">due {dl}</span>'
752
+ ) if dl and dl.lower() not in ("null","none","") else ""
753
+ html += (f'<div style="padding:8px 10px;margin-bottom:4px;background:#0c0f1a;border-radius:8px;'
754
+ f'border-left:3px solid {c};opacity:{"0.4" if done else "1"}">'
755
+ f'<div style="color:#cbd5e1;font-size:13px">{"βœ…" if done else "⬜"} '
756
+ f'<b>#{t["id"]}</b> {t["title"]}{dl_badge}</div>'
757
+ f'<div style="color:#475569;font-size:11px;margin-top:2px">'
758
+ f'{t.get("life_area","β€”")} Β· {sm} Β· {t.get("time_estimate","?")}min</div></div>')
759
+ html += '</div>'
760
+ if unscheduled:
761
+ html += ('<div style="margin-top:10px"><div style="color:#334155;font-size:11px;font-weight:700;'
762
+ 'letter-spacing:1px;text-transform:uppercase;padding:6px 0;border-bottom:1px solid #1e293b;margin-bottom:8px">'
763
+ '⬜ UNSCHEDULED β€” Use Plan My Day to assign dates</div>')
764
+ for t in unscheduled:
765
+ sm = t.get("state_of_mind","Easy")
766
+ c = COLOR.get(sm,"#6366f1")
767
+ dl = (t.get("deadline_date") or "").strip()
768
+ dl_badge = (f'<span style="background:#f8714422;color:#f87171;font-size:10px;'
769
+ f'padding:1px 5px;border-radius:4px;margin-left:6px">due {dl}</span>'
770
+ ) if dl and dl.lower() not in ("null","none","") else ""
771
+ html += (f'<div style="padding:8px 10px;margin-bottom:4px;background:#0c0f1a;border-radius:8px;'
772
+ f'border-left:3px solid {c}">'
773
+ f'<div style="color:#94a3b8;font-size:13px">⬜ <b>#{t["id"]}</b> {t["title"]}{dl_badge}</div>'
774
+ f'<div style="color:#475569;font-size:11px;margin-top:2px">'
775
+ f'{t.get("life_area","β€”")} Β· {sm} Β· {t.get("time_estimate","?")}min</div></div>')
776
+ html += '</div>'
777
+ return html or "<p style='color:#475569'>No tasks.</p>"
778
+
779
+
780
  # ═══════════════════════════════════════════════════════════════════════════════
781
  # UI BUILD
782
  # ═══════════════════════════════════════════════════════════════════════════════
 
886
  f_title = gr.Textbox(label="Title", scale=3)
887
  f_area = gr.Dropdown(label="Life Area",
888
  choices=["Work","Health","Finance","Learning","Personal","Family","Other"],
889
+ scale=1)
890
  with gr.Row():
891
  f_urgency = gr.Dropdown(label="Urgency",
892
  choices=["Urgent","Not Urgent","Habit"], value="Not Urgent")
 
897
  choices=["Flow","Easy","Quick","Personal"], value="Easy")
898
  with gr.Row():
899
  f_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
900
+ f_date = gr.Textbox(label="Deadline (YYYY-MM-DD, optional)", placeholder="e.g. 2026-03-15", value="", scale=1)
901
  f_is_habit = gr.Checkbox(label="♻️ This is a habit", scale=1)
902
  f_interval = gr.Dropdown(label="Recurs",
903
  choices=["Daily","Weekly","Weekdays","Weekends","Monthly"],
 
922
  # ── Plan My Day panel ─────────────────────────────────────
923
  with gr.Column(visible=False, elem_classes="panel") as plan_panel:
924
  gr.HTML('<div class="sec-label">Plan My Day</div>')
925
+ gr.HTML('<p style="color:#94a3b8;font-size:13px;margin-bottom:10px">AI schedules ALL your unscheduled tasks using your patterns and goals. Review before applying.</p>')
926
  plan_prompt = gr.Textbox(
927
+ label="Instructions (optional)",
928
+ placeholder="e.g. 'Schedule everything for this week' or 'I have a meeting at 2pm, focus on Work tasks'",
929
+ lines=2
930
  )
931
  with gr.Row():
932
+ btn_gen_sched = gr.Button("🧠 Generate Plan", elem_classes="btn-primary")
933
  btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary")
934
+ schedule_html = gr.HTML("")
935
+ sched_pending = gr.State("")
936
+ with gr.Column(visible=False) as sched_confirm_row:
937
+ gr.HTML('<div style="padding:10px;background:#0c1a0c;border-radius:8px;margin:8px 0">'
938
+ '<span style="color:#4ade80;font-weight:600;font-size:13px">πŸ‘† Review the plan above, then:</span></div>')
939
+ with gr.Row():
940
+ btn_sched_ok = gr.Button("βœ… Confirm & Apply", elem_classes="btn-success", scale=2)
941
+ btn_sched_discard = gr.Button("βœ— Discard", elem_classes="btn-danger", scale=1)
942
+ gr.HTML('<div style="color:#475569;font-size:12px;margin:10px 0 4px">πŸ’¬ Or refine it:</div>')
943
+ with gr.Row():
944
+ sched_refine_txt = gr.Textbox(label="", placeholder="e.g. Move Flow tasks to morning, skip Fridays", scale=4)
945
+ btn_sched_refine = gr.Button("πŸ”„ Re-plan", elem_classes="btn-accent", scale=1)
946
+ gr.HTML('<div style="color:#475569;font-size:12px;margin:10px 0 4px">πŸ“Œ Or manually assign a task:</div>')
947
+ with gr.Row():
948
+ manual_tid = gr.Number(label="Task ID", precision=0, scale=1)
949
+ manual_date = gr.Textbox(label="Date (YYYY-MM-DD)", placeholder=str(date.today()), scale=2)
950
+ btn_manual = gr.Button("Assign Date", elem_classes="btn-secondary", scale=1)
951
+ manual_msg = gr.HTML("")
952
+ sched_status = gr.HTML("")
953
 
954
  # ── Today's task table ────────────────────────────────────
955
  gr.HTML('<div class="sec-label" style="margin-top:20px">Today\'s Tasks</div>')
 
979
  all_action_msg = gr.HTML("")
980
 
981
  # ─────────────────────────────────────────────────────────────
982
+ # TAB 3: CALENDAR
983
+ # ─────────────────────────────────────────────────────────────
984
+ with gr.Tab("πŸ“† Calendar"):
985
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">All tasks grouped by scheduled date. Unscheduled tasks appear at the bottom β€” use Plan My Day to assign them.</p>')
986
+ with gr.Row():
987
+ btn_cal_refresh = gr.Button("↻ Refresh", elem_classes="btn-secondary", scale=1)
988
+ calendar_html = gr.HTML("")
989
+
990
+ # ─────────────────────────────────────────────────────────────
991
+ # TAB 4: JOURNAL
992
  # ─────────────────────────────────────────────────────────────
993
  with gr.Tab("πŸ““ Journal"):
994
  gr.HTML('<p style="color:#334155;font-size:13px;margin-bottom:16px">Mark today\'s tasks complete, then reflect. The AI updates your profile to improve tomorrow\'s schedule.</p>')
 
1090
  ).then(
1091
  refresh_today, [user_id_state],
1092
  [today_df, stat_total, stat_done, stat_remain]
1093
+ ).then(
1094
+ lambda uid: _fmt_tasks(get_tasks(uid)) if uid else [],
1095
+ [user_id_state], [all_df]
1096
+ ).then(
1097
+ render_calendar, [user_id_state], [calendar_html]
1098
  ).then(
1099
  lambda uid: render_areas(uid)[0:2],
1100
  [user_id_state], [areas_display, del_area_dd]
 
1115
  ).then(
1116
  refresh_today, [user_id_state],
1117
  [today_df, stat_total, stat_done, stat_remain]
1118
+ ).then(
1119
+ lambda uid: _fmt_tasks(get_tasks(uid)) if uid else [],
1120
+ [user_id_state], [all_df]
1121
+ ).then(
1122
+ render_calendar, [user_id_state], [calendar_html]
1123
  ).then(
1124
  lambda uid: render_areas(uid)[0:2],
1125
  [user_id_state], [areas_display, del_area_dd]
 
1184
  handle_confirm_task,
1185
  [user_id_state, f_title, f_area, f_urgency, f_importance, f_state,
1186
  f_time, f_date, f_is_habit, f_interval],
1187
+ [save_msg, today_df, stat_total, stat_done, stat_remain, all_df, parsed_panel]
1188
+ ).then(render_calendar, [user_id_state], [calendar_html])
1189
 
1190
  btn_gen_sched.click(
1191
  handle_generate_schedule,
1192
  [user_id_state, plan_prompt],
1193
+ [schedule_html, sched_pending, sched_confirm_row]
1194
+ )
1195
+ btn_sched_ok.click(
1196
+ handle_confirm_schedule,
1197
+ [user_id_state, sched_pending],
1198
+ [sched_status, sched_confirm_row, all_df, today_df]
1199
+ ).then(render_calendar, [user_id_state], [calendar_html])
1200
+ btn_sched_discard.click(
1201
+ lambda: ("", gr.update(visible=False)),
1202
+ outputs=[schedule_html, sched_confirm_row]
1203
  )
1204
+ btn_sched_refine.click(
1205
+ handle_refine_schedule,
1206
+ [user_id_state, sched_refine_txt, sched_pending],
1207
+ [schedule_html, sched_pending, sched_confirm_row]
1208
+ ).then(lambda: "", outputs=[sched_refine_txt])
1209
+ btn_manual.click(
1210
+ handle_manual_assign,
1211
+ [manual_tid, manual_date, user_id_state],
1212
+ [manual_msg]
1213
+ ).then(
1214
+ lambda uid: _fmt_tasks(get_tasks(uid)) if uid else [],
1215
+ [user_id_state], [all_df]
1216
+ ).then(render_calendar, [user_id_state], [calendar_html])
1217
 
1218
  btn_today_done.click(
1219
  handle_toggle_today,
1220
  [today_done_id, user_id_state],
1221
  [today_df, stat_total, stat_done, stat_remain, today_action_msg]
1222
+ ).then(
1223
+ lambda uid: _fmt_tasks(get_tasks(uid)) if uid else [],
1224
+ [user_id_state], [all_df]
1225
+ ).then(render_calendar, [user_id_state], [calendar_html])
1226
  btn_today_del.click(
1227
  handle_delete_today,
1228
  [today_del_id, user_id_state],
1229
  [today_df, stat_total, stat_done, stat_remain, today_action_msg]
1230
+ ).then(
1231
+ lambda uid: _fmt_tasks(get_tasks(uid)) if uid else [],
1232
+ [user_id_state], [all_df]
1233
+ ).then(render_calendar, [user_id_state], [calendar_html])
1234
+
1235
+ # ── Calendar tab ──────────────────────────────────────────────────────────
1236
+ btn_cal_refresh.click(render_calendar, [user_id_state], [calendar_html])
1237
 
1238
  # ── All Tasks tab ──────────────────────────────────────────────────────
1239
 
 
1256
  handle_toggle_all,
1257
  [all_done_id, user_id_state, all_filter, all_today_chk],
1258
  [all_df, all_action_msg]
1259
+ ).then(render_calendar, [user_id_state], [calendar_html])
1260
  btn_all_del.click(
1261
  handle_delete_all,
1262
  [all_del_id, user_id_state, all_filter, all_today_chk],
1263
  [all_df, all_action_msg]
1264
+ ).then(render_calendar, [user_id_state], [calendar_html])
1265
 
1266
  # ── Journal tab ────────────────────────────────────────────────────────
1267