From edd5e1227428d5fc64692f1415c3d4b8b846887a Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Mon, 23 Feb 2026 14:21:44 +0500
Subject: [PATCH 01/78] fix: add loading state and disabled guard to Google
OAuth buttons on signup and login pages
---
website/app/login/page.tsx | 30 ++++++++++++++++++++++--------
website/app/signup/page.tsx | 30 ++++++++++++++++++++++--------
2 files changed, 44 insertions(+), 16 deletions(-)
diff --git a/website/app/login/page.tsx b/website/app/login/page.tsx
index 376c5150..29f7f055 100644
--- a/website/app/login/page.tsx
+++ b/website/app/login/page.tsx
@@ -12,6 +12,7 @@ export default function LoginPage() {
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
+ const [oauthLoading, setOauthLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -40,6 +41,8 @@ export default function LoginPage() {
const handleGoogleSignIn = async () => {
if (!isLoaded) return;
+ setOauthLoading(true);
+ setError("");
try {
await signIn.authenticateWithRedirect({
strategy: "oauth_google",
@@ -49,6 +52,7 @@ export default function LoginPage() {
} catch (err: unknown) {
const clerkError = err as { errors?: { message: string }[] };
setError(clerkError.errors?.[0]?.message || "OAuth sign in failed");
+ setOauthLoading(false);
}
};
@@ -84,15 +88,25 @@ export default function LoginPage() {
-
-
-
-
-
-
- Continue with Google
+ {oauthLoading ? (
+ <>
+
+ Redirecting...
+ >
+ ) : (
+ <>
+
+
+
+
+
+
+ Continue with Google
+ >
+ )}
diff --git a/website/app/signup/page.tsx b/website/app/signup/page.tsx
index 59bf7f90..c08e945d 100644
--- a/website/app/signup/page.tsx
+++ b/website/app/signup/page.tsx
@@ -14,6 +14,7 @@ export default function SignupPage() {
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
+ const [oauthLoading, setOauthLoading] = useState(false);
const [pendingVerification, setPendingVerification] = useState(false);
const [code, setCode] = useState("");
@@ -66,6 +67,8 @@ export default function SignupPage() {
const handleGoogleSignUp = async () => {
if (!isLoaded) return;
+ setOauthLoading(true);
+ setError("");
try {
await signUp.authenticateWithRedirect({
strategy: "oauth_google",
@@ -75,6 +78,7 @@ export default function SignupPage() {
} catch (err: unknown) {
const clerkError = err as { errors?: { message: string }[] };
setError(clerkError.errors?.[0]?.message || "OAuth sign up failed");
+ setOauthLoading(false);
}
};
@@ -154,15 +158,25 @@ export default function SignupPage() {
-
-
-
-
-
-
- Continue with Google
+ {oauthLoading ? (
+ <>
+
+ Redirecting...
+ >
+ ) : (
+ <>
+
+
+
+
+
+
+ Continue with Google
+ >
+ )}
From d67d616739e4780f254327b624f1aab128254318 Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Mon, 23 Feb 2026 21:44:08 +0500
Subject: [PATCH 02/78] feat(dashboard): build habit dashboard UI with donut
chart, heatmap, goals, activity logs, and UI polish
---
.../app/components/dashboard/ActivityItem.tsx | 81 ++++++
.../app/components/dashboard/ActivityList.tsx | 43 +++
.../components/dashboard/ActivityLogList.tsx | 56 ++++
.../dashboard/ActivityLogsSection.tsx | 100 +++++++
.../dashboard/ClassificationPanel.tsx | 160 +++++++++++
.../app/components/dashboard/DonutChart.tsx | 141 ++++++++++
website/app/components/dashboard/GoalCard.tsx | 153 +++++++++++
.../app/components/dashboard/GoalsSection.tsx | 180 +++++++++++++
.../app/components/dashboard/HabitHeatmap.tsx | 250 ++++++++++++++++++
.../components/dashboard/TodayOverview.tsx | 57 ++++
website/app/dashboard/page.tsx | 16 ++
website/app/globals.css | 20 ++
12 files changed, 1257 insertions(+)
create mode 100644 website/app/components/dashboard/ActivityItem.tsx
create mode 100644 website/app/components/dashboard/ActivityList.tsx
create mode 100644 website/app/components/dashboard/ActivityLogList.tsx
create mode 100644 website/app/components/dashboard/ActivityLogsSection.tsx
create mode 100644 website/app/components/dashboard/ClassificationPanel.tsx
create mode 100644 website/app/components/dashboard/DonutChart.tsx
create mode 100644 website/app/components/dashboard/GoalCard.tsx
create mode 100644 website/app/components/dashboard/GoalsSection.tsx
create mode 100644 website/app/components/dashboard/HabitHeatmap.tsx
create mode 100644 website/app/components/dashboard/TodayOverview.tsx
diff --git a/website/app/components/dashboard/ActivityItem.tsx b/website/app/components/dashboard/ActivityItem.tsx
new file mode 100644
index 00000000..b4bd10bb
--- /dev/null
+++ b/website/app/components/dashboard/ActivityItem.tsx
@@ -0,0 +1,81 @@
+// ─── Shared types & config (used by ActivityLogList + ClassificationPanel) ────
+
+export type LogCategory = "Work" | "Health" | "Relationships";
+
+export interface ActivityLog {
+ id: string;
+ time: string; // "HH:MM"
+ text: string;
+ category: LogCategory;
+}
+
+export const CATEGORY_CONFIG: Record<
+ LogCategory,
+ { badge: string; color: string; highlight: string }
+> = {
+ Work: {
+ badge: "bg-blue-500/15 text-blue-300",
+ color: "#3b82f6",
+ highlight: "bg-blue-500/20 text-blue-200 not-italic",
+ },
+ Health: {
+ badge: "bg-emerald-500/15 text-emerald-300",
+ color: "#10b981",
+ highlight: "bg-emerald-500/20 text-emerald-200 not-italic",
+ },
+ Relationships: {
+ badge: "bg-amber-500/15 text-amber-300",
+ color: "#f59e0b",
+ highlight: "bg-amber-500/20 text-amber-200 not-italic",
+ },
+};
+
+// ─── ActivityItem ─────────────────────────────────────────────────────────────
+
+interface ActivityItemProps {
+ log: ActivityLog;
+ isLast: boolean;
+ isLatest: boolean;
+}
+
+export default function ActivityItem({ log, isLast, isLatest }: ActivityItemProps) {
+ const config = CATEGORY_CONFIG[log.category];
+
+ return (
+
+ {/* Left col: time + connecting line */}
+
+
+ {log.time}
+
+ {!isLast && (
+
+ )}
+
+
+ {/* Dot */}
+
+
+ {/* Content */}
+
+
{log.text}
+
+ {log.category}
+
+
+
+ );
+}
diff --git a/website/app/components/dashboard/ActivityList.tsx b/website/app/components/dashboard/ActivityList.tsx
new file mode 100644
index 00000000..0052ae8e
--- /dev/null
+++ b/website/app/components/dashboard/ActivityList.tsx
@@ -0,0 +1,43 @@
+export interface Activity {
+ id: string;
+ title: string;
+ category: "Work" | "Health" | "Relationships";
+ time: string;
+ icon: string;
+}
+
+interface ActivityListProps {
+ activities: Activity[];
+}
+
+const CATEGORY_STYLES: Record = {
+ Work: "bg-blue-500/15 text-blue-300",
+ Health: "bg-emerald-500/15 text-emerald-300",
+ Relationships: "bg-amber-500/15 text-amber-300",
+};
+
+export default function ActivityList({ activities }: ActivityListProps) {
+ return (
+
+ {activities.map((activity) => (
+
+
{activity.icon}
+
+
{activity.title}
+
{activity.time}
+
+
+ {activity.category}
+
+
+ ))}
+
+ );
+}
diff --git a/website/app/components/dashboard/ActivityLogList.tsx b/website/app/components/dashboard/ActivityLogList.tsx
new file mode 100644
index 00000000..f38b472a
--- /dev/null
+++ b/website/app/components/dashboard/ActivityLogList.tsx
@@ -0,0 +1,56 @@
+import ActivityItem, { ActivityLog } from "./ActivityItem";
+
+// ─── Mock data ────────────────────────────────────────────────────────────────
+
+export const MOCK_LOGS: ActivityLog[] = [
+ { id: "1", time: "07:00", text: "Morning workout at the gym", category: "Health" },
+ { id: "2", time: "08:15", text: "Journaled thoughts for 15 minutes", category: "Health" },
+ { id: "3", time: "09:30", text: "Team standup meeting", category: "Work" },
+ { id: "4", time: "10:00", text: "Deep work on project proposal", category: "Work" },
+ { id: "5", time: "12:30", text: "Lunch break — took a walk outside", category: "Health" },
+ { id: "6", time: "13:15", text: "Called mum to check in", category: "Relationships" },
+ { id: "7", time: "14:00", text: "Project review with the team", category: "Work" },
+ { id: "8", time: "17:30", text: "Dinner with family at home", category: "Relationships" },
+ { id: "9", time: "19:00", text: "Evening run — 5 km", category: "Health" },
+ { id: "10", time: "21:00", text: "Read before bed", category: "Health" },
+];
+
+// ─── ActivityLogList ──────────────────────────────────────────────────────────
+
+interface ActivityLogListProps {
+ logs?: ActivityLog[];
+}
+
+export default function ActivityLogList({ logs = MOCK_LOGS }: ActivityLogListProps) {
+ const categoryCount = logs.reduce>((acc, l) => {
+ acc[l.category] = (acc[l.category] ?? 0) + 1;
+ return acc;
+ }, {});
+
+ return (
+
+
+
Today's Log
+
+ {Object.entries(categoryCount).map(([cat, count]) => (
+
+ {count} {cat}
+
+ ))}
+
+
+
+ {/* Scrollable timeline */}
+
+ {logs.map((log, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/website/app/components/dashboard/ActivityLogsSection.tsx b/website/app/components/dashboard/ActivityLogsSection.tsx
new file mode 100644
index 00000000..3ebe2937
--- /dev/null
+++ b/website/app/components/dashboard/ActivityLogsSection.tsx
@@ -0,0 +1,100 @@
+import ClassificationPanel from "./ClassificationPanel";
+import ActivityLogList from "./ActivityLogList";
+
+// ─── Pipeline step ────────────────────────────────────────────────────────────
+
+function PipelineStep({
+ icon,
+ label,
+ isLast = false,
+}: {
+ icon: React.ReactNode;
+ label: string;
+ isLast?: boolean;
+}) {
+ return (
+ <>
+
+ {icon}
+ {label}
+
+ {!isLast && (
+
+
+
+ )}
+ >
+ );
+}
+
+// ─── ActivityLogsSection ──────────────────────────────────────────────────────
+
+export default function ActivityLogsSection() {
+ return (
+
+ {/* Header */}
+
+
+
Activity Logs
+
How your messages become insights
+
+
+ Today
+
+
+
+ {/* Pipeline indicator */}
+
+
+
+
+ }
+ label="WhatsApp message"
+ />
+
+
+
+ }
+ label="AI classifies intent"
+ />
+
+
+
+ }
+ label="Categorized log entry"
+ />
+
+
+
+ }
+ label="Dashboard insights"
+ isLast
+ />
+
+
+ {/* Body: 2-column — Classification | Log timeline */}
+
+
+ );
+}
diff --git a/website/app/components/dashboard/ClassificationPanel.tsx b/website/app/components/dashboard/ClassificationPanel.tsx
new file mode 100644
index 00000000..ec66b64b
--- /dev/null
+++ b/website/app/components/dashboard/ClassificationPanel.tsx
@@ -0,0 +1,160 @@
+import { CATEGORY_CONFIG, LogCategory } from "./ActivityItem";
+
+// ─── Types ────────────────────────────────────────────────────────────────────
+
+interface ClassificationSegment {
+ text: string;
+ category: LogCategory;
+}
+
+interface ClassificationExample {
+ id: string;
+ rawInput: string;
+ segments: ClassificationSegment[];
+}
+
+// ─── Mock classification examples ─────────────────────────────────────────────
+
+const MOCK_CLASSIFICATIONS: ClassificationExample[] = [
+ {
+ id: "1",
+ rawInput: "Had lunch with my parents and went for a run after",
+ segments: [
+ { text: "lunch with my parents", category: "Relationships" },
+ { text: "went for a run", category: "Health" },
+ ],
+ },
+ {
+ id: "2",
+ rawInput: "Deep work session all morning, then called Sarah",
+ segments: [
+ { text: "Deep work session", category: "Work" },
+ { text: "called Sarah", category: "Relationships" },
+ ],
+ },
+ {
+ id: "3",
+ rawInput: "Morning gym, team standup, reading before sleep",
+ segments: [
+ { text: "Morning gym", category: "Health" },
+ { text: "team standup", category: "Work" },
+ { text: "reading before sleep", category: "Health" },
+ ],
+ },
+];
+
+// ─── Highlighted text renderer ────────────────────────────────────────────────
+
+function HighlightedText({
+ raw,
+ segments,
+}: {
+ raw: string;
+ segments: ClassificationSegment[];
+}) {
+ const parts: { text: string; category: LogCategory | null }[] = [];
+ let remaining = raw;
+
+ for (const seg of segments) {
+ const idx = remaining.indexOf(seg.text);
+ if (idx === -1) continue;
+ if (idx > 0) parts.push({ text: remaining.slice(0, idx), category: null });
+ parts.push({ text: seg.text, category: seg.category });
+ remaining = remaining.slice(idx + seg.text.length);
+ }
+ if (remaining.length > 0) parts.push({ text: remaining, category: null });
+
+ return (
+
+ “
+ {parts.map((part, i) =>
+ part.category ? (
+
+ {part.text}
+
+ ) : (
+ {part.text}
+ )
+ )}
+ ”
+
+ );
+}
+
+// ─── ClassificationPanel ──────────────────────────────────────────────────────
+
+export default function ClassificationPanel() {
+ return (
+
+
+ Data Classification
+
+
+
+ {MOCK_CLASSIFICATIONS.map((ex) => (
+
+ {/* Input bubble */}
+
+ {/* WhatsApp-style avatar dot */}
+
+
+
+
+ {/* Arrow + "classified as" label */}
+
+
+
+
+
+ classified as
+
+
+
+ {/* Extracted category tags */}
+
+ {ex.segments.map((seg, i) => (
+
+
+ {seg.category}
+
+ ))}
+
+
+ ))}
+
+
+ );
+}
diff --git a/website/app/components/dashboard/DonutChart.tsx b/website/app/components/dashboard/DonutChart.tsx
new file mode 100644
index 00000000..f9e342c2
--- /dev/null
+++ b/website/app/components/dashboard/DonutChart.tsx
@@ -0,0 +1,141 @@
+"use client";
+
+import { useState } from "react";
+
+export interface CategoryData {
+ label: string;
+ value: number;
+ color: string;
+}
+
+interface DonutChartProps {
+ data: CategoryData[];
+ size?: number;
+ strokeWidth?: number;
+}
+
+const SEGMENT_GAP = 4;
+
+export default function DonutChart({ data, size = 216, strokeWidth = 20 }: DonutChartProps) {
+ const [hovered, setHovered] = useState(null);
+
+ const center = size / 2;
+ const radius = (size - strokeWidth) / 2;
+ const circumference = 2 * Math.PI * radius;
+ const total = data.reduce((sum, d) => sum + d.value, 0);
+
+ let cumulativeAngle = -90;
+
+ const segments = data.map((item) => {
+ const percent = item.value / total;
+ const dashLength = Math.max(0, percent * circumference - SEGMENT_GAP);
+ const rotation = cumulativeAngle;
+ cumulativeAngle += percent * 360;
+ return { ...item, dashLength, rotation, percent };
+ });
+
+ const activeSegment = hovered ? segments.find((s) => s.label === hovered) : null;
+
+ return (
+
+
+
+ {/* Background track */}
+
+ {segments.map((seg) => (
+ setHovered(seg.label)}
+ onMouseLeave={() => setHovered(null)}
+ />
+ ))}
+
+
+ {/* Center label */}
+
+ {activeSegment ? (
+ <>
+
+ {activeSegment.value}%
+
+ {activeSegment.label}
+ >
+ ) : (
+ <>
+ Today
+ {data.length} areas
+ >
+ )}
+
+
+
+ {/* Legend */}
+
+ {segments.map((seg) => (
+
setHovered(seg.label)}
+ onMouseLeave={() => setHovered(null)}
+ >
+
+
+
+
+ {seg.label}
+
+
+
+ {seg.value}%
+
+
+ {/* Mini progress bar */}
+
+
+ ))}
+
+
+ );
+}
diff --git a/website/app/components/dashboard/GoalCard.tsx b/website/app/components/dashboard/GoalCard.tsx
new file mode 100644
index 00000000..caa77484
--- /dev/null
+++ b/website/app/components/dashboard/GoalCard.tsx
@@ -0,0 +1,153 @@
+"use client";
+
+// ─── Types ────────────────────────────────────────────────────────────────────
+
+export type GoalCategory = "Work" | "Health" | "Relationships";
+
+export interface Goal {
+ id: string;
+ name: string;
+ category: GoalCategory;
+ streak: number;
+ completionRate: number; // 0–100
+ frequency: string;
+ icon: string;
+ recentDays: boolean[]; // last 7 days — true = completed
+}
+
+// ─── Category config (mirrors donut chart + activity list colors) ─────────────
+
+const CATEGORY_CONFIG: Record<
+ GoalCategory,
+ { badge: string; barColor: string; glowColor: string }
+> = {
+ Work: {
+ badge: "bg-blue-500/15 text-blue-300",
+ barColor: "#3b82f6",
+ glowColor: "rgba(59,130,246,0.15)",
+ },
+ Health: {
+ badge: "bg-emerald-500/15 text-emerald-300",
+ barColor: "#10b981",
+ glowColor: "rgba(16,185,129,0.15)",
+ },
+ Relationships: {
+ badge: "bg-amber-500/15 text-amber-300",
+ barColor: "#f59e0b",
+ glowColor: "rgba(245,158,11,0.15)",
+ },
+};
+
+// ─── Sub-components ───────────────────────────────────────────────────────────
+
+function StreakBadge({ streak }: { streak: number }) {
+ if (streak === 0) {
+ return No streak yet ;
+ }
+ const isHot = streak >= 10;
+ return (
+
+ 🔥{" "}
+
+ {streak}
+ {" "}
+ day{streak !== 1 ? "s" : ""}
+ {isHot && 🏆 }
+
+ );
+}
+
+function RecentDays({
+ days,
+ barColor,
+}: {
+ days: boolean[];
+ barColor: string;
+}) {
+ return (
+
+
7d
+ {days.map((done, i) => (
+
+ ))}
+
+ );
+}
+
+// ─── GoalCard ─────────────────────────────────────────────────────────────────
+
+interface GoalCardProps {
+ goal: Goal;
+}
+
+export default function GoalCard({ goal }: GoalCardProps) {
+ const config = CATEGORY_CONFIG[goal.category];
+ const isStruggling = goal.completionRate < 40;
+ const barColor = isStruggling ? "#f43f5e" : config.barColor;
+
+ return (
+
+ {/* Colored top-edge accent — animates in on hover */}
+
+
+ {/* Row 1: icon + name + category tag */}
+
+
+
{goal.icon}
+
+
{goal.name}
+
{goal.frequency}
+
+
+
+ {goal.category}
+
+
+
+ {/* Row 2: completion rate progress bar */}
+
+
+ Completion
+
+ {goal.completionRate}%
+
+
+
+
+
+ {/* Row 3: streak + recent days dots */}
+
+
+
+
+
+ );
+}
diff --git a/website/app/components/dashboard/GoalsSection.tsx b/website/app/components/dashboard/GoalsSection.tsx
new file mode 100644
index 00000000..0b2ec39e
--- /dev/null
+++ b/website/app/components/dashboard/GoalsSection.tsx
@@ -0,0 +1,180 @@
+"use client";
+
+import { useState, useMemo } from "react";
+import GoalCard, { Goal } from "./GoalCard";
+
+// ─── Mock data ────────────────────────────────────────────────────────────────
+
+const MOCK_GOALS: Goal[] = [
+ {
+ id: "1",
+ name: "Go to gym",
+ category: "Health",
+ streak: 5,
+ completionRate: 80,
+ frequency: "3x per week",
+ icon: "🏋️",
+ recentDays: [true, true, false, true, true, true, true],
+ },
+ {
+ id: "2",
+ name: "Deep work sessions",
+ category: "Work",
+ streak: 3,
+ completionRate: 60,
+ frequency: "Daily",
+ icon: "💻",
+ recentDays: [true, false, true, false, true, true, true],
+ },
+ {
+ id: "3",
+ name: "Read before bed",
+ category: "Health",
+ streak: 12,
+ completionRate: 92,
+ frequency: "Daily",
+ icon: "📚",
+ recentDays: [true, true, true, true, true, true, true],
+ },
+ {
+ id: "4",
+ name: "Call a friend or family",
+ category: "Relationships",
+ streak: 1,
+ completionRate: 35,
+ frequency: "2x per week",
+ icon: "📞",
+ recentDays: [false, false, false, false, true, false, false],
+ },
+ {
+ id: "5",
+ name: "Morning walk",
+ category: "Health",
+ streak: 0,
+ completionRate: 20,
+ frequency: "Daily",
+ icon: "🚶",
+ recentDays: [false, true, false, false, false, false, false],
+ },
+ {
+ id: "6",
+ name: "Team check-ins",
+ category: "Work",
+ streak: 8,
+ completionRate: 95,
+ frequency: "Daily",
+ icon: "💼",
+ recentDays: [true, true, true, true, true, true, true],
+ },
+];
+
+// ─── Sort modes ───────────────────────────────────────────────────────────────
+
+type SortMode = "streak" | "needs-work";
+
+const SORT_OPTIONS: { id: SortMode; label: string }[] = [
+ { id: "streak", label: "🔥 Top Streaks" },
+ { id: "needs-work", label: "⚠️ Needs Work" },
+];
+
+function sortGoals(goals: Goal[], mode: SortMode): Goal[] {
+ return [...goals].sort((a, b) =>
+ mode === "streak"
+ ? b.streak - a.streak
+ : a.completionRate - b.completionRate
+ );
+}
+
+// ─── Summary stats ────────────────────────────────────────────────────────────
+
+function computeStats(goals: Goal[]) {
+ const active = goals.filter((g) => g.streak > 0).length;
+ const avgCompletion = Math.round(
+ goals.reduce((s, g) => s + g.completionRate, 0) / goals.length
+ );
+ const topStreak = Math.max(...goals.map((g) => g.streak));
+ const struggling = goals.filter((g) => g.completionRate < 40).length;
+ return { active, avgCompletion, topStreak, struggling };
+}
+
+// ─── GoalsSection ─────────────────────────────────────────────────────────────
+
+export default function GoalsSection() {
+ const [sortMode, setSortMode] = useState("streak");
+
+ const sorted = useMemo(() => sortGoals(MOCK_GOALS, sortMode), [sortMode]);
+ const stats = useMemo(() => computeStats(MOCK_GOALS), []);
+
+ return (
+
+ {/* Header */}
+
+
+
Goals & Progress
+
{MOCK_GOALS.length} active goals this month
+
+
+ {/* Sort toggle */}
+
+ {SORT_OPTIONS.map((opt) => (
+ setSortMode(opt.id)}
+ className={`px-3.5 py-1.5 rounded-lg text-xs font-semibold transition-all duration-150 cursor-pointer ${
+ sortMode === opt.id
+ ? "bg-slate-700 text-white shadow-sm"
+ : "text-slate-500 hover:text-slate-300"
+ }`}
+ >
+ {opt.label}
+
+ ))}
+
+
+
+ {/* Goal cards grid */}
+
+ {sorted.map((goal) => (
+
+ ))}
+
+
+ {/* Summary footer */}
+
+
+
+
On streak
+
+ {stats.active}
+ / {MOCK_GOALS.length}
+
+
+
+
Avg completion
+
+ {stats.avgCompletion}
+ %
+
+
+
+
Best streak
+
+ 🔥 {stats.topStreak}
+ days
+
+
+ {stats.struggling > 0 && (
+
+
Needs attention
+
+ {stats.struggling}
+ goal{stats.struggling !== 1 ? "s" : ""}
+
+
+ )}
+
+
last 7 days shown per goal
+
+
+ );
+}
diff --git a/website/app/components/dashboard/HabitHeatmap.tsx b/website/app/components/dashboard/HabitHeatmap.tsx
new file mode 100644
index 00000000..41a78d76
--- /dev/null
+++ b/website/app/components/dashboard/HabitHeatmap.tsx
@@ -0,0 +1,250 @@
+"use client";
+
+// ─── Types ───────────────────────────────────────────────────────────────────
+
+export interface HabitDay {
+ date: string; // "YYYY-MM-DD"
+ value: number; // 0–100
+}
+
+interface HabitHeatmapProps {
+ data?: HabitDay[];
+ month?: string; // "YYYY-MM"
+}
+
+// ─── Mock data (Feb 2026 — realistic weekday/weekend pattern) ─────────────────
+
+const MOCK_DATA: HabitDay[] = [
+ { date: "2026-02-01", value: 30 }, // Sun
+ { date: "2026-02-02", value: 75 }, // Mon
+ { date: "2026-02-03", value: 85 }, // Tue
+ { date: "2026-02-04", value: 60 }, // Wed
+ { date: "2026-02-05", value: 90 }, // Thu
+ { date: "2026-02-06", value: 70 }, // Fri
+ { date: "2026-02-07", value: 20 }, // Sat
+ { date: "2026-02-08", value: 0 }, // Sun — full rest
+ { date: "2026-02-09", value: 80 }, // Mon
+ { date: "2026-02-10", value: 65 }, // Tue
+ { date: "2026-02-11", value: 88 }, // Wed
+ { date: "2026-02-12", value: 72 }, // Thu
+ { date: "2026-02-13", value: 55 }, // Fri
+ { date: "2026-02-14", value: 35 }, // Sat
+ { date: "2026-02-15", value: 25 }, // Sun
+ { date: "2026-02-16", value: 92 }, // Mon — peak
+ { date: "2026-02-17", value: 78 }, // Tue
+ { date: "2026-02-18", value: 0 }, // Wed — off day
+ { date: "2026-02-19", value: 45 }, // Thu — recovering
+ { date: "2026-02-20", value: 68 }, // Fri
+ { date: "2026-02-21", value: 40 }, // Sat
+ { date: "2026-02-22", value: 15 }, // Sun
+ { date: "2026-02-23", value: 82 }, // Mon — today
+ { date: "2026-02-24", value: 58 }, // Tue
+ { date: "2026-02-25", value: 76 }, // Wed
+ { date: "2026-02-26", value: 88 }, // Thu
+ { date: "2026-02-27", value: 62 }, // Fri
+ { date: "2026-02-28", value: 30 }, // Sat
+];
+
+// ─── Utilities ────────────────────────────────────────────────────────────────
+
+function getIntensityColor(value: number): string {
+ if (value === 0) return "rgba(30,41,59,0.7)";
+ if (value <= 25) return "rgba(16,185,129,0.22)";
+ if (value <= 50) return "rgba(16,185,129,0.45)";
+ if (value <= 75) return "rgba(16,185,129,0.68)";
+ return "rgba(16,185,129,0.95)";
+}
+
+function formatTooltipDate(dateStr: string): string {
+ return new Date(dateStr + "T00:00:00").toLocaleDateString("en-US", {
+ weekday: "short",
+ month: "short",
+ day: "numeric",
+ });
+}
+
+function formatTooltipValue(value: number): string {
+ if (value === 0) return "No activity";
+ if (value <= 25) return `${value}% — light day`;
+ if (value <= 50) return `${value}% — moderate`;
+ if (value <= 75) return `${value}% — solid`;
+ return `${value}% — excellent`;
+}
+
+// ─── Constants ────────────────────────────────────────────────────────────────
+
+const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+const LEGEND_LEVELS = [0, 20, 45, 70, 95] as const;
+
+// ─── Component ────────────────────────────────────────────────────────────────
+
+export default function HabitHeatmap({
+ data = MOCK_DATA,
+ month = "2026-02",
+}: HabitHeatmapProps) {
+ const today = new Date().toISOString().split("T")[0];
+
+ const [year, monthNum] = month.split("-").map(Number);
+ const firstDay = new Date(year, monthNum - 1, 1);
+ const daysInMonth = new Date(year, monthNum, 0).getDate();
+ const startDayOfWeek = firstDay.getDay();
+
+ const monthLabel = firstDay.toLocaleDateString("en-US", {
+ month: "long",
+ year: "numeric",
+ });
+
+ const dataMap = new Map(data.map((d) => [d.date, d.value]));
+
+ // Build flat cell array: leading empty + days + trailing empty
+ type Cell =
+ | { type: "empty"; key: string }
+ | { type: "day"; day: number; date: string; value: number };
+
+ const cells: Cell[] = [];
+
+ for (let i = 0; i < startDayOfWeek; i++) {
+ cells.push({ type: "empty", key: `pre-${i}` });
+ }
+
+ for (let d = 1; d <= daysInMonth; d++) {
+ const dateStr = `${year}-${String(monthNum).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
+ cells.push({ type: "day", day: d, date: dateStr, value: dataMap.get(dateStr) ?? 0 });
+ }
+
+ const remainder = cells.length % 7;
+ if (remainder !== 0) {
+ for (let i = 0; i < 7 - remainder; i++) {
+ cells.push({ type: "empty", key: `post-${i}` });
+ }
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
Monthly Habit Overview
+
{monthLabel}
+
+
+ {/* Legend */}
+
+
Less
+
+ {LEGEND_LEVELS.map((v) => (
+
+ ))}
+
+
More
+
+
+
+ {/* Grid */}
+
+ {/* Weekday labels */}
+
+ {WEEKDAYS.map((label) => (
+
+ {label}
+
+ ))}
+
+
+ {/* Day cells */}
+
+ {cells.map((cell, idx) => {
+ if (cell.type === "empty") {
+ return (
+
+ );
+ }
+
+ const isToday = cell.date === today;
+
+ return (
+
+ {/* Cell square */}
+
+ {/* Day number — subtle overlay */}
+
+ {cell.day}
+
+
+
+ {/* Tooltip */}
+
+
+
+ {formatTooltipDate(cell.date)}
+ {isToday && (
+ Today
+ )}
+
+
+ {formatTooltipValue(cell.value)}
+
+
+ {/* Arrow */}
+
+
+
+ );
+ })}
+
+
+ {/* Month summary row */}
+
+
+
+
Active days
+
+ {data.filter((d) => d.value > 0).length}
+ / {daysInMonth}
+
+
+
+
Avg score
+
+ {Math.round(data.filter((d) => d.value > 0).reduce((s, d) => s + d.value, 0) / data.filter((d) => d.value > 0).length)}
+ %
+
+
+
+
Best day
+
+ {Math.max(...data.map((d) => d.value))}
+ %
+
+
+
+
hover cells for details
+
+
+
+ );
+}
diff --git a/website/app/components/dashboard/TodayOverview.tsx b/website/app/components/dashboard/TodayOverview.tsx
new file mode 100644
index 00000000..1d937d8e
--- /dev/null
+++ b/website/app/components/dashboard/TodayOverview.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import DonutChart, { CategoryData } from "./DonutChart";
+import ActivityList, { Activity } from "./ActivityList";
+
+const MOCK_CATEGORIES: CategoryData[] = [
+ { label: "Work", value: 50, color: "#3b82f6" },
+ { label: "Health", value: 30, color: "#10b981" },
+ { label: "Relationships", value: 20, color: "#f59e0b" },
+];
+
+const MOCK_ACTIVITIES: Activity[] = [
+ { id: "1", title: "Morning workout", category: "Health", time: "7:00 AM", icon: "🏋️" },
+ { id: "2", title: "Team standup", category: "Work", time: "9:30 AM", icon: "💼" },
+ { id: "3", title: "Deep work session", category: "Work", time: "10:00 AM", icon: "💻" },
+ { id: "4", title: "Lunch with family", category: "Relationships", time: "1:00 PM", icon: "👨👩👧" },
+ { id: "5", title: "Project review", category: "Work", time: "3:00 PM", icon: "📊" },
+ { id: "6", title: "Evening run", category: "Health", time: "6:30 PM", icon: "🏃" },
+];
+
+export default function TodayOverview() {
+ const today = new Date().toLocaleDateString("en-US", {
+ weekday: "long",
+ month: "long",
+ day: "numeric",
+ });
+
+ return (
+
+ {/* Card header */}
+
+
+
Today's Overview
+
{today}
+
+
+ Today
+
+
+
+ {/* Card body */}
+
+ {/* Left: Donut chart */}
+
+
+ {/* Right: Activity list */}
+
+
+
+ );
+}
diff --git a/website/app/dashboard/page.tsx b/website/app/dashboard/page.tsx
index c77fc8ad..9da17312 100644
--- a/website/app/dashboard/page.tsx
+++ b/website/app/dashboard/page.tsx
@@ -3,6 +3,10 @@ import { useUser, useClerk } from "@clerk/nextjs";
import { useRouter } from "next/navigation";
import Image from "next/image";
import Link from "next/link";
+import TodayOverview from "@/app/components/dashboard/TodayOverview";
+import HabitHeatmap from "@/app/components/dashboard/HabitHeatmap";
+import GoalsSection from "@/app/components/dashboard/GoalsSection";
+import ActivityLogsSection from "@/app/components/dashboard/ActivityLogsSection";
import { useState, useRef, useEffect, useCallback } from "react";
interface WhatsAppSession {
@@ -647,6 +651,18 @@ export default function DashboardPage() {
+ {/* Today Overview */}
+
+
+ {/* Monthly Habit Heatmap */}
+
+
+ {/* Goals & Progress */}
+
+
+ {/* Activity Logs & Classification */}
+
+
{/* Main Content */}
{/* WhatsApp Session Detail */}
diff --git a/website/app/globals.css b/website/app/globals.css
index 24370d3f..9a4aa46e 100644
--- a/website/app/globals.css
+++ b/website/app/globals.css
@@ -78,6 +78,17 @@ body {
}
}
+@keyframes fade-in-up {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
.animate-fade-in {
animation: fade-in 1s ease-out;
}
@@ -92,6 +103,15 @@ body {
animation-fill-mode: both;
}
+/* Dashboard section entrance animations */
+.animate-fade-in-up {
+ animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;
+}
+.animate-fade-in-up-1 { animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.05s both; }
+.animate-fade-in-up-2 { animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.12s both; }
+.animate-fade-in-up-3 { animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.19s both; }
+.animate-fade-in-up-4 { animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.26s both; }
+
/* Scroll-triggered reveal */
.reveal {
opacity: 0;
From f0714222220e37a23595bf73db112388d70bbf7f Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Mon, 23 Feb 2026 21:57:05 +0500
Subject: [PATCH 03/78] feat(dashboard): add AI processing pipeline section
with animated 3-step input-to-output demo
---
.../app/components/dashboard/AIPipeline.tsx | 101 ++++++++++++++++++
.../components/dashboard/AIProcessingCard.tsx | 95 ++++++++++++++++
.../app/components/dashboard/RawInputCard.tsx | 54 ++++++++++
.../dashboard/StructuredOutputCard.tsx | 68 ++++++++++++
website/app/dashboard/page.tsx | 4 +
website/app/globals.css | 15 +++
6 files changed, 337 insertions(+)
create mode 100644 website/app/components/dashboard/AIPipeline.tsx
create mode 100644 website/app/components/dashboard/AIProcessingCard.tsx
create mode 100644 website/app/components/dashboard/RawInputCard.tsx
create mode 100644 website/app/components/dashboard/StructuredOutputCard.tsx
diff --git a/website/app/components/dashboard/AIPipeline.tsx b/website/app/components/dashboard/AIPipeline.tsx
new file mode 100644
index 00000000..8902b1e9
--- /dev/null
+++ b/website/app/components/dashboard/AIPipeline.tsx
@@ -0,0 +1,101 @@
+import RawInputCard from "./RawInputCard";
+import AIProcessingCard from "./AIProcessingCard";
+import StructuredOutputCard from "./StructuredOutputCard";
+
+// ─── Arrow between steps (responsive: horizontal on desktop, vertical on mobile)
+
+function FlowArrow() {
+ return (
+ <>
+ {/* Desktop — horizontal */}
+
+ {/* Mobile — vertical */}
+
+ >
+ );
+}
+
+// ─── AIPipeline ───────────────────────────────────────────────────────────────
+
+export default function AIPipeline() {
+ return (
+
+ {/* Header */}
+
+
+
AI Processing Pipeline
+
+ How raw journal input becomes structured dashboard data
+
+
+
+
+ Live Demo
+
+
+
+ {/* 3-step pipeline body */}
+
+
+ {/* Connection footer — links output to dashboard sections */}
+
+
+
+
+ This output powers your dashboard below
+
+
+
+
+ activities{" "}
+ → {" "}
+ Activity Logs
+
+ ·
+
+ summary{" "}
+ → {" "}
+ Donut Chart
+
+
+
+
+ );
+}
diff --git a/website/app/components/dashboard/AIProcessingCard.tsx b/website/app/components/dashboard/AIProcessingCard.tsx
new file mode 100644
index 00000000..5a27f0d4
--- /dev/null
+++ b/website/app/components/dashboard/AIProcessingCard.tsx
@@ -0,0 +1,95 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+// ─── Mock processing steps (cycles on a timer) ────────────────────────────────
+
+const PROCESSING_STEPS = [
+ "Identifying activity mentions...",
+ "Classifying by category...",
+ "Extracting duration metrics...",
+ "Building structured output...",
+];
+
+const CONCEPT_TAGS = [
+ { label: "Health", style: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20" },
+ { label: "Work", style: "bg-blue-500/10 text-blue-400 border-blue-500/20" },
+ { label: "Relationships", style: "bg-amber-500/10 text-amber-400 border-amber-500/20" },
+ { label: "Duration", style: "bg-violet-500/10 text-violet-400 border-violet-500/20" },
+];
+
+// ─── AIProcessingCard ─────────────────────────────────────────────────────────
+
+export default function AIProcessingCard() {
+ const [step, setStep] = useState(0);
+
+ useEffect(() => {
+ const id = setInterval(() => setStep((s) => (s + 1) % PROCESSING_STEPS.length), 1800);
+ return () => clearInterval(id);
+ }, []);
+
+ return (
+
+ {/* Step label */}
+
+
+ 2
+
+
+ AI Extraction
+
+
+
+ {/* Card */}
+
+ {/* Brain / AI icon */}
+
+
+ {/* Pulsing indicator dots */}
+
+ {[0, 150, 300].map((delay, i) => (
+
+ ))}
+
+
+ {/* Cycling step message */}
+
+ {PROCESSING_STEPS[step]}
+
+
+ {/* Extracted concept tags */}
+
+ {CONCEPT_TAGS.map(({ label, style }) => (
+
+ {label}
+
+ ))}
+
+
+
+ );
+}
diff --git a/website/app/components/dashboard/RawInputCard.tsx b/website/app/components/dashboard/RawInputCard.tsx
new file mode 100644
index 00000000..93519bd3
--- /dev/null
+++ b/website/app/components/dashboard/RawInputCard.tsx
@@ -0,0 +1,54 @@
+// ─── RawInputCard ─────────────────────────────────────────────────────────────
+// Step 1 of the AI pipeline — the raw text message from the user
+
+const MOCK_MESSAGE =
+ "Had a great day. Went to the gym in the morning, worked 5 hours on the project proposal, and had dinner with my parents.";
+
+export default function RawInputCard() {
+ return (
+
+ {/* Step label */}
+
+
+ 1
+
+
+ Raw Input
+
+
+
+ {/* Card */}
+
+ {/* Source row */}
+
+
+
+
Journal · WhatsApp
+
Today · 21:03
+
+
+
+ {/* Message bubble */}
+
+
+ {/* Metadata chips */}
+
+
+ unstructured text
+
+
+ 3 activities detected
+
+
+
+
+ );
+}
diff --git a/website/app/components/dashboard/StructuredOutputCard.tsx b/website/app/components/dashboard/StructuredOutputCard.tsx
new file mode 100644
index 00000000..6671cbfe
--- /dev/null
+++ b/website/app/components/dashboard/StructuredOutputCard.tsx
@@ -0,0 +1,68 @@
+// ─── Mock JSON output ─────────────────────────────────────────────────────────
+
+const MOCK_JSON = `{
+ "activities": [
+ { "text": "Morning gym", "category": "Health" },
+ { "text": "5h deep work", "category": "Work",
+ "duration": "5h" },
+ { "text": "Dinner with parents", "category": "Relationships" }
+ ],
+ "summary": {
+ "health": 30,
+ "work": 50,
+ "relationships": 20
+ }
+}`;
+
+// ─── Simple JSON syntax highlighter (no library) ──────────────────────────────
+
+function syntaxHighlight(json: string): string {
+ return (
+ json
+ // Keys: "key":
+ .replace(/"([^"]+)"(\s*:)/g, '"$1" $2')
+ // String values: : "value"
+ .replace(/:\s*"([^"]+)"/g, ': "$1" ')
+ // Numbers: : 30
+ .replace(/:\s*(\d+)/g, ': $1 ')
+ // Braces and brackets
+ .replace(/[{}\[\]]/g, '$& ')
+ );
+}
+
+// ─── StructuredOutputCard ─────────────────────────────────────────────────────
+
+export default function StructuredOutputCard() {
+ return (
+
+ {/* Step label */}
+
+
+ 3
+
+
+ Structured Output
+
+
+
+ {/* Card */}
+
+ {/* Fake code-editor chrome */}
+
+
+ {/* Highlighted JSON */}
+
+
+
+ );
+}
diff --git a/website/app/dashboard/page.tsx b/website/app/dashboard/page.tsx
index 9da17312..0ac49aa7 100644
--- a/website/app/dashboard/page.tsx
+++ b/website/app/dashboard/page.tsx
@@ -3,6 +3,7 @@ import { useUser, useClerk } from "@clerk/nextjs";
import { useRouter } from "next/navigation";
import Image from "next/image";
import Link from "next/link";
+import AIPipeline from "@/app/components/dashboard/AIPipeline";
import TodayOverview from "@/app/components/dashboard/TodayOverview";
import HabitHeatmap from "@/app/components/dashboard/HabitHeatmap";
import GoalsSection from "@/app/components/dashboard/GoalsSection";
@@ -651,6 +652,9 @@ export default function DashboardPage() {
+ {/* AI Processing Pipeline */}
+
+
{/* Today Overview */}
diff --git a/website/app/globals.css b/website/app/globals.css
index 9a4aa46e..ebf330fd 100644
--- a/website/app/globals.css
+++ b/website/app/globals.css
@@ -112,6 +112,21 @@ body {
.animate-fade-in-up-3 { animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.19s both; }
.animate-fade-in-up-4 { animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.26s both; }
+/* AI Pipeline — shimmer text sweep */
+@keyframes shimmer-sweep {
+ 0% { background-position: -200% center; }
+ 100% { background-position: 200% center; }
+}
+
+.shimmer-text {
+ background: linear-gradient(90deg, #475569 0%, #94a3b8 45%, #475569 100%);
+ background-size: 200% auto;
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ animation: shimmer-sweep 2.5s linear infinite;
+}
+
/* Scroll-triggered reveal */
.reveal {
opacity: 0;
From 43d43e35751203d2a394d5a1cbd7b15f3fec6372 Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Tue, 24 Feb 2026 09:05:40 +0500
Subject: [PATCH 04/78] Fix division by zero in HabitHeatmap average
calculation when no active days exist
---
website/app/components/dashboard/HabitHeatmap.tsx | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/website/app/components/dashboard/HabitHeatmap.tsx b/website/app/components/dashboard/HabitHeatmap.tsx
index 41a78d76..43b98540 100644
--- a/website/app/components/dashboard/HabitHeatmap.tsx
+++ b/website/app/components/dashboard/HabitHeatmap.tsx
@@ -230,7 +230,12 @@ export default function HabitHeatmap({
Avg score
- {Math.round(data.filter((d) => d.value > 0).reduce((s, d) => s + d.value, 0) / data.filter((d) => d.value > 0).length)}
+ {(() => {
+ const activeDays = data.filter((d) => d.value > 0);
+ return activeDays.length > 0
+ ? Math.round(activeDays.reduce((s, d) => s + d.value, 0) / activeDays.length)
+ : 0;
+ })()}
%
From 901259b965b5848408d3a659d89a16910ae76441 Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Tue, 24 Feb 2026 09:11:18 +0500
Subject: [PATCH 05/78] Fix lint errors: refactor DonutChart variable
reassignment, suppress intentional unused vars
---
.../app/components/dashboard/DonutChart.tsx | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/website/app/components/dashboard/DonutChart.tsx b/website/app/components/dashboard/DonutChart.tsx
index f9e342c2..34e8b63a 100644
--- a/website/app/components/dashboard/DonutChart.tsx
+++ b/website/app/components/dashboard/DonutChart.tsx
@@ -24,15 +24,16 @@ export default function DonutChart({ data, size = 216, strokeWidth = 20 }: Donut
const circumference = 2 * Math.PI * radius;
const total = data.reduce((sum, d) => sum + d.value, 0);
- let cumulativeAngle = -90;
-
- const segments = data.map((item) => {
- const percent = item.value / total;
- const dashLength = Math.max(0, percent * circumference - SEGMENT_GAP);
- const rotation = cumulativeAngle;
- cumulativeAngle += percent * 360;
- return { ...item, dashLength, rotation, percent };
- });
+ const segments = data.reduce>(
+ (acc, item) => {
+ const percent = item.value / total;
+ const dashLength = Math.max(0, percent * circumference - SEGMENT_GAP);
+ const prevAngle = acc.length > 0 ? acc[acc.length - 1].rotation + acc[acc.length - 1].percent * 360 : -90;
+ acc.push({ ...item, dashLength, rotation: prevAngle, percent });
+ return acc;
+ },
+ []
+ );
const activeSegment = hovered ? segments.find((s) => s.label === hovered) : null;
From 50080ddf05a1e5be914def72f98b965fd6bd8c5f Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Tue, 24 Feb 2026 09:19:09 +0500
Subject: [PATCH 06/78] Rename "Avg score" and "Best day" labels to "Avg." and
"Best" in habit heatmap
---
website/app/components/dashboard/HabitHeatmap.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/website/app/components/dashboard/HabitHeatmap.tsx b/website/app/components/dashboard/HabitHeatmap.tsx
index 43b98540..179f8ffb 100644
--- a/website/app/components/dashboard/HabitHeatmap.tsx
+++ b/website/app/components/dashboard/HabitHeatmap.tsx
@@ -228,7 +228,7 @@ export default function HabitHeatmap({
-
Avg score
+
Avg.
{(() => {
const activeDays = data.filter((d) => d.value > 0);
@@ -240,7 +240,7 @@ export default function HabitHeatmap({
-
Best day
+
Best
{Math.max(...data.map((d) => d.value))}
%
From 40831193112c814bc717d7cdd489a8daa80bfbcb Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Tue, 24 Feb 2026 09:52:34 +0500
Subject: [PATCH 07/78] Polish monthly habit heatmap with responsive scaling,
streak emphasis, and calendar-grade visual clarity.
---
.../app/components/dashboard/HabitHeatmap.tsx | 479 ++++++++++++------
1 file changed, 328 insertions(+), 151 deletions(-)
diff --git a/website/app/components/dashboard/HabitHeatmap.tsx b/website/app/components/dashboard/HabitHeatmap.tsx
index 179f8ffb..2d092178 100644
--- a/website/app/components/dashboard/HabitHeatmap.tsx
+++ b/website/app/components/dashboard/HabitHeatmap.tsx
@@ -1,4 +1,5 @@
"use client";
+import { useState } from "react";
// ─── Types ───────────────────────────────────────────────────────────────────
@@ -12,47 +13,59 @@ interface HabitHeatmapProps {
month?: string; // "YYYY-MM"
}
-// ─── Mock data (Feb 2026 — realistic weekday/weekend pattern) ─────────────────
+interface StatsPanelProps {
+ activeDays: number;
+ daysInMonth: number;
+ avgPercent: number;
+ bestValue: number;
+ bestDayLabel: string;
+ currentStreak: number;
+}
+
+// ─── Mock data (Jan 2026 — 31-day max layout test) ─────────────────────────────
const MOCK_DATA: HabitDay[] = [
- { date: "2026-02-01", value: 30 }, // Sun
- { date: "2026-02-02", value: 75 }, // Mon
- { date: "2026-02-03", value: 85 }, // Tue
- { date: "2026-02-04", value: 60 }, // Wed
- { date: "2026-02-05", value: 90 }, // Thu
- { date: "2026-02-06", value: 70 }, // Fri
- { date: "2026-02-07", value: 20 }, // Sat
- { date: "2026-02-08", value: 0 }, // Sun — full rest
- { date: "2026-02-09", value: 80 }, // Mon
- { date: "2026-02-10", value: 65 }, // Tue
- { date: "2026-02-11", value: 88 }, // Wed
- { date: "2026-02-12", value: 72 }, // Thu
- { date: "2026-02-13", value: 55 }, // Fri
- { date: "2026-02-14", value: 35 }, // Sat
- { date: "2026-02-15", value: 25 }, // Sun
- { date: "2026-02-16", value: 92 }, // Mon — peak
- { date: "2026-02-17", value: 78 }, // Tue
- { date: "2026-02-18", value: 0 }, // Wed — off day
- { date: "2026-02-19", value: 45 }, // Thu — recovering
- { date: "2026-02-20", value: 68 }, // Fri
- { date: "2026-02-21", value: 40 }, // Sat
- { date: "2026-02-22", value: 15 }, // Sun
- { date: "2026-02-23", value: 82 }, // Mon — today
- { date: "2026-02-24", value: 58 }, // Tue
- { date: "2026-02-25", value: 76 }, // Wed
- { date: "2026-02-26", value: 88 }, // Thu
- { date: "2026-02-27", value: 62 }, // Fri
- { date: "2026-02-28", value: 30 }, // Sat
+ { date: "2026-01-01", value: 70 }, // Thu
+ { date: "2026-01-02", value: 82 }, // Fri
+ { date: "2026-01-03", value: 35 }, // Sat
+ { date: "2026-01-04", value: 20 }, // Sun
+ { date: "2026-01-05", value: 76 }, // Mon
+ { date: "2026-01-06", value: 88 }, // Tue
+ { date: "2026-01-07", value: 64 }, // Wed
+ { date: "2026-01-08", value: 90 }, // Thu
+ { date: "2026-01-09", value: 72 }, // Fri
+ { date: "2026-01-10", value: 28 }, // Sat
+ { date: "2026-01-11", value: 0 }, // Sun — reset
+ { date: "2026-01-12", value: 68 }, // Mon
+ { date: "2026-01-13", value: 80 }, // Tue
+ { date: "2026-01-14", value: 92 }, // Wed — peak
+ { date: "2026-01-15", value: 74 }, // Thu
+ { date: "2026-01-16", value: 58 }, // Fri
+ { date: "2026-01-17", value: 32 }, // Sat
+ { date: "2026-01-18", value: 18 }, // Sun
+ { date: "2026-01-19", value: 84 }, // Mon
+ { date: "2026-01-20", value: 66 }, // Tue
+ { date: "2026-01-21", value: 0 }, // Wed — off day
+ { date: "2026-01-22", value: 52 }, // Thu
+ { date: "2026-01-23", value: 74 }, // Fri
+ { date: "2026-01-24", value: 40 }, // Sat
+ { date: "2026-01-25", value: 22 }, // Sun
+ { date: "2026-01-26", value: 86 }, // Mon
+ { date: "2026-01-27", value: 79 }, // Tue
+ { date: "2026-01-28", value: 67 }, // Wed
+ { date: "2026-01-29", value: 83 }, // Thu
+ { date: "2026-01-30", value: 71 }, // Fri
+ { date: "2026-01-31", value: 36 }, // Sat
];
// ─── Utilities ────────────────────────────────────────────────────────────────
function getIntensityColor(value: number): string {
- if (value === 0) return "rgba(30,41,59,0.7)";
- if (value <= 25) return "rgba(16,185,129,0.22)";
- if (value <= 50) return "rgba(16,185,129,0.45)";
- if (value <= 75) return "rgba(16,185,129,0.68)";
- return "rgba(16,185,129,0.95)";
+ if (value === 0) return "rgba(39,39,42,0.82)";
+ if (value <= 25) return "rgba(34,197,94,0.24)";
+ if (value <= 50) return "rgba(34,197,94,0.4)";
+ if (value <= 75) return "rgba(34,197,94,0.56)";
+ return "rgba(34,197,94,0.7)";
}
function formatTooltipDate(dateStr: string): string {
@@ -64,11 +77,14 @@ function formatTooltipDate(dateStr: string): string {
}
function formatTooltipValue(value: number): string {
- if (value === 0) return "No activity";
- if (value <= 25) return `${value}% — light day`;
- if (value <= 50) return `${value}% — moderate`;
- if (value <= 75) return `${value}% — solid`;
- return `${value}% — excellent`;
+ return `${value}% productive`;
+}
+
+function formatShortMonthDay(dateStr: string): string {
+ return new Date(dateStr + "T00:00:00").toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ });
}
// ─── Constants ────────────────────────────────────────────────────────────────
@@ -76,13 +92,82 @@ function formatTooltipValue(value: number): string {
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const LEGEND_LEVELS = [0, 20, 45, 70, 95] as const;
+function StatsPanel({
+ activeDays,
+ daysInMonth,
+ avgPercent,
+ bestValue,
+ bestDayLabel,
+ currentStreak,
+}: StatsPanelProps) {
+ return (
+
+
+
+
Month Snapshot
+
+
+
Active days
+
+ {activeDays}
+ / {daysInMonth}
+
+
+
+
Average
+
+ {avgPercent}
+ %
+
+
+
+
Best
+
+ {bestValue}
+ %
+
+
+
+
+
+
+
+
+
Insights
+
Best day: {bestDayLabel} ({bestValue}%)
+
Current streak: {currentStreak} days
+
+
+
+
+
+
+
Less
+
+ {LEGEND_LEVELS.map((v) => (
+
+ ))}
+
+
More
+
+
+
+
+ );
+}
+
// ─── Component ────────────────────────────────────────────────────────────────
export default function HabitHeatmap({
data = MOCK_DATA,
- month = "2026-02",
+ month = "2026-01",
}: HabitHeatmapProps) {
const today = new Date().toISOString().split("T")[0];
+ const [hoveredStreakId, setHoveredStreakId] = useState(null);
const [year, monthNum] = month.split("-").map(Number);
const firstDay = new Date(year, monthNum - 1, 1);
@@ -95,11 +180,92 @@ export default function HabitHeatmap({
});
const dataMap = new Map(data.map((d) => [d.date, d.value]));
+ const sortedData = [...data].sort((a, b) => a.date.localeCompare(b.date));
+ const activeDays = data.filter((d) => d.value > 0).length;
+ const avgPercent =
+ activeDays > 0
+ ? Math.round(data.filter((d) => d.value > 0).reduce((sum, d) => sum + d.value, 0) / activeDays)
+ : 0;
+ const bestDay =
+ sortedData.length > 0
+ ? sortedData.reduce((best, day) => (day.value > best.value ? day : best), sortedData[0])
+ : null;
+
+ const monthDays = Array.from({ length: daysInMonth }, (_, i) => {
+ const day = i + 1;
+ const date = `${year}-${String(monthNum).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
+ return { day, date, value: dataMap.get(date) ?? 0 };
+ });
+
+ type StreakMeta = {
+ streakId: number | null;
+ isStreak: boolean;
+ isCurrentStreak: boolean;
+ };
+
+ const streakMetaByDate = new Map();
+ monthDays.forEach((entry) =>
+ streakMetaByDate.set(entry.date, {
+ streakId: null,
+ isStreak: false,
+ isCurrentStreak: false,
+ }),
+ );
+
+ let streakCounter = 0;
+ let i = 0;
+ while (i < monthDays.length) {
+ if (monthDays[i].value <= 0) {
+ i += 1;
+ continue;
+ }
+ const start = i;
+ while (i < monthDays.length && monthDays[i].value > 0) i += 1;
+ const end = i - 1;
+ const runLength = end - start + 1;
+ const streakId = runLength >= 2 ? streakCounter++ : null;
+ for (let idx = start; idx <= end; idx++) {
+ streakMetaByDate.set(monthDays[idx].date, {
+ streakId,
+ isStreak: runLength >= 2,
+ isCurrentStreak: false,
+ });
+ }
+ }
+
+ let currentStreak = 0;
+ let currentEnd = monthDays.length - 1;
+ while (currentEnd >= 0 && monthDays[currentEnd].value <= 0) currentEnd -= 1;
+ if (currentEnd >= 0) {
+ let currentStart = currentEnd;
+ while (currentStart >= 0 && monthDays[currentStart].value > 0) currentStart -= 1;
+ currentStart += 1;
+ currentStreak = currentEnd - currentStart + 1;
+ for (let idx = currentStart; idx <= currentEnd; idx++) {
+ const prevMeta = streakMetaByDate.get(monthDays[idx].date) ?? {
+ streakId: null,
+ isStreak: false,
+ isCurrentStreak: false,
+ };
+ streakMetaByDate.set(monthDays[idx].date, {
+ ...prevMeta,
+ isCurrentStreak: true,
+ });
+ }
+ }
// Build flat cell array: leading empty + days + trailing empty
type Cell =
| { type: "empty"; key: string }
- | { type: "day"; day: number; date: string; value: number };
+ | {
+ type: "day";
+ day: number;
+ date: string;
+ value: number;
+ streakId: number | null;
+ isStreak: boolean;
+ isCurrentStreak: boolean;
+ };
const cells: Cell[] = [];
@@ -109,7 +275,20 @@ export default function HabitHeatmap({
for (let d = 1; d <= daysInMonth; d++) {
const dateStr = `${year}-${String(monthNum).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
- cells.push({ type: "day", day: d, date: dateStr, value: dataMap.get(dateStr) ?? 0 });
+ const streakMeta = streakMetaByDate.get(dateStr) ?? {
+ streakId: null,
+ isStreak: false,
+ isCurrentStreak: false,
+ };
+ cells.push({
+ type: "day",
+ day: d,
+ date: dateStr,
+ value: dataMap.get(dateStr) ?? 0,
+ streakId: streakMeta.streakId,
+ isStreak: streakMeta.isStreak,
+ isCurrentStreak: streakMeta.isCurrentStreak,
+ });
}
const remainder = cells.length % 7;
@@ -122,132 +301,130 @@ export default function HabitHeatmap({
return (
{/* Header */}
-
+
-
Monthly Habit Overview
-
{monthLabel}
-
-
- {/* Legend */}
-
-
Less
-
- {LEGEND_LEVELS.map((v) => (
-
- ))}
-
-
More
+
Monthly Habit Overview
+
{monthLabel}
{/* Grid */}
-
- {/* Weekday labels */}
-
- {WEEKDAYS.map((label) => (
-
- {label}
+
+
+
+
+
Less
+
+ {LEGEND_LEVELS.map((v) => (
+
+ ))}
+
+
More
- ))}
-
- {/* Day cells */}
-
- {cells.map((cell, idx) => {
- if (cell.type === "empty") {
- return (
+ {/* Weekday labels */}
+
+ {WEEKDAYS.map((label) => (
- );
- }
+ key={label}
+ className="text-center text-[11px] font-medium text-white/50 py-0 select-none"
+ >
+ {label}
+
+ ))}
+
- const isToday = cell.date === today;
+ {/* Day cells */}
+
+ {cells.map((cell, idx) => {
+ if (cell.type === "empty") {
+ return (
+
+ );
+ }
- return (
-
- {/* Cell square */}
-
setHoveredStreakId(cell.streakId)}
+ onMouseLeave={() => setHoveredStreakId(null)}
+ >
+ {/* Cell square */}
+
- {/* Day number — subtle overlay */}
-
- {cell.day}
-
-
+ style={{ backgroundColor: getIntensityColor(cell.value) }}
+ >
+ {/* Day number — subtle overlay */}
+
+ {cell.day}
+
+ {cell.isStreak && (
+
+ )}
+
- {/* Tooltip */}
-
-
-
- {formatTooltipDate(cell.date)}
- {isToday && (
- Today
- )}
-
-
- {formatTooltipValue(cell.value)}
-
-
- {/* Arrow */}
-
+
+ {formatTooltipDate(cell.date)}
+ {isToday && (
+ Today
+ )}
+
+
+ {formatTooltipValue(cell.value)}
+
+
+ {/* Arrow */}
+
-
-
- );
- })}
-
-
- {/* Month summary row */}
-
-
-
-
Active days
-
- {data.filter((d) => d.value > 0).length}
- / {daysInMonth}
-
-
-
-
Avg.
-
- {(() => {
- const activeDays = data.filter((d) => d.value > 0);
- return activeDays.length > 0
- ? Math.round(activeDays.reduce((s, d) => s + d.value, 0) / activeDays.length)
- : 0;
- })()}
- %
-
-
-
-
Best
-
- {Math.max(...data.map((d) => d.value))}
- %
-
+ />
+
+
+ );
+ })}
-
hover cells for details
+
+
+
+
From 58a5294a847cf505ed925b9ee6ef973dac4306c1 Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Tue, 24 Feb 2026 10:04:29 +0500
Subject: [PATCH 08/78] feat(dashboard): replace structured JSON output with
animated live dashboard mapping preview
---
.../app/components/dashboard/AIPipeline.tsx | 17 +++-
.../dashboard/AnimatedActivityList.tsx | 45 ++++++++++
.../components/dashboard/AnimatedHeatmap.tsx | 39 ++++++++
.../dashboard/AnimatedProgressBar.tsx | 27 ++++++
.../components/dashboard/DashboardPreview.tsx | 38 ++++++++
.../app/components/dashboard/RawInputCard.tsx | 40 ++++++++-
.../dashboard/StructuredOutputCard.tsx | 90 +++++++++++--------
7 files changed, 254 insertions(+), 42 deletions(-)
create mode 100644 website/app/components/dashboard/AnimatedActivityList.tsx
create mode 100644 website/app/components/dashboard/AnimatedHeatmap.tsx
create mode 100644 website/app/components/dashboard/AnimatedProgressBar.tsx
create mode 100644 website/app/components/dashboard/DashboardPreview.tsx
diff --git a/website/app/components/dashboard/AIPipeline.tsx b/website/app/components/dashboard/AIPipeline.tsx
index 8902b1e9..69266e69 100644
--- a/website/app/components/dashboard/AIPipeline.tsx
+++ b/website/app/components/dashboard/AIPipeline.tsx
@@ -1,3 +1,6 @@
+"use client";
+
+import { useEffect, useState } from "react";
import RawInputCard from "./RawInputCard";
import AIProcessingCard from "./AIProcessingCard";
import StructuredOutputCard from "./StructuredOutputCard";
@@ -49,6 +52,16 @@ function FlowArrow() {
// ─── AIPipeline ───────────────────────────────────────────────────────────────
export default function AIPipeline() {
+ const [activeMappingStep, setActiveMappingStep] = useState(0);
+
+ useEffect(() => {
+ const id = setInterval(() => {
+ setActiveMappingStep((prev) => (prev + 1) % 4);
+ }, 1800);
+
+ return () => clearInterval(id);
+ }, []);
+
return (
{/* Header */}
@@ -67,11 +80,11 @@ export default function AIPipeline() {
{/* 3-step pipeline body */}
{/* Connection footer — links output to dashboard sections */}
diff --git a/website/app/components/dashboard/AnimatedActivityList.tsx b/website/app/components/dashboard/AnimatedActivityList.tsx
new file mode 100644
index 00000000..3536a34e
--- /dev/null
+++ b/website/app/components/dashboard/AnimatedActivityList.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+type AnimatedActivityListProps = {
+ activeMappingStep: number;
+};
+
+const BASE_ITEMS = [
+ "Reviewed sprint plan",
+ "Quick walk after lunch",
+];
+
+export default function AnimatedActivityList({ activeMappingStep }: AnimatedActivityListProps) {
+ const showNewItem = activeMappingStep >= 3;
+
+ return (
+
+
+
+
+ {BASE_ITEMS.map((item) => (
+
+ {item}
+
+ ))}
+
+
+ Dinner with parents
+
+
+
+ );
+}
diff --git a/website/app/components/dashboard/AnimatedHeatmap.tsx b/website/app/components/dashboard/AnimatedHeatmap.tsx
new file mode 100644
index 00000000..6fad305f
--- /dev/null
+++ b/website/app/components/dashboard/AnimatedHeatmap.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+type AnimatedHeatmapProps = {
+ activeMappingStep: number;
+};
+
+const CELLS = 14;
+const ACTIVE_INDICES = [1, 2, 4, 5, 8, 10, 11];
+
+export default function AnimatedHeatmap({ activeMappingStep }: AnimatedHeatmapProps) {
+ const isActive = activeMappingStep >= 1;
+
+ return (
+
+
+
Health Heatmap
+
Weekly
+
+
+
+ {Array.from({ length: CELLS }).map((_, index) => {
+ const shouldLight = isActive && ACTIVE_INDICES.includes(index);
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/website/app/components/dashboard/AnimatedProgressBar.tsx b/website/app/components/dashboard/AnimatedProgressBar.tsx
new file mode 100644
index 00000000..600c4fda
--- /dev/null
+++ b/website/app/components/dashboard/AnimatedProgressBar.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+type AnimatedProgressBarProps = {
+ activeMappingStep: number;
+};
+
+export default function AnimatedProgressBar({ activeMappingStep }: AnimatedProgressBarProps) {
+ const value = activeMappingStep >= 2 ? 50 : 0;
+
+ return (
+
+
+
Work Progress
+
{value}%
+
+
+
+
+
"5h deep work" mapped to today's work target
+
+ );
+}
diff --git a/website/app/components/dashboard/DashboardPreview.tsx b/website/app/components/dashboard/DashboardPreview.tsx
new file mode 100644
index 00000000..404b627d
--- /dev/null
+++ b/website/app/components/dashboard/DashboardPreview.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import AnimatedHeatmap from "./AnimatedHeatmap";
+import AnimatedProgressBar from "./AnimatedProgressBar";
+import AnimatedActivityList from "./AnimatedActivityList";
+
+type DashboardPreviewProps = {
+ activeMappingStep: number;
+};
+
+const STEP_COPY = [
+ 'Highlighting "Morning gym" in your raw input',
+ "Mapping it into Health and updating the weekly heatmap",
+ 'Mapping "5h deep work" into your Work progress',
+ 'Adding "Dinner with parents" to Activity Logs',
+];
+
+export default function DashboardPreview({ activeMappingStep }: DashboardPreviewProps) {
+ return (
+
+
+ How this data updates your dashboard
+
+
+
+
+ {STEP_COPY[activeMappingStep]}
+
+
+
+
+
+ );
+}
diff --git a/website/app/components/dashboard/RawInputCard.tsx b/website/app/components/dashboard/RawInputCard.tsx
index 93519bd3..d67fd234 100644
--- a/website/app/components/dashboard/RawInputCard.tsx
+++ b/website/app/components/dashboard/RawInputCard.tsx
@@ -4,7 +4,43 @@
const MOCK_MESSAGE =
"Had a great day. Went to the gym in the morning, worked 5 hours on the project proposal, and had dinner with my parents.";
-export default function RawInputCard() {
+const ACTIVE_PHRASES = [
+ "gym in the morning",
+ "gym in the morning",
+ "worked 5 hours",
+ "dinner with my parents",
+];
+
+type RawInputCardProps = {
+ activeMappingStep?: number;
+};
+
+function renderHighlightedMessage(activeMappingStep = 0) {
+ const phrase = ACTIVE_PHRASES[activeMappingStep] ?? ACTIVE_PHRASES[0];
+ const phraseIndex = MOCK_MESSAGE.toLowerCase().indexOf(phrase.toLowerCase());
+
+ if (phraseIndex === -1) {
+ return MOCK_MESSAGE;
+ }
+
+ const before = MOCK_MESSAGE.slice(0, phraseIndex);
+ const match = MOCK_MESSAGE.slice(phraseIndex, phraseIndex + phrase.length);
+ const after = MOCK_MESSAGE.slice(phraseIndex + phrase.length);
+
+ return (
+ <>
+ {before}
+
+ {match}
+
+ {after}
+ >
+ );
+}
+
+export default function RawInputCard({ activeMappingStep = 0 }: RawInputCardProps) {
return (
{/* Step label */}
@@ -35,7 +71,7 @@ export default function RawInputCard() {
{/* Message bubble */}
- “{MOCK_MESSAGE}”
+ “{renderHighlightedMessage(activeMappingStep)}”
diff --git a/website/app/components/dashboard/StructuredOutputCard.tsx b/website/app/components/dashboard/StructuredOutputCard.tsx
index 6671cbfe..4ab1a4f6 100644
--- a/website/app/components/dashboard/StructuredOutputCard.tsx
+++ b/website/app/components/dashboard/StructuredOutputCard.tsx
@@ -1,38 +1,29 @@
-// ─── Mock JSON output ─────────────────────────────────────────────────────────
+"use client";
+
+import { useMemo, useState } from "react";
+import DashboardPreview from "./DashboardPreview";
+
+type StructuredOutputCardProps = {
+ activeMappingStep: number;
+};
const MOCK_JSON = `{
"activities": [
- { "text": "Morning gym", "category": "Health" },
- { "text": "5h deep work", "category": "Work",
- "duration": "5h" },
+ { "text": "Morning gym", "category": "Health" },
+ { "text": "5h deep work", "category": "Work", "duration": "5h" },
{ "text": "Dinner with parents", "category": "Relationships" }
],
- "summary": {
- "health": 30,
- "work": 50,
- "relationships": 20
- }
+ "summary": { "health": 30, "work": 50, "relationships": 20 }
}`;
-// ─── Simple JSON syntax highlighter (no library) ──────────────────────────────
+export default function StructuredOutputCard({ activeMappingStep }: StructuredOutputCardProps) {
+ const [viewMode, setViewMode] = useState<"user" | "developer">("user");
-function syntaxHighlight(json: string): string {
- return (
- json
- // Keys: "key":
- .replace(/"([^"]+)"(\s*:)/g, '
"$1" $2')
- // String values: : "value"
- .replace(/:\s*"([^"]+)"/g, ':
"$1" ')
- // Numbers: : 30
- .replace(/:\s*(\d+)/g, ':
$1 ')
- // Braces and brackets
- .replace(/[{}\[\]]/g, '
$& ')
+ const stepTitle = useMemo(
+ () => (viewMode === "user" ? "Dashboard Update" : "Structured Output"),
+ [viewMode],
);
-}
-
-// ─── StructuredOutputCard ─────────────────────────────────────────────────────
-export default function StructuredOutputCard() {
return (
{/* Step label */}
@@ -41,27 +32,50 @@ export default function StructuredOutputCard() {
3
- Structured Output
+ {stepTitle}
{/* Card */}
- {/* Fake code-editor chrome */}
-
-
-
-
-
+
+
+ {viewMode === "user" ? "Live Preview" : "Developer View"}
+
+
+
+ setViewMode("user")}
+ className={`px-2.5 py-1 text-[10px] font-semibold rounded-md transition-colors ${
+ viewMode === "user"
+ ? "bg-violet-500/20 text-violet-200"
+ : "text-slate-500 hover:text-slate-300"
+ }`}
+ >
+ User View
+
+ setViewMode("developer")}
+ className={`px-2.5 py-1 text-[10px] font-semibold rounded-md transition-colors ${
+ viewMode === "developer"
+ ? "bg-emerald-500/20 text-emerald-200"
+ : "text-slate-500 hover:text-slate-300"
+ }`}
+ >
+ Developer View
+
-
output.json
- {/* Highlighted JSON */}
-
+ {viewMode === "user" ? (
+
+ ) : (
+
+ {MOCK_JSON}
+
+ )}
);
From 1e44d43f167ddd533b9e171fb4b08be87bffd778 Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Tue, 24 Feb 2026 10:20:50 +0500
Subject: [PATCH 09/78] feat(features): move AI pipeline demo to features page
and remove it from dashboard
---
.../app/components/dashboard/AIPipeline.tsx | 26 ++++++--
.../dashboard/StructuredOutputCard.tsx | 65 +++++++++++--------
.../components/sections/AIPipelineDemo.tsx | 47 ++++++++++++++
website/app/dashboard/page.tsx | 4 --
website/app/features/page.tsx | 3 +
5 files changed, 109 insertions(+), 36 deletions(-)
create mode 100644 website/app/components/sections/AIPipelineDemo.tsx
diff --git a/website/app/components/dashboard/AIPipeline.tsx b/website/app/components/dashboard/AIPipeline.tsx
index 69266e69..d5f38c57 100644
--- a/website/app/components/dashboard/AIPipeline.tsx
+++ b/website/app/components/dashboard/AIPipeline.tsx
@@ -5,6 +5,12 @@ import RawInputCard from "./RawInputCard";
import AIProcessingCard from "./AIProcessingCard";
import StructuredOutputCard from "./StructuredOutputCard";
+type AIPipelineProps = {
+ isActive?: boolean;
+ allowDeveloperView?: boolean;
+ className?: string;
+};
+
// ─── Arrow between steps (responsive: horizontal on desktop, vertical on mobile)
function FlowArrow() {
@@ -51,19 +57,28 @@ function FlowArrow() {
// ─── AIPipeline ───────────────────────────────────────────────────────────────
-export default function AIPipeline() {
+export default function AIPipeline({
+ isActive = true,
+ allowDeveloperView = true,
+ className = "",
+}: AIPipelineProps) {
const [activeMappingStep, setActiveMappingStep] = useState(0);
useEffect(() => {
+ if (!isActive) {
+ setActiveMappingStep(0);
+ return;
+ }
+
const id = setInterval(() => {
setActiveMappingStep((prev) => (prev + 1) % 4);
}, 1800);
return () => clearInterval(id);
- }, []);
+ }, [isActive]);
return (
-
+
{/* Header */}
@@ -84,7 +99,10 @@ export default function AIPipeline() {
-
+
{/* Connection footer — links output to dashboard sections */}
diff --git a/website/app/components/dashboard/StructuredOutputCard.tsx b/website/app/components/dashboard/StructuredOutputCard.tsx
index 4ab1a4f6..73ff231c 100644
--- a/website/app/components/dashboard/StructuredOutputCard.tsx
+++ b/website/app/components/dashboard/StructuredOutputCard.tsx
@@ -5,6 +5,7 @@ import DashboardPreview from "./DashboardPreview";
type StructuredOutputCardProps = {
activeMappingStep: number;
+ allowDeveloperView?: boolean;
};
const MOCK_JSON = `{
@@ -16,12 +17,18 @@ const MOCK_JSON = `{
"summary": { "health": 30, "work": 50, "relationships": 20 }
}`;
-export default function StructuredOutputCard({ activeMappingStep }: StructuredOutputCardProps) {
+export default function StructuredOutputCard({
+ activeMappingStep,
+ allowDeveloperView = true,
+}: StructuredOutputCardProps) {
const [viewMode, setViewMode] = useState<"user" | "developer">("user");
const stepTitle = useMemo(
- () => (viewMode === "user" ? "Dashboard Update" : "Structured Output"),
- [viewMode],
+ () => {
+ if (!allowDeveloperView) return "Dashboard Update";
+ return viewMode === "user" ? "Dashboard Update" : "Structured Output";
+ },
+ [allowDeveloperView, viewMode],
);
return (
@@ -43,33 +50,35 @@ export default function StructuredOutputCard({ activeMappingStep }: StructuredOu
{viewMode === "user" ? "Live Preview" : "Developer View"}
-
- setViewMode("user")}
- className={`px-2.5 py-1 text-[10px] font-semibold rounded-md transition-colors ${
- viewMode === "user"
- ? "bg-violet-500/20 text-violet-200"
- : "text-slate-500 hover:text-slate-300"
- }`}
- >
- User View
-
- setViewMode("developer")}
- className={`px-2.5 py-1 text-[10px] font-semibold rounded-md transition-colors ${
- viewMode === "developer"
- ? "bg-emerald-500/20 text-emerald-200"
- : "text-slate-500 hover:text-slate-300"
- }`}
- >
- Developer View
-
-
+ {allowDeveloperView && (
+
+ setViewMode("user")}
+ className={`px-2.5 py-1 text-[10px] font-semibold rounded-md transition-colors ${
+ viewMode === "user"
+ ? "bg-violet-500/20 text-violet-200"
+ : "text-slate-500 hover:text-slate-300"
+ }`}
+ >
+ User View
+
+ setViewMode("developer")}
+ className={`px-2.5 py-1 text-[10px] font-semibold rounded-md transition-colors ${
+ viewMode === "developer"
+ ? "bg-emerald-500/20 text-emerald-200"
+ : "text-slate-500 hover:text-slate-300"
+ }`}
+ >
+ Developer View
+
+
+ )}
- {viewMode === "user" ? (
+ {viewMode === "user" || !allowDeveloperView ? (
) : (
diff --git a/website/app/components/sections/AIPipelineDemo.tsx b/website/app/components/sections/AIPipelineDemo.tsx
new file mode 100644
index 00000000..4f02b94d
--- /dev/null
+++ b/website/app/components/sections/AIPipelineDemo.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import AIPipeline from "../dashboard/AIPipeline";
+
+export default function AIPipelineDemo() {
+ const sectionRef = useRef(null);
+ const [isInView, setIsInView] = useState(false);
+
+ useEffect(() => {
+ const node = sectionRef.current;
+ if (!node) return;
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ setIsInView(true);
+ } else if (entry.intersectionRatio === 0) {
+ setIsInView(false);
+ }
+ });
+ },
+ { threshold: 0.25 },
+ );
+
+ observer.observe(node);
+ return () => observer.disconnect();
+ }, []);
+
+ return (
+
+
+
From journal to insights
+
+ Write naturally. We extract, classify, and visualize your habits automatically.
+
+
+
+
+
+ );
+}
diff --git a/website/app/dashboard/page.tsx b/website/app/dashboard/page.tsx
index 0ac49aa7..9da17312 100644
--- a/website/app/dashboard/page.tsx
+++ b/website/app/dashboard/page.tsx
@@ -3,7 +3,6 @@ import { useUser, useClerk } from "@clerk/nextjs";
import { useRouter } from "next/navigation";
import Image from "next/image";
import Link from "next/link";
-import AIPipeline from "@/app/components/dashboard/AIPipeline";
import TodayOverview from "@/app/components/dashboard/TodayOverview";
import HabitHeatmap from "@/app/components/dashboard/HabitHeatmap";
import GoalsSection from "@/app/components/dashboard/GoalsSection";
@@ -652,9 +651,6 @@ export default function DashboardPage() {
- {/* AI Processing Pipeline */}
-
-
{/* Today Overview */}
diff --git a/website/app/features/page.tsx b/website/app/features/page.tsx
index a07ca02f..4bcc70be 100644
--- a/website/app/features/page.tsx
+++ b/website/app/features/page.tsx
@@ -2,6 +2,7 @@
import React from "react";
import Image from "next/image";
import Link from "next/link";
+import AIPipelineDemo from "../components/sections/AIPipelineDemo";
// Channel icons as SVG components
const ChannelIcons = {
@@ -303,6 +304,8 @@ export default function FeaturesPage() {
+
+
{/* Category Comparison Table */}
From 7f12116e2cf1c7d8aa3bbf6afbf31033d210384a Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Tue, 24 Feb 2026 14:32:55 +0500
Subject: [PATCH 10/78] Improve dashboard-to-logs navigation with contextual
links, breadcrumbs, and highlighted log context
---
.../app/components/dashboard/ActivityList.tsx | 38 ++++---
.../app/components/dashboard/DonutChart.tsx | 11 +-
.../components/dashboard/TodayOverview.tsx | 31 +++++-
.../components/logs/ClassificationPanel.tsx | 47 ++++++++
website/app/components/logs/LogItem.tsx | 38 +++++++
.../app/components/logs/LogsControlBar.tsx | 83 ++++++++++++++
website/app/components/logs/LogsList.tsx | 41 +++++++
website/app/components/logs/LogsPage.tsx | 91 ++++++++++++++++
website/app/components/logs/mockData.ts | 101 ++++++++++++++++++
website/app/dashboard/page.tsx | 16 ++-
website/app/logs/page.tsx | 7 ++
11 files changed, 480 insertions(+), 24 deletions(-)
create mode 100644 website/app/components/logs/ClassificationPanel.tsx
create mode 100644 website/app/components/logs/LogItem.tsx
create mode 100644 website/app/components/logs/LogsControlBar.tsx
create mode 100644 website/app/components/logs/LogsList.tsx
create mode 100644 website/app/components/logs/LogsPage.tsx
create mode 100644 website/app/components/logs/mockData.ts
create mode 100644 website/app/logs/page.tsx
diff --git a/website/app/components/dashboard/ActivityList.tsx b/website/app/components/dashboard/ActivityList.tsx
index 0052ae8e..5126737c 100644
--- a/website/app/components/dashboard/ActivityList.tsx
+++ b/website/app/components/dashboard/ActivityList.tsx
@@ -1,3 +1,5 @@
+import Link from "next/link";
+
export interface Activity {
id: string;
title: string;
@@ -8,6 +10,7 @@ export interface Activity {
interface ActivityListProps {
activities: Activity[];
+ getActivityHref?: (activity: Activity) => string;
}
const CATEGORY_STYLES: Record = {
@@ -16,27 +19,32 @@ const CATEGORY_STYLES: Record = {
Relationships: "bg-amber-500/15 text-amber-300",
};
-export default function ActivityList({ activities }: ActivityListProps) {
+export default function ActivityList({ activities, getActivityHref }: ActivityListProps) {
return (
{activities.map((activity) => (
-
-
{activity.icon}
-
-
{activity.title}
-
{activity.time}
-
-
- {activity.category}
-
-
+
{activity.icon}
+
+
{activity.title}
+
{activity.time}
+
+
+ {activity.category}
+
+
+
))}
);
diff --git a/website/app/components/dashboard/DonutChart.tsx b/website/app/components/dashboard/DonutChart.tsx
index 34e8b63a..b6f63ef5 100644
--- a/website/app/components/dashboard/DonutChart.tsx
+++ b/website/app/components/dashboard/DonutChart.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
+import { useRouter } from "next/navigation";
export interface CategoryData {
label: string;
@@ -12,12 +13,14 @@ interface DonutChartProps {
data: CategoryData[];
size?: number;
strokeWidth?: number;
+ getCategoryHref?: (label: string) => string;
}
const SEGMENT_GAP = 4;
-export default function DonutChart({ data, size = 216, strokeWidth = 20 }: DonutChartProps) {
+export default function DonutChart({ data, size = 216, strokeWidth = 20, getCategoryHref }: DonutChartProps) {
const [hovered, setHovered] = useState(null);
+ const router = useRouter();
const center = size / 2;
const radius = (size - strokeWidth) / 2;
@@ -37,6 +40,10 @@ export default function DonutChart({ data, size = 216, strokeWidth = 20 }: Donut
const activeSegment = hovered ? segments.find((s) => s.label === hovered) : null;
+ const handleCategoryClick = (label: string) => {
+ router.push(getCategoryHref ? getCategoryHref(label) : `/logs?category=${encodeURIComponent(label.toLowerCase())}`);
+ };
+
return (
@@ -72,6 +79,7 @@ export default function DonutChart({ data, size = 216, strokeWidth = 20 }: Donut
}}
onMouseEnter={() => setHovered(seg.label)}
onMouseLeave={() => setHovered(null)}
+ onClick={() => handleCategoryClick(seg.label)}
/>
))}
@@ -105,6 +113,7 @@ export default function DonutChart({ data, size = 216, strokeWidth = 20 }: Donut
className="cursor-pointer group"
onMouseEnter={() => setHovered(seg.label)}
onMouseLeave={() => setHovered(null)}
+ onClick={() => handleCategoryClick(seg.label)}
>
diff --git a/website/app/components/dashboard/TodayOverview.tsx b/website/app/components/dashboard/TodayOverview.tsx
index 1d937d8e..a72bbba4 100644
--- a/website/app/components/dashboard/TodayOverview.tsx
+++ b/website/app/components/dashboard/TodayOverview.tsx
@@ -1,5 +1,6 @@
"use client";
+import Link from "next/link";
import DonutChart, { CategoryData } from "./DonutChart";
import ActivityList, { Activity } from "./ActivityList";
@@ -19,6 +20,8 @@ const MOCK_ACTIVITIES: Activity[] = [
];
export default function TodayOverview() {
+ const selectedDate = "2026-02-23";
+
const today = new Date().toLocaleDateString("en-US", {
weekday: "long",
month: "long",
@@ -33,9 +36,17 @@ export default function TodayOverview() {
Today's Overview
{today}
-
- Today
-
+
+
+ Today
+
+
+ View All Logs ->
+
+
{/* Card body */}
@@ -43,13 +54,23 @@ export default function TodayOverview() {
{/* Left: Donut chart */}
Time Distribution
-
+
`/logs?category=${encodeURIComponent(label.toLowerCase())}&from=dashboard`}
+ />
{/* Right: Activity list */}
Today's Activities
-
+
+ `/logs?date=${selectedDate}&highlight=${encodeURIComponent(activity.title)}&category=${encodeURIComponent(
+ activity.category.toLowerCase()
+ )}&from=dashboard`
+ }
+ />
diff --git a/website/app/components/logs/ClassificationPanel.tsx b/website/app/components/logs/ClassificationPanel.tsx
new file mode 100644
index 00000000..5d19adb6
--- /dev/null
+++ b/website/app/components/logs/ClassificationPanel.tsx
@@ -0,0 +1,47 @@
+import { CATEGORY_STYLES, MOCK_CLASSIFICATION_EXAMPLES } from "./mockData";
+
+export default function ClassificationPanel() {
+ return (
+
+
+
Data Classification
+
How AI parses raw messages into meaningful categories.
+
+
+ {MOCK_CLASSIFICATION_EXAMPLES.map((example) => (
+
+
+ “{example.rawMessage}”
+ Expand
+
+
+
+
+
Raw Message
+
{example.rawMessage}
+
+
+
+
Extracted Categories
+
+ {example.extractedCategories.map((category) => (
+
+ {category}
+
+ ))}
+
+
+
+
+
Classification Notes
+
{example.notes}
+
+
+
+ ))}
+
+ );
+}
diff --git a/website/app/components/logs/LogItem.tsx b/website/app/components/logs/LogItem.tsx
new file mode 100644
index 00000000..baeb84a3
--- /dev/null
+++ b/website/app/components/logs/LogItem.tsx
@@ -0,0 +1,38 @@
+import { CATEGORY_STYLES, LOG_TYPE_STYLES, LogEntry } from "./mockData";
+
+interface LogItemProps {
+ log: LogEntry;
+ isHighlighted?: boolean;
+}
+
+export default function LogItem({ log, isHighlighted = false }: LogItemProps) {
+ return (
+
+
+
+
+
+
{log.text}
+
+ {log.categories.map((category) => (
+
+ {category}
+
+ ))}
+
+ {log.type}
+
+
+
+
+
+ );
+}
diff --git a/website/app/components/logs/LogsControlBar.tsx b/website/app/components/logs/LogsControlBar.tsx
new file mode 100644
index 00000000..0338457d
--- /dev/null
+++ b/website/app/components/logs/LogsControlBar.tsx
@@ -0,0 +1,83 @@
+import { DAILY_OPTIONS, DailyOption, LogsView, RECENT_COUNT_OPTIONS, RecentCountOption } from "./mockData";
+
+interface LogsControlBarProps {
+ view: LogsView;
+ selectedDay: DailyOption;
+ selectedRecentCount: RecentCountOption;
+ onViewChange: (view: LogsView) => void;
+ onDayChange: (day: DailyOption) => void;
+ onRecentCountChange: (count: RecentCountOption) => void;
+}
+
+function ToggleButton({
+ active,
+ label,
+ onClick,
+}: {
+ active: boolean;
+ label: string;
+ onClick: () => void;
+}) {
+ return (
+
+ {label}
+
+ );
+}
+
+export default function LogsControlBar({
+ view,
+ selectedDay,
+ selectedRecentCount,
+ onViewChange,
+ onDayChange,
+ onRecentCountChange,
+}: LogsControlBarProps) {
+ return (
+
+
+ onViewChange("daily")} />
+ onViewChange("recent")} />
+
+
+ {view === "daily" ? (
+
+ Date:
+ onDayChange(event.target.value as DailyOption)}
+ className="rounded-lg bg-slate-950/70 border border-slate-700/70 text-slate-200 text-sm px-3 py-1.5 outline-none focus:border-emerald-500/40 transition-colors"
+ >
+ {DAILY_OPTIONS.map((option) => (
+
+ {option}
+
+ ))}
+
+
+ ) : (
+
+ Last:
+ onRecentCountChange(Number(event.target.value) as RecentCountOption)}
+ className="rounded-lg bg-slate-950/70 border border-slate-700/70 text-slate-200 text-sm px-3 py-1.5 outline-none focus:border-emerald-500/40 transition-colors"
+ >
+ {RECENT_COUNT_OPTIONS.map((option) => (
+
+ {option}
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/website/app/components/logs/LogsList.tsx b/website/app/components/logs/LogsList.tsx
new file mode 100644
index 00000000..8d84a2d7
--- /dev/null
+++ b/website/app/components/logs/LogsList.tsx
@@ -0,0 +1,41 @@
+import LogItem from "./LogItem";
+import { LogEntry } from "./mockData";
+
+interface LogsListProps {
+ logs: LogEntry[];
+ highlightedLogText?: string | null;
+}
+
+export default function LogsList({ logs, highlightedLogText }: LogsListProps) {
+ return (
+
+
+
Logs Explorer
+
{logs.length} messages
+
+
+
+ {logs.map((log) => (
+
+ ))}
+
+
+
+
+ < Prev
+
+ Page 1
+
+ Next >
+
+
+
+ );
+}
diff --git a/website/app/components/logs/LogsPage.tsx b/website/app/components/logs/LogsPage.tsx
new file mode 100644
index 00000000..b59e3464
--- /dev/null
+++ b/website/app/components/logs/LogsPage.tsx
@@ -0,0 +1,91 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import Link from "next/link";
+import { useSearchParams } from "next/navigation";
+import ClassificationPanel from "./ClassificationPanel";
+import LogsControlBar from "./LogsControlBar";
+import LogsList from "./LogsList";
+import { DailyOption, LogsView, MOCK_LOGS, RecentCountOption } from "./mockData";
+
+function formatCategoryLabel(value: string) {
+ if (!value) return "";
+ return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
+}
+
+export default function LogsPage() {
+ const searchParams = useSearchParams();
+ const [view, setView] = useState("daily");
+ const [selectedDay, setSelectedDay] = useState("Feb 23");
+ const [selectedRecentCount, setSelectedRecentCount] = useState(10);
+ const source = searchParams.get("from");
+ const selectedDate = searchParams.get("date");
+ const selectedCategory = searchParams.get("category");
+ const highlightedText = searchParams.get("highlight");
+
+ const visibleLogs = useMemo(() => {
+ if (view === "recent") {
+ return MOCK_LOGS.slice(0, selectedRecentCount);
+ }
+ return MOCK_LOGS;
+ }, [view, selectedRecentCount, selectedDay]);
+
+ const contextBannerText = useMemo(() => {
+ if (selectedDate) {
+ return `Showing logs for: ${selectedDate}`;
+ }
+ if (selectedCategory) {
+ return `Filtered by: ${formatCategoryLabel(selectedCategory)}`;
+ }
+ return null;
+ }, [selectedDate, selectedCategory]);
+
+ return (
+
+
+
+
+
+ Dashboard
+
+ /
+ Logs
+
+
+ ← Back to Dashboard
+
+
+
+
+ Activity Logs
+ Explore how messages are classified into insights
+
+
+ {contextBannerText && (
+
+ {contextBannerText}
+ {source === "dashboard" ? " (from Dashboard)" : ""}
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/website/app/components/logs/mockData.ts b/website/app/components/logs/mockData.ts
new file mode 100644
index 00000000..0e6cf6a3
--- /dev/null
+++ b/website/app/components/logs/mockData.ts
@@ -0,0 +1,101 @@
+export type LogCategory = "Work" | "Health" | "Relationships" | "Other";
+export type LogType = "Life Log" | "Ignored";
+export type LogsView = "daily" | "recent";
+export type DailyOption = "Feb 23" | "Feb 22" | "Feb 21";
+export type RecentCountOption = 10 | 20 | 50;
+
+export interface LogEntry {
+ id: string;
+ time: string;
+ text: string;
+ categories: LogCategory[];
+ type: LogType;
+}
+
+export interface ClassificationExample {
+ id: string;
+ rawMessage: string;
+ extractedCategories: LogCategory[];
+ notes: string;
+}
+
+export const CATEGORY_STYLES: Record = {
+ Work: "bg-blue-500/15 text-blue-300 border border-blue-500/25",
+ Health: "bg-emerald-500/15 text-emerald-300 border border-emerald-500/25",
+ Relationships: "bg-amber-500/15 text-amber-300 border border-amber-500/25",
+ Other: "bg-slate-500/20 text-slate-300 border border-slate-500/25",
+};
+
+export const LOG_TYPE_STYLES: Record = {
+ "Life Log": "bg-violet-500/15 text-violet-300 border border-violet-500/25",
+ Ignored: "bg-rose-500/15 text-rose-300 border border-rose-500/25",
+};
+
+export const DAILY_OPTIONS: DailyOption[] = ["Feb 23", "Feb 22", "Feb 21"];
+export const RECENT_COUNT_OPTIONS: RecentCountOption[] = [10, 20, 50];
+
+export const MOCK_LOGS: LogEntry[] = [
+ {
+ id: "log-1",
+ time: "06:45",
+ text: "Morning workout",
+ categories: ["Health"],
+ type: "Life Log",
+ },
+ {
+ id: "log-2",
+ time: "08:30",
+ text: "Focused sprint for quarterly planning",
+ categories: ["Work"],
+ type: "Life Log",
+ },
+ {
+ id: "log-3",
+ time: "09:05",
+ text: "What's the weather today?",
+ categories: ["Other"],
+ type: "Ignored",
+ },
+ {
+ id: "log-4",
+ time: "12:20",
+ text: "Lunch with my sister",
+ categories: ["Relationships", "Health"],
+ type: "Life Log",
+ },
+ {
+ id: "log-5",
+ time: "15:10",
+ text: "Reminder to pay electricity bill",
+ categories: ["Other"],
+ type: "Ignored",
+ },
+ {
+ id: "log-6",
+ time: "19:40",
+ text: "Evening walk and podcast",
+ categories: ["Health"],
+ type: "Life Log",
+ },
+];
+
+export const MOCK_CLASSIFICATION_EXAMPLES: ClassificationExample[] = [
+ {
+ id: "example-1",
+ rawMessage: "Had standup, then did deep work, and called mom in the evening",
+ extractedCategories: ["Work", "Relationships"],
+ notes: "Multiple high-signal actions were grouped as meaningful life activity.",
+ },
+ {
+ id: "example-2",
+ rawMessage: "Can you check if it will rain tomorrow?",
+ extractedCategories: ["Other"],
+ notes: "Utility query with low journaling value, marked as non-log.",
+ },
+ {
+ id: "example-3",
+ rawMessage: "Gym session before breakfast and a 20-minute walk after dinner",
+ extractedCategories: ["Health"],
+ notes: "Repeated behavior with clear wellbeing relevance, stored as life log.",
+ },
+];
diff --git a/website/app/dashboard/page.tsx b/website/app/dashboard/page.tsx
index 9da17312..e15e943f 100644
--- a/website/app/dashboard/page.tsx
+++ b/website/app/dashboard/page.tsx
@@ -6,7 +6,6 @@ import Link from "next/link";
import TodayOverview from "@/app/components/dashboard/TodayOverview";
import HabitHeatmap from "@/app/components/dashboard/HabitHeatmap";
import GoalsSection from "@/app/components/dashboard/GoalsSection";
-import ActivityLogsSection from "@/app/components/dashboard/ActivityLogsSection";
import { useState, useRef, useEffect, useCallback } from "react";
interface WhatsAppSession {
@@ -660,8 +659,19 @@ export default function DashboardPage() {
{/* Goals & Progress */}
- {/* Activity Logs & Classification */}
-
+ {/* Activity Logs entry point */}
+
+
+
Activity Logs
+
Jump from your overview into detailed logs exploration
+
+
+ View All Logs ->
+
+
{/* Main Content */}
diff --git a/website/app/logs/page.tsx b/website/app/logs/page.tsx
new file mode 100644
index 00000000..b1b80944
--- /dev/null
+++ b/website/app/logs/page.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import LogsPage from "@/app/components/logs/LogsPage";
+
+export default function LogsRoutePage() {
+ return
;
+}
From ba968f0ca7083ba403e2daa2380f738502ef0263 Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Tue, 24 Feb 2026 14:43:36 +0500
Subject: [PATCH 11/78] Polish nav UI: better arrow on View All Logs, remove
redundant Back to Dashboard link
---
website/app/components/dashboard/TodayOverview.tsx | 8 ++++++--
website/app/components/logs/LogsPage.tsx | 5 +----
website/app/dashboard/page.tsx | 8 ++++++--
3 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/website/app/components/dashboard/TodayOverview.tsx b/website/app/components/dashboard/TodayOverview.tsx
index a72bbba4..e1ab6356 100644
--- a/website/app/components/dashboard/TodayOverview.tsx
+++ b/website/app/components/dashboard/TodayOverview.tsx
@@ -42,9 +42,13 @@ export default function TodayOverview() {
- View All Logs ->
+ View All Logs
+
+
+
+
diff --git a/website/app/components/logs/LogsPage.tsx b/website/app/components/logs/LogsPage.tsx
index b59e3464..6ed9f773 100644
--- a/website/app/components/logs/LogsPage.tsx
+++ b/website/app/components/logs/LogsPage.tsx
@@ -43,7 +43,7 @@ export default function LogsPage() {
return (
-
+
Dashboard
@@ -51,9 +51,6 @@ export default function LogsPage() {
/
Logs
-
- ← Back to Dashboard
-
diff --git a/website/app/dashboard/page.tsx b/website/app/dashboard/page.tsx
index e15e943f..40f227e4 100644
--- a/website/app/dashboard/page.tsx
+++ b/website/app/dashboard/page.tsx
@@ -667,9 +667,13 @@ export default function DashboardPage() {
- View All Logs ->
+ View All Logs
+
+
+
+
From b14b1a0286ba58770730a6b3cf4dc01caee4f610 Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Tue, 24 Feb 2026 14:55:24 +0500
Subject: [PATCH 12/78] Comment out Goals sort toggle and Needs attention
summary
---
website/app/components/dashboard/GoalsSection.tsx | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/website/app/components/dashboard/GoalsSection.tsx b/website/app/components/dashboard/GoalsSection.tsx
index 0b2ec39e..d8939879 100644
--- a/website/app/components/dashboard/GoalsSection.tsx
+++ b/website/app/components/dashboard/GoalsSection.tsx
@@ -73,8 +73,8 @@ const MOCK_GOALS: Goal[] = [
type SortMode = "streak" | "needs-work";
const SORT_OPTIONS: { id: SortMode; label: string }[] = [
- { id: "streak", label: "🔥 Top Streaks" },
- { id: "needs-work", label: "⚠️ Needs Work" },
+ // { id: "streak", label: "🔥 Top Streaks" },
+ // { id: "needs-work", label: "⚠️ Needs Work" },
];
function sortGoals(goals: Goal[], mode: SortMode): Goal[] {
@@ -114,7 +114,7 @@ export default function GoalsSection() {
{MOCK_GOALS.length} active goals this month
- {/* Sort toggle */}
+ {/* Sort toggle – commented out for now
{SORT_OPTIONS.map((opt) => (
))}
+ */}
{/* Goal cards grid */}
@@ -163,6 +164,7 @@ export default function GoalsSection() {
days
+ {/* Needs attention – commented out for now
{stats.struggling > 0 && (
Needs attention
@@ -172,6 +174,7 @@ export default function GoalsSection() {
)}
+ */}
last 7 days shown per goal
From 122e6860ec1a5fa85b3306985b6b63f0b3b9f0ad Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Tue, 24 Feb 2026 15:15:01 +0500
Subject: [PATCH 13/78] Remove Duration label from AI Extraction concept tags
---
website/app/components/dashboard/AIProcessingCard.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/website/app/components/dashboard/AIProcessingCard.tsx b/website/app/components/dashboard/AIProcessingCard.tsx
index 5a27f0d4..31cfd2c1 100644
--- a/website/app/components/dashboard/AIProcessingCard.tsx
+++ b/website/app/components/dashboard/AIProcessingCard.tsx
@@ -15,7 +15,6 @@ const CONCEPT_TAGS = [
{ label: "Health", style: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20" },
{ label: "Work", style: "bg-blue-500/10 text-blue-400 border-blue-500/20" },
{ label: "Relationships", style: "bg-amber-500/10 text-amber-400 border-amber-500/20" },
- { label: "Duration", style: "bg-violet-500/10 text-violet-400 border-violet-500/20" },
];
// ─── AIProcessingCard ─────────────────────────────────────────────────────────
From c9740dc725ce0fab52a2aaf9b03b646118dc2d5d Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Tue, 24 Feb 2026 19:37:35 +0500
Subject: [PATCH 14/78] fix: resolve eslint errors and warnings in dashboard
and logs components
---
website/app/components/dashboard/AIPipeline.tsx | 10 +++++-----
.../app/components/dashboard/AnimatedProgressBar.tsx | 2 +-
website/app/components/dashboard/GoalsSection.tsx | 7 +------
website/app/components/logs/LogsPage.tsx | 2 +-
4 files changed, 8 insertions(+), 13 deletions(-)
diff --git a/website/app/components/dashboard/AIPipeline.tsx b/website/app/components/dashboard/AIPipeline.tsx
index d5f38c57..438b2fdc 100644
--- a/website/app/components/dashboard/AIPipeline.tsx
+++ b/website/app/components/dashboard/AIPipeline.tsx
@@ -65,16 +65,16 @@ export default function AIPipeline({
const [activeMappingStep, setActiveMappingStep] = useState(0);
useEffect(() => {
- if (!isActive) {
- setActiveMappingStep(0);
- return;
- }
+ if (!isActive) return;
const id = setInterval(() => {
setActiveMappingStep((prev) => (prev + 1) % 4);
}, 1800);
- return () => clearInterval(id);
+ return () => {
+ clearInterval(id);
+ setActiveMappingStep(0);
+ };
}, [isActive]);
return (
diff --git a/website/app/components/dashboard/AnimatedProgressBar.tsx b/website/app/components/dashboard/AnimatedProgressBar.tsx
index 600c4fda..9b42d336 100644
--- a/website/app/components/dashboard/AnimatedProgressBar.tsx
+++ b/website/app/components/dashboard/AnimatedProgressBar.tsx
@@ -21,7 +21,7 @@ export default function AnimatedProgressBar({ activeMappingStep }: AnimatedProgr
/>
- "5h deep work" mapped to today's work target
+ “5h deep work” mapped to today's work target
);
}
diff --git a/website/app/components/dashboard/GoalsSection.tsx b/website/app/components/dashboard/GoalsSection.tsx
index d8939879..58e0bedc 100644
--- a/website/app/components/dashboard/GoalsSection.tsx
+++ b/website/app/components/dashboard/GoalsSection.tsx
@@ -72,11 +72,6 @@ const MOCK_GOALS: Goal[] = [
type SortMode = "streak" | "needs-work";
-const SORT_OPTIONS: { id: SortMode; label: string }[] = [
- // { id: "streak", label: "🔥 Top Streaks" },
- // { id: "needs-work", label: "⚠️ Needs Work" },
-];
-
function sortGoals(goals: Goal[], mode: SortMode): Goal[] {
return [...goals].sort((a, b) =>
mode === "streak"
@@ -100,7 +95,7 @@ function computeStats(goals: Goal[]) {
// ─── GoalsSection ─────────────────────────────────────────────────────────────
export default function GoalsSection() {
- const [sortMode, setSortMode] = useState("streak");
+ const [sortMode] = useState("streak");
const sorted = useMemo(() => sortGoals(MOCK_GOALS, sortMode), [sortMode]);
const stats = useMemo(() => computeStats(MOCK_GOALS), []);
diff --git a/website/app/components/logs/LogsPage.tsx b/website/app/components/logs/LogsPage.tsx
index 6ed9f773..74437a14 100644
--- a/website/app/components/logs/LogsPage.tsx
+++ b/website/app/components/logs/LogsPage.tsx
@@ -28,7 +28,7 @@ export default function LogsPage() {
return MOCK_LOGS.slice(0, selectedRecentCount);
}
return MOCK_LOGS;
- }, [view, selectedRecentCount, selectedDay]);
+ }, [view, selectedRecentCount]);
const contextBannerText = useMemo(() => {
if (selectedDate) {
From 8b7ecc34a0ccea3409cca7cdd0767788e8528877 Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Wed, 25 Feb 2026 08:26:27 +0500
Subject: [PATCH 15/78] Wrap logs page in Suspense for useSearchParams
---
website/app/logs/page.tsx | 7 ++++++-
website/next-env.d.ts | 2 +-
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/website/app/logs/page.tsx b/website/app/logs/page.tsx
index b1b80944..f16286c1 100644
--- a/website/app/logs/page.tsx
+++ b/website/app/logs/page.tsx
@@ -1,7 +1,12 @@
"use client";
+import { Suspense } from "react";
import LogsPage from "@/app/components/logs/LogsPage";
export default function LogsRoutePage() {
- return ;
+ return (
+ Loading...}>
+
+
+ );
}
diff --git a/website/next-env.d.ts b/website/next-env.d.ts
index c4b7818f..9edff1c7 100644
--- a/website/next-env.d.ts
+++ b/website/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/dev/types/routes.d.ts";
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
From ad3aad61edf5f549883f091371cdfad00e94d7ed Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Wed, 25 Feb 2026 08:56:52 +0500
Subject: [PATCH 16/78] Layout: make Month Snapshot card fill horizontal space
and match calendar height
---
website/app/components/dashboard/HabitHeatmap.tsx | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/website/app/components/dashboard/HabitHeatmap.tsx b/website/app/components/dashboard/HabitHeatmap.tsx
index 2d092178..2913a923 100644
--- a/website/app/components/dashboard/HabitHeatmap.tsx
+++ b/website/app/components/dashboard/HabitHeatmap.tsx
@@ -101,8 +101,8 @@ function StatsPanel({
currentStreak,
}: StatsPanelProps) {
return (
-
-
+
+
Month Snapshot
@@ -130,7 +130,7 @@ function StatsPanel({
-
+
Insights
@@ -139,7 +139,7 @@ function StatsPanel({
-
+
Less
@@ -310,7 +310,7 @@ export default function HabitHeatmap({
{/* Grid */}
-
+
Less
@@ -415,7 +415,7 @@ export default function HabitHeatmap({
-
+
Date: Wed, 25 Feb 2026 11:46:39 +0500
Subject: [PATCH 17/78] Add /stats page with mock analytics, charts, and event
inspect modal
---
website/app/stats/page.tsx | 281 ++++++++++++++++
website/components/stats/AreaChart.tsx | 91 ++++++
website/components/stats/ChartControls.tsx | 65 ++++
.../components/stats/DistributionChart.tsx | 39 +++
website/components/stats/KPICard.tsx | 40 +++
website/components/stats/Sparkline.tsx | 34 ++
website/components/stats/TimeSeriesChart.tsx | 149 +++++++++
website/data/mock-stats.ts | 111 +++++++
website/package.json | 1 +
website/pnpm-lock.yaml | 304 ++++++++++++++++++
10 files changed, 1115 insertions(+)
create mode 100644 website/app/stats/page.tsx
create mode 100644 website/components/stats/AreaChart.tsx
create mode 100644 website/components/stats/ChartControls.tsx
create mode 100644 website/components/stats/DistributionChart.tsx
create mode 100644 website/components/stats/KPICard.tsx
create mode 100644 website/components/stats/Sparkline.tsx
create mode 100644 website/components/stats/TimeSeriesChart.tsx
create mode 100644 website/data/mock-stats.ts
diff --git a/website/app/stats/page.tsx b/website/app/stats/page.tsx
new file mode 100644
index 00000000..9a8780fd
--- /dev/null
+++ b/website/app/stats/page.tsx
@@ -0,0 +1,281 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import KPICard from "@/components/stats/KPICard";
+import TimeSeriesChart, { type SeriesConfig, type SeriesKey } from "@/components/stats/TimeSeriesChart";
+import AreaChart from "@/components/stats/AreaChart";
+import DistributionChart, { type HistogramBucket } from "@/components/stats/DistributionChart";
+import ChartControls from "@/components/stats/ChartControls";
+import {
+ mockDailyStats,
+ mockSessionLengths,
+ mockTopEvents,
+ RANGE_OPTIONS,
+ type DateRange,
+ type TopEvent,
+} from "@/data/mock-stats";
+
+const SERIES_CONFIG: SeriesConfig[] = [
+ { key: "total", label: "Total", color: "#22d3ee" },
+ { key: "work", label: "Work", color: "#60a5fa" },
+ { key: "health", label: "Health", color: "#34d399" },
+ { key: "relationships", label: "Relationships", color: "#f59e0b" },
+];
+
+function average(values: number[]): number {
+ if (values.length === 0) return 0;
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
+}
+
+function median(values: number[]): number {
+ if (values.length === 0) return 0;
+ const sorted = [...values].sort((a, b) => a - b);
+ const mid = Math.floor(sorted.length / 2);
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
+}
+
+function changePercent(current: number, previous: number): number {
+ if (previous === 0) return 0;
+ return ((current - previous) / previous) * 100;
+}
+
+function buildHistogram(values: number[]): HistogramBucket[] {
+ const step = 10;
+ const max = 100;
+ const buckets: HistogramBucket[] = [];
+
+ for (let start = 0; start < max; start += step) {
+ const end = start + step;
+ const count = values.filter((value) => value >= start && value < end).length;
+ buckets.push({ range: `${start}-${end}`, count });
+ }
+
+ return buckets;
+}
+
+function formatImportanceColor(importance: TopEvent["importance"]): string {
+ if (importance === "Critical") return "bg-rose-500/15 text-rose-300";
+ if (importance === "High") return "bg-orange-500/15 text-orange-300";
+ if (importance === "Medium") return "bg-amber-500/15 text-amber-300";
+ return "bg-slate-700/70 text-slate-300";
+}
+
+export default function StatsPage() {
+ const [range, setRange] = useState(30);
+ const [smoothing, setSmoothing] = useState(true);
+ const [hiddenSeries, setHiddenSeries] = useState>>({});
+ const [selectedEvent, setSelectedEvent] = useState(null);
+
+ const filteredData = useMemo(() => mockDailyStats.slice(-range), [range]);
+ const previousPeriodData = useMemo(() => mockDailyStats.slice(-range * 2, -range), [range]);
+
+ const visibleSessionLengths = useMemo(() => {
+ const approximateSamplesPerDay = 6;
+ return mockSessionLengths.slice(-range * approximateSamplesPerDay);
+ }, [range]);
+
+ const previousSessionLengths = useMemo(() => {
+ const approximateSamplesPerDay = 6;
+ return mockSessionLengths.slice(-range * approximateSamplesPerDay * 2, -range * approximateSamplesPerDay);
+ }, [range]);
+
+ const histogramData = useMemo(() => buildHistogram(visibleSessionLengths), [visibleSessionLengths]);
+
+ const kpis = useMemo(() => {
+ const currentTotals = filteredData.map((d) => d.total);
+ const previousTotals = previousPeriodData.map((d) => d.total);
+
+ const avgDaily = average(currentTotals);
+ const previousAvgDaily = average(previousTotals);
+
+ const totalActivities = currentTotals.reduce((sum, value) => sum + value, 0);
+ const previousTotalActivities = previousTotals.reduce((sum, value) => sum + value, 0);
+
+ const activeDays = filteredData.filter((d) => d.total > 0).length;
+ const previousActiveDays = previousPeriodData.filter((d) => d.total > 0).length;
+
+ const medianSession = median(visibleSessionLengths);
+ const previousMedianSession = median(previousSessionLengths);
+
+ return {
+ avgDaily,
+ avgDailyChange: changePercent(avgDaily, previousAvgDaily),
+ totalActivities,
+ totalActivitiesChange: changePercent(totalActivities, previousTotalActivities),
+ activeDays,
+ activeDaysChange: changePercent(activeDays, previousActiveDays),
+ medianSession,
+ medianSessionChange: changePercent(medianSession, previousMedianSession),
+ };
+ }, [filteredData, previousPeriodData, previousSessionLengths, visibleSessionLengths]);
+
+ const sparklineTotals = useMemo(() => filteredData.slice(-24).map((d) => d.total), [filteredData]);
+
+ const events = useMemo(() => mockTopEvents.filter((event) => event.date >= filteredData[0]?.date), [filteredData]);
+
+ const toggleSeries = (key: SeriesKey) => {
+ setHiddenSeries((current) => ({
+ ...current,
+ [key]: !current[key],
+ }));
+ };
+
+ const handleExportCsv = () => {
+ const visibleSeries = SERIES_CONFIG.filter((series) => !hiddenSeries[series.key]);
+ const header = ["date", ...visibleSeries.map((s) => s.key)].join(",");
+ const lines = filteredData.map((row) => {
+ const values = visibleSeries.map((s) => row[s.key]);
+ return [row.date, ...values].join(",");
+ });
+ const csvContent = [header, ...lines].join("\n");
+
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = `loglife-stats-last-${range}-days.csv`;
+ link.click();
+ URL.revokeObjectURL(url);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ d.work + d.health + d.relationships)}
+ />
+ (d.total > 0 ? 1 : 0))}
+ />
+
+
+
+
+
+
+
+
+
+
Top Events & Anomalies
+
Click inspect to open the raw event payload
+
+
{events.length} rows
+
+
+
+
+
+
+ Date
+ Time
+ Event
+ Category
+ Importance
+ Action
+
+
+
+ {events.map((event) => (
+
+ {event.date}
+ {event.time}
+ {event.text}
+ {event.category}
+
+
+ {event.importance}
+
+
+
+ setSelectedEvent(event)}
+ className="cursor-pointer rounded-md border border-slate-700 bg-slate-950 px-2.5 py-1 text-xs text-slate-200 transition-colors hover:bg-slate-800/70"
+ >
+ Inspect
+
+
+
+ ))}
+
+
+
+
+
+ {selectedEvent ? (
+
+
+
+
Event JSON
+ setSelectedEvent(null)}
+ className="cursor-pointer rounded-md px-2 py-1 text-xs text-slate-400 hover:bg-slate-800 hover:text-slate-200"
+ >
+ Close
+
+
+
+
+ {JSON.stringify(selectedEvent, null, 2)}
+
+
+
+
+ ) : null}
+
+
+ );
+}
diff --git a/website/components/stats/AreaChart.tsx b/website/components/stats/AreaChart.tsx
new file mode 100644
index 00000000..15f67498
--- /dev/null
+++ b/website/components/stats/AreaChart.tsx
@@ -0,0 +1,91 @@
+"use client";
+
+import type { StatsPoint } from "@/data/mock-stats";
+import {
+ Area,
+ AreaChart as RechartsAreaChart,
+ CartesianGrid,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+
+type AreaChartProps = {
+ data: StatsPoint[];
+ smoothing: boolean;
+};
+
+function formatShortDate(value: string): string {
+ const date = new Date(`${value}T00:00:00`);
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
+}
+
+function tooltipLabelFormatter(label: unknown): string {
+ if (typeof label !== "string") return "";
+ return formatShortDate(label);
+}
+
+export default function AreaChart({ data, smoothing }: AreaChartProps) {
+ return (
+
+ Category Breakdown
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/website/components/stats/ChartControls.tsx b/website/components/stats/ChartControls.tsx
new file mode 100644
index 00000000..4204c589
--- /dev/null
+++ b/website/components/stats/ChartControls.tsx
@@ -0,0 +1,65 @@
+"use client";
+
+import type { DateRange } from "@/data/mock-stats";
+
+type ChartControlsProps = {
+ range: DateRange;
+ ranges: readonly DateRange[];
+ smoothing: boolean;
+ onRangeChange: (range: DateRange) => void;
+ onSmoothingChange: (value: boolean) => void;
+ onExportCsv: () => void;
+};
+
+export default function ChartControls({
+ range,
+ ranges,
+ smoothing,
+ onRangeChange,
+ onSmoothingChange,
+ onExportCsv,
+}: ChartControlsProps) {
+ return (
+
+
+
Range
+
+ {ranges.map((value) => {
+ const active = value === range;
+ return (
+ onRangeChange(value)}
+ className={`cursor-pointer rounded-md px-2.5 py-1.5 text-xs font-medium transition-colors ${
+ active
+ ? "bg-emerald-500/20 text-emerald-300"
+ : "text-slate-400 hover:bg-slate-800/70 hover:text-slate-100"
+ }`}
+ >
+ Last {value}
+
+ );
+ })}
+ days
+
+
+
+
+ onSmoothingChange(event.target.checked)}
+ />
+ Smooth lines
+
+
+
+ Export CSV
+
+
+ );
+}
diff --git a/website/components/stats/DistributionChart.tsx b/website/components/stats/DistributionChart.tsx
new file mode 100644
index 00000000..f8df3f15
--- /dev/null
+++ b/website/components/stats/DistributionChart.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
+
+export type HistogramBucket = {
+ range: string;
+ count: number;
+};
+
+type DistributionChartProps = {
+ data: HistogramBucket[];
+};
+
+export default function DistributionChart({ data }: DistributionChartProps) {
+ return (
+
+ Session Length Distribution
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/website/components/stats/KPICard.tsx b/website/components/stats/KPICard.tsx
new file mode 100644
index 00000000..d5c44a46
--- /dev/null
+++ b/website/components/stats/KPICard.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import Sparkline from "@/components/stats/Sparkline";
+
+type KPICardProps = {
+ label: string;
+ value: string;
+ changePct: number;
+ helperText?: string;
+ sparklineValues?: number[];
+};
+
+export default function KPICard({
+ label,
+ value,
+ changePct,
+ helperText,
+ sparklineValues = [],
+}: KPICardProps) {
+ const positive = changePct >= 0;
+
+ return (
+
+
+
+
{label}
+
{value}
+
+ {positive ? "+" : ""}
+ {changePct.toFixed(1)}% vs previous period
+
+ {helperText ?
{helperText}
: null}
+
+ {sparklineValues.length > 0 ? (
+
+ ) : null}
+
+
+ );
+}
diff --git a/website/components/stats/Sparkline.tsx b/website/components/stats/Sparkline.tsx
new file mode 100644
index 00000000..5205677b
--- /dev/null
+++ b/website/components/stats/Sparkline.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import { Line, LineChart, ResponsiveContainer } from "recharts";
+
+type SparklineProps = {
+ values: number[];
+ color?: string;
+};
+
+type SparklinePoint = {
+ idx: number;
+ value: number;
+};
+
+export default function Sparkline({ values, color = "#34d399" }: SparklineProps) {
+ const data: SparklinePoint[] = values.map((value, idx) => ({ idx, value }));
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/website/components/stats/TimeSeriesChart.tsx b/website/components/stats/TimeSeriesChart.tsx
new file mode 100644
index 00000000..d699ba76
--- /dev/null
+++ b/website/components/stats/TimeSeriesChart.tsx
@@ -0,0 +1,149 @@
+"use client";
+
+import type { StatsPoint } from "@/data/mock-stats";
+import {
+ CartesianGrid,
+ Line,
+ LineChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+
+export type SeriesKey = "total" | "work" | "health" | "relationships";
+
+export type SeriesConfig = {
+ key: SeriesKey;
+ label: string;
+ color: string;
+};
+
+type TimeSeriesChartProps = {
+ data: StatsPoint[];
+ series: SeriesConfig[];
+ hiddenSeries: Partial>;
+ smoothing: boolean;
+ onToggleSeries: (key: SeriesKey) => void;
+};
+
+type TooltipPayload = {
+ payload?: StatsPoint;
+ dataKey?: string;
+ color?: string;
+ value?: number;
+ name?: string;
+};
+
+function formatShortDate(value: string): string {
+ const date = new Date(`${value}T00:00:00`);
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
+}
+
+function CustomTooltip({
+ active,
+ payload,
+ label,
+ data,
+}: {
+ active?: boolean;
+ payload?: TooltipPayload[];
+ label?: string;
+ data: StatsPoint[];
+}) {
+ if (!active || !payload || payload.length === 0 || !label) return null;
+
+ const currentIndex = data.findIndex((point) => point.date === label);
+ const previous = currentIndex > 0 ? data[currentIndex - 1] : null;
+
+ return (
+
+
{formatShortDate(label)}
+
+ {payload.map((item) => {
+ const dataKey = item.dataKey as SeriesKey;
+ const currentValue = Number(item.value ?? 0);
+ const previousValue = previous ? Number(previous[dataKey]) : null;
+ const changePct =
+ previousValue && previousValue !== 0
+ ? ((currentValue - previousValue) / previousValue) * 100
+ : null;
+
+ return (
+
+
+
+ {item.name}
+
+ {currentValue.toLocaleString()}
+ = 0 ? "text-emerald-400" : "text-rose-400"}`}>
+ {changePct == null ? "-" : `${changePct >= 0 ? "+" : ""}${changePct.toFixed(1)}%`}
+
+
+ );
+ })}
+
+
+ );
+}
+
+export default function TimeSeriesChart({
+ data,
+ series,
+ hiddenSeries,
+ smoothing,
+ onToggleSeries,
+}: TimeSeriesChartProps) {
+ return (
+
+
+
Daily Activity Timeline
+
+ {series.map((item) => {
+ const hidden = Boolean(hiddenSeries[item.key]);
+ return (
+ onToggleSeries(item.key)}
+ className={`cursor-pointer rounded-md border px-2 py-1 text-xs transition-colors ${
+ hidden
+ ? "border-slate-700 bg-slate-900 text-slate-500"
+ : "border-slate-600 bg-slate-800/70 text-slate-100"
+ }`}
+ >
+
+ {item.label}
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+ } />
+ {series.map((item) => {
+ if (hiddenSeries[item.key]) return null;
+ return (
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/website/data/mock-stats.ts b/website/data/mock-stats.ts
new file mode 100644
index 00000000..da7b9f13
--- /dev/null
+++ b/website/data/mock-stats.ts
@@ -0,0 +1,111 @@
+export type StatsPoint = {
+ date: string;
+ total: number;
+ work: number;
+ health: number;
+ relationships: number;
+};
+
+export type TopEvent = {
+ id: string;
+ date: string;
+ time: string;
+ text: string;
+ category: "Work" | "Health" | "Relationships";
+ importance: "Low" | "Medium" | "High" | "Critical";
+};
+
+const DAY_COUNT = 180;
+const START_DATE = new Date("2025-09-01T00:00:00Z");
+
+function formatDate(date: Date): string {
+ return date.toISOString().slice(0, 10);
+}
+
+function clamp(value: number, min: number, max: number): number {
+ return Math.max(min, Math.min(max, value));
+}
+
+function pseudoNoise(index: number, seed: number): number {
+ return Math.sin(index * 0.37 + seed) * 0.5 + Math.cos(index * 0.17 + seed * 2.1) * 0.5;
+}
+
+export const mockDailyStats: StatsPoint[] = Array.from({ length: DAY_COUNT }, (_, i) => {
+ const date = new Date(START_DATE);
+ date.setUTCDate(START_DATE.getUTCDate() + i);
+
+ const weeklyWave = Math.sin((2 * Math.PI * i) / 7) * 14;
+ const monthlyWave = Math.sin((2 * Math.PI * i) / 30) * 26;
+ const trend = i * 0.42;
+ const baseline = 210 + trend + weeklyWave + monthlyWave;
+ const total = Math.round(clamp(baseline + pseudoNoise(i, 3.4) * 18, 120, 420));
+
+ const workRatio = clamp(0.47 + Math.sin(i / 13) * 0.07 + pseudoNoise(i, 1.2) * 0.03, 0.32, 0.62);
+ const healthRatio = clamp(0.24 + Math.cos(i / 11) * 0.05 + pseudoNoise(i, 2.1) * 0.02, 0.16, 0.38);
+ const relationshipsRatio = clamp(1 - workRatio - healthRatio, 0.12, 0.36);
+
+ let work = Math.round(total * workRatio);
+ let health = Math.round(total * healthRatio);
+ let relationships = Math.round(total * relationshipsRatio);
+
+ const diff = total - (work + health + relationships);
+ work += diff;
+
+ return {
+ date: formatDate(date),
+ total,
+ work,
+ health,
+ relationships,
+ };
+});
+
+export const mockSessionLengths: number[] = Array.from({ length: 1200 }, (_, i) => {
+ const base = 18 + (i % 7) * 2 + pseudoNoise(i, 5.7) * 11;
+ const occasionalLongSession = i % 37 === 0 ? 20 + (i % 50) : 0;
+ return Math.round(clamp(base + occasionalLongSession, 5, 95));
+});
+
+const EVENT_TEXT: Record = {
+ Work: [
+ "High-focus writing sprint completed",
+ "Strategy planning block exceeded target",
+ "Context switching spike detected",
+ "Late-night review session impacted recovery",
+ ],
+ Health: [
+ "Workout streak milestone reached",
+ "Sleep quality dip after travel day",
+ "Hydration target exceeded for 5 days",
+ "Morning run consistency improved",
+ ],
+ Relationships: [
+ "Quality time trend increased this week",
+ "Long call with family boosted mood score",
+ "Skipped social check-ins on busy days",
+ "Weekend gathering generated positive momentum",
+ ],
+};
+
+const CATEGORIES: TopEvent["category"][] = ["Work", "Health", "Relationships"];
+const IMPORTANCE: TopEvent["importance"][] = ["Low", "Medium", "High", "Critical"];
+
+export const mockTopEvents: TopEvent[] = Array.from({ length: 14 }, (_, i) => {
+ const dayOffset = DAY_COUNT - 3 - i * 5;
+ const date = new Date(START_DATE);
+ date.setUTCDate(START_DATE.getUTCDate() + dayOffset);
+ const category = CATEGORIES[i % CATEGORIES.length];
+ const textOptions = EVENT_TEXT[category];
+
+ return {
+ id: `evt-${1000 + i}`,
+ date: formatDate(date),
+ time: `${String((8 + (i * 3) % 12)).padStart(2, "0")}:${String((i * 7) % 60).padStart(2, "0")}`,
+ text: textOptions[i % textOptions.length],
+ category,
+ importance: IMPORTANCE[(i + (category === "Work" ? 1 : 0)) % IMPORTANCE.length],
+ };
+});
+
+export const RANGE_OPTIONS = [7, 30, 90, 180] as const;
+export type DateRange = (typeof RANGE_OPTIONS)[number];
diff --git a/website/package.json b/website/package.json
index d15adad6..770d1801 100644
--- a/website/package.json
+++ b/website/package.json
@@ -17,6 +17,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-markdown": "^10.1.0",
+ "recharts": "^3.7.0",
"web-vitals": "^5.1.0"
},
"devDependencies": {
diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml
index 5fc9f167..26bcb71c 100644
--- a/website/pnpm-lock.yaml
+++ b/website/pnpm-lock.yaml
@@ -29,6 +29,9 @@ importers:
react-markdown:
specifier: ^10.1.0
version: 10.1.0(@types/react@19.2.11)(react@19.1.0)
+ recharts:
+ specifier: ^3.7.0
+ version: 3.7.0(@types/react@19.2.11)(react-dom@19.1.0(react@19.1.0))(react-is@16.13.1)(react@19.1.0)(redux@5.0.1)
web-vitals:
specifier: ^5.1.0
version: 5.1.0
@@ -499,6 +502,17 @@ packages:
react: '>= 16.8'
react-dom: '>= 16.8'
+ '@reduxjs/toolkit@2.11.2':
+ resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==}
+ peerDependencies:
+ react: ^16.9.0 || ^17.0.0 || ^18 || ^19
+ react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-redux:
+ optional: true
+
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
@@ -532,6 +546,12 @@ packages:
'@stablelib/base64@1.0.1':
resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@standard-schema/utils@0.3.0':
+ resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@@ -630,6 +650,33 @@ packages:
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
+ '@types/d3-array@3.2.2':
+ resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
+
+ '@types/d3-color@3.1.3':
+ resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+ '@types/d3-ease@3.0.2':
+ resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+
+ '@types/d3-interpolate@3.0.4':
+ resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+ '@types/d3-path@3.1.1':
+ resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
+
+ '@types/d3-scale@4.0.9':
+ resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
+
+ '@types/d3-shape@3.1.8':
+ resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
+
+ '@types/d3-time@3.0.4':
+ resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
+
+ '@types/d3-timer@3.0.2':
+ resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -671,6 +718,9 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+ '@types/use-sync-external-store@0.0.6':
+ resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
+
'@typescript-eslint/eslint-plugin@8.54.0':
resolution: {integrity: sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -991,6 +1041,10 @@ packages:
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1017,6 +1071,50 @@ packages:
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+ d3-array@3.2.4:
+ resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+ engines: {node: '>=12'}
+
+ d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+
+ d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+
+ d3-format@3.1.2:
+ resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
+ engines: {node: '>=12'}
+
+ d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+
+ d3-path@3.1.0:
+ resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+ engines: {node: '>=12'}
+
+ d3-scale@4.0.2:
+ resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+ engines: {node: '>=12'}
+
+ d3-shape@3.2.0:
+ resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+ engines: {node: '>=12'}
+
+ d3-time-format@4.1.0:
+ resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+ engines: {node: '>=12'}
+
+ d3-time@3.1.0:
+ resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+ engines: {node: '>=12'}
+
+ d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+
damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -1049,6 +1147,9 @@ packages:
supports-color:
optional: true
+ decimal.js-light@2.5.1:
+ resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+
decode-named-character-reference@1.3.0:
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
@@ -1124,6 +1225,9 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
+ es-toolkit@1.44.0:
+ resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==}
+
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -1257,6 +1361,9 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
+ eventemitter3@5.0.4:
+ resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
+
events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
@@ -1424,6 +1531,12 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
+ immer@10.2.0:
+ resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
+
+ immer@11.1.4:
+ resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==}
+
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@@ -1439,6 +1552,10 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
+ internmap@2.0.3:
+ resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+ engines: {node: '>=12'}
+
is-alphabetical@2.0.1:
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
@@ -1993,10 +2110,38 @@ packages:
'@types/react': '>=18'
react: '>=18'
+ react-redux@9.2.0:
+ resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
+ peerDependencies:
+ '@types/react': ^18.2.25 || ^19
+ react: ^18.0 || ^19
+ redux: ^5.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ redux:
+ optional: true
+
react@19.1.0:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'}
+ recharts@3.7.0:
+ resolution: {integrity: sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ redux-thunk@3.1.0:
+ resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
+ peerDependencies:
+ redux: ^5.0.0
+
+ redux@5.0.1:
+ resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
+
reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
@@ -2011,6 +2156,9 @@ packages:
remark-rehype@11.1.2:
resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
+ reselect@5.1.1:
+ resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -2194,6 +2342,9 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
+ tiny-invariant@1.3.3:
+ resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
@@ -2293,6 +2444,9 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+ victory-vendor@37.3.6:
+ resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
+
web-vitals@5.1.0:
resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==}
@@ -2749,6 +2903,18 @@ snapshots:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
+ '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.11)(react@19.1.0)(redux@5.0.1))(react@19.1.0)':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@standard-schema/utils': 0.3.0
+ immer: 11.1.4
+ redux: 5.0.1
+ redux-thunk: 3.1.0(redux@5.0.1)
+ reselect: 5.1.1
+ optionalDependencies:
+ react: 19.1.0
+ react-redux: 9.2.0(@types/react@19.2.11)(react@19.1.0)(redux@5.0.1)
+
'@rtsao/scc@1.1.0': {}
'@rushstack/eslint-patch@1.15.0': {}
@@ -2783,6 +2949,10 @@ snapshots:
'@stablelib/base64@1.0.1': {}
+ '@standard-schema/spec@1.1.0': {}
+
+ '@standard-schema/utils@0.3.0': {}
+
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
@@ -2861,6 +3031,30 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@types/d3-array@3.2.2': {}
+
+ '@types/d3-color@3.1.3': {}
+
+ '@types/d3-ease@3.0.2': {}
+
+ '@types/d3-interpolate@3.0.4':
+ dependencies:
+ '@types/d3-color': 3.1.3
+
+ '@types/d3-path@3.1.1': {}
+
+ '@types/d3-scale@4.0.9':
+ dependencies:
+ '@types/d3-time': 3.0.4
+
+ '@types/d3-shape@3.1.8':
+ dependencies:
+ '@types/d3-path': 3.1.1
+
+ '@types/d3-time@3.0.4': {}
+
+ '@types/d3-timer@3.0.2': {}
+
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
@@ -2901,6 +3095,8 @@ snapshots:
'@types/unist@3.0.3': {}
+ '@types/use-sync-external-store@0.0.6': {}
+
'@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -3239,6 +3435,8 @@ snapshots:
client-only@0.0.1: {}
+ clsx@2.1.1: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -3261,6 +3459,44 @@ snapshots:
csstype@3.2.3: {}
+ d3-array@3.2.4:
+ dependencies:
+ internmap: 2.0.3
+
+ d3-color@3.1.0: {}
+
+ d3-ease@3.0.1: {}
+
+ d3-format@3.1.2: {}
+
+ d3-interpolate@3.0.1:
+ dependencies:
+ d3-color: 3.1.0
+
+ d3-path@3.1.0: {}
+
+ d3-scale@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+ d3-format: 3.1.2
+ d3-interpolate: 3.0.1
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+
+ d3-shape@3.2.0:
+ dependencies:
+ d3-path: 3.1.0
+
+ d3-time-format@4.1.0:
+ dependencies:
+ d3-time: 3.1.0
+
+ d3-time@3.1.0:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-timer@3.0.1: {}
+
damerau-levenshtein@1.0.8: {}
data-view-buffer@1.0.2:
@@ -3289,6 +3525,8 @@ snapshots:
dependencies:
ms: 2.1.3
+ decimal.js-light@2.5.1: {}
+
decode-named-character-reference@1.3.0:
dependencies:
character-entities: 2.0.2
@@ -3435,6 +3673,8 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
+ es-toolkit@1.44.0: {}
+
escalade@3.2.0: {}
escape-string-regexp@4.0.0: {}
@@ -3648,6 +3888,8 @@ snapshots:
esutils@2.0.3: {}
+ eventemitter3@5.0.4: {}
+
events@3.3.0: {}
extend@3.0.2: {}
@@ -3824,6 +4066,10 @@ snapshots:
ignore@7.0.5: {}
+ immer@10.2.0: {}
+
+ immer@11.1.4: {}
+
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
@@ -3839,6 +4085,8 @@ snapshots:
hasown: 2.0.2
side-channel: 1.1.0
+ internmap@2.0.3: {}
+
is-alphabetical@2.0.1: {}
is-alphanumerical@2.0.1:
@@ -4516,8 +4764,43 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ react-redux@9.2.0(@types/react@19.2.11)(react@19.1.0)(redux@5.0.1):
+ dependencies:
+ '@types/use-sync-external-store': 0.0.6
+ react: 19.1.0
+ use-sync-external-store: 1.6.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.2.11
+ redux: 5.0.1
+
react@19.1.0: {}
+ recharts@3.7.0(@types/react@19.2.11)(react-dom@19.1.0(react@19.1.0))(react-is@16.13.1)(react@19.1.0)(redux@5.0.1):
+ dependencies:
+ '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.11)(react@19.1.0)(redux@5.0.1))(react@19.1.0)
+ clsx: 2.1.1
+ decimal.js-light: 2.5.1
+ es-toolkit: 1.44.0
+ eventemitter3: 5.0.4
+ immer: 10.2.0
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ react-is: 16.13.1
+ react-redux: 9.2.0(@types/react@19.2.11)(react@19.1.0)(redux@5.0.1)
+ reselect: 5.1.1
+ tiny-invariant: 1.3.3
+ use-sync-external-store: 1.6.0(react@19.1.0)
+ victory-vendor: 37.3.6
+ transitivePeerDependencies:
+ - '@types/react'
+ - redux
+
+ redux-thunk@3.1.0(redux@5.0.1):
+ dependencies:
+ redux: 5.0.1
+
+ redux@5.0.1: {}
+
reflect.getprototypeof@1.0.10:
dependencies:
call-bind: 1.0.8
@@ -4555,6 +4838,8 @@ snapshots:
unified: 11.0.5
vfile: 6.0.3
+ reselect@5.1.1: {}
+
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@@ -4800,6 +5085,8 @@ snapshots:
tapable@2.3.0: {}
+ tiny-invariant@1.3.3: {}
+
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
@@ -4955,6 +5242,23 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
+ victory-vendor@37.3.6:
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/d3-ease': 3.0.2
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-scale': 4.0.9
+ '@types/d3-shape': 3.1.8
+ '@types/d3-time': 3.0.4
+ '@types/d3-timer': 3.0.2
+ d3-array: 3.2.4
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-scale: 4.0.2
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-timer: 3.0.1
+
web-vitals@5.1.0: {}
which-boxed-primitive@1.1.1:
From 02e0e2736d1faf5e292b7e845294b0bdebef40cc Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Wed, 25 Feb 2026 16:37:29 +0500
Subject: [PATCH 18/78] Add dual dashboard modes: new compact view and legacy
classic view
---
.../dashboard/ActivityLogsSection.tsx | 8 +-
.../app/components/dashboard/DonutChart.tsx | 49 +-
.../app/components/dashboard/GoalsSection.tsx | 8 +-
.../app/components/dashboard/HabitHeatmap.tsx | 12 +-
.../components/dashboard/TodayOverview.tsx | 12 +-
.../dashboard/legacy/LegacyDonutChart.tsx | 149 ++++
.../dashboard/legacy/LegacyGoalsSection.tsx | 138 ++++
.../dashboard/legacy/LegacyHabitHeatmap.tsx | 413 ++++++++++
.../dashboard/legacy/LegacyTodayOverview.tsx | 87 ++
website/app/dashboard/page.tsx | 779 ++++++++++--------
website/app/globals.css | 1 +
11 files changed, 1278 insertions(+), 378 deletions(-)
create mode 100644 website/app/components/dashboard/legacy/LegacyDonutChart.tsx
create mode 100644 website/app/components/dashboard/legacy/LegacyGoalsSection.tsx
create mode 100644 website/app/components/dashboard/legacy/LegacyHabitHeatmap.tsx
create mode 100644 website/app/components/dashboard/legacy/LegacyTodayOverview.tsx
diff --git a/website/app/components/dashboard/ActivityLogsSection.tsx b/website/app/components/dashboard/ActivityLogsSection.tsx
index 3ebe2937..7e4c2ac6 100644
--- a/website/app/components/dashboard/ActivityLogsSection.tsx
+++ b/website/app/components/dashboard/ActivityLogsSection.tsx
@@ -41,9 +41,9 @@ function PipelineStep({
export default function ActivityLogsSection() {
return (
-
+
{/* Header */}
-
+
Activity Logs
How your messages become insights
@@ -54,7 +54,7 @@ export default function ActivityLogsSection() {
{/* Pipeline indicator */}
-
+
@@ -91,7 +91,7 @@ export default function ActivityLogsSection() {
{/* Body: 2-column — Classification | Log timeline */}
-
+
diff --git a/website/app/components/dashboard/DonutChart.tsx b/website/app/components/dashboard/DonutChart.tsx
index b6f63ef5..138c0981 100644
--- a/website/app/components/dashboard/DonutChart.tsx
+++ b/website/app/components/dashboard/DonutChart.tsx
@@ -14,11 +14,18 @@ interface DonutChartProps {
size?: number;
strokeWidth?: number;
getCategoryHref?: (label: string) => string;
+ legendBelow?: boolean;
}
const SEGMENT_GAP = 4;
-export default function DonutChart({ data, size = 216, strokeWidth = 20, getCategoryHref }: DonutChartProps) {
+export default function DonutChart({
+ data,
+ size = 216,
+ strokeWidth = 20,
+ getCategoryHref,
+ legendBelow = false,
+}: DonutChartProps) {
const [hovered, setHovered] = useState
(null);
const router = useRouter();
@@ -45,8 +52,14 @@ export default function DonutChart({ data, size = 216, strokeWidth = 20, getCate
};
return (
-
-
+
+
{/* Background track */}
{/* Legend */}
-
+
{segments.map((seg) => (
setHovered(null)}
onClick={() => handleCategoryClick(seg.label)}
>
-
-
+
+
-
+
{seg.label}
-
- {seg.value}%
-
+ {!legendBelow && (
+
+ {seg.value}%
+
+ )}
{/* Mini progress bar */}
@@ -143,6 +158,14 @@ export default function DonutChart({ data, size = 216, strokeWidth = 20, getCate
}}
/>
+ {legendBelow && (
+
+ {seg.value}%
+
+ )}
))}
diff --git a/website/app/components/dashboard/GoalsSection.tsx b/website/app/components/dashboard/GoalsSection.tsx
index 58e0bedc..9d45c4a9 100644
--- a/website/app/components/dashboard/GoalsSection.tsx
+++ b/website/app/components/dashboard/GoalsSection.tsx
@@ -101,9 +101,9 @@ export default function GoalsSection() {
const stats = useMemo(() => computeStats(MOCK_GOALS), []);
return (
-
+
{/* Header */}
-
+
Goals & Progress
{MOCK_GOALS.length} active goals this month
@@ -129,14 +129,14 @@ export default function GoalsSection() {
{/* Goal cards grid */}
-
+
{sorted.map((goal) => (
))}
{/* Summary footer */}
-
+
On streak
diff --git a/website/app/components/dashboard/HabitHeatmap.tsx b/website/app/components/dashboard/HabitHeatmap.tsx
index 2913a923..42359c67 100644
--- a/website/app/components/dashboard/HabitHeatmap.tsx
+++ b/website/app/components/dashboard/HabitHeatmap.tsx
@@ -299,9 +299,9 @@ export default function HabitHeatmap({
}
return (
-
+
{/* Header */}
-
+
Monthly Habit Overview
{monthLabel}
@@ -309,9 +309,9 @@ export default function HabitHeatmap({
{/* Grid */}
-
-
-
+
+
+
Less
@@ -415,7 +415,7 @@ export default function HabitHeatmap({
-
+
+
{/* Card header */}
-
+
Today's Overview
{today}
@@ -54,18 +54,20 @@ export default function TodayOverview() {
{/* Card body */}
-
+
{/* Left: Donut chart */}
-
+
Time Distribution
`/logs?category=${encodeURIComponent(label.toLowerCase())}&from=dashboard`}
/>
{/* Right: Activity list */}
-
+
Today's Activities
string;
+}
+
+const SEGMENT_GAP = 4;
+
+export default function LegacyDonutChart({
+ data,
+ size = 216,
+ strokeWidth = 20,
+ getCategoryHref,
+}: LegacyDonutChartProps) {
+ const [hovered, setHovered] = useState(null);
+ const router = useRouter();
+
+ const center = size / 2;
+ const radius = (size - strokeWidth) / 2;
+ const circumference = 2 * Math.PI * radius;
+ const total = data.reduce((sum, d) => sum + d.value, 0);
+
+ const segments = data.reduce>(
+ (acc, item) => {
+ const percent = item.value / total;
+ const dashLength = Math.max(0, percent * circumference - SEGMENT_GAP);
+ const prevAngle = acc.length > 0 ? acc[acc.length - 1].rotation + acc[acc.length - 1].percent * 360 : -90;
+ acc.push({ ...item, dashLength, rotation: prevAngle, percent });
+ return acc;
+ },
+ [],
+ );
+
+ const activeSegment = hovered ? segments.find((s) => s.label === hovered) : null;
+
+ const handleCategoryClick = (label: string) => {
+ router.push(getCategoryHref ? getCategoryHref(label) : `/logs?category=${encodeURIComponent(label.toLowerCase())}`);
+ };
+
+ return (
+
+
+
+
+ {segments.map((seg) => (
+ setHovered(seg.label)}
+ onMouseLeave={() => setHovered(null)}
+ onClick={() => handleCategoryClick(seg.label)}
+ />
+ ))}
+
+
+
+ {activeSegment ? (
+ <>
+
+ {activeSegment.value}%
+
+ {activeSegment.label}
+ >
+ ) : (
+ <>
+ Today
+ {data.length} areas
+ >
+ )}
+
+
+
+
+ {segments.map((seg) => (
+
setHovered(seg.label)}
+ onMouseLeave={() => setHovered(null)}
+ onClick={() => handleCategoryClick(seg.label)}
+ >
+
+
+
+
+ {seg.label}
+
+
+
+ {seg.value}%
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/website/app/components/dashboard/legacy/LegacyGoalsSection.tsx b/website/app/components/dashboard/legacy/LegacyGoalsSection.tsx
new file mode 100644
index 00000000..a56d2d8d
--- /dev/null
+++ b/website/app/components/dashboard/legacy/LegacyGoalsSection.tsx
@@ -0,0 +1,138 @@
+"use client";
+
+import { useState, useMemo } from "react";
+import GoalCard, { Goal } from "../GoalCard";
+
+const MOCK_GOALS: Goal[] = [
+ {
+ id: "1",
+ name: "Go to gym",
+ category: "Health",
+ streak: 5,
+ completionRate: 80,
+ frequency: "3x per week",
+ icon: "🏋️",
+ recentDays: [true, true, false, true, true, true, true],
+ },
+ {
+ id: "2",
+ name: "Deep work sessions",
+ category: "Work",
+ streak: 3,
+ completionRate: 60,
+ frequency: "Daily",
+ icon: "💻",
+ recentDays: [true, false, true, false, true, true, true],
+ },
+ {
+ id: "3",
+ name: "Read before bed",
+ category: "Health",
+ streak: 12,
+ completionRate: 92,
+ frequency: "Daily",
+ icon: "📚",
+ recentDays: [true, true, true, true, true, true, true],
+ },
+ {
+ id: "4",
+ name: "Call a friend or family",
+ category: "Relationships",
+ streak: 1,
+ completionRate: 35,
+ frequency: "2x per week",
+ icon: "📞",
+ recentDays: [false, false, false, false, true, false, false],
+ },
+ {
+ id: "5",
+ name: "Morning walk",
+ category: "Health",
+ streak: 0,
+ completionRate: 20,
+ frequency: "Daily",
+ icon: "🚶",
+ recentDays: [false, true, false, false, false, false, false],
+ },
+ {
+ id: "6",
+ name: "Team check-ins",
+ category: "Work",
+ streak: 8,
+ completionRate: 95,
+ frequency: "Daily",
+ icon: "💼",
+ recentDays: [true, true, true, true, true, true, true],
+ },
+];
+
+type SortMode = "streak" | "needs-work";
+
+function sortGoals(goals: Goal[], mode: SortMode): Goal[] {
+ return [...goals].sort((a, b) =>
+ mode === "streak"
+ ? b.streak - a.streak
+ : a.completionRate - b.completionRate,
+ );
+}
+
+function computeStats(goals: Goal[]) {
+ const active = goals.filter((g) => g.streak > 0).length;
+ const avgCompletion = Math.round(
+ goals.reduce((s, g) => s + g.completionRate, 0) / goals.length,
+ );
+ const topStreak = Math.max(...goals.map((g) => g.streak));
+ const struggling = goals.filter((g) => g.completionRate < 40).length;
+ return { active, avgCompletion, topStreak, struggling };
+}
+
+export default function LegacyGoalsSection() {
+ const [sortMode] = useState("streak");
+
+ const sorted = useMemo(() => sortGoals(MOCK_GOALS, sortMode), [sortMode]);
+ const stats = useMemo(() => computeStats(MOCK_GOALS), []);
+
+ return (
+
+
+
+
Goals & Progress
+
{MOCK_GOALS.length} active goals this month
+
+
+
+
+ {sorted.map((goal) => (
+
+ ))}
+
+
+
+
+
+
On streak
+
+ {stats.active}
+ / {MOCK_GOALS.length}
+
+
+
+
Avg completion
+
+ {stats.avgCompletion}
+ %
+
+
+
+
Best streak
+
+ 🔥 {stats.topStreak}
+ days
+
+
+
+
last 7 days shown per goal
+
+
+ );
+}
diff --git a/website/app/components/dashboard/legacy/LegacyHabitHeatmap.tsx b/website/app/components/dashboard/legacy/LegacyHabitHeatmap.tsx
new file mode 100644
index 00000000..8d0c9bf6
--- /dev/null
+++ b/website/app/components/dashboard/legacy/LegacyHabitHeatmap.tsx
@@ -0,0 +1,413 @@
+"use client";
+import { useState } from "react";
+
+export interface HabitDay {
+ date: string;
+ value: number;
+}
+
+interface HabitHeatmapProps {
+ data?: HabitDay[];
+ month?: string;
+}
+
+interface StatsPanelProps {
+ activeDays: number;
+ daysInMonth: number;
+ avgPercent: number;
+ bestValue: number;
+ bestDayLabel: string;
+ currentStreak: number;
+}
+
+const MOCK_DATA: HabitDay[] = [
+ { date: "2026-01-01", value: 70 },
+ { date: "2026-01-02", value: 82 },
+ { date: "2026-01-03", value: 35 },
+ { date: "2026-01-04", value: 20 },
+ { date: "2026-01-05", value: 76 },
+ { date: "2026-01-06", value: 88 },
+ { date: "2026-01-07", value: 64 },
+ { date: "2026-01-08", value: 90 },
+ { date: "2026-01-09", value: 72 },
+ { date: "2026-01-10", value: 28 },
+ { date: "2026-01-11", value: 0 },
+ { date: "2026-01-12", value: 68 },
+ { date: "2026-01-13", value: 80 },
+ { date: "2026-01-14", value: 92 },
+ { date: "2026-01-15", value: 74 },
+ { date: "2026-01-16", value: 58 },
+ { date: "2026-01-17", value: 32 },
+ { date: "2026-01-18", value: 18 },
+ { date: "2026-01-19", value: 84 },
+ { date: "2026-01-20", value: 66 },
+ { date: "2026-01-21", value: 0 },
+ { date: "2026-01-22", value: 52 },
+ { date: "2026-01-23", value: 74 },
+ { date: "2026-01-24", value: 40 },
+ { date: "2026-01-25", value: 22 },
+ { date: "2026-01-26", value: 86 },
+ { date: "2026-01-27", value: 79 },
+ { date: "2026-01-28", value: 67 },
+ { date: "2026-01-29", value: 83 },
+ { date: "2026-01-30", value: 71 },
+ { date: "2026-01-31", value: 36 },
+];
+
+function getIntensityColor(value: number): string {
+ if (value === 0) return "rgba(39,39,42,0.82)";
+ if (value <= 25) return "rgba(34,197,94,0.24)";
+ if (value <= 50) return "rgba(34,197,94,0.4)";
+ if (value <= 75) return "rgba(34,197,94,0.56)";
+ return "rgba(34,197,94,0.7)";
+}
+
+function formatTooltipDate(dateStr: string): string {
+ return new Date(dateStr + "T00:00:00").toLocaleDateString("en-US", {
+ weekday: "short",
+ month: "short",
+ day: "numeric",
+ });
+}
+
+function formatTooltipValue(value: number): string {
+ return `${value}% productive`;
+}
+
+function formatShortMonthDay(dateStr: string): string {
+ return new Date(dateStr + "T00:00:00").toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ });
+}
+
+const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+const LEGEND_LEVELS = [0, 20, 45, 70, 95] as const;
+
+function StatsPanel({
+ activeDays,
+ daysInMonth,
+ avgPercent,
+ bestValue,
+ bestDayLabel,
+ currentStreak,
+}: StatsPanelProps) {
+ return (
+
+
+
+
Month Snapshot
+
+
+
Active days
+
+ {activeDays}
+ / {daysInMonth}
+
+
+
+
Average
+
+ {avgPercent}
+ %
+
+
+
+
Best
+
+ {bestValue}
+ %
+
+
+
+
+
+
+
+
+
Insights
+
Best day: {bestDayLabel} ({bestValue}%)
+
Current streak: {currentStreak} days
+
+
+
+
+
+
+
Less
+
+ {LEGEND_LEVELS.map((v) => (
+
+ ))}
+
+
More
+
+
+
+
+ );
+}
+
+export default function LegacyHabitHeatmap({
+ data = MOCK_DATA,
+ month = "2026-01",
+}: HabitHeatmapProps) {
+ const today = new Date().toISOString().split("T")[0];
+ const [hoveredStreakId, setHoveredStreakId] = useState(null);
+
+ const [year, monthNum] = month.split("-").map(Number);
+ const firstDay = new Date(year, monthNum - 1, 1);
+ const daysInMonth = new Date(year, monthNum, 0).getDate();
+ const startDayOfWeek = firstDay.getDay();
+
+ const monthLabel = firstDay.toLocaleDateString("en-US", {
+ month: "long",
+ year: "numeric",
+ });
+
+ const dataMap = new Map(data.map((d) => [d.date, d.value]));
+ const sortedData = [...data].sort((a, b) => a.date.localeCompare(b.date));
+ const activeDays = data.filter((d) => d.value > 0).length;
+ const avgPercent =
+ activeDays > 0
+ ? Math.round(data.filter((d) => d.value > 0).reduce((sum, d) => sum + d.value, 0) / activeDays)
+ : 0;
+ const bestDay =
+ sortedData.length > 0
+ ? sortedData.reduce((best, day) => (day.value > best.value ? day : best), sortedData[0])
+ : null;
+
+ const monthDays = Array.from({ length: daysInMonth }, (_, i) => {
+ const day = i + 1;
+ const date = `${year}-${String(monthNum).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
+ return { day, date, value: dataMap.get(date) ?? 0 };
+ });
+
+ type StreakMeta = {
+ streakId: number | null;
+ isStreak: boolean;
+ isCurrentStreak: boolean;
+ };
+
+ const streakMetaByDate = new Map();
+ monthDays.forEach((entry) =>
+ streakMetaByDate.set(entry.date, {
+ streakId: null,
+ isStreak: false,
+ isCurrentStreak: false,
+ }),
+ );
+
+ let streakCounter = 0;
+ let i = 0;
+ while (i < monthDays.length) {
+ if (monthDays[i].value <= 0) {
+ i += 1;
+ continue;
+ }
+ const start = i;
+ while (i < monthDays.length && monthDays[i].value > 0) i += 1;
+ const end = i - 1;
+ const runLength = end - start + 1;
+ const streakId = runLength >= 2 ? streakCounter++ : null;
+ for (let idx = start; idx <= end; idx++) {
+ streakMetaByDate.set(monthDays[idx].date, {
+ streakId,
+ isStreak: runLength >= 2,
+ isCurrentStreak: false,
+ });
+ }
+ }
+
+ let currentStreak = 0;
+ let currentEnd = monthDays.length - 1;
+ while (currentEnd >= 0 && monthDays[currentEnd].value <= 0) currentEnd -= 1;
+ if (currentEnd >= 0) {
+ let currentStart = currentEnd;
+ while (currentStart >= 0 && monthDays[currentStart].value > 0) currentStart -= 1;
+ currentStart += 1;
+ currentStreak = currentEnd - currentStart + 1;
+ for (let idx = currentStart; idx <= currentEnd; idx++) {
+ const prevMeta = streakMetaByDate.get(monthDays[idx].date) ?? {
+ streakId: null,
+ isStreak: false,
+ isCurrentStreak: false,
+ };
+ streakMetaByDate.set(monthDays[idx].date, {
+ ...prevMeta,
+ isCurrentStreak: true,
+ });
+ }
+ }
+
+ type Cell =
+ | { type: "empty"; key: string }
+ | {
+ type: "day";
+ day: number;
+ date: string;
+ value: number;
+ streakId: number | null;
+ isStreak: boolean;
+ isCurrentStreak: boolean;
+ };
+
+ const cells: Cell[] = [];
+
+ for (let j = 0; j < startDayOfWeek; j++) {
+ cells.push({ type: "empty", key: `pre-${j}` });
+ }
+
+ for (let d = 1; d <= daysInMonth; d++) {
+ const dateStr = `${year}-${String(monthNum).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
+ const streakMeta = streakMetaByDate.get(dateStr) ?? {
+ streakId: null,
+ isStreak: false,
+ isCurrentStreak: false,
+ };
+ cells.push({
+ type: "day",
+ day: d,
+ date: dateStr,
+ value: dataMap.get(dateStr) ?? 0,
+ streakId: streakMeta.streakId,
+ isStreak: streakMeta.isStreak,
+ isCurrentStreak: streakMeta.isCurrentStreak,
+ });
+ }
+
+ const remainder = cells.length % 7;
+ if (remainder !== 0) {
+ for (let j = 0; j < 7 - remainder; j++) {
+ cells.push({ type: "empty", key: `post-${j}` });
+ }
+ }
+
+ return (
+
+
+
+
Monthly Habit Overview
+
{monthLabel}
+
+
+
+
+
+
+
+
Less
+
+ {LEGEND_LEVELS.map((v) => (
+
+ ))}
+
+
More
+
+
+
+ {WEEKDAYS.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+ {cells.map((cell, idx) => {
+ if (cell.type === "empty") {
+ return (
+
+ );
+ }
+
+ const isToday = cell.date === today;
+ const isHoveredStreak = cell.streakId !== null && hoveredStreakId === cell.streakId;
+ const isInactive = cell.value <= 0;
+
+ return (
+
setHoveredStreakId(cell.streakId)}
+ onMouseLeave={() => setHoveredStreakId(null)}
+ >
+
+
+ {cell.day}
+
+ {cell.isStreak && (
+
+ )}
+
+
+
+
+
+ {formatTooltipDate(cell.date)}
+ {isToday && (
+ Today
+ )}
+
+
+ {formatTooltipValue(cell.value)}
+
+
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/website/app/components/dashboard/legacy/LegacyTodayOverview.tsx b/website/app/components/dashboard/legacy/LegacyTodayOverview.tsx
new file mode 100644
index 00000000..eb1acb14
--- /dev/null
+++ b/website/app/components/dashboard/legacy/LegacyTodayOverview.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import Link from "next/link";
+import ActivityList, { Activity } from "../ActivityList";
+import LegacyDonutChart, { LegacyCategoryData } from "./LegacyDonutChart";
+
+const MOCK_CATEGORIES: LegacyCategoryData[] = [
+ { label: "Work", value: 50, color: "#3b82f6" },
+ { label: "Health", value: 30, color: "#10b981" },
+ { label: "Relationships", value: 20, color: "#f59e0b" },
+];
+
+const MOCK_ACTIVITIES: Activity[] = [
+ { id: "1", title: "Morning workout", category: "Health", time: "7:00 AM", icon: "🏋️" },
+ { id: "2", title: "Team standup", category: "Work", time: "9:30 AM", icon: "💼" },
+ { id: "3", title: "Deep work session", category: "Work", time: "10:00 AM", icon: "💻" },
+ { id: "4", title: "Lunch with family", category: "Relationships", time: "1:00 PM", icon: "👨👩👧" },
+ { id: "5", title: "Project review", category: "Work", time: "3:00 PM", icon: "📊" },
+ { id: "6", title: "Evening run", category: "Health", time: "6:30 PM", icon: "🏃" },
+];
+
+export default function LegacyTodayOverview() {
+ const selectedDate = "2026-02-23";
+
+ const today = new Date().toLocaleDateString("en-US", {
+ weekday: "long",
+ month: "long",
+ day: "numeric",
+ });
+
+ return (
+
+
+
+
Today's Overview
+
{today}
+
+
+
+ Today
+
+
+ View All Logs
+
+
+
+
+
+
+
+
+
+
+
Time Distribution
+
`/logs?category=${encodeURIComponent(label.toLowerCase())}&from=dashboard`}
+ />
+
+
+
+
Today's Activities
+
+ `/logs?date=${selectedDate}&highlight=${encodeURIComponent(activity.title)}&category=${encodeURIComponent(
+ activity.category.toLowerCase(),
+ )}&from=dashboard`
+ }
+ />
+
+
+
+ );
+}
diff --git a/website/app/dashboard/page.tsx b/website/app/dashboard/page.tsx
index 40f227e4..444fec19 100644
--- a/website/app/dashboard/page.tsx
+++ b/website/app/dashboard/page.tsx
@@ -6,6 +6,9 @@ import Link from "next/link";
import TodayOverview from "@/app/components/dashboard/TodayOverview";
import HabitHeatmap from "@/app/components/dashboard/HabitHeatmap";
import GoalsSection from "@/app/components/dashboard/GoalsSection";
+import LegacyTodayOverview from "@/app/components/dashboard/legacy/LegacyTodayOverview";
+import LegacyHabitHeatmap from "@/app/components/dashboard/legacy/LegacyHabitHeatmap";
+import LegacyGoalsSection from "@/app/components/dashboard/legacy/LegacyGoalsSection";
import { useState, useRef, useEffect, useCallback } from "react";
interface WhatsAppSession {
@@ -78,6 +81,7 @@ export default function DashboardPage() {
const [session, setSession] = useState(null);
const [sessionLoading, setSessionLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
+ const [compactMode, setCompactMode] = useState(false);
const menuRef = useRef(null);
const [countryCode, setCountryCode] = useState("1");
@@ -256,89 +260,127 @@ export default function DashboardPage() {
}
};
- const countdownSeconds = pollUntil
- ? Math.max(0, Math.ceil((pollUntil - pollNow) / 1000))
- : 0;
+ const dashboardHeader = (
+
+
+
+
Dashboard
+
+ Welcome back, {user.firstName || user.emailAddresses[0]?.emailAddress}
+
+
+
fetchSession(true)}
+ disabled={refreshing || sessionLoading}
+ title="Refresh session data"
+ className="ml-1 mt-0.5 cursor-pointer rounded-lg p-2 text-slate-400 transition-all hover:bg-slate-800/50 hover:text-white disabled:cursor-not-allowed disabled:opacity-50"
+ >
+
+
+
+
+
- return (
-
-
- {/* Header */}
-
-
-
-
Dashboard
-
- Welcome back, {user.firstName || user.emailAddresses[0]?.emailAddress}
-
-
-
fetchSession(true)}
- disabled={refreshing || sessionLoading}
- title="Refresh session data"
- className="ml-1 mt-0.5 p-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/50 transition-all cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
- >
-
-
-
-
-
-
- {/* User Menu */}
-
-
setMenuOpen(!menuOpen)}
- className="relative w-9 h-9 rounded-full overflow-hidden bg-slate-700 flex items-center justify-center ring-2 ring-slate-700 hover:ring-slate-600 transition-all cursor-pointer"
- >
- {user.imageUrl ? (
-
- ) : (
-
- {user.firstName?.[0] || user.emailAddresses[0]?.emailAddress[0]?.toUpperCase()}
-
- )}
-
-
- {menuOpen && (
-
-
-
{user.fullName || "User"}
-
- {user.emailAddresses[0]?.emailAddress}
-
-
-
-
-
setMenuOpen(false)}
- className="flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-slate-400 hover:bg-slate-800/50 hover:text-white transition-all"
- >
-
-
-
-
- Account settings
-
-
-
-
-
- Sign out
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Logs
+
+
+
+
+
+
+
+
+
+ Stats
+
+
+
setCompactMode((prev) => !prev)}
+ className="inline-flex items-center gap-2 rounded-lg border border-slate-700/60 bg-slate-800/60 px-3 py-2 text-xs font-medium text-slate-200 transition-colors hover:border-slate-600 hover:text-white"
+ >
+
+ Compact mode: {compactMode ? "ON" : "OFF"}
+
+
+
+ setMenuOpen(!menuOpen)}
+ className="relative flex h-9 w-9 cursor-pointer items-center justify-center overflow-hidden rounded-full bg-slate-700 ring-2 ring-slate-700 transition-all hover:ring-slate-600"
+ >
+ {user.imageUrl ? (
+
+ ) : (
+
+ {user.firstName?.[0] || user.emailAddresses[0]?.emailAddress[0]?.toUpperCase()}
+
)}
-
+
+
+ {menuOpen && (
+
+
+
{user.fullName || "User"}
+
+ {user.emailAddresses[0]?.emailAddress}
+
+
+
+
+
setMenuOpen(false)}
+ className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm text-slate-400 transition-all hover:bg-slate-800/50 hover:text-white"
+ >
+
+
+
+
+ Account settings
+
+
+
+
+
+ Sign out
+
+
+
+ )}
+
+
+ );
+
+ return (
+
+
+
+ {dashboardHeader}
{sessionLoading ? (
@@ -569,318 +611,363 @@ export default function DashboardPage() {
- ) : (
- <>
- {/* Stats Grid */}
-
-
-
-
-
-
-
-
+ ) : compactMode ? (
+
+
+
+
Active Sessions
+
1
+
WhatsApp connected
- WhatsApp connected
-
-
-
-
-
-
Total Tokens
-
{formatTokens(session?.totalTokens)}
-
-
-
-
-
+
+
+
Total Tokens
+
{formatTokens(session?.totalTokens)}
+
+ {formatTokens(session?.inputTokens)} in / {formatTokens(session?.outputTokens)} out
+
-
- {formatTokens(session?.inputTokens)} in
- /
- {formatTokens(session?.outputTokens)} out
-
-
-
-
-
-
-
Model
-
{session?.model || "N/A"}
-
-
-
-
-
+
+
+
Model
+
{session?.model || "N/A"}
+
AI provider
-
AI provider
-
-
-
-
-
-
Status
-
- {session?.abortedLastRun ? (
- Error
- ) : (
- Active
- )}
+
+
+
Status
+
+ {session?.abortedLastRun ? Error : Active }
-
-
- {session?.abortedLastRun ? (
-
-
-
- ) : (
-
-
-
- )}
+
Last active {formatRelativeTime(session?.updatedAt)}
-
Last active {formatRelativeTime(session?.updatedAt)}
-
-
-
- {/* Today Overview */}
-
-
- {/* Monthly Habit Heatmap */}
-
-
- {/* Goals & Progress */}
-
-
- {/* Activity Logs entry point */}
-
-
-
Activity Logs
-
Jump from your overview into detailed logs exploration
-
-
- View All Logs
-
-
-
-
-
-
- {/* Main Content */}
-
- {/* WhatsApp Session Detail */}
-
-
-
-
-
-
-
WhatsApp Session
-
-
- {session?.abortedLastRun ? "Error" : "Active"}
-
+
+
-
- {/* User & Channel */}
-
-
-
-
{session?.origin?.label || "Unknown"}
-
WhatsApp Direct Message
-
-
-
{formatRelativeTime(session?.updatedAt)}
-
last active
+
+
+
+
+
+
+
+ ) : (
+ <>
+
+
- {/* Session Details Grid */}
-
-
-
Channel
-
-
-
{session?.lastChannel || "N/A"}
+
+
+
+
Total Tokens
+
{formatTokens(session?.totalTokens)}
+
+
-
-
Chat Type
-
{session?.chatType || "N/A"}
-
-
-
Compactions
-
{session?.compactionCount ?? 0}
-
-
-
Model
-
{session?.model || "N/A"}
+
+ {formatTokens(session?.inputTokens)} in
+ /
+ {formatTokens(session?.outputTokens)} out
- {/* Token Usage Bar */}
-
-
-
Token Usage
-
{formatTokens(session?.totalTokens)} total
-
-
-
-
-
-
- Input: {formatTokens(session?.inputTokens)}
-
-
-
- Output: {formatTokens(session?.outputTokens)}
-
+
+
+
+
Model
+
{session?.model || "N/A"}
+
+
-
of 128k context
+
AI provider
- {/* Origin Details */}
-
-
Delivery Context
-
-
-
From
-
{session?.origin?.from || "N/A"}
-
+
+
-
To
-
{session?.deliveryContext?.to || "N/A"}
-
-
-
Channel
-
{session?.deliveryContext?.channel || "N/A"}
+
Status
+
+ {session?.abortedLastRun ? (
+ Error
+ ) : (
+ Active
+ )}
+
-
-
Model
-
{session?.model || "N/A"}
+
+ {session?.abortedLastRun ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
Last active {formatRelativeTime(session?.updatedAt)}
-
- {/* Quick Actions */}
-
-
-
Quick Actions
+ {/* Today Overview */}
+
+
+ {/* Monthly Habit Heatmap */}
+
+
+ {/* Goals & Progress */}
+
+
+ {/* Activity Logs entry point */}
+
+
+
Activity Logs
+
Jump from your overview into detailed logs exploration
+
+
+ View All Logs
+
+
+
+
+
-
-
-
-
-
New Journal Entry
-
Record a thought or reflection
-
-
-
-
-
-
View Timeline
-
See your D/W/M/Q/Y highlights
+ {/* Main Content */}
+
+ {/* WhatsApp Session Detail */}
+
+
+
+
+
+
+
WhatsApp Session
+
+
+ {session?.abortedLastRun ? "Error" : "Active"}
+
-
+
+ {/* User & Channel */}
+
+
+
+
{session?.origin?.label || "Unknown"}
+
WhatsApp Direct Message
+
+
+
{formatRelativeTime(session?.updatedAt)}
+
last active
+
+
-
-
-
-
Insights
-
Explore patterns and progress
+ {/* Session Details Grid */}
+
+
+
Channel
+
+
+
{session?.lastChannel || "N/A"}
+
+
+
+
Chat Type
+
{session?.chatType || "N/A"}
+
+
+
Compactions
+
{session?.compactionCount ?? 0}
+
+
+
Model
+
{session?.model || "N/A"}
+
+
+
+ {/* Token Usage Bar */}
+
+
+
Token Usage
+
{formatTokens(session?.totalTokens)} total
+
+
+
+
+
+
+ Input: {formatTokens(session?.inputTokens)}
+
+
+
+ Output: {formatTokens(session?.outputTokens)}
+
+
+
of 128k context
+
+
+
+ {/* Origin Details */}
+
+
Delivery Context
+
+
+
From
+
{session?.origin?.from || "N/A"}
+
+
+
To
+
{session?.deliveryContext?.to || "N/A"}
+
+
+
Channel
+
{session?.deliveryContext?.channel || "N/A"}
+
+
+
Model
+
{session?.model || "N/A"}
+
+
+
-
+
-
-
-
-
-
+ {/* Quick Actions */}
+
+
+
Quick Actions
-
-
Documentation
-
Learn how to use LogLife
+
-
+
-
-
- {/* Recent Activity */}
-
-
-
Recent Activity
-
-
-
-
-
-
-
+ {/* Recent Activity */}
+
+
+
Recent Activity
-
-
WhatsApp session active with {session?.origin?.label || "Unknown"}
-
{formatRelativeTime(session?.updatedAt)} · {formatTokens(session?.totalTokens)} tokens used · {session?.model || "N/A"}
-
-
- Active
-
-
+
+
+
+
+
WhatsApp session active with {session?.origin?.label || "Unknown"}
+
{formatRelativeTime(session?.updatedAt)} · {formatTokens(session?.totalTokens)} tokens used · {session?.model || "N/A"}
+
+
+ Active
+
+
-
+
-
-
-
-
WhatsApp number {whatsappPhone} verified
-
Account connected via phone verification
+
+
+
+
WhatsApp number {whatsappPhone} verified
+
Account connected via phone verification
+
+
+ Verified
+
+
-
- Verified
-
-
-
- >
+ >
)}
-
+
+
);
}
diff --git a/website/app/globals.css b/website/app/globals.css
index ebf330fd..253a9d76 100644
--- a/website/app/globals.css
+++ b/website/app/globals.css
@@ -138,3 +138,4 @@ body {
opacity: 1;
transform: translateY(0);
}
+
From a9f7251ab709a888917510c8e707b7fe44646944 Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Wed, 25 Feb 2026 16:39:25 +0500
Subject: [PATCH 19/78] Remove logs page breadcrumb to match statistics
navigation
---
website/app/components/logs/LogsPage.tsx | 11 -----------
1 file changed, 11 deletions(-)
diff --git a/website/app/components/logs/LogsPage.tsx b/website/app/components/logs/LogsPage.tsx
index 74437a14..9e887a45 100644
--- a/website/app/components/logs/LogsPage.tsx
+++ b/website/app/components/logs/LogsPage.tsx
@@ -1,7 +1,6 @@
"use client";
import { useMemo, useState } from "react";
-import Link from "next/link";
import { useSearchParams } from "next/navigation";
import ClassificationPanel from "./ClassificationPanel";
import LogsControlBar from "./LogsControlBar";
@@ -43,16 +42,6 @@ export default function LogsPage() {
return (
-
-
-
- Dashboard
-
- /
- Logs
-
-
-
Activity Logs
Explore how messages are classified into insights
From 64f5364977589f2d21bf123cda5769ecc32ca62f Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Wed, 25 Feb 2026 16:45:24 +0500
Subject: [PATCH 20/78] Remove Account Settings breadcrumb for sidebar-only
dashboard navigation
---
website/app/account/page.tsx | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/website/app/account/page.tsx b/website/app/account/page.tsx
index d82a74b1..9e1ac5f1 100644
--- a/website/app/account/page.tsx
+++ b/website/app/account/page.tsx
@@ -159,16 +159,6 @@ export default function AccountPage() {
return (
- {/* Breadcrumb */}
-
-
-
-
-
- Back to Dashboard
-
-
-
{/* Header */}
Account Settings
From 2397d72b4bd30110dd3f9627d50a789ccdf206d0 Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Wed, 25 Feb 2026 16:47:23 +0500
Subject: [PATCH 21/78] fix: align dashboard top spacing with logs, stats, and
account pages
---
website/app/dashboard/page.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/website/app/dashboard/page.tsx b/website/app/dashboard/page.tsx
index 444fec19..f260f4d7 100644
--- a/website/app/dashboard/page.tsx
+++ b/website/app/dashboard/page.tsx
@@ -378,7 +378,7 @@ export default function DashboardPage() {
return (
-
+
{dashboardHeader}
From dfb83a514ffd157acb6dab1a0b5f970dc9416769 Mon Sep 17 00:00:00 2001
From: hafiz-ahtasham-ali
Date: Wed, 25 Feb 2026 17:53:36 +0500
Subject: [PATCH 22/78] Add Support & Feedback form with Resend integration and
premium modal UI
---
website/app/api/support/route.ts | 42 +++
website/app/components/WhatsAppWidget.tsx | 4 +-
website/app/layout.tsx | 2 +
website/components/SupportButton.tsx | 56 ++++
website/components/SupportModal.tsx | 333 ++++++++++++++++++++++
website/package.json | 1 +
website/pnpm-lock.yaml | 36 +++
7 files changed, 472 insertions(+), 2 deletions(-)
create mode 100644 website/app/api/support/route.ts
create mode 100644 website/components/SupportButton.tsx
create mode 100644 website/components/SupportModal.tsx
diff --git a/website/app/api/support/route.ts b/website/app/api/support/route.ts
new file mode 100644
index 00000000..7e3fd4d8
--- /dev/null
+++ b/website/app/api/support/route.ts
@@ -0,0 +1,42 @@
+import { NextRequest, NextResponse } from "next/server";
+import { Resend } from "resend";
+
+const resend = new Resend(process.env.RESEND_API_KEY);
+
+// Resend's onboarding@resend.dev can only send to your Resend account email.
+// Set SUPPORT_EMAIL in .env.local to that address for testing, or verify a domain
+// and use a custom "from" to send to any address.
+const SUPPORT_EMAIL = process.env.SUPPORT_EMAIL ?? "hafizahtasham07@gmail.com";
+
+export async function POST(req: NextRequest) {
+ let body: { type?: string; subject?: string; email?: string; message?: string };
+ try {
+ body = await req.json();
+ } catch {
+ return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
+ }
+
+ const { type, subject, email, message } = body;
+
+ if (!message?.trim()) {
+ return NextResponse.json({ error: "Message is required" }, { status: 400 });
+ }
+
+ try {
+ await resend.emails.send({
+ from: "LogLife Support ",
+ to: SUPPORT_EMAIL,
+ subject: `[${type ?? "General"}] ${subject ?? "No subject"}`,
+ text: [
+ `Type: ${type ?? "—"}`,
+ `From: ${email ?? "—"}`,
+ "",
+ message,
+ ].join("\n"),
+ });
+
+ return NextResponse.json({ success: true });
+ } catch {
+ return NextResponse.json({ error: "Failed to send message" }, { status: 500 });
+ }
+}
diff --git a/website/app/components/WhatsAppWidget.tsx b/website/app/components/WhatsAppWidget.tsx
index 6794ab12..99e4b060 100644
--- a/website/app/components/WhatsAppWidget.tsx
+++ b/website/app/components/WhatsAppWidget.tsx
@@ -134,7 +134,7 @@ export default function WhatsAppWidget() {
<>
{isOpen && (
+
{/* Chat panel: enter/exit transition */}
{isPanelVisible && (
{children}
+