Skip to main content

Overview

All components are client-side ("use client") and located in src/components/. The component hierarchy follows a clear separation between page-level orchestration, data visualization, and UI primitives.

Component Hierarchy

Page-Level Components

ClientPage

file
path
src/components/client-page.tsx
Root client component that manages multi-profile state and handles the transition between upload and dashboard views. Props:
initialData
DashboardData | null
Initial data loaded from server (local mode) or null (upload mode)
State Management:
interface Profile {
  id: string;
  name: string;
  data: DashboardData;
}

const [profiles, setProfiles] = useState<Profile[]>();
const [activeProfileId, setActiveProfileId] = useState<string | null>();
const [addingProfile, setAddingProfile] = useState(false);
Behavior:
  • If profiles.length === 0: Renders UploadZone
  • If addingProfile === true: Shows upload overlay on top of dashboard
  • Otherwise: Renders Dashboard with active profile data
Code Example:
ClientPage Usage
function generateProfileName(data: DashboardData, index: number): string {
  if (data.account?.accountUUID) {
    return `Profile ${data.account.accountUUID.slice(0, 8)}`;
  }
  return `Profile ${index + 1}`;
}

const handleDataLoaded = useCallback(
  (data: DashboardData) => {
    const id = `upload-${Date.now()}`;
    const name = generateProfileName(data, profiles.length);
    setProfiles((prev) => [...prev, { id, name, data }]);
    setActiveProfileId(id);
    setAddingProfile(false);
  },
  [profiles.length]
);

Dashboard

file
path
src/components/dashboard.tsx
Main layout component with header, stats cards, and tabbed content sections. Props:
data
DashboardData
required
Analytics data for the active profile
profiles
Profile[]
Array of all available profiles
activeProfileId
string
ID of the currently active profile
onSwitchProfile
(id: string) => void
Callback when user switches profiles
onAddProfile
() => void
Callback when user clicks ”+ Profile” button
Features:
Extracts unique project paths and provides dropdown filter. Filters sessions and history arrays, but leaves stats/heatmap unfiltered.
const projects = useMemo(() => {
  const map = new Map<string, string>();
  for (const s of data.sessions) {
    if (s.project_path && !map.has(s.project_path)) {
      const segments = s.project_path.replace(/\/$/, "").split("/");
      map.set(s.project_path, segments[segments.length - 1] || s.project_path);
    }
  }
  return Array.from(map.entries())
    .sort((a, b) => a[1].localeCompare(b[1]))
    .map(([value, label]) => ({ value, label }));
}, [data.sessions]);
5 tabs: Activity, Sessions, Models, Tools, Prompts
  • Activity: Heatmap + Hour Chart + Project Breakdown
  • Sessions: Searchable, expandable session table
  • Models: Daily tokens chart + Model breakdown (pie + bar + table)
  • Tools: Tool usage chart + Languages used
  • Prompts: Searchable prompt history
Only shown when profiles.length > 1. Allows switching between uploaded profiles.
Code Example:
Dashboard Layout
<Tabs value={activeTab} onValueChange={setActiveTab}>
  <TabsList className="grid w-full grid-cols-5">
    <TabsTrigger value="activity">Activity</TabsTrigger>
    <TabsTrigger value="sessions">Sessions</TabsTrigger>
    <TabsTrigger value="models">Models</TabsTrigger>
    <TabsTrigger value="tools">Tools</TabsTrigger>
    <TabsTrigger value="history">Prompts</TabsTrigger>
  </TabsList>

  <TabsContent value="activity" className="mt-6 space-y-6">
    <ActivityHeatmap stats={data.stats} />
    <div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
      <HourChart stats={data.stats} />
      <ProjectBreakdown sessions={data.sessions} />
    </div>
  </TabsContent>
  {/* ... other tabs ... */}
</Tabs>

UploadZone

file
path
src/components/upload-zone.tsx
Drag-and-drop or click-to-upload interface for JSON export files. Props:
onDataLoaded
(data: DashboardData) => void
required
Callback invoked when file is successfully parsed
Features:
  • Drag-and-drop file upload with visual feedback
  • File validation and error handling
  • JSON parsing with backward compatibility
  • Instructions for using export script
