diff --git a/frontend/src/app/(dashboard)/(personalAccount)/settings/layout.tsx b/frontend/src/app/(dashboard)/(personalAccount)/settings/layout.tsx
index 52c81f4d..ef0664cf 100644
--- a/frontend/src/app/(dashboard)/(personalAccount)/settings/layout.tsx
+++ b/frontend/src/app/(dashboard)/(personalAccount)/settings/layout.tsx
@@ -15,6 +15,7 @@ export default function PersonalAccountSettingsPage({
// { name: "Profile", href: "/settings" },
// { name: "Teams", href: "/settings/teams" },
{ name: 'Billing', href: '/settings/billing' },
+ { name: 'Usage Logs', href: '/settings/usage-logs' },
];
return (
<>
diff --git a/frontend/src/app/(dashboard)/(personalAccount)/settings/usage-logs/page.tsx b/frontend/src/app/(dashboard)/(personalAccount)/settings/usage-logs/page.tsx
new file mode 100644
index 00000000..51ec08f7
--- /dev/null
+++ b/frontend/src/app/(dashboard)/(personalAccount)/settings/usage-logs/page.tsx
@@ -0,0 +1,15 @@
+import { createClient } from '@/lib/supabase/server';
+import UsageLogs from '@/components/billing/usage-logs';
+
+export default async function UsageLogsPage() {
+ const supabaseClient = await createClient();
+ const { data: personalAccount } = await supabaseClient.rpc(
+ 'get_personal_account',
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/components/billing/usage-logs.tsx b/frontend/src/components/billing/usage-logs.tsx
new file mode 100644
index 00000000..7ded3c3a
--- /dev/null
+++ b/frontend/src/components/billing/usage-logs.tsx
@@ -0,0 +1,468 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { createClient } from '@/lib/supabase/client';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from '@/components/ui/accordion';
+import { Badge } from '@/components/ui/badge';
+import { Skeleton } from '@/components/ui/skeleton';
+import { Button } from '@/components/ui/button';
+import { ExternalLink, Loader2 } from 'lucide-react';
+import { isLocalMode } from '@/lib/config';
+
+interface UsageLogEntry {
+ message_id: string;
+ thread_id: string;
+ created_at: string;
+ content: {
+ usage: {
+ prompt_tokens: number;
+ completion_tokens: number;
+ };
+ model: string;
+ };
+ total_tokens: number;
+ estimated_cost: number;
+ project_id: string;
+}
+
+interface DailyUsage {
+ date: string;
+ logs: UsageLogEntry[];
+ totalTokens: number;
+ totalCost: number;
+ requestCount: number;
+ models: string[];
+}
+
+interface Props {
+ accountId: string;
+}
+
+export default function UsageLogs({ accountId }: Props) {
+ const [usageLogs, setUsageLogs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [page, setPage] = useState(0);
+ const [hasMore, setHasMore] = useState(true);
+ const [loadingMore, setLoadingMore] = useState(false);
+ const [modelPricing, setModelPricing] = useState<
+ Record
+ >({});
+
+ const ITEMS_PER_PAGE = 1000;
+
+ const fetchModelPricing = async () => {
+ try {
+ const response = await fetch('/api/billing/available-models');
+ if (!response.ok) throw new Error('Failed to fetch model pricing');
+
+ const data = await response.json();
+ const pricing: Record = {};
+
+ data.models.forEach((model: any) => {
+ if (
+ model.input_cost_per_million_tokens &&
+ model.output_cost_per_million_tokens
+ ) {
+ pricing[model.id] = {
+ input: model.input_cost_per_million_tokens,
+ output: model.output_cost_per_million_tokens,
+ };
+ }
+ });
+
+ setModelPricing(pricing);
+ } catch (error) {
+ console.error('Error fetching model pricing:', error);
+ }
+ };
+
+ const calculateTokenCost = (
+ promptTokens: number,
+ completionTokens: number,
+ model: string,
+ ): number => {
+ // Use fetched pricing data from available-models API
+ const costs = modelPricing[model];
+
+ if (costs) {
+ // Convert from per-million to per-token costs
+ return (
+ (promptTokens / 1000000) * costs.input +
+ (completionTokens / 1000000) * costs.output
+ );
+ }
+
+ // Fallback to a reasonable average if no pricing data available
+ const fallbackCost = 0.002; // per 1K tokens
+ return ((promptTokens + completionTokens) / 1000) * fallbackCost;
+ };
+
+ const fetchUsageLogs = async (
+ pageNum: number = 0,
+ append: boolean = false,
+ ) => {
+ try {
+ if (!append) setLoading(true);
+ else setLoadingMore(true);
+
+ const supabase = createClient();
+
+ // First, get all thread IDs for this user
+ const { data: threads, error: threadsError } = await supabase
+ .from('threads')
+ .select('thread_id')
+ .eq('account_id', accountId);
+
+ if (threadsError) throw threadsError;
+
+ if (!threads || threads.length === 0) {
+ setUsageLogs([]);
+ setLoading(false);
+ setLoadingMore(false);
+ setHasMore(false);
+ return;
+ }
+
+ const threadIds = threads.map((t) => t.thread_id);
+
+ // Then fetch usage messages with pagination, including thread project info
+ const { data: messages, error: messagesError } = await supabase
+ .from('messages')
+ .select(
+ `
+ message_id,
+ thread_id,
+ created_at,
+ content,
+ threads!inner(project_id)
+ `,
+ )
+ .in('thread_id', threadIds)
+ .eq('type', 'assistant_response_end')
+ .order('created_at', { ascending: false })
+ .range(pageNum * ITEMS_PER_PAGE, (pageNum + 1) * ITEMS_PER_PAGE - 1);
+
+ if (messagesError) throw messagesError;
+
+ const processedLogs: UsageLogEntry[] = (messages || []).map((message) => {
+ const usage = message.content?.usage || {
+ prompt_tokens: 0,
+ completion_tokens: 0,
+ };
+ const model = message.content?.model || 'unknown';
+ const totalTokens = usage.prompt_tokens + usage.completion_tokens;
+ const estimatedCost = calculateTokenCost(
+ usage.prompt_tokens,
+ usage.completion_tokens,
+ model,
+ );
+
+ return {
+ message_id: message.message_id,
+ thread_id: message.thread_id,
+ created_at: message.created_at,
+ content: {
+ usage,
+ model,
+ },
+ total_tokens: totalTokens,
+ estimated_cost: estimatedCost,
+ project_id: message.threads?.[0]?.project_id || 'unknown',
+ };
+ });
+
+ if (append) {
+ setUsageLogs((prev) => [...prev, ...processedLogs]);
+ } else {
+ setUsageLogs(processedLogs);
+ }
+
+ setHasMore(processedLogs.length === ITEMS_PER_PAGE);
+ } catch (err) {
+ console.error('Error fetching usage logs:', err);
+ setError(
+ err instanceof Error ? err.message : 'Failed to fetch usage logs',
+ );
+ } finally {
+ setLoading(false);
+ setLoadingMore(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchModelPricing();
+ fetchUsageLogs(0, false);
+ }, [accountId]);
+
+ const loadMore = () => {
+ const nextPage = page + 1;
+ setPage(nextPage);
+ fetchUsageLogs(nextPage, true);
+ };
+
+ const formatDate = (dateString: string) => {
+ return new Date(dateString).toLocaleString();
+ };
+
+ const formatCost = (cost: number) => {
+ return `$${cost.toFixed(4)}`;
+ };
+
+ const formatDateOnly = (dateString: string) => {
+ return new Date(dateString).toLocaleDateString('en-US', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ };
+
+ const handleThreadClick = (threadId: string, projectId: string) => {
+ // Navigate to the thread using the correct project_id
+ const threadUrl = `/projects/${projectId}/thread/${threadId}`;
+ window.open(threadUrl, '_blank');
+ };
+
+ // Group usage logs by date
+ const groupLogsByDate = (logs: UsageLogEntry[]): DailyUsage[] => {
+ const grouped = logs.reduce(
+ (acc, log) => {
+ const date = new Date(log.created_at).toDateString();
+
+ if (!acc[date]) {
+ acc[date] = {
+ date,
+ logs: [],
+ totalTokens: 0,
+ totalCost: 0,
+ requestCount: 0,
+ models: [],
+ };
+ }
+
+ acc[date].logs.push(log);
+ acc[date].totalTokens += log.total_tokens;
+ acc[date].totalCost += log.estimated_cost;
+ acc[date].requestCount += 1;
+
+ if (!acc[date].models.includes(log.content.model)) {
+ acc[date].models.push(log.content.model);
+ }
+
+ return acc;
+ },
+ {} as Record,
+ );
+
+ return Object.values(grouped).sort(
+ (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
+ );
+ };
+
+ if (isLocalMode()) {
+ return (
+
+
+ Usage Logs
+
+
+
+
+ Usage logs are not available in local development mode
+
+
+
+
+ );
+ }
+
+ if (loading) {
+ return (
+
+
+ Usage Logs
+ Loading your token usage history...
+
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ Usage Logs
+
+
+
+
+
+ );
+ }
+
+ const dailyUsage = groupLogsByDate(usageLogs);
+ const totalUsage = usageLogs.reduce(
+ (sum, log) => sum + log.estimated_cost,
+ 0,
+ );
+
+ return (
+
+ {/* Usage Logs Accordion */}
+
+
+ Daily Usage Logs
+
+ Your token usage organized by day, sorted by most recent
+
+
+
+ {dailyUsage.length === 0 ? (
+
+ ) : (
+ <>
+
+ {dailyUsage.map((day) => (
+
+
+
+
+
+ {formatDateOnly(day.date)}
+
+
+ {day.requestCount} request
+ {day.requestCount !== 1 ? 's' : ''} •{' '}
+ {day.models.join(', ')}
+
+
+
+
+ {formatCost(day.totalCost)}
+
+
+ {day.totalTokens.toLocaleString()} tokens
+
+
+
+
+
+
+
+
+
+ Time
+ Model
+
+ Total
+
+ Cost
+
+ Thread
+
+
+
+
+ {day.logs.map((log) => (
+
+
+ {new Date(
+ log.created_at,
+ ).toLocaleTimeString()}
+
+
+
+ {log.content.model}
+
+
+
+ {log.total_tokens.toLocaleString()}
+
+
+ {formatCost(log.estimated_cost)}
+
+
+
+
+
+ ))}
+
+
+
+
+
+ ))}
+
+
+ {hasMore && (
+
+
+
+ )}
+ >
+ )}
+
+
+
+ );
+}