"""
app.py — Second Brain Gradio Application
==========================================
Modular structure:
core/database.py — SQLite persistence (users, tasks, life areas, goals, AI context)
core/ai_engine.py — All Groq API calls (task parsing, scheduling, journaling)
core/styles.py — CSS
Run locally:
pip install -r requirements.txt
GROQ_API_KEY=gsk_... python app.py
HuggingFace Spaces:
Add GROQ_API_KEY in Space Settings → Repository Secrets
"""
import os
import gradio as gr
from datetime import date, datetime
# ── Core modules ──────────────────────────────────────────────────────────────
from core.database import (
init_db, register_user, login_user, get_username,
create_default_life_areas, save_goals, get_goals,
get_life_areas, get_life_area_names, add_life_area, delete_life_area,
get_tasks, save_task, toggle_task_complete, delete_task, get_today_stats,
load_user_context, save_user_context, spawn_due_habits,
)
from core.ai_engine import (
init_groq, create_blank_context,
parse_task_with_groq, generate_schedule,
build_opening_question, get_next_journal_question, synthesize_journal,
)
from core.styles import CSS
# ── Boot ──────────────────────────────────────────────────────────────────────
init_db()
init_groq() # reads GROQ_API_KEY from env
TASK_HEADERS = ["ID", "Done", "Title", "Area", "Urgency", "Importance", "Mind", "Min", "Date"]
# ═══════════════════════════════════════════════════════════════════════════════
# SHARED HELPERS
# ═══════════════════════════════════════════════════════════════════════════════
def _ok(msg): return f'✓ {msg}'
def _err(msg): return f'⚠ {msg}'
def _info(msg): return f'ℹ {msg}'
def _stat(val, label, color):
return f'
'
def _fmt_tasks(tasks: list) -> list:
rows = []
for t in tasks:
rows.append([
t["id"],
"✅" if t["is_completed"] else "⬜",
("🔁 " if t["is_habit"] else "") + t["title"],
t["life_area"] or "—",
t["urgency"] or "—",
t["importance"] or "—",
t["state_of_mind"] or "—",
str(t["time_estimate"]) + "m" if t["time_estimate"] else "—",
t["scheduled_date"] or "—",
])
return rows
def _area_choices(user_id):
if not user_id:
return ["All"]
return ["All"] + get_life_area_names(user_id)
def _ensure_context(user_id):
"""Load or create blank AI context for a user."""
ctx = load_user_context(user_id)
if not ctx:
ctx = create_blank_context(user_id)
save_user_context(user_id, ctx)
return ctx
# ═══════════════════════════════════════════════════════════════════════════════
# AUTH HANDLERS
# ═══════════════════════════════════════════════════════════════════════════════
def handle_login(username, password):
uid, msg = login_user(username, password)
if not uid:
return None, "", _err(msg), gr.update(visible=True), gr.update(visible=False)
spawn_due_habits(uid)
uname = get_username(uid)
return uid, uname, _ok(msg), gr.update(visible=False), gr.update(visible=True)
def handle_register(username, password, wake, sleep, focus, goals_text):
uid, msg = register_user(username, password)
if not uid:
return None, "", _err(msg), gr.update(visible=True), gr.update(visible=False)
create_default_life_areas(uid)
if goals_text.strip():
save_goals(uid, goals_text)
prefs = {"wake_time": wake or "07:30", "sleep_time": sleep or "23:00", "focus_peak": focus or "Morning"}
ctx = create_blank_context(uid, prefs)
save_user_context(uid, ctx)
spawn_due_habits(uid)
uname = get_username(uid)
return uid, uname, _ok(msg + " Logged in!"), gr.update(visible=False), gr.update(visible=True)
def handle_logout(user_id):
return None, "", gr.update(visible=True), gr.update(visible=False)
# ═══════════════════════════════════════════════════════════════════════════════
# TODAY TAB HANDLERS
# ═══════════════════════════════════════════════════════════════════════════════
def refresh_today(user_id):
if not user_id:
return [], _stat("—","Today","#a78bfa"), _stat("—","Done","#4ade80"), _stat("—","Left","#f87171")
tasks = get_tasks(user_id, only_today=True)
s = get_today_stats(user_id)
c = "#4ade80" if s["remaining"] == 0 and s["total"] > 0 else "#f87171"
return (
_fmt_tasks(tasks),
_stat(s["total"], "Today", "#a78bfa"),
_stat(s["done"], "Done", "#4ade80"),
_stat(s["remaining"], "Left", c),
)
def show_text_panel():
return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
def show_voice_panel():
return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
def show_plan_panel():
return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
def hide_panels():
return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
def handle_parse_text(task_text, user_id):
"""Call Groq to classify a typed task, return pre-filled fields."""
if not task_text.strip():
return (gr.update(visible=False),
"", "", "Work", "Not Urgent", "Important", "Easy", 30, str(date.today()), False, "Daily", "")
ctx = _ensure_context(user_id)
goals = get_goals(user_id)
areas = get_life_area_names(user_id) if user_id else []
result = parse_task_with_groq(task_text, ctx, goals, areas)
clarifications = result.get("clarifications_needed", [])
clar_html = ""
if clarifications:
items = "".join(f"{q}" for q in clarifications)
clar_html = (
f''
f'
⚠ Please clarify:
'
f'
'
)
area_choices = areas if areas else ["Work", "Health", "Finance", "Learning", "Personal", "Family", "Other"]
area_val = result.get("life_area") or (area_choices[0] if area_choices else "Work")
if area_val not in area_choices:
area_choices = [area_val] + area_choices
return (
gr.update(visible=True),
result.get("title", task_text),
clar_html,
area_val,
result.get("urgency", "Not Urgent"),
result.get("importance", "Important"),
result.get("state_of_mind", "Easy"),
int(result.get("time_estimate") or 30),
str(date.today()),
False,
"Daily",
_ok("AI classified your task"),
)
def handle_transcribe_voice(audio_path, user_id):
"""Whisper transcription then Groq parse — same pipeline as notebook."""
if audio_path is None:
return gr.update(visible=False), _err("No audio recorded.")
try:
import whisper
global _whisper_model
if _whisper_model is None:
_whisper_model = whisper.load_model("small")
result = _whisper_model.transcribe(audio_path)
text = result["text"].strip()
return gr.update(visible=True, value=text), _ok(f'Transcribed: "{text[:60]}..."')
except Exception as e:
return gr.update(visible=False), _err(f"Transcription error: {e}")
_whisper_model = None
def handle_confirm_task(user_id, title, area, urgency, importance, state,
time_est, sched_date, is_habit, habit_interval):
if not user_id:
return _err("Not logged in."), [], _stat("—","Today","#a78bfa"), _stat("—","Done","#4ade80"), _stat("—","Left","#f87171"), gr.update(visible=False)
if not title.strip():
return _err("Title cannot be empty."), [], _stat("—","Today","#a78bfa"), _stat("—","Done","#4ade80"), _stat("—","Left","#f87171"), gr.update(visible=True)
task = {
"title": title, "life_area": area,
"urgency": urgency, "importance": importance, "state_of_mind": state,
"time_estimate": int(time_est or 30),
"is_habit": is_habit,
"habit_interval": habit_interval if is_habit else "",
}
save_task(user_id, task, sched_date)
rows, s1, s2, s3 = refresh_today(user_id)
return _ok("Task saved!"), rows, s1, s2, s3, gr.update(visible=False)
def handle_generate_schedule(user_id, prompt):
if not user_id:
return _err("Not logged in.")
tasks = get_tasks(user_id, only_today=True, include_completed=False)
if not tasks:
return _err("No incomplete tasks today to schedule.")
ctx = _ensure_context(user_id)
goals = get_goals(user_id)
sched = generate_schedule(ctx, tasks, prompt or "Schedule my day sensibly.", goals)
if "error" in sched and "scheduled_tasks" not in sched:
return _err(f"Scheduling failed: {sched.get('error')}")
COLOR = {"Flow": "#0ea5e9", "Easy": "#4ade80", "Quick": "#a78bfa", "Personal": "#f87171"}
html = (
f''
f'
📅 {sched.get("schedule_date","Today")}'
f'
{sched.get("day_summary","")}
'
f'
'
)
for t in sched.get("scheduled_tasks", []):
sm = t.get("state_of_mind", "Easy")
c = COLOR.get(sm, "#6366f1")
html += (
f''
f'
{t.get("start_time")} – {t.get("end_time")}
'
f'
{t.get("title")}
'
f'
{t.get("life_area","—")} · {sm} · {t.get("duration_minutes")}min
'
f'
{t.get("scheduling_reason","")}
'
f'
'
)
if sched.get("deferred_tasks"):
html += 'Deferred
'
for t in sched["deferred_tasks"]:
html += f'✗ {t["title"]} — {t.get("reason","")}
'
for w in sched.get("warnings", []):
html += f'⚠ {w}
'
return html
def handle_toggle_today(task_id, user_id):
if task_id and user_id:
toggle_task_complete(int(task_id), user_id)
rows, s1, s2, s3 = refresh_today(user_id)
return rows, s1, s2, s3, _ok("Updated")
def handle_delete_today(task_id, user_id):
if task_id and user_id:
delete_task(int(task_id), user_id)
rows, s1, s2, s3 = refresh_today(user_id)
return rows, s1, s2, s3, _ok("Deleted")
# ═══════════════════════════════════════════════════════════════════════════════
# ALL TASKS TAB HANDLERS
# ═══════════════════════════════════════════════════════════════════════════════
def refresh_all_tasks(user_id, filter_area="All", only_today=False):
if not user_id:
return []
return _fmt_tasks(get_tasks(user_id, filter_area=filter_area, only_today=only_today))
def handle_toggle_all(task_id, user_id, filter_area, only_today):
if task_id and user_id:
toggle_task_complete(int(task_id), user_id)
return refresh_all_tasks(user_id, filter_area, only_today), _ok("Updated")
def handle_delete_all(task_id, user_id, filter_area, only_today):
if task_id and user_id:
delete_task(int(task_id), user_id)
return refresh_all_tasks(user_id, filter_area, only_today), _ok("Deleted")
# ═══════════════════════════════════════════════════════════════════════════════
# JOURNAL TAB HANDLERS
# ═══════════════════════════════════════════════════════════════════════════════
def handle_load_journal_tasks(user_id):
if not user_id:
return "Not logged in.
", gr.update(visible=False), []
tasks = get_tasks(user_id, only_today=True)
if not tasks:
return "No tasks for today.
", gr.update(visible=False), []
rows = ""
state = []
for t in tasks:
status = "✅" if t["is_completed"] else "⬜"
actual = f" ({t['actual_duration']}m actual)" if t["actual_duration"] else ""
rows += (
f''
f'#{t["id"]}'
f'{status}'
f'{t["title"]}'
f'{t["life_area"] or "—"}'
f'{t["time_estimate"]}m{actual}'
f'
'
)
state.append({
"task_id": t["id"], "title": t["title"],
"state_of_mind": t["state_of_mind"],
"start_time": "", "end_time": "",
"time_estimate": t["time_estimate"],
"completed": bool(t["is_completed"]),
"actual_duration": t["actual_duration"],
})
return f'{rows}
', gr.update(visible=True), state
def handle_journal_mark(task_id, actual_mins, user_id, tasks_state, mark_done: bool):
if not task_id or not user_id:
return tasks_state, _err("Enter a task ID.")
tid = int(task_id)
if mark_done:
toggle_task_complete(tid, user_id, int(actual_mins) if actual_mins else None)
else:
toggle_task_complete(tid, user_id)
for t in tasks_state:
if t["task_id"] == tid:
t["completed"] = mark_done
if mark_done and actual_mins:
t["actual_duration"] = int(actual_mins)
return tasks_state, _ok("Marked ✅" if mark_done else "Unmarked")
def handle_start_journal(user_id, tasks_state):
if not user_id:
return [], [], False, gr.update(interactive=False), gr.update(interactive=False), _err("Not logged in.")
if not tasks_state:
return [], [], False, gr.update(interactive=False), gr.update(interactive=False), _err("Load today's tasks first.")
ctx = _ensure_context(user_id)
opening = build_opening_question(ctx, tasks_state)
chat = [(None, opening["question"])]
hist = [{"role": "assistant", "content": opening["question"], "focus": opening["question_focus"]}]
return (
chat, hist, True,
gr.update(interactive=True),
gr.update(interactive=True),
_ok("Session started — type your answer below"),
)
def handle_send_answer(user_id, answer, chat, hist, tasks_state, active):
if not active or not answer.strip():
return chat, hist, "", ""
chat = list(chat) + [(answer, None)]
hist = list(hist) + [{"role": "user", "content": answer}]
ctx = _ensure_context(user_id)
result = get_next_journal_question(ctx, tasks_state, hist)
if result.get("session_complete"):
chat[-1] = (answer, "✅ That's enough for today. Click **Finish & Save** to update your profile.")
return chat, hist, "", _ok("Session complete — save when ready")
next_q = result.get("question", "")
hist.append({"role": "assistant", "content": next_q, "focus": result.get("question_focus", "")})
chat[-1] = (answer, next_q)
return chat, hist, "", ""
def handle_finish_journal(user_id, chat, hist, tasks_state):
if not user_id or not hist:
return chat, _err("Nothing to save.")
ctx = _ensure_context(user_id)
updated = synthesize_journal(ctx, tasks_state, hist)
save_user_context(user_id, updated)
total = len(tasks_state)
done = sum(1 for t in tasks_state if t.get("completed"))
rate = round(done / total * 100) if total else 0
notes = updated.get("learned_patterns", {}).get("notes", [])
note_html = "".join(f"{n}" for n in notes[-3:]) if notes else "No new patterns yet"
return (
chat,
_ok(f"Insights saved! {done}/{total} tasks ({rate}%) completed.")
+ f''
)
def handle_restart_journal():
return [], [], False, gr.update(interactive=False), gr.update(interactive=False), ""
# ═══════════════════════════════════════════════════════════════════════════════
# LIFE AREAS & GOALS HANDLERS
# ═══════════════════════════════════════════════════════════════════════════════
def render_areas(user_id):
if not user_id:
return "", [], []
areas = get_life_areas(user_id)
chips = ""
for a in areas:
chips += (
f''
f'{a["name"]} '
)
html = f'{chips}
' if chips else 'No areas yet.
'
names = [a["name"] for a in areas]
return html, names, names
def handle_add_area(user_id, name, color):
ok, msg = add_life_area(user_id, name, color)
css = "ok" if ok else "err"
html, names, _ = render_areas(user_id)
return (
f'{msg}',
html, gr.update(choices=names, value=None), gr.update(value=""),
)
def handle_del_area(user_id, name):
if not name:
return _err("Select an area first."), "", []
ok, msg = delete_life_area(user_id, name)
html, names, _ = render_areas(user_id)
return (
f'{msg}',
html, gr.update(choices=names, value=None),
)
def handle_save_goals(user_id, goals_text):
if not user_id:
return _err("Not logged in.")
save_goals(user_id, goals_text)
return _ok("Goals saved!")
def load_goals_txt(user_id):
if not user_id:
return ""
return "\n".join(get_goals(user_id))
# ═══════════════════════════════════════════════════════════════════════════════
# PREFERENCES HANDLERS
# ═══════════════════════════════════════════════════════════════════════════════
def load_prefs(user_id):
ctx = load_user_context(user_id) if user_id else None
if not ctx:
return "07:30", "23:00", "Morning", 10, 90
p = ctx.get("preferences", {})
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),
)
def handle_save_prefs(user_id, wake, sleep, focus, brk, flow_max):
if not user_id:
return _err("Not logged in.")
ctx = _ensure_context(user_id)
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),
})
save_user_context(user_id, ctx)
return _ok("Preferences saved!")
def render_context_html(user_id):
if not user_id:
return "Not logged in.
"
ctx = load_user_context(user_id)
if not ctx:
return "No AI context yet. Complete a journaling session to start building it.
"
lp = ctx.get("learned_patterns", {})
sf = ctx.get("scheduling_feedback", {})
pref = ctx.get("preferences", {})
def row(label, val):
return (
f''
f'{label}'
f'{val}'
f'
'
)
def lst(v): return ", ".join(v) if v else "—"
html = (
f''
f'
'
f'
Preferences
'
f'{row("Wake", pref.get("wake_time","—"))}'
f'{row("Sleep", pref.get("sleep_time","—"))}'
f'{row("Peak focus", pref.get("focus_peak","—"))}'
f'{row("Break", str(pref.get("break_duration_minutes","—")) + "min")}'
f'{row("Max flow block", str(pref.get("max_flow_block_minutes","—")) + "min")}'
f'
'
f'
'
f'
Scheduling Stats
'
f'{row("Days scheduled", sf.get("total_days_scheduled",0))}'
f'{row("Avg completion", str(round(sf.get("avg_completion_rate",0)*100)) + "%")}'
f'{row("Context version", ctx.get("version",1))}'
f'{row("Last updated", (ctx.get("last_updated","—") or "—")[:16])}'
f'
'
f'
'
f''
f'
Learned Patterns
'
f'{row("Productive times", lst(lp.get("productive_times",[])))}'
f'{row("Low energy times", lst(lp.get("low_energy_times",[])))}'
f'{row("Avg task overrun", str(lp.get("avg_task_overrun_pct",0)) + "%")}'
f'{row("Flow batchable", str(lp.get("flow_batch_capable","Unknown")))}'
f'{row("Best morning areas", lst(lp.get("best_life_areas_morning",[])))}'
f'{row("Skipped types", lst(lp.get("common_skipped_task_types",[])))}'
f'
'
)
notes = lp.get("notes", [])
if notes:
note_rows = "".join(
f'• {n}
'
for n in notes[-5:]
)
html += f''
history = ctx.get("history_summary", [])
if history:
hist_rows = "".join(
f'• {h}
'
for h in history[-5:]
)
html += f'Recent History
{hist_rows}
'
return html
# ═══════════════════════════════════════════════════════════════════════════════
# UI BUILD
# ═══════════════════════════════════════════════════════════════════════════════
with gr.Blocks(css=CSS, title="🧠 Second Brain") as demo:
# ── Session state ──────────────────────────────────────────────────────────
user_id_state = gr.State(None)
username_state = gr.State("")
# Journal states
journal_chat_state = gr.State([])
journal_hist_state = gr.State([])
journal_tasks_state = gr.State([])
journal_active = gr.State(False)
# ═════════════════════════════════════════════════════════════════════════
# AUTH SECTION
# ═════════════════════════════════════════════════════════════════════════
with gr.Column(visible=True, elem_id="auth-card") as auth_section:
gr.HTML('🧠 Second Brain
')
gr.HTML('Your intelligent productivity companion
')
with gr.Tabs():
# ── Sign In ────────────────────────────────────────────────────
with gr.Tab("Sign In"):
login_user_in = gr.Textbox(label="Username", placeholder="your username")
login_pass_in = gr.Textbox(label="Password", type="password", placeholder="••••••••")
login_btn = gr.Button("Sign In", elem_classes="btn-primary")
login_msg = gr.HTML("")
# ── Create Account ─────────────────────────────────────────────
with gr.Tab("Create Account"):
reg_user_in = gr.Textbox(label="Username", placeholder="choose a username")
reg_pass_in = gr.Textbox(label="Password (min 6 chars)", type="password", placeholder="••••••••")
with gr.Row():
reg_wake = gr.Textbox(label="Wake time", value="07:30", scale=1)
reg_sleep = gr.Textbox(label="Sleep time", value="23:00", scale=1)
reg_focus = gr.Dropdown(label="Peak focus",
choices=["Morning","Afternoon","Evening","Night"],
value="Morning", scale=1)
reg_goals = gr.Textbox(
label="Big-picture goals (one per line, optional)",
lines=3,
placeholder="Launch my startup\nGet fit\nLearn machine learning"
)
reg_btn = gr.Button("Create Account", elem_classes="btn-primary")
reg_msg = gr.HTML("")
# ═════════════════════════════════════════════════════════════════════════
# MAIN APP SECTION
# ═════════════════════════════════════════════════════════════════════════
with gr.Column(visible=False) as app_section:
# ── Top header ─────────────────────────────────────────────────────
with gr.Row(elem_id="top-header"):
header_html = gr.HTML('🧠 Second Brain
')
logout_btn = gr.Button("Sign Out", elem_classes="btn-secondary", scale=0)
# ── Tabs ────────────────────────────────────────────────────────────
with gr.Tabs() as main_tabs:
# ─────────────────────────────────────────────────────────────
# TAB 1: TODAY
# ─────────────────────────────────────────────────────────────
with gr.Tab("📅 Today"):
# Stats row
with gr.Row():
stat_total = gr.HTML(_stat("—","Today","#a78bfa"))
stat_done = gr.HTML(_stat("—","Done","#4ade80"))
stat_remain = gr.HTML(_stat("—","Left","#f87171"))
# Action buttons
with gr.Row():
btn_add_text = gr.Button("✏️ Add Task (Text)", elem_classes="btn-primary", scale=3)
btn_add_voice = gr.Button("🎙️ Add Task (Voice)", elem_classes="btn-accent", scale=3)
btn_plan_day = gr.Button("🗓 Plan My Day", elem_classes="btn-secondary", scale=3)
btn_refresh = gr.Button("↻", elem_classes="btn-secondary", scale=1)
# ── Text add panel ────────────────────────────────────────
with gr.Column(visible=False, elem_classes="panel") as text_panel:
gr.HTML('Describe Your Task
')
task_input = gr.Textbox(
label="",
placeholder="e.g. Finish the client proposal by tomorrow, it's really important",
lines=2
)
with gr.Row():
btn_parse = gr.Button("🤖 Parse with AI", elem_classes="btn-primary")
btn_cancel_text = gr.Button("Cancel", elem_classes="btn-secondary")
with gr.Column(visible=False) as parsed_panel:
parse_status = gr.HTML("")
clar_html = gr.HTML("")
gr.HTML('Review & Confirm
')
with gr.Row():
f_title = gr.Textbox(label="Title", scale=3)
f_area = gr.Dropdown(label="Life Area",
choices=["Work","Health","Finance","Learning","Personal","Family","Other"],
scale=1)
with gr.Row():
f_urgency = gr.Dropdown(label="Urgency",
choices=["Urgent","Not Urgent","Habit"], value="Not Urgent")
f_importance = gr.Dropdown(label="Importance",
choices=["Move the Needle","Important","Not Important"],
value="Important")
f_state = gr.Dropdown(label="State of Mind",
choices=["Flow","Easy","Quick","Personal"], value="Easy")
with gr.Row():
f_time = gr.Number(label="Minutes", value=30, minimum=5, scale=1)
f_date = gr.Textbox(label="Scheduled Date", value=str(date.today()), scale=1)
f_is_habit = gr.Checkbox(label="♻️ This is a habit", scale=1)
f_interval = gr.Dropdown(label="Recurs",
choices=["Daily","Weekly","Weekdays","Weekends","Monthly"],
value="Daily", visible=False, scale=1)
with gr.Row():
btn_confirm = gr.Button("✓ Save Task", elem_classes="btn-success")
btn_discard = gr.Button("✗ Discard", elem_classes="btn-danger")
save_msg = gr.HTML("")
# ── Voice add panel ───────────────────────────────────────
with gr.Column(visible=False, elem_classes="panel") as voice_panel:
gr.HTML('Record or Upload Audio
')
gr.HTML('Whisper transcribes it, then AI classifies the task automatically.
')
audio_input = gr.Audio(sources=["microphone","upload"], type="filepath", label="")
transcribed = gr.Textbox(label="Transcribed text (editable before parsing)",
visible=False, lines=2)
with gr.Row():
btn_transcribe = gr.Button("🤖 Transcribe & Parse", elem_classes="btn-primary")
btn_cancel_voice = gr.Button("Cancel", elem_classes="btn-secondary")
voice_msg = gr.HTML("")
# ── Plan My Day panel ─────────────────────────────────────
with gr.Column(visible=False, elem_classes="panel") as plan_panel:
gr.HTML('Plan My Day
')
gr.HTML('The AI reads your tasks, context, and goals to build the optimal schedule.
')
plan_prompt = gr.Textbox(
label="Any fixed events or preferences today?",
placeholder="e.g. I have a meeting at 10am, want deep work in the morning, done by 7pm",
lines=3
)
with gr.Row():
btn_gen_sched = gr.Button("✨ Generate Schedule", elem_classes="btn-primary")
btn_cancel_plan = gr.Button("Cancel", elem_classes="btn-secondary")
schedule_html = gr.HTML("")
# ── Today's task table ────────────────────────────────────
gr.HTML('Today\'s Tasks
')
today_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
with gr.Row():
today_done_id = gr.Number(label="Task ID — toggle done", precision=0, scale=2)
btn_today_done = gr.Button("✅ Mark Done", elem_classes="btn-success", scale=2)
today_del_id = gr.Number(label="Task ID — delete", precision=0, scale=2)
btn_today_del = gr.Button("🗑 Delete", elem_classes="btn-danger", scale=2)
today_action_msg = gr.HTML("")
# ─────────────────────────────────────────────────────────────
# TAB 2: ALL TASKS
# ─────────────────────────────────────────────────────────────
with gr.Tab("📋 All Tasks"):
with gr.Row():
all_filter = gr.Dropdown(label="Filter by Area", choices=["All"], value="All", scale=4)
all_today_chk = gr.Checkbox(label="Today only", value=False, scale=1)
btn_all_ref = gr.Button("↻ Refresh", elem_classes="btn-secondary", scale=1)
all_df = gr.Dataframe(headers=TASK_HEADERS, interactive=False, wrap=True)
with gr.Row():
all_done_id = gr.Number(label="Task ID — toggle done", precision=0, scale=2)
btn_all_done = gr.Button("✅ Done", elem_classes="btn-success", scale=2)
all_del_id = gr.Number(label="Task ID — delete", precision=0, scale=2)
btn_all_del = gr.Button("🗑 Delete", elem_classes="btn-danger", scale=2)
all_action_msg = gr.HTML("")
# ─────────────────────────────────────────────────────────────
# TAB 3: JOURNAL
# ─────────────────────────────────────────────────────────────
with gr.Tab("📓 Journal"):
gr.HTML('Mark today\'s tasks complete, then reflect. The AI updates your profile to improve tomorrow\'s schedule.
')
# Step 1 — Mark tasks
with gr.Column(elem_classes="panel"):
gr.HTML('Step 1 — Mark Tasks Complete
')
journal_task_list = gr.HTML('Click "Load Tasks" to begin.
')
btn_load_tasks = gr.Button("Load Today's Tasks", elem_classes="btn-secondary")
with gr.Row(visible=False) as journal_mark_row:
j_task_id = gr.Number(label="Task ID", precision=0, scale=2)
j_actual_min = gr.Number(label="Actual time (min)", precision=0, value=0, scale=2)
btn_j_done = gr.Button("✅ Mark Complete", elem_classes="btn-success", scale=2)
btn_j_undone = gr.Button("↩ Unmark", elem_classes="btn-secondary", scale=1)
journal_mark_msg = gr.HTML("")
# Step 2 — Chat
with gr.Column(elem_classes="panel"):
gr.HTML('Step 2 — Reflect
')
journal_chatbot = gr.Chatbot(label="", height=380, bubble_full_width=False)
with gr.Row():
btn_j_start = gr.Button("▶ Start Reflection", elem_classes="btn-primary", scale=2)
btn_j_finish = gr.Button("✓ Finish & Save Insights", elem_classes="btn-success", scale=2)
btn_j_restart = gr.Button("↺ Restart", elem_classes="btn-secondary", scale=1)
j_answer = gr.Textbox(label="Your answer", placeholder="Type here…", lines=2, interactive=False)
btn_j_send = gr.Button("Send →", elem_classes="btn-accent", interactive=False)
journal_msg = gr.HTML("")
# ─────────────────────────────────────────────────────────────
# TAB 4: LIFE AREAS & GOALS
# ─────────────────────────────────────────────────────────────
with gr.Tab("🗂 Areas & Goals"):
gr.HTML('Your Life Areas
')
areas_display = gr.HTML("")
with gr.Row():
new_area_name = gr.Textbox(label="New area name", placeholder="e.g. Side Project", scale=3)
new_area_color = gr.ColorPicker(label="Color", value="#6366f1", scale=1)
btn_add_area = gr.Button("Add", elem_classes="btn-primary", scale=1)
area_msg = gr.HTML("")
gr.HTML('Remove Area
')
with gr.Row():
del_area_dd = gr.Dropdown(label="Select area to remove", choices=[], scale=4)
btn_del_area = gr.Button("Remove", elem_classes="btn-danger", scale=1)
del_area_msg = gr.HTML("")
gr.HTML('
')
gr.HTML('Big-Picture Goals
')
gr.HTML('The AI uses these when scheduling and prioritising your tasks.
')
goals_input = gr.Textbox(label="One goal per line", lines=5,
placeholder="Launch my SaaS\nRun 5K\nLearn ML")
btn_save_goals = gr.Button("Save Goals", elem_classes="btn-primary")
goals_msg = gr.HTML("")
# ─────────────────────────────────────────────────────────────
# TAB 5: PREFERENCES
# ─────────────────────────────────────────────────────────────
with gr.Tab("⚙️ Preferences"):
gr.HTML('Scheduling Preferences
')
with gr.Row():
pref_wake = gr.Textbox(label="Wake time", placeholder="07:30", scale=1)
pref_sleep = gr.Textbox(label="Sleep time", placeholder="23:00", scale=1)
pref_focus = gr.Dropdown(label="Peak focus",
choices=["Morning","Afternoon","Evening","Night"],
value="Morning", scale=1)
with gr.Row():
pref_break = gr.Number(label="Break between tasks (min)", value=10, minimum=0, scale=1)
pref_flow_max = gr.Number(label="Max Flow block (min)", value=90, minimum=30, scale=1)
btn_save_prefs = gr.Button("Save Preferences", elem_classes="btn-primary")
prefs_msg = gr.HTML("")
gr.HTML('
')
gr.HTML('AI Memory
')
gr.HTML('Everything the AI has learned about you. Updates after each journaling session.
')
ctx_display = gr.HTML("")
btn_ref_ctx = gr.Button("↻ Refresh", elem_classes="btn-secondary")
# ═══════════════════════════════════════════════════════════════════════
# EVENT WIRING
# ═══════════════════════════════════════════════════════════════════════
# ── Auth ──────────────────────────────────────────────────────────────
def _post_login(uid, uname):
if not uid:
return uid, uname
spawn_due_habits(uid)
return uid, uname
login_btn.click(
handle_login,
[login_user_in, login_pass_in],
[user_id_state, username_state, login_msg, auth_section, app_section]
).then(
lambda uid, uname: f'🧠 Second Brain
',
[user_id_state, username_state], [header_html]
).then(
refresh_today, [user_id_state],
[today_df, stat_total, stat_done, stat_remain]
).then(
lambda uid: render_areas(uid)[0:2],
[user_id_state], [areas_display, del_area_dd]
).then(
load_goals_txt, [user_id_state], [goals_input]
).then(
lambda uid: load_prefs(uid),
[user_id_state], [pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max]
)
reg_btn.click(
handle_register,
[reg_user_in, reg_pass_in, reg_wake, reg_sleep, reg_focus, reg_goals],
[user_id_state, username_state, reg_msg, auth_section, app_section]
).then(
lambda uid, uname: f'🧠 Second Brain
',
[user_id_state, username_state], [header_html]
).then(
refresh_today, [user_id_state],
[today_df, stat_total, stat_done, stat_remain]
).then(
lambda uid: render_areas(uid)[0:2],
[user_id_state], [areas_display, del_area_dd]
)
logout_btn.click(
handle_logout,
[user_id_state],
[user_id_state, username_state, auth_section, app_section]
)
# ── Today tab ─────────────────────────────────────────────────────────
btn_add_text.click(show_text_panel, outputs=[text_panel, voice_panel, plan_panel])
btn_add_voice.click(show_voice_panel, outputs=[text_panel, voice_panel, plan_panel])
btn_plan_day.click(show_plan_panel, outputs=[text_panel, voice_panel, plan_panel])
btn_cancel_text.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
btn_cancel_voice.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
btn_cancel_plan.click(hide_panels, outputs=[text_panel, voice_panel, plan_panel])
btn_refresh.click(
refresh_today, [user_id_state],
[today_df, stat_total, stat_done, stat_remain]
)
# Show habit interval dropdown only when habit checked
f_is_habit.change(
lambda v: gr.update(visible=v),
[f_is_habit], [f_interval]
)
btn_parse.click(
handle_parse_text,
[task_input, user_id_state],
[parsed_panel, f_title, clar_html, f_area, f_urgency, f_importance,
f_state, f_time, f_date, f_is_habit, f_interval, parse_status]
)
btn_transcribe.click(
handle_transcribe_voice,
[audio_input, user_id_state],
[transcribed, voice_msg]
)
btn_discard.click(
lambda: gr.update(visible=False),
outputs=[parsed_panel]
)
btn_confirm.click(
handle_confirm_task,
[user_id_state, f_title, f_area, f_urgency, f_importance, f_state,
f_time, f_date, f_is_habit, f_interval],
[save_msg, today_df, stat_total, stat_done, stat_remain, parsed_panel]
)
btn_gen_sched.click(
handle_generate_schedule,
[user_id_state, plan_prompt],
[schedule_html]
)
btn_today_done.click(
handle_toggle_today,
[today_done_id, user_id_state],
[today_df, stat_total, stat_done, stat_remain, today_action_msg]
)
btn_today_del.click(
handle_delete_today,
[today_del_id, user_id_state],
[today_df, stat_total, stat_done, stat_remain, today_action_msg]
)
# ── All Tasks tab ──────────────────────────────────────────────────────
btn_all_ref.click(
refresh_all_tasks,
[user_id_state, all_filter, all_today_chk],
[all_df]
)
all_filter.change(
refresh_all_tasks,
[user_id_state, all_filter, all_today_chk],
[all_df]
)
all_today_chk.change(
refresh_all_tasks,
[user_id_state, all_filter, all_today_chk],
[all_df]
)
btn_all_done.click(
handle_toggle_all,
[all_done_id, user_id_state, all_filter, all_today_chk],
[all_df, all_action_msg]
)
btn_all_del.click(
handle_delete_all,
[all_del_id, user_id_state, all_filter, all_today_chk],
[all_df, all_action_msg]
)
# ── Journal tab ────────────────────────────────────────────────────────
btn_load_tasks.click(
handle_load_journal_tasks,
[user_id_state],
[journal_task_list, journal_mark_row, journal_tasks_state]
)
btn_j_done.click(
lambda tid, mins, uid, st: handle_journal_mark(tid, mins, uid, st, True),
[j_task_id, j_actual_min, user_id_state, journal_tasks_state],
[journal_tasks_state, journal_mark_msg]
).then(
handle_load_journal_tasks,
[user_id_state],
[journal_task_list, journal_mark_row, journal_tasks_state]
)
btn_j_undone.click(
lambda tid, mins, uid, st: handle_journal_mark(tid, mins, uid, st, False),
[j_task_id, j_actual_min, user_id_state, journal_tasks_state],
[journal_tasks_state, journal_mark_msg]
).then(
handle_load_journal_tasks,
[user_id_state],
[journal_task_list, journal_mark_row, journal_tasks_state]
)
btn_j_start.click(
handle_start_journal,
[user_id_state, journal_tasks_state],
[journal_chatbot, journal_hist_state, journal_active,
j_answer, btn_j_send, journal_msg]
)
btn_j_send.click(
handle_send_answer,
[user_id_state, j_answer, journal_chatbot, journal_hist_state,
journal_tasks_state, journal_active],
[journal_chatbot, journal_hist_state, j_answer, journal_msg]
)
j_answer.submit(
handle_send_answer,
[user_id_state, j_answer, journal_chatbot, journal_hist_state,
journal_tasks_state, journal_active],
[journal_chatbot, journal_hist_state, j_answer, journal_msg]
)
btn_j_finish.click(
handle_finish_journal,
[user_id_state, journal_chatbot, journal_hist_state, journal_tasks_state],
[journal_chatbot, journal_msg]
)
btn_j_restart.click(
handle_restart_journal,
outputs=[journal_chatbot, journal_hist_state, journal_active,
j_answer, btn_j_send, journal_msg]
)
# ── Areas & Goals tab ─────────────────────────────────────────────────
btn_add_area.click(
handle_add_area,
[user_id_state, new_area_name, new_area_color],
[area_msg, areas_display, del_area_dd, new_area_name]
).then(
lambda uid: gr.update(choices=_area_choices(uid), value="All"),
[user_id_state], [all_filter]
)
btn_del_area.click(
handle_del_area,
[user_id_state, del_area_dd],
[del_area_msg, areas_display, del_area_dd]
).then(
lambda uid: gr.update(choices=_area_choices(uid), value="All"),
[user_id_state], [all_filter]
)
btn_save_goals.click(
handle_save_goals,
[user_id_state, goals_input],
[goals_msg]
)
# ── Preferences tab ───────────────────────────────────────────────────
btn_save_prefs.click(
handle_save_prefs,
[user_id_state, pref_wake, pref_sleep, pref_focus, pref_break, pref_flow_max],
[prefs_msg]
)
btn_ref_ctx.click(
render_context_html,
[user_id_state],
[ctx_display]
)
# Refresh AI context display whenever the refresh button is clicked
# (Gradio Tabs.select doesn't support conditional output, so we use the button)
# ── Launch ─────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860)