Code Example:
File Processing
const processFile = useCallback(async (file: File) => {
  setLoading(true);
  setError(null);
  try {
    const text = await file.text();
    const data = JSON.parse(text) as DashboardData;

    if (!data.sessions && !data.stats) {
      setError("Invalid file format. Please use the export script to generate the data file.");
      return;
    }

    // Ensure arrays exist
    data.sessions = data.sessions || [];
    data.history = data.history || [];
    data.memories = data.memories || [];

    // Normalize old memory format (string[] → {name, content}[])
    for (const mem of data.memories) {
      if (mem.files?.length && typeof mem.files[0] === "string") {
        mem.files = (mem.files as unknown as string[]).map((f) => ({
          name: f,
          content: "",
        }));
      }
    }

    onDataLoaded(data);
  } catch {
    setError("Failed to parse file. Make sure it's a valid JSON export.");
  } finally {
    setLoading(false);
  }
}, [onDataLoaded]);

Data Visualization Components

StatsOverview

file
path
src/components/stats-overview.tsx
7 clickable stat cards in a responsive grid. Props:
stats
StatsCache | null
required
Pre-aggregated statistics
sessions
SessionMeta[]
required
Session metadata for calculating derived stats
onNavigate
(tab: string) => void
required
Callback to navigate to specific tab when card is clicked
Cards:

Total Sessions

Session count + total hoursNavigates to: sessions

Messages

Total messages + per-session averageNavigates to: history

Tool Calls

Total tool invocationsNavigates to: tools

Lines Changed

Lines added + lines removedNavigates to: sessions

Files Modified

Total files + commit countNavigates to: sessions

Avg Session

Average duration + longest sessionNavigates to: activity

Total Cost

USD cost across all modelsNavigates to: models
Code Example:
Stat Card Structure
const cards = [
  {
    title: "Total Sessions",
    value: totalSessions.toLocaleString(),
    sub: `${totalHours}h total`,
    icon: Terminal,
    tab: "sessions",
  },
  // ... other cards
];

return (
  <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-7">
    {cards.map((card) => (
      <Card
        key={card.title}
        className="cursor-pointer transition-all duration-200 hover:scale-[1.02] active:scale-[0.98]"
        onClick={() => onNavigate(card.tab)}
      >
        <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
          <CardTitle className="text-sm font-medium text-gray-400">
            {card.title}
          </CardTitle>
          <card.icon className="h-4 w-4 text-gray-400" />
        </CardHeader>
        <CardContent>
          <div className="text-2xl font-bold text-white">{card.value}</div>
          <p className="text-xs text-gray-500">{card.sub}</p>
        </CardContent>
      </Card>
    ))}
  </div>
);

ActivityHeatmap

file
path
src/components/activity-heatmap.tsx
GitHub-style contribution heatmap showing daily message activity. Props:
stats
StatsCache | null
required
Uses stats.dailyActivity for heatmap data
Features:
  • Week-by-week grid layout
  • Hover tooltips with date and message count
  • Color gradient based on message intensity
  • Responsive to viewport width
Code Example:
Color Calculation
function getColor(count: number, max: number): string {
  if (count === 0) return "bg-[#1a1a1a]";
  const ratio = count / max;
  if (ratio > 0.75) return "bg-white";
  if (ratio > 0.5) return "bg-gray-300";
  if (ratio > 0.25) return "bg-gray-500";
  return "bg-gray-700";
}

HourChart

file
path
src/components/hour-chart.tsx
Bar chart showing session distribution by hour of day (0-23). Props:
stats
StatsCache | null
required
Uses stats.hourCounts for chart data
Implementation:
Hour Chart Data
const data = Array.from({ length: 24 }, (_, i) => ({
  hour: `${i.toString().padStart(2, "0")}:00`,
  sessions: Number(stats.hourCounts[String(i)] ?? 0),
}));

return (
  <ResponsiveContainer width="100%" height={250}>
    <BarChart data={data}>
      <XAxis dataKey="hour" tick={{ fontSize: 11 }} interval={2} stroke="#555555" />
      <YAxis tick={{ fontSize: 11 }} stroke="#555555" />
      <Tooltip
        cursor={false}
        contentStyle={{
          backgroundColor: "#141414",
          border: "1px solid #2a2a2a",
          borderRadius: "8px",
          color: "#e5e7eb",
        }}
      />
      <Bar dataKey="sessions" fill="#e5e7eb" radius={[4, 4, 0, 0]} />
    </BarChart>
  </ResponsiveContainer>
);

