eraz3r commited on
Commit
93725db
·
verified ·
1 Parent(s): b274992

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1028 -311
app.py CHANGED
@@ -1,330 +1,1047 @@
1
  """
2
- core/database.py
3
- SQLite persistence for Second Brain.
4
  """
5
 
6
- import sqlite3
7
- import json
8
- import bcrypt
9
- from datetime import datetime, date
10
- from typing import Optional
11
-
12
- DB_PATH = "second_brain.db"
13
-
14
-
15
- def get_db() -> sqlite3.Connection:
16
- conn = sqlite3.connect(DB_PATH)
17
- conn.row_factory = sqlite3.Row
18
- conn.execute("PRAGMA foreign_keys = ON")
19
- return conn
20
-
21
-
22
- def init_db():
23
- conn = get_db()
24
- c = conn.cursor()
25
-
26
- c.execute("""
27
- CREATE TABLE IF NOT EXISTS users (
28
- id INTEGER PRIMARY KEY AUTOINCREMENT,
29
- username TEXT UNIQUE NOT NULL,
30
- password_hash TEXT NOT NULL,
31
- created_at TEXT DEFAULT (datetime('now'))
32
- )
33
- """)
34
-
35
- c.execute("""
36
- CREATE TABLE IF NOT EXISTS life_areas (
37
- id INTEGER PRIMARY KEY AUTOINCREMENT,
38
- user_id INTEGER NOT NULL,
39
- name TEXT NOT NULL,
40
- color TEXT DEFAULT '#6366f1',
41
- created_at TEXT DEFAULT (datetime('now')),
42
- FOREIGN KEY (user_id) REFERENCES users(id)
43
- )
44
- """)
45
-
46
- c.execute("""
47
- CREATE TABLE IF NOT EXISTS user_goals (
48
- id INTEGER PRIMARY KEY AUTOINCREMENT,
49
- user_id INTEGER NOT NULL,
50
- goal_text TEXT NOT NULL,
51
- created_at TEXT DEFAULT (datetime('now')),
52
- FOREIGN KEY (user_id) REFERENCES users(id)
53
- )
54
- """)
55
-
56
- c.execute("""
57
- CREATE TABLE IF NOT EXISTS tasks (
58
- id INTEGER PRIMARY KEY AUTOINCREMENT,
59
- user_id INTEGER NOT NULL,
60
- title TEXT NOT NULL,
61
- life_area TEXT DEFAULT '',
62
- urgency TEXT DEFAULT 'Not Urgent',
63
- importance TEXT DEFAULT 'Important',
64
- state_of_mind TEXT DEFAULT 'Easy',
65
- time_estimate INTEGER DEFAULT 30,
66
- scheduled_date TEXT DEFAULT '',
67
- deadline_date TEXT DEFAULT '',
68
- is_completed INTEGER DEFAULT 0,
69
- actual_duration INTEGER,
70
- is_habit INTEGER DEFAULT 0,
71
- habit_interval TEXT DEFAULT '',
72
- raw_input TEXT DEFAULT '',
73
- created_at TEXT DEFAULT (datetime('now')),
74
- FOREIGN KEY (user_id) REFERENCES users(id)
75
- )
76
- """)
77
-
78
- # Migrate existing DBs — add deadline_date if missing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  try:
80
- c.execute("ALTER TABLE tasks ADD COLUMN deadline_date TEXT DEFAULT ''")
 
81
  except Exception:
82
- pass # column already exists
83
 
84
- # Migrate existing DBs — clear default date so old tasks aren't shown as today
85
- # (only affects new installs; existing data keeps its dates)
86
 
87
- c.execute("""
88
- CREATE TABLE IF NOT EXISTS user_context (
89
- user_id INTEGER PRIMARY KEY,
90
- context TEXT NOT NULL,
91
- updated_at TEXT DEFAULT (datetime('now')),
92
- FOREIGN KEY (user_id) REFERENCES users(id)
93
- )
94
- """)
 
 
 
 
 
95
 
96
- conn.commit()
97
- conn.close()
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
- # ── Auth ──────────────────────────────────────────────────────────────────────
101
 
102
- def register_user(username: str, password: str) -> tuple:
103
- username = username.strip().lower()
104
- if not username or not password:
105
- return None, "Username and password cannot be empty."
106
- if len(password) < 6:
107
- return None, "Password must be at least 6 characters."
108
- conn = get_db()
109
- try:
110
- pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
111
- conn.execute("INSERT INTO users (username, password_hash) VALUES (?, ?)", (username, pw_hash))
112
- conn.commit()
113
- row = conn.execute("SELECT id FROM users WHERE username = ?", (username,)).fetchone()
114
- return row["id"], "Account created!"
115
- except sqlite3.IntegrityError:
116
- return None, "Username already taken."
117
- finally:
118
- conn.close()
119
-
120
-
121
- def login_user(username: str, password: str) -> tuple:
122
- username = username.strip().lower()
123
- if not username or not password:
124
- return None, "Please enter your credentials."
125
- conn = get_db()
126
- row = conn.execute("SELECT id, password_hash FROM users WHERE username = ?", (username,)).fetchone()
127
- conn.close()
128
- if not row:
129
- return None, "Username not found."
130
- if not bcrypt.checkpw(password.encode(), row["password_hash"].encode()):
131
- return None, "Incorrect password."
132
- return row["id"], f"Welcome back, {username}!"
133
-
134
-
135
- def get_username(user_id: int) -> str:
136
- conn = get_db()
137
- row = conn.execute("SELECT username FROM users WHERE id = ?", (user_id,)).fetchone()
138
- conn.close()
139
- return row["username"].capitalize() if row else "User"
140
-
141
-
142
- # ── Life Areas ────────────────────────────────────────────────────────────────
143
-
144
- DEFAULT_AREAS = [
145
- ("Work", "#4F8EF7"), ("Health", "#4CAF87"), ("Finance", "#F7A84F"),
146
- ("Learning", "#A855F7"), ("Personal", "#EC4899"), ("Family", "#F59E0B"),
147
- ]
148
-
149
- def create_default_life_areas(user_id: int):
150
- conn = get_db()
151
- for name, color in DEFAULT_AREAS:
152
- conn.execute("INSERT INTO life_areas (user_id, name, color) VALUES (?, ?, ?)", (user_id, name, color))
153
- conn.commit()
154
- conn.close()
155
-
156
- def get_life_areas(user_id: int) -> list:
157
- conn = get_db()
158
- rows = conn.execute("SELECT id, name, color FROM life_areas WHERE user_id = ? ORDER BY id", (user_id,)).fetchall()
159
- conn.close()
160
- return [dict(r) for r in rows]
161
-
162
- def get_life_area_names(user_id: int) -> list:
163
- return [a["name"] for a in get_life_areas(user_id)]
164
-
165
- def add_life_area(user_id: int, name: str, color: str = "#6366f1") -> tuple:
166
- name = name.strip()
167
- if not name: return False, "Name cannot be empty."
168
- conn = get_db()
169
- exists = conn.execute("SELECT id FROM life_areas WHERE user_id = ? AND LOWER(name) = LOWER(?)", (user_id, name)).fetchone()
170
- if exists:
171
- conn.close(); return False, f'"{name}" already exists.'
172
- conn.execute("INSERT INTO life_areas (user_id, name, color) VALUES (?, ?, ?)", (user_id, name, color))
173
- conn.commit(); conn.close()
174
- return True, f'"{name}" added.'
175
-
176
- def delete_life_area(user_id: int, name: str) -> tuple:
177
- conn = get_db()
178
- conn.execute("DELETE FROM life_areas WHERE user_id = ? AND name = ?", (user_id, name))
179
- conn.commit(); conn.close()
180
- return True, f'"{name}" removed.'
181
-
182
-
183
- # ── Goals ─────────────────────────────────────────────────────────────────────
184
-
185
- def save_goals(user_id: int, goals_text: str):
186
- conn = get_db()
187
- conn.execute("DELETE FROM user_goals WHERE user_id = ?", (user_id,))
188
- for line in goals_text.strip().splitlines():
189
- line = line.strip("•- ").strip()
190
- if line:
191
- conn.execute("INSERT INTO user_goals (user_id, goal_text) VALUES (?, ?)", (user_id, line))
192
- conn.commit(); conn.close()
193
-
194
- def get_goals(user_id: int) -> list:
195
- conn = get_db()
196
- rows = conn.execute("SELECT goal_text FROM user_goals WHERE user_id = ? ORDER BY id", (user_id,)).fetchall()
197
- conn.close()
198
- return [r["goal_text"] for r in rows]
199
-
200
-
201
- # ── Tasks ─────────────────────────────────────────────────────────────────────
202
-
203
- def save_task(user_id: int, task: dict, scheduled_date: str = None) -> int:
204
  """
205
- Save a task. scheduled_date=None means unscheduled (awaiting AI assignment).
206
- Pass scheduled_date="" explicitly to also leave unscheduled.
 
207
  """
