diff --git a/src/app/(dashboard)/dashboard/logs/CompressionLogTab.tsx b/src/app/(dashboard)/dashboard/logs/CompressionLogTab.tsx new file mode 100644 index 0000000000..fafd577750 --- /dev/null +++ b/src/app/(dashboard)/dashboard/logs/CompressionLogTab.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card } from "@/shared/components"; +import { useTranslations } from "next-intl"; + +interface CompressionStats { + originalTokens: number; + compressedTokens: number; + savingsPercent: number; + techniquesUsed: string[]; + mode: string; + timestamp: number; + rulesApplied?: string[]; + durationMs?: number; +} + +interface LogEntry { + id: string; + timestamp: string; + model: string; + provider: string; + compressionStats?: CompressionStats | null; +} + +export default function CompressionLogTab() { + const t = useTranslations("settings"); + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch("/api/logs?filter=compressed&limit=50") + .then((r) => (r.ok ? r.json() : [])) + .then((data) => { + setLogs(Array.isArray(data) ? data : []); + }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, []); + + if (loading) { + return ( + +

{t("loading")}

+
+ ); + } + + if (logs.length === 0) { + return ( + +
+ compress +

{t("compressionLogTitle")}

+
+

{t("compressionLogEmpty")}

+
+ ); + } + + return ( + +
+ compress +

{t("compressionLogTitle")}

+
+ +
+ {logs.map((entry) => { + const stats = entry.compressionStats; + if (!stats) return null; + + return ( +
+
+
+ + {entry.provider}/{entry.model} + + + {stats.mode} + +
+
+ + {stats.originalTokens} → {stats.compressedTokens} {t("tokens")} + + = 25 + ? "text-emerald-400" + : stats.savingsPercent >= 10 + ? "text-yellow-400" + : "text-text-muted" + }`} + > + -{stats.savingsPercent.toFixed(1)}% + + {stats.durationMs !== undefined && {stats.durationMs}ms} +
+
+ + {stats.techniquesUsed.length > 0 && ( +
+ {stats.techniquesUsed.map((technique) => ( + + {technique} + + ))} +
+ )} + + {stats.rulesApplied && stats.rulesApplied.length > 0 && ( +
+ {stats.rulesApplied.map((rule) => ( + + {rule.replace(/_/g, " ")} + + ))} +
+ )} +
+ ); + })} +
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx new file mode 100644 index 0000000000..91bb0c71d0 --- /dev/null +++ b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx @@ -0,0 +1,416 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, Button } from "@/shared/components"; +import { useTranslations } from "next-intl"; + +type CompressionMode = "off" | "lite" | "standard" | "aggressive" | "ultra"; + +interface CavemanConfig { + enabled: boolean; + compressRoles: ("user" | "assistant" | "system")[]; + skipRules: string[]; + minMessageLength: number; + preservePatterns: string[]; +} + +interface CompressionConfig { + enabled: boolean; + defaultMode: CompressionMode; + autoTriggerTokens: number; + cacheMinutes: number; + preserveSystemPrompt: boolean; + comboOverrides: Record; + cavemanConfig?: CavemanConfig; +} + +const MODES: { value: CompressionMode; labelKey: string; descKey: string; icon: string }[] = [ + { + value: "off", + labelKey: "compressionModeOff", + descKey: "compressionModeOffDesc", + icon: "block", + }, + { + value: "lite", + labelKey: "compressionModeLite", + descKey: "compressionModeLiteDesc", + icon: "compress", + }, + { + value: "standard", + labelKey: "compressionModeStandard", + descKey: "compressionModeStandardDesc", + icon: "speed", + }, +]; + +const ROLE_OPTIONS: { value: "user" | "assistant" | "system"; labelKey: string }[] = [ + { value: "user", labelKey: "compressionRoleUser" }, + { value: "assistant", labelKey: "compressionRoleAssistant" }, + { value: "system", labelKey: "compressionRoleSystem" }, +]; + +const ALL_CAVEMAN_RULES = [ + "hedging_disclaimer", + "hedging_apology", + "hedging_uncertainty", + "redundant_please", + "redundant_note", + "redundant_remember", + "redundant_important_notice", + "structural_numbered_list_intro", + "structural_bullet_intro", + "structural_section_divider", + "structural_toc", + "whitespace_excessive_newlines", + "whitespace_trailing_spaces", + "markup_bold_emphasis", + "markup_italic_emphasis", + "markup_heading_decorations", + "content_repetition", + "question_to_directive", + "meta_instructional", + "filler_transition", +]; + +export default function CompressionSettingsTab() { + const t = useTranslations("settings"); + const [config, setConfig] = useState({ + enabled: false, + defaultMode: "off", + autoTriggerTokens: 0, + cacheMinutes: 5, + preserveSystemPrompt: true, + comboOverrides: {}, + cavemanConfig: { + enabled: true, + compressRoles: ["user"], + skipRules: [], + minMessageLength: 50, + preservePatterns: [], + }, + }); + const [saving, setSaving] = useState(false); + const [loading, setLoading] = useState(true); + const [status, setStatus] = useState<"" | "saved" | "error">(""); + + useEffect(() => { + fetch("/api/settings/compression") + .then((r) => (r.ok ? r.json() : null)) + .then((data) => { + if (data) setConfig(data); + }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, []); + + const save = async (updates: Partial) => { + const newConfig = { ...config, ...updates }; + setConfig(newConfig); + setSaving(true); + setStatus(""); + try { + const res = await fetch("/api/settings/compression", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newConfig), + }); + if (res.ok) { + setStatus("saved"); + setTimeout(() => setStatus(""), 2000); + } else { + setStatus("error"); + } + } catch { + setStatus("error"); + } finally { + setSaving(false); + } + }; + + const toggleCavemanRole = (role: "user" | "assistant" | "system") => { + const currentRoles = config.cavemanConfig?.compressRoles ?? ["user"]; + const newRoles = currentRoles.includes(role) + ? currentRoles.filter((r) => r !== role) + : [...currentRoles, role]; + save({ + cavemanConfig: { ...config.cavemanConfig!, compressRoles: newRoles }, + }); + }; + + const toggleCavemanRule = (rule: string) => { + const currentSkip = config.cavemanConfig?.skipRules ?? []; + const newSkip = currentSkip.includes(rule) + ? currentSkip.filter((r) => r !== rule) + : [...currentSkip, rule]; + save({ + cavemanConfig: { ...config.cavemanConfig!, skipRules: newSkip }, + }); + }; + + if (loading) { + return ( + +

{t("loading")}

+
+ ); + } + + return ( + +
+
+ +
+
+

{t("compressionTitle")}

+

{t("compressionDesc")}

+
+ {status === "saved" && ( + + check_circle {t("saved")} + + )} + {status === "error" && ( + + error {t("saveFailed")} + + )} +
+ +
+ + + {config.enabled && ( +
+

{t("compressionMode")}

+
+ {MODES.map((m) => ( + + ))} +
+
+ )} + + {config.enabled && ( +
+

{t("compressionGeneral")}

+ + + + + + +
+ )} + + {config.enabled && + config.defaultMode !== "off" && + config.defaultMode !== "lite" && + config.cavemanConfig && ( +
+
+
+

+ {t("compressionCavemanConfig")} +

+

+ {t("compressionCavemanConfigDesc")} +

+
+ +
+ + {config.cavemanConfig.enabled && ( + <> +
+

{t("compressionRoles")}

+
+ {ROLE_OPTIONS.map((opt) => ( + + ))} +
+
+ + + +
+

{t("compressionSkipRules")}

+

{t("compressionSkipRulesDesc")}

+
+ {ALL_CAVEMAN_RULES.map((rule) => ( + + ))} +
+
+ +
+

{t("compressionPreservePatterns")}

+

+ {t("compressionPreservePatternsDesc")} +

+