mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 06:32:24 +00:00
330 lines
12 KiB
TypeScript
330 lines
12 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
import { useT } from '../../../lib/i18n/I18nContext';
|
|
import {
|
|
getTokenjuiceSavings,
|
|
getTokenjuiceSettings,
|
|
resetTokenjuiceSavings,
|
|
type SavingsStats,
|
|
type TokenjuiceSettings,
|
|
type TokenjuiceSettingsPatch,
|
|
updateTokenjuiceSettings,
|
|
} from '../../../utils/tauriCommands/tokenjuice';
|
|
import Button from '../../ui/Button';
|
|
import {
|
|
SettingsNumberField,
|
|
SettingsRow,
|
|
SettingsSection,
|
|
SettingsStatusLine,
|
|
SettingsSwitch,
|
|
} from '../controls';
|
|
import SettingsPanel from '../layout/SettingsPanel';
|
|
|
|
function formatInt(n: number): string {
|
|
return Math.round(n).toLocaleString();
|
|
}
|
|
|
|
function formatUsd(n: number): string {
|
|
if (n > 0 && n < 0.01) return '<$0.01';
|
|
return `$${n.toFixed(2)}`;
|
|
}
|
|
|
|
function formatBytes(n: number): string {
|
|
if (n < 1024) return `${n} B`;
|
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
|
|
interface StatTileProps {
|
|
label: string;
|
|
value: string;
|
|
hint?: string;
|
|
}
|
|
|
|
const StatTile = ({ label, value, hint }: StatTileProps) => (
|
|
<div className="rounded-2xl border border-stone-200 dark:border-neutral-800 p-4 bg-gradient-to-br from-white to-stone-50 dark:from-neutral-900 dark:to-neutral-950">
|
|
<div className="text-xs font-medium text-neutral-500 dark:text-neutral-400">{label}</div>
|
|
<div className="mt-1 text-2xl font-semibold text-neutral-900 dark:text-neutral-50 tabular-nums">
|
|
{value}
|
|
</div>
|
|
{hint && <div className="mt-0.5 text-xs text-neutral-400 dark:text-neutral-500">{hint}</div>}
|
|
</div>
|
|
);
|
|
|
|
const TokenUsagePanel = () => {
|
|
const { t } = useT();
|
|
|
|
const [settings, setSettings] = useState<TokenjuiceSettings | null>(null);
|
|
const [savings, setSavings] = useState<SavingsStats | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [saving, setSaving] = useState(false);
|
|
const [savedNote, setSavedNote] = useState<string | null>(null);
|
|
|
|
// Local editable string for the token-threshold field.
|
|
const [minTokensInput, setMinTokensInput] = useState('');
|
|
const savedMinTokensRef = useRef<number | null>(null);
|
|
|
|
const loadSavings = useCallback(async () => {
|
|
try {
|
|
setSavings(await getTokenjuiceSavings());
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : String(e));
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
const load = async () => {
|
|
try {
|
|
const [s, v] = await Promise.all([getTokenjuiceSettings(), getTokenjuiceSavings()]);
|
|
if (cancelled) return;
|
|
setSettings(s);
|
|
setSavings(v);
|
|
setMinTokensInput(String(s.ccr_min_tokens));
|
|
savedMinTokensRef.current = s.ccr_min_tokens;
|
|
} catch (e) {
|
|
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
}
|
|
};
|
|
void load();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
const patch = useCallback(
|
|
async (p: TokenjuiceSettingsPatch) => {
|
|
setSaving(true);
|
|
setError(null);
|
|
setSavedNote(null);
|
|
try {
|
|
const next = await updateTokenjuiceSettings(p);
|
|
setSettings(next);
|
|
setMinTokensInput(String(next.ccr_min_tokens));
|
|
savedMinTokensRef.current = next.ccr_min_tokens;
|
|
setSavedNote(t('settings.tokenUsage.saved'));
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : String(e));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
},
|
|
[t]
|
|
);
|
|
|
|
const commitMinTokens = useCallback(() => {
|
|
const parsed = Number.parseInt(minTokensInput, 10);
|
|
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
setMinTokensInput(String(savedMinTokensRef.current ?? 0));
|
|
return;
|
|
}
|
|
if (parsed === savedMinTokensRef.current) return;
|
|
void patch({ ccr_min_tokens: parsed });
|
|
}, [minTokensInput, patch]);
|
|
|
|
const onReset = useCallback(async () => {
|
|
try {
|
|
await resetTokenjuiceSavings();
|
|
await loadSavings();
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : String(e));
|
|
}
|
|
}, [loadSavings]);
|
|
|
|
const total = savings?.total;
|
|
|
|
return (
|
|
<SettingsPanel description={t('settings.tokenUsage.menuDesc')}>
|
|
{/* ── Savings statistics ─────────────────────────────────────────── */}
|
|
<SettingsSection
|
|
title={t('settings.tokenUsage.savingsTitle')}
|
|
description={
|
|
savings
|
|
? t('settings.tokenUsage.attributedTo').replace('{model}', savings.attributionModel)
|
|
: undefined
|
|
}>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 px-1">
|
|
<StatTile
|
|
label={t('settings.tokenUsage.tokensSaved')}
|
|
value={total ? formatInt(total.tokensSaved) : '—'}
|
|
hint={
|
|
total
|
|
? t('settings.tokenUsage.overEvents').replace('{count}', formatInt(total.events))
|
|
: undefined
|
|
}
|
|
/>
|
|
<StatTile
|
|
label={t('settings.tokenUsage.costSaved')}
|
|
value={total ? formatUsd(total.costSavedUsd) : '—'}
|
|
/>
|
|
<StatTile
|
|
label={t('settings.tokenUsage.cacheOccupancy')}
|
|
value={savings ? formatInt(savings.cache.entries) : '—'}
|
|
hint={savings ? formatBytes(savings.cache.bytes) : undefined}
|
|
/>
|
|
<StatTile
|
|
label={t('settings.tokenUsage.compactions')}
|
|
value={total ? formatInt(total.events) : '—'}
|
|
/>
|
|
</div>
|
|
|
|
{savings && Object.keys(savings.byCompressor).length > 0 && (
|
|
<div className="px-1 mt-3">
|
|
<div className="text-xs font-medium text-neutral-500 dark:text-neutral-400 mb-1.5">
|
|
{t('settings.tokenUsage.byCompressor')}
|
|
</div>
|
|
<div className="rounded-xl border border-stone-200 dark:border-neutral-800 divide-y divide-stone-100 dark:divide-neutral-800">
|
|
{Object.entries(savings.byCompressor)
|
|
.sort((a, b) => b[1].tokensSaved - a[1].tokensSaved)
|
|
.map(([name, b]) => (
|
|
<div key={name} className="flex items-center justify-between px-3 py-2 text-sm">
|
|
<span className="font-mono text-neutral-700 dark:text-neutral-300">{name}</span>
|
|
<span className="tabular-nums text-neutral-500 dark:text-neutral-400">
|
|
{formatInt(b.tokensSaved)} tok · {formatUsd(b.costSavedUsd)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="px-1 mt-3 flex items-center gap-2">
|
|
<Button variant="secondary" size="sm" onClick={() => void loadSavings()}>
|
|
{t('settings.tokenUsage.refresh')}
|
|
</Button>
|
|
<Button variant="secondary" size="sm" onClick={() => void onReset()}>
|
|
{t('settings.tokenUsage.reset')}
|
|
</Button>
|
|
</div>
|
|
</SettingsSection>
|
|
|
|
{/* ── Compression toggles ────────────────────────────────────────── */}
|
|
<SettingsSection
|
|
title={t('settings.tokenUsage.compressionTitle')}
|
|
description={t('settings.tokenUsage.compressionDesc')}>
|
|
<div className="rounded-xl border border-stone-200 dark:border-neutral-800 divide-y divide-stone-100 dark:divide-neutral-800">
|
|
<SettingsRow
|
|
label={t('settings.tokenUsage.routerEnabled')}
|
|
description={t('settings.tokenUsage.routerEnabledDesc')}
|
|
control={
|
|
<SettingsSwitch
|
|
id="tj-router-enabled"
|
|
checked={settings?.router_enabled ?? false}
|
|
onCheckedChange={v => void patch({ router_enabled: v })}
|
|
aria-label={t('settings.tokenUsage.routerEnabled')}
|
|
/>
|
|
}
|
|
/>
|
|
<SettingsRow
|
|
label={t('settings.tokenUsage.search')}
|
|
description={t('settings.tokenUsage.searchDesc')}
|
|
control={
|
|
<SettingsSwitch
|
|
id="tj-search-enabled"
|
|
checked={settings?.search_enabled ?? false}
|
|
onCheckedChange={v => void patch({ search_enabled: v })}
|
|
aria-label={t('settings.tokenUsage.search')}
|
|
/>
|
|
}
|
|
/>
|
|
<SettingsRow
|
|
label={t('settings.tokenUsage.code')}
|
|
description={t('settings.tokenUsage.codeDesc')}
|
|
control={
|
|
<SettingsSwitch
|
|
id="tj-code-enabled"
|
|
checked={settings?.code_enabled ?? false}
|
|
onCheckedChange={v => void patch({ code_enabled: v })}
|
|
aria-label={t('settings.tokenUsage.code')}
|
|
/>
|
|
}
|
|
/>
|
|
<SettingsRow
|
|
label={t('settings.tokenUsage.html')}
|
|
description={t('settings.tokenUsage.htmlDesc')}
|
|
control={
|
|
<SettingsSwitch
|
|
id="tj-html-enabled"
|
|
checked={settings?.html_enabled ?? false}
|
|
onCheckedChange={v => void patch({ html_enabled: v })}
|
|
aria-label={t('settings.tokenUsage.html')}
|
|
/>
|
|
}
|
|
/>
|
|
<SettingsRow
|
|
label={t('settings.tokenUsage.ml')}
|
|
description={t('settings.tokenUsage.mlDesc')}
|
|
control={
|
|
<SettingsSwitch
|
|
id="tj-ml-enabled"
|
|
checked={settings?.ml_compression_enabled ?? false}
|
|
onCheckedChange={v => void patch({ ml_compression_enabled: v })}
|
|
aria-label={t('settings.tokenUsage.ml')}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
</SettingsSection>
|
|
|
|
{/* ── CCR cache ──────────────────────────────────────────────────── */}
|
|
<SettingsSection
|
|
title={t('settings.tokenUsage.ccrTitle')}
|
|
description={t('settings.tokenUsage.ccrDesc')}>
|
|
<div className="rounded-xl border border-stone-200 dark:border-neutral-800 divide-y divide-stone-100 dark:divide-neutral-800">
|
|
<SettingsRow
|
|
label={t('settings.tokenUsage.ccrEnabled')}
|
|
description={t('settings.tokenUsage.ccrEnabledDesc')}
|
|
control={
|
|
<SettingsSwitch
|
|
id="tj-ccr-enabled"
|
|
checked={settings?.ccr_enabled ?? false}
|
|
onCheckedChange={v => void patch({ ccr_enabled: v })}
|
|
aria-label={t('settings.tokenUsage.ccrEnabled')}
|
|
/>
|
|
}
|
|
/>
|
|
<SettingsRow
|
|
stacked
|
|
label={t('settings.tokenUsage.ccrMinTokens')}
|
|
description={t('settings.tokenUsage.ccrMinTokensDesc')}
|
|
control={
|
|
<SettingsNumberField
|
|
id="tj-ccr-min-tokens"
|
|
value={minTokensInput}
|
|
onChange={setMinTokensInput}
|
|
onCommit={commitMinTokens}
|
|
min={0}
|
|
max={1000000}
|
|
unit={t('settings.tokenUsage.tokensUnit')}
|
|
aria-label={t('settings.tokenUsage.ccrMinTokens')}
|
|
/>
|
|
}
|
|
/>
|
|
<SettingsRow
|
|
label={t('settings.tokenUsage.ccrDisk')}
|
|
description={t('settings.tokenUsage.ccrDiskDesc')}
|
|
control={
|
|
<SettingsSwitch
|
|
id="tj-ccr-disk"
|
|
checked={settings?.ccr_disk_enabled ?? false}
|
|
onCheckedChange={v => void patch({ ccr_disk_enabled: v })}
|
|
aria-label={t('settings.tokenUsage.ccrDisk')}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="px-1 mt-2">
|
|
<SettingsStatusLine
|
|
saving={saving}
|
|
savedNote={savedNote}
|
|
error={error}
|
|
savingLabel={t('settings.tokenUsage.saving')}
|
|
/>
|
|
</div>
|
|
</SettingsSection>
|
|
</SettingsPanel>
|
|
);
|
|
};
|
|
|
|
export default TokenUsagePanel;
|