208
- conn = get_db()
209
- cursor = conn.execute("""
210
- INSERT INTO tasks
211
- (user_id, title, life_area, urgency, importance, state_of_mind,
212
- time_estimate, scheduled_date, deadline_date, raw_input, is_habit, habit_interval)
213
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
214
- """, (
215
- user_id,
216
- task.get("title", "Untitled"),
217
- task.get("life_area", ""),
218
- task.get("urgency", "Not Urgent"),
219
- task.get("importance", "Important"),
220
- task.get("state_of_mind", "Easy"),
221
- int(task.get("time_estimate") or 30),
222
- scheduled_date if scheduled_date is not None else "", # "" = unscheduled
223
- task.get("deadline_date", ""),
224
- task.get("raw_input", ""),
225
- 1 if task.get("is_habit") else 0,
226
- task.get("habit_interval", ""),
227
- ))
228
- task_id = cursor.lastrowid
229
- conn.commit(); conn.close()
230
- return task_id
231
-
232
-
233
- def assign_task_date(task_id: int, user_id: int, scheduled_date: str):
234
- """Update a task's scheduled_date (called by the AI scheduler)."""
235
- conn = get_db()
236
- conn.execute(
237
- "UPDATE tasks SET scheduled_date = ? WHERE id = ? AND user_id = ?",
238
- (scheduled_date, task_id, user_id)
239
  )
240
- conn.commit(); conn.close()
241
-
242
-
243
- def get_tasks(user_id: int, filter_area: str = "All", only_today: bool = False,
244
- include_completed: bool = True, only_unscheduled: bool = False) -> list:
245
- conn = get_db()
246
- q = "SELECT * FROM tasks WHERE user_id = ?"
247
- params = [user_id]
248
- if filter_area and filter_area != "All":
249
- q += " AND life_area = ?"; params.append(filter_area)
250
- if only_today:
251
- q += " AND scheduled_date = ?"; params.append(str(date.today()))
252
- if only_unscheduled:
253
- q += " AND (scheduled_date = '' OR scheduled_date IS NULL)"
254
- if not include_completed:
255
- q += " AND is_completed = 0"
256
- q += " ORDER BY is_completed ASC, CASE WHEN deadline_date = '' THEN '9999' ELSE deadline_date END ASC, created_at DESC"
257
- rows = conn.execute(q, params).fetchall()
258
- conn.close()
259
- return [dict(r) for r in rows]
260
-
261
-
262
- def toggle_task_complete(task_id: int, user_id: int, actual_duration: int = None):
263
- conn = get_db()
264
- task = conn.execute("SELECT is_completed FROM tasks WHERE id = ? AND user_id = ?", (task_id, user_id)).fetchone()
265
- if task:
266
- new_status = 1 - task["is_completed"]
267
- if actual_duration and new_status == 1:
268
- conn.execute("UPDATE tasks SET is_completed = ?, actual_duration = ? WHERE id = ? AND user_id = ?",
269
- (new_status, actual_duration, task_id, user_id))
270
- else:
271
- conn.execute("UPDATE tasks SET is_completed = ? WHERE id = ? AND user_id = ?",
272
- (new_status, task_id, user_id))
273
- conn.commit(); conn.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
 
276
- def delete_task(task_id: int, user_id: int):
277
- conn = get_db()
278
- conn.execute("DELETE FROM tasks WHERE id = ? AND user_id = ?", (task_id, user_id))
279
- conn.commit(); conn.close()
280
 
 
 
 
 
 
 
 
 
 
281
 
282
- def get_today_stats(user_id: int) -> dict:
 
 
 
 
 
 
 
283
  tasks = get_tasks(user_id, only_today=True)
284
- total = len(tasks); done = sum(1 for t in tasks if t["is_completed"])
285
- return {"total": total, "done": done, "remaining": total - done}
286
-
287
-
288
- # ── Habit recurrence ──────────────────────────────────────────────────────────
289
-
290
- def spawn_due_habits(user_id: int):
291
- today = str(date.today())
292
- conn = get_db()
293
- habits = conn.execute("SELECT * FROM tasks WHERE user_id = ? AND is_habit = 1", (user_id,)).fetchall()
294
- for h in habits:
295
- existing = conn.execute(
296
- "SELECT id FROM tasks WHERE user_id = ? AND title = ? AND is_habit = 1 AND scheduled_date = ?",
297
- (user_id, h["title"], today)
298
- ).fetchone()
299
- if existing: continue
300
- if h["scheduled_date"] and h["scheduled_date"] >= today: continue
301
- conn.execute("""
302
- INSERT INTO tasks (user_id, title, life_area, urgency, importance,
303
- state_of_mind, time_estimate, scheduled_date, is_habit,
304
- habit_interval, raw_input)
305
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
306
- """, (user_id, h["title"], h["life_area"], "Habit", h["importance"],
307
- h["state_of_mind"], h["time_estimate"], today, h["habit_interval"], h["raw_input"]))
308
- conn.commit(); conn.close()
309
-
310
-
311
- # ── AI Context ────────────────────────────────────────────────────────────────
312
-
313
- def load_user_context(user_id: int) -> Optional[dict]:
314
- conn = get_db()
315
- row = conn.execute("SELECT context FROM user_context WHERE user_id = ?", (user_id,)).fetchone()
316
- conn.close()
317
- if row:
318
- try: return json.loads(row["context"])
319
- except Exception: return None
320
- return None
321
-
322
-
323
- def save_user_context(user_id: int, context: dict):
324
- conn = get_db()
325
- conn.execute("""
326
- INSERT INTO user_context (user_id, context, updated_at)
327
- VALUES (?, ?, datetime('now'))
328
- ON CONFLICT(user_id) DO UPDATE SET context = excluded.context, updated_at = excluded.updated_at
329
- """, (user_id, json.dumps(context)))
330
- conn.commit(); conn.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ app.py - Second Brain Gradio Application
 