ProjectBreakdown

file
path
src/components/project-breakdown.tsx
Horizontal bar chart showing top 10 projects ranked by total time spent. Props:
sessions
SessionMeta[]
required
Aggregates session durations by project
Code Example:
Project Aggregation
const projectMap = new Map<string, { sessions: number; minutes: number; lines: number }>();

for (const s of sessions) {
  const name = s.project_path.split("/").pop() || s.project_path;
  const existing = projectMap.get(name) ?? { sessions: 0, minutes: 0, lines: 0 };
  existing.sessions++;
  existing.minutes += s.duration_minutes;
  existing.lines += s.lines_added + s.lines_removed;
  projectMap.set(name, existing);
}

const data = Array.from(projectMap.entries())
  .map(([name, v]) => ({ name: name.length > 15 ? name.slice(0, 15) + ".." : name, ...v }))
  .sort((a, b) => b.minutes - a.minutes)
  .slice(0, 10);

DailyTokensChart

file
path
src/components/daily-tokens-chart.tsx
Stacked area chart showing token consumption over time by model. Props:
stats
StatsCache | null
required
Uses stats.dailyModelTokens for chart data
Implementation:
Stacked Area Chart
const chartData = stats.dailyModelTokens.map((day) => {
  const entry: Record<string, string | number> = {
    date: day.date.slice(5), // MM-DD
  };
  for (const model of modelList) {
    entry[model] = day.tokensByModel[model] ?? 0;
  }
  return entry;
});

return (
  <AreaChart data={chartData}>
    {modelList.map((model, i) => (
      <Area
        key={model}
        type="monotone"
        dataKey={model}
        stackId="1"
        stroke={AREA_COLORS[i % AREA_COLORS.length]}
        fill={AREA_COLORS[i % AREA_COLORS.length]}
        fillOpacity={0.3}
        name={model}
      />
    ))}
  </AreaChart>
);

ModelBreakdown

file
path
src/components/model-breakdown.tsx
Comprehensive model usage visualization with three sections. Props:
stats
StatsCache | null
required
Uses stats.modelUsage for all visualizations
Sections:
Model usage distribution by total tokens (input + output)
const pieData = Object.entries(stats.modelUsage).map(([model, usage]) => ({
  name: formatModelName(model),
  value: usage.outputTokens + usage.inputTokens,
}));
Helper Function:
Model Name Formatting
function formatModelName(name: string): string {
  return name
    .replace("claude-", "")
    .replace(/-\d{8}$/, "")  // Remove date suffix
    .replace(/-(\d+)-(\d+)$/, " $1.$2")  // "3-7" → "3.7"
    .replace(/-/g, " ")
    .replace(/\b\w/g, (c) => c.toUpperCase());
}
// "claude-3-7-sonnet-20250219" → "3.7 Sonnet"

SessionTable

file
path
src/components/session-table.tsx
Searchable, expandable session list with full conversation viewer. Props:
sessions
SessionMeta[]
required
Array of session metadata objects
Features:
Click to expand and fetch full conversation from /api/session-messages
const fetchMessages = useCallback(async (session: SessionMeta) => {
  if (messages[session.session_id]) return;
  setLoading(session.session_id);
  try {
    const params = new URLSearchParams({
      session_id: session.session_id,
      project_path: session.project_path,
    });
    const res = await fetch(`/api/session-messages?${params}`);
    if (res.ok) {
      const data = await res.json();
      setMessages((prev) => ({ ...prev, [session.session_id]: data.messages }));
    }
  } catch {
    // silently fail
  } finally {
    setLoading(null);
  }
}, [messages]);
Each session shows:
  • Duration, messages, lines changed
  • Commits, files modified
  • Languages used
  • Tools used
  • Web search/MCP/agent badges
  • Tool errors
Each message displays:
  • User or assistant role
  • Timestamp
  • Message text (expandable if >300 chars)
  • Tool use badges
function MessageBubble({ message }: { message: SessionMessage }) {
  const [expanded, setExpanded] = useState(false);
  const isUser = message.role === "user";
  const isLong = message.text.length > 300;
  const displayText = expanded ? message.text : message.text.slice(0, 300);
  
  // ... render logic
}

ToolUsageChart

