From 71702a086c0bc32985820aa8e3bd25c29dd9beb6 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Wed, 29 Apr 2026 11:50:11 +0700 Subject: [PATCH 1/2] feat(compression): Phase 5 - analytics DB, APIs, dashboard UI, analytics tab, combo override, playground preview - Add compression_analytics table (migration 032) - Add compressionAnalytics.ts DB module (insert + summary query) - Add GET /api/analytics/compression endpoint (since=24h|7d|30d|all) - Add POST /api/compression/preview endpoint (Zod-validated) - Add CompressionAnalyticsTab.tsx with KPI cards, mode/provider bars, hourly chart - Wire Compression tab into /dashboard/analytics page - Add /dashboard/compression page (thin wrapper over CompressionSettingsTab) - Add ultra mode to MODES array in CompressionSettingsTab - Add per-combo compression override dropdown (persists via PUT /api/combos/:id) - Add compressionOverride field to updateComboSchema Zod validation - Add compression preview panel to PlaygroundMode (collapsible, CSS-only) --- .../analytics/CompressionAnalyticsTab.tsx | 299 ++++++++++++++++++ .../(dashboard)/dashboard/analytics/page.tsx | 4 + src/app/(dashboard)/dashboard/combos/page.tsx | 37 +++ .../dashboard/compression/page.tsx | 20 ++ .../components/CompressionSettingsTab.tsx | 6 + .../translator/components/PlaygroundMode.tsx | 123 +++++++ src/app/api/analytics/compression/route.ts | 29 ++ src/app/api/compression/preview/route.ts | 86 +++++ src/lib/db/compressionAnalytics.ts | 134 ++++++++ .../migrations/032_compression_analytics.sql | 13 + src/lib/localDb.ts | 10 + src/shared/validation/schemas.ts | 4 +- 12 files changed, 764 insertions(+), 1 deletion(-) create mode 100644 src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx create mode 100644 src/app/(dashboard)/dashboard/compression/page.tsx create mode 100644 src/app/api/analytics/compression/route.ts create mode 100644 src/app/api/compression/preview/route.ts create mode 100644 src/lib/db/compressionAnalytics.ts create mode 100644 src/lib/db/migrations/032_compression_analytics.sql diff --git a/src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx b/src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx new file mode 100644 index 0000000000..038b4afc71 --- /dev/null +++ b/src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx @@ -0,0 +1,299 @@ +/** + * Compression Analytics Tab + * + * Shows compression request stats from call_logs (request_type = 'compression'), + * mode breakdown, provider breakdown, and cost/token savings summary. + */ + +"use client"; + +import { useEffect, useState } from "react"; + +interface CompressionAnalyticsSummary { + totalRequests: number; + totalTokensSaved: number; + avgSavingsPct: number; + avgDurationMs: number; + byMode: Record; + byProvider: Record; + last24h: Array<{ hour: string; count: number; tokensSaved: number }>; +} + +function StatCard({ + icon, + label, + value, + sub, +}: { + icon: string; + label: string; + value: string | number; + sub?: string; +}) { + return ( +
+
+ {icon} + {label} +
+
{value}
+ {sub &&
{sub}
} +
+ ); +} + +function ModeBar({ + mode, + count, + total, + tokensSaved, +}: { + mode: string; + count: number; + total: number; + tokensSaved: number; +}) { + const pct = total > 0 ? Math.round((count / total) * 100) : 0; + return ( +
+
+ {mode} + + {count} requests · {tokensSaved.toLocaleString()} tokens saved + +
+
+
+
+
{pct}%
+
+ ); +} + +function ProviderBar({ + provider, + count, + total, + tokensSaved, +}: { + provider: string; + count: number; + total: number; + tokensSaved: number; +}) { + const pct = total > 0 ? Math.round((count / total) * 100) : 0; + return ( +
+
+ {provider} + + {count} requests · {tokensSaved.toLocaleString()} tokens saved + +
+
+
+
+
{pct}%
+
+ ); +} + +export default function CompressionAnalyticsTab() { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [since, setSince] = useState<"24h" | "7d" | "30d" | "all">("24h"); + + useEffect(() => { + fetch(`/api/analytics/compression?since=${since}`) + .then((r) => r.json()) + .then((d) => { + setStats(d); + setLoading(false); + }) + .catch((e) => { + setError(e.message); + setLoading(false); + }); + }, [since]); + + if (loading) { + return ( +
+ progress_activity + Loading compression analytics… +
+ ); + } + + if (error || !stats) { + return ( +
+ compress + {error || "No compression data yet."} +

+ Compression requests will appear here after the first request via /v1/chat/completions + with compression enabled. +

+
+ ); + } + + const modes = Object.entries(stats.byMode).sort(([, a], [, b]) => b.count - a.count); + const providers = Object.entries(stats.byProvider).sort(([, a], [, b]) => b.count - a.count); + + // Calculate max tokens for hourly chart scaling + const maxTokensPerHour = Math.max(...stats.last24h.map((h) => h.tokensSaved), 1); + const maxCountPerHour = Math.max(...stats.last24h.map((h) => h.count), 1); + + return ( +
+ {/* Time Range Selector */} +
+ {(["24h", "7d", "30d", "all"] as const).map((range) => ( + + ))} +
+ + {/* KPI Cards */} +
+ + + + +
+ + {/* Mode Breakdown */} + {modes.length > 0 && ( +
+

+ tune + Mode Breakdown +

+
+ {modes.map(([mode, data]) => ( + + ))} +
+
+ )} + + {/* Provider Breakdown */} + {providers.length > 0 && ( +
+

+ hub + Provider Breakdown +

+
+ {providers.map(([prov, data]) => ( + + ))} +
+
+ )} + + {/* Last 24h Hourly Chart (CSS-only height-based bars) */} + {stats.last24h.length > 0 && ( +
+

+ show_chart + Last 24 Hours +

+
+ {stats.last24h.map((entry, idx) => { + const countPct = (entry.count / maxCountPerHour) * 100; + const tokenPct = (entry.tokensSaved / maxTokensPerHour) * 100; + return ( +
+
+
+ {entry.count} +
+
+
+ {entry.hour.substring(0, 2)} +
+
+ ); + })} +
+
+
Max requests/hour: {maxCountPerHour}
+
Max tokens/hour: {maxTokensPerHour.toLocaleString()}
+
+
+ )} + + {/* Empty state */} + {stats.totalRequests === 0 && ( +
+ + compress + +

No compression data yet

+

+ Use POST /v1/chat/completions with + compression configuration to start tracking compression analytics. +

+
+ )} + + {/* Info note */} +
+ info + + Compression analytics: Token savings tracked per mode (off, lite, + standard, aggressive, ultra) and provider. Hover over charts for details. Use the time + selector to view different time periods. + +
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/analytics/page.tsx b/src/app/(dashboard)/dashboard/analytics/page.tsx index ba2022064f..7cf7a94ab1 100644 --- a/src/app/(dashboard)/dashboard/analytics/page.tsx +++ b/src/app/(dashboard)/dashboard/analytics/page.tsx @@ -4,6 +4,7 @@ import { useState, Suspense } from "react"; import { UsageAnalytics, CardSkeleton, SegmentedControl } from "@/shared/components"; import EvalsTab from "../usage/components/EvalsTab"; import SearchAnalyticsTab from "./SearchAnalyticsTab"; +import CompressionAnalyticsTab from "./CompressionAnalyticsTab"; import DiversityScoreCard from "./components/DiversityScoreCard"; import ProviderUtilizationTab from "./ProviderUtilizationTab"; import ComboHealthTab from "./ComboHealthTab"; @@ -19,6 +20,7 @@ export default function AnalyticsPage() { search: "Search request analytics — provider breakdown, cache hit rate, and cost tracking.", utilization: t("utilizationDescription"), comboHealth: t("comboHealthDescription"), + compression: "Compression analytics — token savings, mode breakdown, and provider stats.", }; return ( @@ -39,6 +41,7 @@ export default function AnalyticsPage() { { value: "search", label: "Search" }, { value: "utilization", label: t("utilization") }, { value: "comboHealth", label: t("comboHealth") }, + { value: "compression", label: "Compression" }, ]} value={activeTab} onChange={setActiveTab} @@ -56,6 +59,7 @@ export default function AnalyticsPage() { {activeTab === "search" && } {activeTab === "utilization" && } {activeTab === "comboHealth" && } + {activeTab === "compression" && }
); } diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index e5932b019a..d93c572aee 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -1495,6 +1495,29 @@ function ComboCard({ const tc = useTranslations("common"); const emailsVisible = useEmailPrivacyStore((s) => s.emailsVisible); const strategyDescription = getStrategyDescription(t, strategy); + const [compressionOverride, setCompressionOverride] = useState(combo.compressionOverride || ""); + const [isSavingCompression, setIsSavingCompression] = useState(false); + + const handleCompressionOverrideChange = async (value) => { + setCompressionOverride(value); + setIsSavingCompression(true); + try { + const response = await fetch(`/api/combos/${combo.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ compressionOverride: value || undefined }), + }); + if (!response.ok) { + console.error("Failed to update compression override"); + setCompressionOverride(combo.compressionOverride || ""); + } + } catch (error) { + console.error("Error updating compression override:", error); + setCompressionOverride(combo.compressionOverride || ""); + } finally { + setIsSavingCompression(false); + } + }; return ( +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx index 4618708e2c..b7a76afda3 100644 --- a/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx @@ -69,6 +69,12 @@ const MODES: { value: CompressionMode; labelKey: string; descKey: string; icon: descKey: "compressionModeAggressiveDesc", icon: "bolt", }, + { + value: "ultra", + labelKey: "compressionModeUltra", + descKey: "compressionModeUltraDesc", + icon: "rocket_launch", + }, ]; const ROLE_OPTIONS: { value: "user" | "assistant" | "system"; labelKey: string }[] = [ diff --git a/src/app/(dashboard)/dashboard/translator/components/PlaygroundMode.tsx b/src/app/(dashboard)/dashboard/translator/components/PlaygroundMode.tsx index b1e0a6de13..251dcf097f 100644 --- a/src/app/(dashboard)/dashboard/translator/components/PlaygroundMode.tsx +++ b/src/app/(dashboard)/dashboard/translator/components/PlaygroundMode.tsx @@ -9,6 +9,15 @@ import dynamic from "next/dynamic"; const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false }); +interface CompressionPreviewResult { + originalTokens: number; + compressedTokens: number; + tokensSaved: number; + savingsPct: number; + techniquesUsed: string[]; + durationMs: number; +} + export default function PlaygroundMode() { const t = useTranslations("translator"); const tc = useTranslations("common"); @@ -20,6 +29,14 @@ export default function PlaygroundMode() { const [translating, setTranslating] = useState(false); const [detecting, setDetecting] = useState(false); const [activeTemplate, setActiveTemplate] = useState(null); + + // Compression preview state + const [compressionMode, setCompressionMode] = useState("standard"); + const [compressionResult, setCompressionResult] = useState(null); + const [compressionLoading, setCompressionLoading] = useState(false); + const [compressionError, setCompressionError] = useState(null); + const [showCompressionPanel, setShowCompressionPanel] = useState(false); + const templates = useMemo(() => getExampleTemplates(t), [t]); // Auto-detect format when input changes @@ -108,6 +125,33 @@ export default function PlaygroundMode() { setDetectedFormat(null); }; + const handleCompressionPreview = async () => { + if (!inputContent.trim()) return; + let messages; + try { + const parsed = JSON.parse(inputContent); + messages = parsed.messages ?? [{ role: "user", content: inputContent }]; + } catch { + messages = [{ role: "user", content: inputContent }]; + } + setCompressionLoading(true); + setCompressionError(null); + try { + const res = await fetch("/api/compression/preview", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ messages, mode: compressionMode }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error ?? "Preview failed"); + setCompressionResult(data); + } catch (e: unknown) { + setCompressionError(e instanceof Error ? e.message : String(e)); + } finally { + setCompressionLoading(false); + } + }; + const srcMeta = FORMAT_META[sourceFormat] || FORMAT_META.openai; const tgtMeta = FORMAT_META[targetFormat] || FORMAT_META.openai; @@ -347,6 +391,85 @@ export default function PlaygroundMode() { )}
+ + {/* Compression Preview Panel */} + + + + {showCompressionPanel && ( +
+
+