3
  """
4
 
5
+ import os
6
+ import gradio as gr
7
+ from datetime import date, datetime
8
+
9
+ from core.database import (
10
+ init_db, register_user, login_user, get_username,
11
+ create_default_life_areas, save_goals, get_goals,
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
22
+
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 [])
77
+
78
+ def _ensure_context(user_id):
79
+ ctx = load_user_context(user_id)
80
+ if not ctx:
81
+ ctx = create_blank_context(user_id)
82
+ save_user_context(user_id, ctx)
83
+ return ctx
84
+
85
+ def _transcribe(audio_path):
86
+ global _whisper_model
87
+ from faster_whisper import WhisperModel
88
+ if _whisper_model is None:
89
+ _whisper_model = WhisperModel("small", device="cpu", compute_type="int8")
90
+ segments, _ = _whisper_model.transcribe(audio_path)
91
+ return " ".join(seg.text for seg in segments).strip()
92
+
93
+ def _build_clar_html(clarifications):
94
+ if not clarifications:
95
+ return ""
96
+ items = "".join(f"<li style='margin:4px 0'>{q}</li>" for q in clarifications)
97
+ return (
98
+ f'<div style="margin:10px 0;padding:12px;background:#0c0f1a;'
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
+
104
+ def _run_parse(task_text, user_id):
105
+ ctx = _ensure_context(user_id)
106
+ goals = get_goals(user_id)
107
+ areas = get_life_area_names(user_id) if user_id else []
108
+ result = parse_task_with_groq(task_text, ctx, goals, areas)
109
+ clars = result.get("clarifications_needed", [])
110
+ area_choices = areas or ["Work","Health","Finance","Learning","Personal","Family","Other"]
111
+ area_val = result.get("life_area") or area_choices[0]
112
+ if area_val not in area_choices:
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),
127
+ clar_html, area_val,
128
+ result.get("urgency", "Not Urgent"),
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,
137
+ )
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)
152
+ if not uid:
153
+ return None, "", _err(msg), gr.update(visible=True), gr.update(visible=False)
154
+ spawn_due_habits(uid)
155
+ return uid, get_username(uid), _ok(msg), gr.update(visible=False), gr.update(visible=True)
156
+
157
+ def handle_register(username, password, wake, sleep, focus, goals_text):
158
+ uid, msg = register_user(username, password)
159
+ if not uid:
160
+ return None, "", _err(msg), gr.update(visible=True), gr.update(visible=False)
161
+ create_default_life_areas(uid)
162
+ if goals_text.strip():
163
+ save_goals(uid, goals_text)
164
+ ctx = create_blank_context(uid, {"wake_time": wake or "07:30", "sleep_time": sleep or "23:00", "focus_peak": focus or "Morning"})
165
+ save_user_context(uid, ctx)
166
+ spawn_due_habits(uid)
167
+ return uid, get_username(uid), _ok(msg + " Logged in!"), gr.update(visible=False), gr.update(visible=True)
168
+
169
+ def handle_logout(user_id):
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)
194
+ def hide_panels(): return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
195
+
196
+ # ── Text parse ─────────────────────────────────────────────────────────────────
197
+
198
+ def handle_parse_text(task_text, user_id):
199
+ if not task_text.strip():
200
+ return _EMPTY_PARSE
201
+ return _parse_result_to_outputs(*_run_parse(task_text, user_id), task_text)
202
+
203
+ def handle_clarification_reply(user_reply, original_task, clarifications, user_id):
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:
232
+ return _EMPTY_PARSE[:11] + (_err("No audio recorded."),) + _EMPTY_PARSE[12:]
233
+ try:
234
+ text = _transcribe(audio_path)
235
+ except Exception as e:
236
+ return _EMPTY_PARSE[:11] + (_err(f"Transcription error: {e}"),) + _EMPTY_PARSE[12:]
237
+ if not text:
238
+ return _EMPTY_PARSE[:11] + (_err("Could not hear anything. Try again."),) + _EMPTY_PARSE[12:]
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") or "—"} · {sm} · {task.get("time_estimate") or "?"}min{deadline_str}</div>'
356
+ f'<div class="sched-why">\U0001f4a1 {a.get("reasoning","")}</div>'
357
+ f'</div>'
358
+ )
359
+
360
+ # Skipped tasks
361
+ skipped = result.get("skipped", [])
362
+ if skipped:
363
+ html += '<div style="margin-top:12px;color:#334155;font-size:11px;font-weight:600;text-transform:uppercase">Not scheduled</div>'
364
+ for s in skipped:
365
+ html += f'<div style="color:#475569;font-size:12px;padding:3px 0">\u2717 {s.get("title","")} \u2014 {s.get("reason","")}</div>'
366
+
367
+ # All Tasks board (update after scheduling)
368
+ all_rows = refresh_all_tasks_auto(user_id)
369
+ return html, all_rows
370
+
371
+ def handle_toggle_today(task_id, user_id):
372
+ if task_id and user_id: toggle_task_complete(int(task_id), user_id)
373
+ rows, s1, s2, s3 = refresh_today(user_id)
374
+ return rows, s1, s2, s3, _ok("Updated")
375
+
376
+ def handle_delete_today(task_id, user_id):
377
+ if task_id and user_id: delete_task(int(task_id), user_id)
378
+ rows, s1, s2, s3 = refresh_today(user_id)
379
+ return rows, s1, s2, s3, _ok("Deleted")
380
+
381
+
382
+ # =============================================================================
383
+ # ALL TASKS TAB
384
+ # =============================================================================
385
+
386
+ def refresh_all_tasks(user_id, filter_area="All", only_today=False):
387
+ if not user_id: return []
388
+ return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today))
389
+
390
+ def handle_toggle_all(task_id, user_id, filter_area, only_today):
391
+ if task_id and user_id: toggle_task_complete(int(task_id), user_id)
392
+ return refresh_all_tasks(user_id, filter_area, only_today), _ok("Updated")
393
+
394
+ def handle_delete_all(task_id, user_id, filter_area, only_today):
395
+ if task_id and user_id: delete_task(int(task_id), user_id)
396
+ return refresh_all_tasks(user_id, filter_area, only_today), _ok("Deleted")
397
 
398
 
399
+ # =============================================================================
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 = [{
427
+ "task_id": t["id"], "title": t["title"], "life_area": t.get("life_area",""),
428
+ "state_of_mind": t.get("state_of_mind",""), "time_estimate": t.get("time_estimate",30),
429
+ "completed": bool(t["is_completed"]), "actual_duration": t.get("actual_duration"),
430
+ } for t in tasks]
431
+
432
+ total = len(tasks_state)
433
+ completed = sum(1 for t in tasks_state if t["completed"])
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)
450
+ hist = [{"role": "assistant", "content": opener, "focus": "task_review"}]
451
+ return (
452
+ [first_msg], hist, True,
453
+ gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True),
454
+ tasks_state, "marking", "",
455
+ )
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:
462
+ transcribed = _transcribe(audio_path)
463
+ if transcribed:
464
+ user_text = transcribed
465
+ voice_used = True
466
+ except Exception as e:
467
+ err = (None, f"\u26a0 Couldn\'t transcribe audio: {e}. Please type instead.")
468
+ return list(chat) + [err], hist, "", None, tasks_state, active, phase, ""
469
+
470
+ if not active or not user_text.strip():
471
+ return chat, hist, "", None, tasks_state, active, phase, ""
472
+
473
+ display_text = f"\U0001f3a4 *{user_text}*" if voice_used else user_text
474
+ chat = list(chat) + [(display_text, None)]
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)
481
+ updated = []
482
+ for t in tasks_state:
483
+ if t["task_id"] in mentioned and not t["completed"]:
484
+ toggle_task_complete(t["task_id"], user_id)
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
+
556
+
557
+ def handle_restart_journal():
558
+ return [], [], False, gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), [], "marking", ""
559
+
560
+
561
+ # =============================================================================
562
+ # LIFE AREAS & GOALS
563
+ # =============================================================================
564
+
565
+ def render_areas(user_id):
566
+ if not user_id: return "", [], []
567
+ areas = get_life_areas(user_id)
568
+ chips = "".join(
569
+ f'<span class="chip" style="background:{a["color"]}22;color:{a["color"]};border:1px solid {a["color"]}44">{a["name"]}</span> '
570
+ for a in areas
571
+ )
572
+ html = f'<div style="margin:4px 0">{chips}</div>' if chips else '<p style="color:#334155;font-size:13px">No areas yet.</p>'
573
+ names = [a["name"] for a in areas]
574
+ return html, names, names
575
+
576
+ def handle_add_area(user_id, name, color):
577
+ ok, msg = add_life_area(user_id, name, color)
578
+ html, names, _ = render_areas(user_id)
579
+ ct = "#4ade80" if ok else "#f87171"
580
+ return f'<span style="color:{ct};font-size:13px">{msg}</span>', html, gr.update(choices=names, value=None), gr.update(value="")
581
+
582
+ def handle_del_area(user_id, name):
583
+ if not name: return _err("Select an area first."), "", []
584
+ ok, msg = delete_life_area(user_id, name)
585
+ html, names, _ = render_areas(user_id)
586
+ ct = "#4ade80" if ok else "#f87171"
587
+ return f'<span style="color:{ct};font-size:13px">{msg}</span>', html, gr.update(choices=names, value=None)
588
+
589
+ def handle_save_goals(user_id, goals_text):
590
+ if not user_id: return _err("Not logged in.")
591
+ save_goals(user_id, goals_text)
592
+ return _ok("Goals saved!")
593
+
594
+ def load_goals_txt(user_id):
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
604
+ if not ctx: return "07:30","23:00","Morning",10,90
605
+ p = ctx.get("preferences",{})
606
+ return p.get("wake_time","07:30"), p.get("sleep_time","23:00"), p.get("focus_peak","Morning"), p.get("break_duration_minutes",10), p.get("max_flow_block_minutes",90)
607
+
608
+ def handle_save_prefs(user_id, wake, sleep, focus, brk, flow_max):
609
+ if not user_id: return _err("Not logged in.")
610
+ ctx = _ensure_context(user_id)
611
+ ctx["preferences"].update({"wake_time":wake or "07:30","sleep_time":sleep or "23:00","focus_peak":focus or "Morning","break_duration_minutes":int(brk or 10),"max_flow_block_minutes":int(flow_max or 90)})
612
+ save_user_context(user_id, ctx)
613
+ return _ok("Preferences saved!")
614
+
615
+ def render_context_html(user_id):
616
+ if not user_id: return "<p style='color:#334155'>Not logged in.</p>"
617
+ ctx = load_user_context(user_id)
618
+ if not ctx: return "<p style='color:#334155;font-size:13px'>No AI context yet. Complete a journal session to build it.</p>"
619
+ lp = ctx.get("learned_patterns",{}); sf = ctx.get("scheduling_feedback",{}); pref = ctx.get("preferences",{})
620
+ def _row(l,v): return f'<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid #1e293b"><span style="color:#475569;font-size:12px">{l}</span><span style="color:#cbd5e1;font-size:12px">{v}</span></div>'
621
+ def _hdr(t): return f'<div style="color:#a78bfa;font-size:10px;font-weight:600;letter-spacing:1px;text-transform:uppercase;margin:14px 0 8px">{t}</div>'
622
+ html = "<div style='font-size:13px'>" + _hdr("Preferences")
623
+ html += _row("Wake",pref.get("wake_time","\u2014"))+_row("Sleep",pref.get("sleep_time","\u2014"))+_row("Peak Focus",pref.get("focus_peak","\u2014"))
624
+ html += _hdr("Learned Patterns")
625
+ html += _row("Avg overrun",f'{lp.get("avg_task_overrun_pct",0)}%')+_row("Flow batching",str(lp.get("flow_batch_capable","Unknown")))
626
+ html += _row("Days tracked",str(sf.get("total_days_scheduled",0)))+_row("Avg completion",f'{round(sf.get("avg_completion_rate",0)*100)}%')
627
+ notes = lp.get("notes",[])
628
+ if notes:
629
+ html += _hdr("AI Notes")
630
+ html += "".join(f'<div style="color:#94a3b8;font-size:12px;padding:3px 0">\u2022 {n}</div>' for n in notes[-5:])
631
+ _ver = ctx.get("version", 1)
632
+ _upd = (ctx.get("last_updated") or "—")[:10]
633
+ html += f'<div style="color:#334155;font-size:11px;margin-top:12px">v{_ver} · {_upd}</div></div>'
634
+ return html
635
+
636
+
637
+ # =============================================================================
638
+ # UI
639
+ # =============================================================================
640
+
641
+ with gr.Blocks(title="\U0001f9e0 Second Brain") as demo:
642
+
643
+ user_id_state = gr.State(None)
644
+ username_state = gr.State("")
645
+ # Journal
646
+ journal_hist_state = gr.State([])
647
+ journal_tasks_state = gr.State([])
648
+ journal_active = gr.State(False)
649
+ journal_phase_state = gr.State("marking")
650
+ # Clarification — text panel
651
+ clar_questions_state = gr.State([])
652
+ original_task_state = gr.State("")
653
+ # Clarification — voice panel
654
+ vclar_questions_state = gr.State([])
655
+ voriginal_task_state = gr.State("")
656
+
657
+ # AUTH
658
+ with gr.Column(visible=True, elem_id="auth-card") as auth_section:
659
+ gr.HTML('<div id="brand-logo">\U0001f9e0 Second Brain</div>')
660
+ gr.HTML('<div id="brand-sub">Your intelligent productivity companion</div>')
661
+ with gr.Tabs():
662
+ with gr.Tab("Sign In"):
663
+ login_user_in = gr.Textbox(label="Username", placeholder="your username")
664
+ login_pass_in = gr.Textbox(label="Password", type="password", placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022")
665
+ login_btn = gr.Button("Sign In", elem_classes="btn-primary")
666
+ login_msg = gr.HTML("")
667
+ with gr.Tab("Create Account"):
668
+ reg_user_in = gr.Textbox(label="Username", placeholder="choose a username")
669
+ reg_pass_in = gr.Textbox(label="Password (min 6 chars)", type="password")
670
+ reg_wake = gr.Textbox(label="Wake time", value="07:30")
671
+ reg_sleep = gr.Textbox(label="Sleep time", value="23:00")
672
+ reg_focus = gr.Dropdown(label="Peak focus", choices=["Morning","Afternoon","Evening","Night"], value="Morning")
673
+ reg_goals = gr.Textbox(label="Big-picture goals (optional)", lines=3, placeholder="Launch my startup\nGet fit\nLearn ML")
674
+ reg_btn = gr.Button("Create Account", elem_classes="btn-primary")
675
+ reg_msg = gr.HTML("")
676
+
677
+ # MAIN APP
678
+ with gr.Column(visible=False) as app_section:
679
+
680
+ with gr.Row(elem_id="top-header"):
681
+ header_html = gr.HTML('<div id="top-header-brand">\U0001f9e0 Second Brain</div><div id="top-header-user">\u2014</div>')
682
+ logout_btn = gr.Button("Sign Out", elem_classes="btn-secondary", scale=0)
683
+
684
+ with gr.Tabs():
685
+
686
+ # TAB 1: TODAY
687
+ with gr.Tab("\U0001f4c5 Today"):
688
+ with gr.Row():
689
+ stat_total = gr.HTML(_stat("\u2014","Today","#a78bfa"))
690
+ stat_done = gr.HTML(_stat("\u2014","Done","#4ade80"))
691
+ stat_remain = gr.HTML(_stat("\u2014","Left","#f87171"))
692
+ with gr.Row():
693
+ btn_add_text = gr.Button("\u270f\ufe0f Add Task (Text)", elem_classes="btn-primary", scale=3)
694
+ btn_add_voice = gr.Button("\U0001f399\ufe0f Add Task (Voice)", elem_classes="btn-accent", scale=3)
695
+ btn_plan_day = gr.Button("\U0001f9e0 Plan My Tasks", elem_classes="btn-secondary", scale=3)
696
+ btn_refresh = gr.Button("\u21bb", elem_classes="btn-secondary", scale=1)
697
+
698
+ # Text panel
699
+ with gr.Column(visible=False, elem_classes="panel") as text_panel:
700
+ gr.HTML('<div class="sec-label">Describe Your Task</div>')
701
+ task_input = gr.Textbox(label="", placeholder="e.g. Finish the client proposal, it\'s really important", lines=2)
702
+ with gr.Row():
703
+ btn_parse = gr.Button("\U0001f916 Parse with AI", elem_classes="btn-primary")
704
+ btn_cancel_text = gr.Button("Cancel", elem_classes="btn-secondary")
705
+
706
+ with gr.Column(visible=False) as parsed_panel:
707
+ parse_status = gr.HTML("")
708
+ clar_html = gr.HTML("")
709
+ with gr.Column(visible=False) as clar_reply_row:
710
+ gr.HTML('<div class="sec-label" style="margin-top:8px">Answer the questions above</div>')
711
+ with gr.Row():
712
+ clar_text_input = gr.Textbox(label="Type your answer", lines=2, scale=4, placeholder="e.g. It\'s urgent, about 2 hours, for my manager")
713
+ clar_audio_input = gr.Audio(sources=["microphone"], type="filepath", label="\U0001f3a4 Or speak", scale=1)
714
+ btn_clar_submit = gr.Button("\U0001f504 Re-classify", elem_classes="btn-accent")
715
+ gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>')
716
+ gr.HTML('<p style="color:#475569;font-size:12px;margin:0 0 10px">Date will be assigned by the AI scheduler \u2014 just save and use \u201cPlan My Tasks\u201d.</p>')
717
+ with gr.Row():
718
+ f_title = gr.Textbox(label="Title", scale=3)
719
+ f_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1)
720
+ with gr.Row():
721
+ f_urgency = gr.Dropdown(label="Urgency", choices=["Urgent","Not Urgent","Habit"], value="Not Urgent")
722
+ f_importance = gr.Dropdown(label="Importance", choices=["Move the Needle","Important","Not Important"], value="Important")
723
+ f_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy")
724
+ with gr.Row():
725
+ f_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
726
+ f_deadline = gr.Textbox(label="\U0001f3af Deadline (YYYY-MM-DD, optional)", placeholder="2026-03-01", scale=2)
727
+ f_is_habit = gr.Checkbox(label="\u267b\ufe0f Habit", scale=1)
728
+ f_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1)
729
+ with gr.Row():
730
+ btn_confirm = gr.Button("\u2713 Save Task", elem_classes="btn-success")
731
+ btn_discard = gr.Button("\u2717 Discard", elem_classes="btn-danger")
732
+ save_msg = gr.HTML("")
733
+
734
+ # Voice panel
735
+ with gr.Column(visible=False, elem_classes="panel") as voice_panel:
736
+ gr.HTML('<div class="sec-label">Add Task by Voice</div>')
737
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">Record your task \u2014 Whisper transcribes it, AI classifies it. Same flow as text.</p>')
738
+ voice_audio_input = gr.Audio(sources=["microphone","upload"], type="filepath", label="")
739
+ with gr.Row():
740
+ btn_voice_parse = gr.Button("\U0001f916 Transcribe & Classify", elem_classes="btn-primary")
741
+ btn_cancel_voice = gr.Button("Cancel", elem_classes="btn-secondary")
742
+ voice_status = gr.HTML("")
743
+
744
+ with gr.Column(visible=False) as voice_parsed_panel:
745
+ voice_transcribed_txt = gr.Textbox(label="Transcribed text (editable)", lines=1)
746
+ voice_clar_html = gr.HTML("")
747
+ with gr.Column(visible=False) as voice_clar_row:
748
+ gr.HTML('<div class="sec-label" style="margin-top:8px">Answer the questions above</div>')
749
+ with gr.Row():
750
+ vclar_text = gr.Textbox(label="Type your answer", lines=2, scale=4)
751
+ vclar_audio = gr.Audio(sources=["microphone"], type="filepath", label="\U0001f3a4 Or speak", scale=1)
752
+ btn_vclar_submit = gr.Button("\U0001f504 Re-classify", elem_classes="btn-accent")
753
+ gr.HTML('<div class="sec-label" style="margin-top:12px">Review & Confirm</div>')
754
+ gr.HTML('<p style="color:#475569;font-size:12px;margin:0 0 10px">Date assigned by AI scheduler after saving.</p>')
755
+ with gr.Row():
756
+ vf_title = gr.Textbox(label="Title", scale=3)
757
+ vf_area = gr.Dropdown(label="Life Area", choices=["Work","Health","Finance","Learning","Personal","Family","Other"], scale=1)
758
+ with gr.Row():
759
+ vf_urgency = gr.Dropdown(label="Urgency", choices=["Urgent","Not Urgent","Habit"], value="Not Urgent")
760
+ vf_importance = gr.Dropdown(label="Importance", choices=["Move the Needle","Important","Not Important"], value="Important")
761
+ vf_state = gr.Dropdown(label="State of Mind", choices=["Flow","Easy","Quick","Personal"], value="Easy")
762
+ with gr.Row():
763
+ vf_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
764
+ vf_deadline = gr.Textbox(label="\U0001f3af Deadline (optional)", placeholder="2026-03-01", scale=2)
765
+ vf_is_habit = gr.Checkbox(label="\u267b\ufe0f Habit", scale=1)
766
+ vf_interval = gr.Dropdown(label="Recurs", choices=["Daily","Weekly","Weekdays","Weekends","Monthly"], value="Daily", visible=False, scale=1)
767
+ with gr.Row():
768
+ btn_voice_confirm = gr.Button("\u2713 Save Task", elem_classes="btn-success")
769
+ btn_voice_discard = gr.Button("\u2717 Discard", elem_classes="btn-danger")
770
+ voice_save_msg = gr.HTML("")
771
+
772
+ # Plan My Tasks panel
773
+ with gr.Column(visible=False, elem_classes="panel") as plan_panel:
774
+ gr.HTML('<div class="sec-label">Plan My Tasks</div>')
775
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">'
776
+ 'The AI reads your unscheduled tasks, your learned patterns, goals, and current time \u2014 '
777
+ 'then assigns each task to the best future date. It never places tasks in the past.'
778
+ '</p>')
779
+ plan_prompt = gr.Textbox(
780
+ label="Tell the AI what you want",
781
+ placeholder="e.g. \'Clear today, schedule everything from tomorrow\' or \'Focus on Work tasks this week\' or \'I have a free morning Thursday\'",
782
+ lines=3
783
+ )
784
+ with gr.Row():
785
+ btn_gen_sched = gr.Button("\U0001f9e0 Run AI Scheduler", elem_classes="btn-primary")
786
+ btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary")
787
+ schedule_html = gr.HTML("")
788
+
789
+ gr.HTML('<div class="sec-label" style="margin-top:20px">Today\'s Tasks</div>')
790
+ today_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
791
+ with gr.Row():
792
+ today_done_id = gr.Number(label="Task ID \u2014 toggle done", precision=0, scale=2)
793
+ btn_today_done = gr.Button("\u2705 Mark Done", elem_classes="btn-success", scale=2)
794
+ today_del_id = gr.Number(label="Task ID \u2014 delete", precision=0, scale=2)
795
+ btn_today_del = gr.Button("\U0001f5d1 Delete", elem_classes="btn-danger", scale=2)
796
+ today_action_msg = gr.HTML("")
797
+
798
+ # TAB 2: ALL TASKS
799
+ with gr.Tab("\U0001f4cb All Tasks"):
800
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">All tasks including unscheduled ones. Filter and manage here.</p>')
801
+ with gr.Row():
802
+ all_filter = gr.Dropdown(label="Filter by Area", choices=["All"], value="All", scale=3)
803
+ all_today_chk = gr.Checkbox(label="Today only", value=False, scale=1)
804
+ all_unsched = gr.Checkbox(label="Unscheduled only", value=False, scale=1)
805
+ btn_all_ref = gr.Button("\u21bb Refresh", elem_classes="btn-secondary", scale=1)
806
+ all_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
807
+ with gr.Row():
808
+ all_done_id = gr.Number(label="Task ID \u2014 toggle done", precision=0, scale=2)
809
+ btn_all_done = gr.Button("\u2705 Done", elem_classes="btn-success", scale=2)
810
+ all_del_id = gr.Number(label="Task ID \u2014 delete", precision=0, scale=2)
811
+ btn_all_del = gr.Button("\U0001f5d1 Delete", elem_classes="btn-danger", scale=2)
812
+ all_action_msg = gr.HTML("")
813
+
814
+ # TAB 3: JOURNAL
815
+ with gr.Tab("\U0001f4d3 Journal"):
816
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:12px">End-of-day reflection. Chat naturally \u2014 AI marks tasks, asks questions, updates your profile.</p>')
817
+ journal_chatbot = gr.Chatbot(label="", height=440)
818
+ with gr.Row():
819
+ j_answer = gr.Textbox(label="", placeholder="Type or use the mic\u2026", lines=1, scale=5, interactive=False, show_label=False)
820
+ j_audio_in = gr.Audio(sources=["microphone"], type="filepath", label="\U0001f3a4", scale=1, interactive=False)
821
+ btn_j_send = gr.Button("Send \u2192", elem_classes="btn-accent", scale=1, interactive=False)
822
+ with gr.Row():
823
+ btn_j_start = gr.Button("\u25b6 Start Session", elem_classes="btn-primary", scale=3)
824
+ btn_j_finish = gr.Button("\u2713 Finish & Save", elem_classes="btn-success", scale=3)
825
+ btn_j_restart = gr.Button("\u21ba Reset", elem_classes="btn-secondary", scale=1)
826
+ journal_msg = gr.HTML("")
827
+
828
+ # TAB 4: AREAS & GOALS
829
+ with gr.Tab("\U0001f5c2 Areas & Goals"):
830
+ gr.HTML('<div class="sec-label">Your Life Areas</div>')
831
+ areas_display = gr.HTML("")
832
+ with gr.Row():
833
+ new_area_name = gr.Textbox(label="New area name", placeholder="e.g. Side Project", scale=3)
834
+ new_area_color = gr.ColorPicker(label="Color", value="#6366f1", scale=1)
835
+ btn_add_area = gr.Button("Add", elem_classes="btn-primary", scale=1)
836
+ area_msg = gr.HTML("")
837
+ gr.HTML('<div class="sec-label" style="margin-top:24px">Remove Area</div>')
838
+ with gr.Row():
839
+ del_area_dd = gr.Dropdown(label="Select area to remove", choices=[], scale=4)
840
+ btn_del_area = gr.Button("Remove", elem_classes="btn-danger", scale=1)
841
+ del_area_msg = gr.HTML("")
842
+ gr.HTML('<hr>')
843
+ gr.HTML('<div class="sec-label">Big-Picture Goals</div>')
844
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">The AI factors these in when scheduling and prioritising.</p>')
845
+ goals_input = gr.Textbox(label="One goal per line", lines=5, placeholder="Launch my SaaS\nRun 5K\nLearn ML")
846
+ btn_save_goals = gr.Button("Save Goals", elem_classes="btn-primary")
847
+ goals_msg = gr.HTML("")
848
+
849
+ # TAB 5: PREFERENCES
850
+ with gr.Tab("\u2699\ufe0f Preferences"):
851
+ gr.HTML('<div class="sec-label">Scheduling Preferences</div>')
852
+ with gr.Row():
853
+ pref_wake = gr.Textbox(label="Wake time", placeholder="07:30", scale=1)
854
+ pref_sleep = gr.Textbox(label="Sleep time", placeholder="23:00", scale=1)
855
+ pref_focus = gr.Dropdown(label="Peak focus", choices=["Morning","Afternoon","Evening","Night"], value="Morning", scale=1)
856
+ with gr.Row():
857
+ pref_break = gr.Number(label="Break between tasks (min)", value=10, minimum=0, scale=1)
858
+ pref_flow_max = gr.Number(label="Max Flow block (min)", value=90, minimum=30, scale=1)
859
+ btn_save_prefs = gr.Button("Save Preferences", elem_classes="btn-primary")
860
+ prefs_msg = gr.HTML("")
861
+ gr.HTML('<hr>')
862
+ gr.HTML('<div class="sec-label">AI Memory</div>')
863
+ gr.HTML('<p style="color:#475569;font-size:13px;margin-bottom:10px">What the AI has learned about you. Updates after each journal session.</p>')
864
+ ctx_display = gr.HTML("")
865
+ btn_ref_ctx = gr.Button("\u21bb Refresh", elem_classes="btn-secondary")
866
+
867
+
868
+ # =========================================================================
869
+ # EVENT WIRING
870
+ # =========================================================================
871
+
872
+ def _header_html(uid, uname):
873
+ return f'<div id="top-header-brand">\U0001f9e0 Second Brain</div><div id="top-header-user">\U0001f464 {uname}</div>'
874
+
875
+ # Auth
876
+ login_btn.click(handle_login, [login_user_in, login_pass_in],
877
+ [user_id_state, username_state, login_msg, auth_section, app_section]
878
+ ).then(_header_html, [user_id_state, username_state], [header_html]
879
+ ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
880
+ ).then(refresh_all_tasks_auto, [user_id_state], [all_df]
881
+ ).then(lambda uid: render_areas(uid)[:2], [user_id_state], [areas_display, del_area_dd]
882
+ ).then(load_prefs, [user_id_state], [pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max]
883
+ ).then(load_goals_txt, [user_id_state], [goals_input])
884
+
885
+ reg_btn.click(handle_register, [reg_user_in, reg_pass_in, reg_wake, reg_sleep, reg_focus, reg_goals],
886
+ [user_id_state, username_state, reg_msg, auth_section, app_section]
887
+ ).then(_header_html, [user_id_state, username_state], [header_html]
888
+ ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain]
889
+ ).then(refresh_all_tasks_auto, [user_id_state], [all_df]
890
+ ).then(lambda uid: render_areas(uid)[:2], [user_id_state], [areas_display, del_area_dd])
891
+
892
+ logout_btn.click(handle_logout, [user_id_state], [user_id_state, username_state, auth_section, app_section])
893
+
894
+ # Panels
895
+ btn_add_text.click(show_text_panel, outputs=[text_panel, voice_panel, plan_panel])
896
+ btn_add_voice.click(show_voice_panel, outputs=[text_panel, voice_panel, plan_panel])
897
+ btn_plan_day.click(show_plan_panel, outputs=[text_panel, voice_panel, plan_panel])
898
+ btn_cancel_text.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
899
+ btn_cancel_voice.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
900
+ btn_cancel_plan.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
901
+ btn_refresh.click(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain])
902
+
903
+ f_is_habit.change(lambda v: gr.update(visible=v), [f_is_habit], [f_interval])
904
+ vf_is_habit.change(lambda v: gr.update(visible=v), [vf_is_habit], [vf_interval])
905
+
906
+ # Text task flow
907
+ # Outputs: parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance,
908
+ # f_state, f_time, f_deadline, f_is_habit, f_interval, parse_status,
909
+ # clar_reply_row, clar_questions_state, original_task_state
910
+ _TEXT_PARSE_OUT = [parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance,
911
+ f_state, f_time, f_deadline, f_is_habit, f_interval, parse_status,
912
+ clar_reply_row, clar_questions_state, original_task_state]
913
+ # clar reply outputs: f_title, clar_html, f_area, f_urgency, f_importance,
914
+ # f_state, f_time, f_deadline, clar_reply_row, clar_questions_state
915
+ _TEXT_CLAR_OUT = [f_title, clar_html, f_area, f_urgency, f_importance,
916
+ f_state, f_time, f_deadline, clar_reply_row, clar_questions_state]
917
+
918
+ btn_parse.click(handle_parse_text, [task_input, user_id_state], _TEXT_PARSE_OUT)
919
+
920
+ btn_clar_submit.click(
921
+ handle_clarification_reply,
922
+ [clar_text_input, original_task_state, clar_questions_state, user_id_state],
923
+ _TEXT_CLAR_OUT
924
+ ).then(lambda: ("", None), outputs=[clar_text_input, clar_audio_input])
925
+
926
+ clar_audio_input.change(
927
+ handle_clar_voice,
928
+ [clar_audio_input, original_task_state, clar_questions_state, user_id_state],
929
+ _TEXT_CLAR_OUT
930
+ ).then(lambda: None, outputs=[clar_audio_input])
931
+
932
+ btn_discard.click(lambda: gr.update(visible=False), outputs=[parsed_panel])
933
+ # Confirm saves task + refreshes BOTH today and all_tasks
934
+ btn_confirm.click(
935
+ handle_confirm_task,
936
+ [user_id_state, f_title, f_area, f_urgency, f_importance, f_state, f_time, f_deadline, f_is_habit, f_interval],
937
+ [save_msg, today_df, stat_total, stat_done, stat_remain, all_df, parsed_panel]
938
+ )
939
+
940
+ # Voice task flow
941
+ _VOICE_PARSE_OUT = [voice_parsed_panel, voice_transcribed_txt,
942
+ voice_clar_html, vf_area, vf_urgency, vf_importance,
943
+ vf_state, vf_time, vf_deadline, vf_is_habit, vf_interval,
944
+ voice_status, voice_clar_row,
945
+ vclar_questions_state, voriginal_task_state]
946
+ _VOICE_CLAR_OUT = [vf_title, voice_clar_html, vf_area, vf_urgency, vf_importance,
947
+ vf_state, vf_time, vf_deadline, voice_clar_row, vclar_questions_state]
948
+
949
+ btn_voice_parse.click(handle_voice_parse, [voice_audio_input, user_id_state], _VOICE_PARSE_OUT)
950
+
951
+ btn_vclar_submit.click(
952
+ handle_clarification_reply,
953
+ [vclar_text, voriginal_task_state, vclar_questions_state, user_id_state],
954
+ _VOICE_CLAR_OUT
955
+ ).then(lambda: ("", None), outputs=[vclar_text, vclar_audio])
956
+
957
+ vclar_audio.change(
958
+ handle_clar_voice,
959
+ [vclar_audio, voriginal_task_state, vclar_questions_state, user_id_state],
960
+ _VOICE_CLAR_OUT
961
+ ).then(lambda: None, outputs=[vclar_audio])
962
+
963
+ btn_voice_discard.click(lambda: gr.update(visible=False), outputs=[voice_parsed_panel])
964
+ btn_voice_confirm.click(
965
+ handle_confirm_task,
966
+ [user_id_state, vf_title, vf_area, vf_urgency, vf_importance, vf_state, vf_time, vf_deadline, vf_is_habit, vf_interval],
967
+ [voice_save_msg, today_df, stat_total, stat_done, stat_remain, all_df, voice_parsed_panel]
968
+ )
969
+
970
+ # Smart scheduler — updates both schedule_html and all_df
971
+ btn_gen_sched.click(
972
+ handle_smart_schedule, [user_id_state, plan_prompt],
973
+ [schedule_html, all_df]
974
+ ).then(refresh_today, [user_id_state], [today_df, stat_total, stat_done, stat_remain])
975
+
976
+ btn_today_done.click(handle_toggle_today, [today_done_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg])
977
+ btn_today_del.click(handle_delete_today, [today_del_id, user_id_state], [today_df, stat_total, stat_done, stat_remain, today_action_msg])
978
+
979
+ # All Tasks
980
+ def refresh_all_tasks_filtered(user_id, filter_area, only_today, only_unsched):
981
+ if not user_id: return []
982
+ return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today, only_unscheduled=only_unsched))
983
+
984
+ btn_all_ref.click(refresh_all_tasks_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
985
+ all_filter.change(refresh_all_tasks_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
986
+ all_today_chk.change(refresh_all_tasks_filtered,[user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
987
+ all_unsched.change(refresh_all_tasks_filtered, [user_id_state, all_filter, all_today_chk, all_unsched], [all_df])
988
+
989
+ def handle_toggle_all_f(task_id, user_id, filter_area, only_today, only_unsched):
990
+ if task_id and user_id: toggle_task_complete(int(task_id), user_id)
991
+ return refresh_all_tasks_filtered(user_id, filter_area, only_today, only_unsched), _ok("Updated")
992
+ def handle_delete_all_f(task_id, user_id, filter_area, only_today, only_unsched):
993
+ if task_id and user_id: delete_task(int(task_id), user_id)
994
+ return refresh_all_tasks_filtered(user_id, filter_area, only_today, only_unsched), _ok("Deleted")
995
+
996
+ btn_all_done.click(handle_toggle_all_f, [all_done_id, user_id_state, all_filter, all_today_chk, all_unsched], [all_df, all_action_msg])
997
+ btn_all_del.click(handle_delete_all_f, [all_del_id, user_id_state, all_filter, all_today_chk, all_unsched], [all_df, all_action_msg])
998
+
999
+ # Journal
1000
+ _J_START_OUT = [journal_chatbot, journal_hist_state, journal_active,
1001
+ j_answer, btn_j_send, j_audio_in, journal_tasks_state,
1002
+ journal_phase_state, journal_msg]
1003
+ _J_MSG_IN = [user_id_state, j_answer, j_audio_in, journal_chatbot,
1004
+ journal_hist_state, journal_tasks_state, journal_active, journal_phase_state]
1005
+ _J_MSG_OUT = [journal_chatbot, journal_hist_state, j_answer, j_audio_in,
1006
+ journal_tasks_state, journal_active, journal_phase_state, journal_msg]
1007
+
1008
+ btn_j_start.click(handle_start_journal, [user_id_state], _J_START_OUT)
1009
+ btn_j_send.click(handle_journal_message, _J_MSG_IN, _J_MSG_OUT)
1010
+ j_answer.submit(handle_journal_message, _J_MSG_IN, _J_MSG_OUT)
1011
+ j_audio_in.change(handle_journal_message, _J_MSG_IN, _J_MSG_OUT)
1012
+
1013
+ btn_j_finish.click(
1014
+ handle_finish_journal,
1015
+ [user_id_state, journal_chatbot, journal_hist_state, journal_tasks_state],
1016
+ [journal_chatbot, journal_msg]
1017
+ )
1018
+ btn_j_restart.click(handle_restart_journal, outputs=[
1019
+ journal_chatbot, journal_hist_state, journal_active,
1020
+ j_answer, btn_j_send, j_audio_in, journal_tasks_state,
1021
+ journal_phase_state, journal_msg
1022
+ ])
1023
+
1024
+ # Areas & Goals
1025
+ btn_add_area.click(
1026
+ handle_add_area, [user_id_state, new_area_name, new_area_color],
1027
+ [area_msg, areas_display, del_area_dd, new_area_name]
1028
+ ).then(lambda uid: gr.update(choices=_area_choices(uid), value="All"), [user_id_state], [all_filter])
1029
+
1030
+ btn_del_area.click(
1031
+ handle_del_area, [user_id_state, del_area_dd],
1032
+ [del_area_msg, areas_display, del_area_dd]
1033
+ ).then(lambda uid: gr.update(choices=_area_choices(uid), value="All"), [user_id_state], [all_filter])
1034
+
1035
+ btn_save_goals.click(handle_save_goals, [user_id_state, goals_input], [goals_msg])
1036
+
1037
+ # Preferences
1038
+ btn_save_prefs.click(
1039
+ handle_save_prefs,
1040
+ [user_id_state, pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max],
1041
+ [prefs_msg]
1042
+ )
1043
+ btn_ref_ctx.click(render_context_html, [user_id_state], [ctx_display])
1044
+
1045
+
1046
+ if __name__ == "__main__":
1047
+ demo.launch(server_name="0.0.0.0", server_port=7860, css=CSS)