file
path
src/components/tool-usage-chart.tsx
Two horizontal bar charts: top tools and top languages. Props:
sessions
SessionMeta[]
required
Aggregates tool_counts and languages across all sessions
Code Example:
Tool Aggregation
const toolMap = new Map<string, number>();

for (const s of sessions) {
  for (const [tool, count] of Object.entries(s.tool_counts)) {
    toolMap.set(tool, (toolMap.get(tool) ?? 0) + count);
  }
}

const data = Array.from(toolMap.entries())
  .map(([name, count]) => ({ name, count }))
  .sort((a, b) => b.count - a.count)
  .slice(0, 15);

PromptHistory

file
path
src/components/prompt-history.tsx
Searchable list of prompts with copy-to-clipboard functionality. Props:
history
HistoryEntry[]
required
Prompt history from history.jsonl
Features:
  • Search by prompt text
  • Expandable long prompts (>120 chars)
  • Copy to clipboard button
  • Character count display
  • Project badge
  • Limited to first 200 prompts for performance
Code Example:
Prompt History Filter
const filtered = history
  .filter(
    (h) =>
      h.display &&
      h.display.trim().length > 0 &&
      !h.display.startsWith("/login") &&
      h.display.toLowerCase().includes(search.toLowerCase())
  )
  .reverse();

UI Primitives (shadcn)

Located in src/components/ui/, these are low-level building blocks styled with the neomorphic dark theme.

Card

card.tsxContainer component with header, content, footer slotsUsage: Stat cards, chart wrappers

Tabs

tabs.tsxRadix UI tabs with trigger list and content panelsUsage: Main navigation (Activity, Sessions, Models, Tools, Prompts)

Badge

badge.tsxSmall label component with variants (default, secondary, outline, destructive)Usage: Session detail tags (commits, files, languages, features)

ScrollArea

scroll-area.tsxRadix UI scroll area with custom scrollbar stylingUsage: Session table, prompt history

Separator

separator.tsxVisual divider (horizontal or vertical)Usage: Section dividers

Select

select.tsxRadix UI select with trigger, content, item componentsUsage: Project filter, profile switcher

Shared Utilities

file
path
src/lib/utils.ts

cn

Merges Tailwind CSS classes using clsx + tailwind-merge.
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

// Usage
<div className={cn("base-class", isActive && "active-class")} />

formatTokens

Formats large numbers with K/M suffixes.
export function formatTokens(n: number): string {
  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
  if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
  return String(n);
}

// Examples
formatTokens(1600000) // "1.6M"
formatTokens(52300)   // "52.3K"
formatTokens(150)     // "150"

formatDuration

Formats milliseconds to human-readable duration.
export function formatDuration(ms: number): string {
  const minutes = ms / 60_000;
  if (minutes < 60) return `${Math.round(minutes)}m`;
  const hours = minutes / 60;
  if (hours < 48) return `${Math.round(hours)}h`;
  return `${Math.round(hours / 24)}d`;
}

// Examples
formatDuration(2520000)    // "42m"
formatDuration(43200000)   // "12h"
formatDuration(864000000)  // "10d"

formatCost

Formats USD cost with 2 decimal places.
export function formatCost(usd: number): string {
  return `$${usd.toFixed(2)}`;
}

// Example
formatCost(12.3456) // "$12.35"

Component Best Practices

Memoization: Use useMemo for expensive calculations like filtering and aggregationLazy Loading: Fetch session messages only when expandedResponsive Design: All components adapt to mobile, tablet, and desktop viewportsError Handling: Gracefully handle missing data and API failures

Styling Patterns

All components follow consistent styling conventions:
// Background colors
"bg-[#0a0a0a]"  // Page background
"bg-[#1a1a1a]"  // Card background
"bg-[#141414]"  // Tooltip background

// Text colors
"text-white"     // Primary text
"text-gray-200"  // Secondary text
"text-gray-400"  // Tertiary text
"text-gray-500"  // Disabled text
"text-gray-600"  // Muted text

// Borders
"border border-white/10"  // Default border
"border-white/5"          // Subtle border
"border-white/20"         // Hover border

// Interactive states
"hover:scale-[1.02]"       // Hover grow
"active:scale-[0.98]"      // Click shrink
"transition-all duration-200"  // Smooth transitions

Build docs developers (and LLMs) love