diff --git a/ui/src/pages/UserProfile.tsx b/ui/src/pages/UserProfile.tsx
index 29fc013bddc..abc3e2eaf87 100644
--- a/ui/src/pages/UserProfile.tsx
+++ b/ui/src/pages/UserProfile.tsx
@@ -1,30 +1,25 @@
import { useEffect, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
-import {
- Activity,
- AlertCircle,
- BarChart3,
- CheckCircle2,
- Clock3,
- Coins,
- Hash,
- MessageSquareText,
- Ticket,
- UserRound,
-} from "lucide-react";
-import type { LucideIcon } from "lucide-react";
+import { AlertCircle, UserRound } from "lucide-react";
import type { UserProfileDailyPoint, UserProfileWindowStats } from "@paperclipai/shared";
import { Link, useParams } from "@/lib/router";
import { userProfilesApi } from "../api/userProfiles";
import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar";
-import { Badge } from "../components/ui/badge";
import { EmptyState } from "../components/EmptyState";
import { PageSkeleton } from "../components/PageSkeleton";
import { StatusBadge } from "../components/StatusBadge";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { useCompany } from "../context/CompanyContext";
import { queryKeys } from "../lib/queryKeys";
-import { cn, formatCents, formatDate, formatShortDate, formatTokens, issueUrl, providerDisplayName, relativeTime } from "../lib/utils";
+import {
+ formatCents,
+ formatDate,
+ formatShortDate,
+ formatTokens,
+ issueUrl,
+ providerDisplayName,
+ relativeTime,
+} from "../lib/utils";
const NO_COMPANY = "__none__";
@@ -44,109 +39,123 @@ function completionRate(stats: UserProfileWindowStats) {
return `${Math.round((stats.completedIssues / stats.touchedIssues) * 100)}%`;
}
-function StatPill({
- icon: Icon,
- label,
- value,
- tone = "default",
-}: {
- icon: LucideIcon;
- label: string;
- value: string;
- tone?: "default" | "green" | "amber" | "blue";
-}) {
+function HeroStat({ label, value, hint }: { label: string; value: string; hint?: string }) {
return (
-
-
-
- {label}
-
-
{value}
+
+
{value}
+
{label}
+ {hint ?
{hint}
: null}
);
}
-function WindowPanel({ stats }: { stats: UserProfileWindowStats }) {
+function WindowColumn({ stats }: { stats: UserProfileWindowStats }) {
+ const tokens = totalTokens(stats);
return (
-
-
-
{stats.label}
-
{formatTokens(totalTokens(stats))}
+
+
+
{stats.label}
+ {completionRate(stats)} done
-
-
-
-
-
+
+
+
+
+
+
-
-
-
Created
-
{stats.createdIssues}
-
-
-
Open
-
{stats.assignedOpenIssues}
-
-
-
Rate
-
{completionRate(stats)}
-
+
+
+ Tokens
+ {formatTokens(tokens)}
+ Spend
+ {formatCents(stats.costCents)}
+ Created
+ {stats.createdIssues}
+ Open
+ {stats.assignedOpenIssues}
);
}
+function Metric({ value, label }: { value: string; label: string }) {
+ return (
+
+ );
+}
+
function UsageChart({ points }: { points: UserProfileDailyPoint[] }) {
- const maxTokens = Math.max(1, ...points.map((point) => totalTokens(point)));
- const maxActivity = Math.max(1, ...points.map((point) => point.activityCount + point.completedIssues));
+ const totals = points.map((point) => totalTokens(point));
+ const maxTokens = Math.max(1, ...totals);
+ const maxCompleted = Math.max(1, ...points.map((point) => point.completedIssues));
+ const totalTokensSum = totals.reduce((sum, value) => sum + value, 0);
return (
-
-
-
-
Two-week signal
-
Token pressure, user actions, and completed work by day.
-
-
-
tokens
-
activity
+
+
+
Last 14 days
+
+ {formatTokens(totalTokensSum)}
+ tokens total
-
+
{points.map((point) => {
- const tokenHeight = Math.max(6, Math.round((totalTokens(point) / maxTokens) * 100));
- const activityHeight = Math.max(4, Math.round(((point.activityCount + point.completedIssues) / maxActivity) * 72));
+ const tokens = totalTokens(point);
+ const heightPct = tokens === 0 ? 0 : Math.max(2, Math.round((tokens / maxTokens) * 100));
+ const completedPct = point.completedIssues === 0
+ ? 0
+ : Math.max(8, Math.round((point.completedIssues / maxCompleted) * 36));
return (
-
-
+
+
+ {completedPct > 0 ? (
-
-
-
{new Date(point.date).getUTCDate()}
+ ) : null}
);
})}
+
+ {points.map((point, index) => (
+
+ {index === 0 || index === 6 || index === 13 ? formatShortDate(point.date) : null}
+
+ ))}
+
+
+
+ tokens / day
+
+
+ completions
+
+
);
}
+interface UsageRow {
+ key: string;
+ label: string;
+ sublabel: string;
+ costCents: number;
+ inputTokens: number;
+ cachedInputTokens: number;
+ outputTokens: number;
+}
+
function UsageList({
title,
empty,
@@ -154,35 +163,32 @@ function UsageList({
}: {
title: string;
empty: string;
- rows: Array<{
- key: string;
- label: string;
- sublabel: string;
- costCents: number;
- inputTokens: number;
- cachedInputTokens: number;
- outputTokens: number;
- }>;
+ rows: UsageRow[];
}) {
return (
-
- {title}
-
- {rows.length === 0 ? (
-
{empty}
- ) : rows.map((row) => (
-
-
-
{row.label}
-
{row.sublabel}
-
-
- {formatTokens(totalTokens(row))}
- {formatCents(row.costCents)}
-
-
- ))}
+
+
+
{title}
+ {rows.length}
+ {rows.length === 0 ? (
+ {empty}
+ ) : (
+
+ )}
);
}
@@ -207,12 +213,12 @@ export function UserProfile() {
const last7 = data?.stats.find((entry) => entry.key === "last7");
const displayName = data?.user.name?.trim() || data?.user.email?.split("@")[0] || "User";
- const agentUsageRows = useMemo(
+ const agentUsageRows = useMemo
(
() =>
(data?.topAgents ?? []).map((row) => ({
key: row.agentId,
label: row.agentName ?? row.agentId.slice(0, 8),
- sublabel: "Issue-linked token usage",
+ sublabel: "Issue-linked usage",
costCents: row.costCents,
inputTokens: row.inputTokens,
cachedInputTokens: row.cachedInputTokens,
@@ -221,7 +227,7 @@ export function UserProfile() {
[data?.topAgents],
);
- const providerUsageRows = useMemo(
+ const providerUsageRows = useMemo(
() =>
(data?.topProviders ?? []).map((row) => ({
key: `${row.provider}:${row.biller}:${row.model}`,
@@ -247,123 +253,103 @@ export function UserProfile() {
return ;
}
+ const allTimeTokens = allTime ? totalTokens(allTime) : 0;
+ const metaParts = [
+ data.user.membershipRole ?? "member",
+ data.user.membershipStatus,
+ `joined ${formatDate(data.user.joinedAt)}`,
+ ];
+
return (
-
-
-
-
-
-
- {data.user.image ? : null}
- {initials(displayName)}
-
-
-
-
{displayName}
- @{data.user.slug}
-
-
- {data.user.email ? {data.user.email} : null}
- {data.user.membershipRole ?? "member"}
- {data.user.membershipStatus}
- joined {formatDate(data.user.joinedAt)}
-
-
+
+
+
+
+ {data.user.image ? : null}
+ {initials(displayName)}
+
+
+
+
{displayName}
+ @{data.user.slug}
-
-
-
-
-
-
-
-
-
-
-
-
- Profile ledger
-
-
-
- All-time spend
- {formatCents(allTime?.costCents ?? 0)}
-
-
- Attributed cost events
- {allTime?.costEventCount ?? 0}
-
-
- Comments
- {allTime?.commentCount ?? 0}
-
-
- Completion rate
- {allTime ? completionRate(allTime) : "0%"}
-
+
+ {data.user.email ? {data.user.email} : null}
+ {data.user.email ? · : null}
+ {metaParts.join(" · ")}
+
+
+
+
+
+
+
-
- {data.stats.map((entry) => )}
-
+
+ {data.stats.map((entry) => )}
+
-
-
-
-
Recent tasks
- {data.recentIssues.length}
-
-
- {data.recentIssues.length === 0 ? (
-
No touched tasks yet.
- ) : data.recentIssues.map((issue) => (
-
-
-
{issue.identifier ?? issue.id.slice(0, 8)}
-
{issue.title}
-
-
-
- {relativeTime(issue.updatedAt)}
-
-
- ))}
+
+
+
+
Recent tasks
+ {data.recentIssues.length}
+ {data.recentIssues.length === 0 ? (
+ No touched tasks yet.
+ ) : (
+
+ {data.recentIssues.map((issue) => (
+ -
+
+ {issue.identifier ?? issue.id.slice(0, 8)}
+ {issue.title}
+
+
+ {relativeTime(issue.updatedAt)}
+
+
+
+ ))}
+
+ )}
-
-
-
Recent activity
-
{data.recentActivity.length}
+
+
+
Recent activity
+ {data.recentActivity.length}
-
- {data.recentActivity.length === 0 ? (
-
No direct user actions recorded yet.
- ) : data.recentActivity.map((event) => (
-
-
-
{event.action.replaceAll("_", " ")}
-
- {event.entityType} - {event.entityId.slice(0, 12)}
+ {data.recentActivity.length === 0 ? (
+
No direct user actions recorded yet.
+ ) : (
+
+ {data.recentActivity.map((event) => (
+ -
+
+
{event.action.replaceAll("_", " ")}
+
+ {event.entityType} · {event.entityId.slice(0, 12)}
+
-
-
{relativeTime(event.createdAt)}
-
- ))}
-
+
{relativeTime(event.createdAt)}
+
+ ))}
+
+ )}
-