files cant't be seen by LLM, but everything else works.
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { RefreshCw, Trash2, BarChart3 } from 'lucide-react';
|
||||
|
||||
interface TokenUsageSummary {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
requestCount: number;
|
||||
}
|
||||
|
||||
interface ModelBreakdown {
|
||||
[key: string]: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
requestCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface TokenUsageHistory {
|
||||
timestamp: number;
|
||||
conversationId: string;
|
||||
model: string;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
}
|
||||
|
||||
interface TokenUsageData {
|
||||
summary: TokenUsageSummary;
|
||||
modelBreakdown: ModelBreakdown;
|
||||
history: TokenUsageHistory[];
|
||||
}
|
||||
|
||||
type TimePeriod = 'all' | 'today' | 'week';
|
||||
|
||||
export default function TokenUsagePanel() {
|
||||
const [data, setData] = useState<TokenUsageData | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [period, setPeriod] = useState<TimePeriod>('all');
|
||||
const [expandedModel, setExpandedModel] = useState<string | null>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/token-usage?period=${period}`);
|
||||
const result = await res.json();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch token usage:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [period]);
|
||||
|
||||
const handleClear = async () => {
|
||||
if (!confirm('Clear all token usage data?')) return;
|
||||
try {
|
||||
await fetch('/api/token-usage', { method: 'DELETE' });
|
||||
setData(null);
|
||||
fetchData();
|
||||
} catch (err) {
|
||||
console.error('Failed to clear token usage:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const formatNumber = (n: number) => n.toLocaleString();
|
||||
|
||||
const formatTime = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-[var(--color-text)]">
|
||||
<BarChart3 size={20} className="text-[var(--color-accent)]" />
|
||||
<h3 className="text-sm font-medium">Token Usage</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={fetchData}
|
||||
disabled={loading}
|
||||
className="p-2 rounded-lg hover:bg-[var(--color-surface-hover)] text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw size={16} className={loading ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClear}
|
||||
disabled={!data || data.summary.requestCount === 0}
|
||||
className="p-2 rounded-lg hover:bg-[var(--color-surface-hover)] text-[var(--color-text-secondary)] hover:text-red-400 transition-colors disabled:opacity-50 disabled:pointer-events-none"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Period filter */}
|
||||
<div className="flex gap-1 p-1 rounded-lg bg-[var(--color-bg)] border border-[var(--color-border)]">
|
||||
{(['all', 'today', 'week'] as TimePeriod[]).map((p) => {
|
||||
const labels = { all: 'All', today: 'Today', week: 'Week' };
|
||||
return (
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => setPeriod(p)}
|
||||
className={`flex-1 px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
||||
period === p
|
||||
? 'bg-[var(--color-accent)] text-white'
|
||||
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text)]'
|
||||
}`}
|
||||
>
|
||||
{labels[p]}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
{data && data.summary.requestCount > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-3 rounded-lg bg-[var(--color-bg)] border border-[var(--color-border)]">
|
||||
<p className="text-xs text-[var(--color-text-muted)] mb-1">Total Tokens</p>
|
||||
<p className="text-lg font-semibold text-[var(--color-text)]">
|
||||
{formatNumber(data.summary.totalTokens)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-[var(--color-bg)] border border-[var(--color-border)]">
|
||||
<p className="text-xs text-[var(--color-text-muted)] mb-1">Requests</p>
|
||||
<p className="text-lg font-semibold text-[var(--color-text)]">
|
||||
{data.summary.requestCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-[var(--color-bg)] border border-[var(--color-border)]">
|
||||
<p className="text-xs text-[var(--color-text-muted)] mb-1">Prompt Tokens</p>
|
||||
<p className="text-sm font-medium text-[var(--color-text)]">
|
||||
{formatNumber(data.summary.promptTokens)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-[var(--color-bg)] border border-[var(--color-border)]">
|
||||
<p className="text-xs text-[var(--color-text-muted)] mb-1">Completion Tokens</p>
|
||||
<p className="text-sm font-medium text-[var(--color-text)]">
|
||||
{formatNumber(data.summary.completionTokens)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-[var(--color-text-muted)]">
|
||||
{loading ? 'Loading...' : 'No token usage data yet'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model breakdown */}
|
||||
{data && Object.keys(data.modelBreakdown).length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-[var(--color-text)]">By Model</p>
|
||||
{Object.entries(data.modelBreakdown)
|
||||
.sort((a, b) => b[1].totalTokens - a[1].totalTokens)
|
||||
.map(([model, stats]) => (
|
||||
<div
|
||||
key={model}
|
||||
className="rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] overflow-hidden"
|
||||
>
|
||||
<button
|
||||
onClick={() => setExpandedModel(expandedModel === model ? null : model)}
|
||||
className="w-full px-4 py-3 flex items-center justify-between text-left hover:bg-[var(--color-surface-hover)] transition-colors"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-[var(--color-text)] truncate">
|
||||
{model}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--color-text-muted)]">
|
||||
{formatNumber(stats.totalTokens)} tokens · {stats.requestCount} requests
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right shrink-0 ml-4">
|
||||
<p className="text-xs text-[var(--color-text-secondary)]">
|
||||
{formatNumber(stats.promptTokens)} / {formatNumber(stats.completionTokens)}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{expandedModel === model && (
|
||||
<div className="px-4 pb-3 border-t border-[var(--color-border)] pt-3">
|
||||
{/* Simple bar chart */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-[var(--color-text-muted)] w-16">Prompt</span>
|
||||
<div className="flex-1 h-3 rounded bg-[var(--color-surface)] overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded bg-[var(--color-accent)] transition-all"
|
||||
style={{
|
||||
width: `${
|
||||
stats.totalTokens > 0
|
||||
? (stats.promptTokens / stats.totalTokens) * 100
|
||||
: 0
|
||||
}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-[var(--color-text)] w-16 text-right">
|
||||
{formatNumber(stats.promptTokens)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-[var(--color-text-muted)] w-16">Completion</span>
|
||||
<div className="flex-1 h-3 rounded bg-[var(--color-surface)] overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded bg-green-500 transition-all"
|
||||
style={{
|
||||
width: `${
|
||||
stats.totalTokens > 0
|
||||
? (stats.completionTokens / stats.totalTokens) * 100
|
||||
: 0
|
||||
}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-[var(--color-text)] w-16 text-right">
|
||||
{formatNumber(stats.completionTokens)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show recent history for this model */}
|
||||
{data.history.filter((h) => h.model === model).length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-[var(--color-border)]">
|
||||
<p className="text-xs text-[var(--color-text-muted)] mb-2">Recent requests</p>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{data.history
|
||||
.filter((h) => h.model === model)
|
||||
.slice(-5)
|
||||
.reverse()
|
||||
.map((entry, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between text-xs px-2 py-1 rounded bg-[var(--color-surface)]"
|
||||
>
|
||||
<span className="text-[var(--color-text-muted)]">{formatTime(entry.timestamp)}</span>
|
||||
<span className="text-[var(--color-text)]">
|
||||
{formatNumber(entry.totalTokens)} tokens
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* History timeline */}
|
||||
{data && data.history.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-[var(--color-text)]">Recent History</p>
|
||||
{data.history.slice(-10).reverse().map((entry, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-3 text-xs px-3 py-2 rounded-lg bg-[var(--color-bg)] border border-[var(--color-border)]"
|
||||
>
|
||||
<span className="text-[var(--color-text-muted)] w-12 shrink-0">
|
||||
{formatTime(entry.timestamp)}
|
||||
</span>
|
||||
<span className="text-[var(--color-text)] truncate flex-1">{entry.model}</span>
|
||||
<span className="text-[var(--color-text-secondary)] shrink-0">
|
||||
{formatNumber(entry.totalTokens)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user