Spaces:
Runtime error
Runtime error
| "use client"; | |
| // CompressionPanel — the single-source engine-grid UI for compression. | |
| // | |
| // Renders the master on/off switch, one row per catalog engine (on/off + level + | |
| // link to its detail page), the cavemanOutput intensity row, the mcpAccessibility | |
| // toggle (its own endpoint / separate store), a read-only derived-pipeline preview, | |
| // and the general settings (auto-trigger tokens + preserve-system-prompt). | |
| // | |
| // Engine rows use the catalog label/description (hardcoded English) directly — NOT | |
| // i18n — so they stay deterministic. Human-facing chrome (master, general) keeps the | |
| // app's i18n via useTranslations("settings"). | |
| import Link from "next/link"; | |
| import { useEffect, useState } from "react"; | |
| import { useTranslations, useLocale } from "next-intl"; | |
| // Import Card/Toggle from their direct module paths rather than the @/shared/components | |
| // barrel: the barrel transitively pulls a heavy/Node-only module that hangs the | |
| // vitest/jsdom component test. Direct imports resolve identically under Next.js. | |
| import Card from "@/shared/components/Card"; | |
| import Toggle from "@/shared/components/Toggle"; | |
| import { | |
| ENGINE_IDS, | |
| engineMeta, | |
| } from "../../../../../../open-sse/services/compression/engineCatalog.ts"; | |
| import { | |
| OUTPUT_STYLE_IDS, | |
| outputStyleMeta, | |
| } from "../../../../../../open-sse/services/compression/outputStyles/catalog.ts"; | |
| import { deriveDefaultPlan } from "../../../../../../open-sse/services/compression/deriveDefaultPlan.ts"; | |
| import { | |
| DEFAULT_CONTEXT_BUDGET, | |
| type ContextBudgetConfig, | |
| } from "../../../../../../open-sse/services/compression/adaptiveCompression/types.ts"; | |
| import { formatAdaptiveTarget } from "./adaptiveTargetLabel.ts"; | |
| type CavemanIntensity = "lite" | "full" | "ultra"; | |
| interface EngineToggle { | |
| enabled: boolean; | |
| level?: string; | |
| } | |
| interface CavemanOutputModeConfig { | |
| enabled: boolean; | |
| intensity: CavemanIntensity; | |
| autoClarity: boolean; | |
| } | |
| interface CompressionConfig { | |
| enabled: boolean; | |
| autoTriggerTokens: number; | |
| preserveSystemPrompt: boolean; | |
| engines: Record<string, EngineToggle>; | |
| activeComboId: string | null; | |
| cavemanOutputMode?: CavemanOutputModeConfig; | |
| outputStyles?: Array<{ id: string; level: CavemanIntensity }>; | |
| // Phase 4 (B): two-tier `ultra` mode controls. | |
| // ultraEngine "heuristic" = Tier-A token pruner (default, byte-identical to pre-B); | |
| // "slm" = Tier-B LLMLingua-2 ONNX worker when available, else fail-open to Tier-A. | |
| ultraEngine?: "heuristic" | "slm"; | |
| // Best-effort pre-warm of the SLM model on enable / cold restart. Default false. | |
| ultraSlmPrewarm?: boolean; | |
| // Phase 4 (C): adaptive context-budget. Absent / mode:"off" = legacy auto-trigger. | |
| // The panel currently surfaces the computed target read-only; mode/policy editors are a | |
| // follow-up (the load/save path does not yet populate this field). | |
| contextBudget?: ContextBudgetConfig; | |
| } | |
| const CAVEMAN_OUTPUT_LEVELS: CavemanIntensity[] = ["lite", "full", "ultra"]; | |
| const DEFAULT_CONFIG: CompressionConfig = { | |
| enabled: false, | |
| autoTriggerTokens: 0, | |
| preserveSystemPrompt: true, | |
| engines: {}, | |
| activeComboId: null, | |
| cavemanOutputMode: { enabled: false, intensity: "full", autoClarity: true }, | |
| outputStyles: [], | |
| ultraEngine: "heuristic", | |
| ultraSlmPrewarm: false, | |
| }; | |
| function normalizeEngines(raw: unknown): Record<string, EngineToggle> { | |
| const engines: Record<string, EngineToggle> = {}; | |
| const source = (raw && typeof raw === "object" ? raw : {}) as Record<string, EngineToggle>; | |
| for (const id of ENGINE_IDS) { | |
| const cur = source[id]; | |
| engines[id] = cur ? { enabled: cur.enabled === true, ...(cur.level ? { level: cur.level } : {}) } : { enabled: false }; | |
| } | |
| return engines; | |
| } | |
| export default function CompressionPanel() { | |
| const t = useTranslations("settings"); | |
| // D-A6/§7: locale-gated styles (e.g. terse-cjk → zh) are only OFFERED under their locale. | |
| // Compare the UI language base ("zh-CN" → "zh") against the style's `locale`. | |
| const uiLang = (useLocale() || "en").split("-")[0]; | |
| const [config, setConfig] = useState<CompressionConfig>(DEFAULT_CONFIG); | |
| const [mcpAccessibility, setMcpAccessibility] = useState(true); | |
| const [loading, setLoading] = useState(true); | |
| const [saving, setSaving] = useState(false); | |
| const [status, setStatus] = useState<"" | "saved" | "error">(""); | |
| useEffect(() => { | |
| fetch("/api/settings/compression") | |
| .then((r) => (r.ok ? r.json() : null)) | |
| .then((data: Partial<CompressionConfig> | null) => { | |
| if (data) { | |
| setConfig({ | |
| ...DEFAULT_CONFIG, | |
| ...data, | |
| engines: normalizeEngines(data.engines), | |
| cavemanOutputMode: data.cavemanOutputMode ?? DEFAULT_CONFIG.cavemanOutputMode, | |
| outputStyles: data.outputStyles ?? DEFAULT_CONFIG.outputStyles, | |
| }); | |
| } | |
| }) | |
| .catch(() => {}) | |
| .finally(() => setLoading(false)); | |
| fetch("/api/settings/compression/mcp-accessibility") | |
| .then((r) => (r.ok ? r.json() : null)) | |
| .then((data: { enabled?: boolean } | null) => { | |
| if (data && typeof data.enabled === "boolean") setMcpAccessibility(data.enabled); | |
| }) | |
| .catch(() => {}); | |
| }, []); | |
| // Persist a merge-patch. The DB persists `engines` as one whole row, so callers that | |
| // touch an engine pass the full engines map to avoid dropping the other engines. | |
| const save = async (updates: Partial<CompressionConfig>) => { | |
| const next = { ...config, ...updates }; | |
| setConfig(next); | |
| setSaving(true); | |
| setStatus(""); | |
| try { | |
| const res = await fetch("/api/settings/compression", { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(updates), | |
| }); | |
| if (res.ok) { | |
| setStatus("saved"); | |
| setTimeout(() => setStatus(""), 2000); | |
| } else { | |
| setStatus("error"); | |
| } | |
| } catch { | |
| setStatus("error"); | |
| } finally { | |
| setSaving(false); | |
| } | |
| }; | |
| const setEngine = (id: string, patch: Partial<EngineToggle>) => { | |
| const engines = { | |
| ...config.engines, | |
| [id]: { ...(config.engines[id] ?? { enabled: false }), ...patch }, | |
| }; | |
| // Send the full engines map — the persistence layer stores it as one JSON row. | |
| save({ engines }); | |
| }; | |
| const setOutputStyle = (id: string, patch: { enabled?: boolean; level?: CavemanIntensity }) => { | |
| const current = config.outputStyles ?? []; | |
| const existing = current.find((s) => s.id === id); | |
| let next = current; | |
| if (patch.enabled === false) { | |
| next = current.filter((s) => s.id !== id); | |
| } else { | |
| const level = patch.level ?? existing?.level ?? "full"; | |
| next = existing | |
| ? current.map((s) => (s.id === id ? { id, level } : s)) | |
| : [...current, { id, level }]; | |
| } | |
| // Persist in catalog order so injection order is stable. | |
| const ordered = OUTPUT_STYLE_IDS.flatMap((sid) => { | |
| const hit = next.find((s) => s.id === sid); | |
| return hit ? [hit] : []; | |
| }); | |
| save({ outputStyles: ordered }); | |
| }; | |
| const toggleMcpAccessibility = async (enabled: boolean) => { | |
| setMcpAccessibility(enabled); | |
| try { | |
| await fetch("/api/settings/compression/mcp-accessibility", { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ enabled }), | |
| }); | |
| } catch { | |
| // Surface nothing — the row reflects optimistic local state; the next mount re-reads. | |
| } | |
| }; | |
| const derived = deriveDefaultPlan(config.engines, config.enabled); | |
| const derivedText = | |
| derived.mode === "off" | |
| ? "off" | |
| : derived.stackedPipeline.length > 0 | |
| ? `runs: ${derived.stackedPipeline.map((s) => s.engine).join(" → ")}` | |
| : `mode: ${derived.mode}`; | |
| if (loading) { | |
| return ( | |
| <Card className="p-6"> | |
| <p className="text-sm text-text-muted">{t("loading")}</p> | |
| </Card> | |
| ); | |
| } | |
| return ( | |
| <Card className="p-6" data-testid="compression-panel"> | |
| {/* Master */} | |
| <div className="mb-5 flex items-start justify-between gap-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="rounded-lg bg-blue-500/10 p-2 text-blue-500"> | |
| <span className="material-symbols-outlined text-[20px]" aria-hidden="true"> | |
| compress | |
| </span> | |
| </div> | |
| <div> | |
| <h3 className="text-lg font-semibold">{t("compressionTitle")}</h3> | |
| <p className="text-sm text-text-muted">{t("compressionDesc")}</p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| {status === "saved" && ( | |
| <span className="flex items-center gap-1 text-xs font-medium text-emerald-500"> | |
| <span className="material-symbols-outlined text-[14px]">check_circle</span>{" "} | |
| {t("saved")} | |
| </span> | |
| )} | |
| {status === "error" && ( | |
| <span className="flex items-center gap-1 text-xs font-medium text-red-500"> | |
| <span className="material-symbols-outlined text-[14px]">error</span>{" "} | |
| {t("saveFailed")} | |
| </span> | |
| )} | |
| <Toggle | |
| size="md" | |
| checked={config.enabled} | |
| onChange={(enabled) => save({ enabled })} | |
| disabled={saving} | |
| ariaLabel={t("compressionTitle")} | |
| /> | |
| </div> | |
| </div> | |
| {/* Derived pipeline preview */} | |
| <div | |
| data-testid="derived-pipeline-preview" | |
| className="mb-4 rounded-md border border-border/60 bg-bg-subtle px-3 py-2 text-xs text-text-muted" | |
| > | |
| <span className="font-medium text-text-main">Effective pipeline:</span> {derivedText} | |
| </div> | |
| {/* Adaptive context-budget — read-only computed target (Phase 4C, D-C1 transparency) */} | |
| <div | |
| data-testid="adaptive-target-preview" | |
| className="mb-4 rounded-md border border-border/60 bg-bg-subtle px-3 py-2 text-xs text-text-muted" | |
| > | |
| {formatAdaptiveTarget(config.contextBudget ?? DEFAULT_CONTEXT_BUDGET, 200000)} | |
| </div> | |
| {/* Engine grid */} | |
| <div className={`divide-y divide-border ${config.enabled ? "" : "opacity-60"}`}> | |
| {ENGINE_IDS.map((id) => { | |
| const meta = engineMeta(id); | |
| const engine = config.engines[id] ?? { enabled: false }; | |
| const levels = meta.levels; | |
| const level = engine.level ?? levels?.[0] ?? ""; | |
| return ( | |
| <div | |
| key={id} | |
| data-testid={`engine-row-${id}`} | |
| className="flex flex-col gap-2 py-3 sm:flex-row sm:items-center sm:justify-between" | |
| > | |
| <div className="min-w-0 flex-1"> | |
| <div className="flex items-center gap-2 text-sm font-medium text-text-main"> | |
| {meta.label} | |
| <Link | |
| href={`/dashboard/context/${id}`} | |
| className="rounded border border-border bg-bg-subtle px-1.5 py-0.5 text-[10px] uppercase tracking-wider text-text-muted hover:border-primary/40 hover:text-primary" | |
| > | |
| {id} | |
| </Link> | |
| </div> | |
| <p className="mt-0.5 text-xs text-text-muted">{meta.description}</p> | |
| </div> | |
| <div className="flex shrink-0 items-center gap-2"> | |
| {levels && ( | |
| <select | |
| value={level} | |
| onChange={(e) => setEngine(id, { level: e.target.value })} | |
| disabled={!config.enabled || !engine.enabled || saving} | |
| className="w-28 rounded border border-border bg-surface px-2 py-1 text-xs text-text-main" | |
| > | |
| {levels.map((lvl) => ( | |
| <option key={lvl} value={lvl}> | |
| {lvl} | |
| </option> | |
| ))} | |
| </select> | |
| )} | |
| <span data-testid={`engine-toggle-${id}`}> | |
| <Toggle | |
| size="sm" | |
| checked={engine.enabled} | |
| onChange={(enabled) => setEngine(id, { enabled })} | |
| disabled={!config.enabled || saving} | |
| ariaLabel={meta.label} | |
| /> | |
| </span> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| {/* Output Styles — response-output instruction injection (Phase 4A, catalog-driven) */} | |
| <div className="mt-2 flex flex-col gap-3 border-t border-border/30 py-3"> | |
| <div className="min-w-0 flex-1"> | |
| <p className="text-sm font-medium text-text-main"> | |
| {t("compressionSettingsOutputStyles")} | |
| </p> | |
| <p className="mt-0.5 text-xs text-text-muted"> | |
| Inject response-shaping instructions without rewriting provider output. Combine freely. | |
| </p> | |
| </div> | |
| {OUTPUT_STYLE_IDS.filter((id) => { | |
| const m = outputStyleMeta(id); | |
| return !m?.locale || m.locale === uiLang; | |
| }).map((id) => { | |
| const meta = outputStyleMeta(id); | |
| const sel = config.outputStyles?.find((s) => s.id === id); | |
| return ( | |
| <div | |
| key={id} | |
| data-testid={`output-style-row-${id}`} | |
| className="flex items-center justify-between gap-2" | |
| > | |
| <div className="min-w-0"> | |
| <p className="text-sm text-text-main">{meta.label}</p> | |
| {meta.description && ( | |
| <p className="text-xs text-text-muted">{meta.description}</p> | |
| )} | |
| </div> | |
| <div className="flex shrink-0 items-center gap-2"> | |
| <select | |
| data-testid={`output-style-level-${id}`} | |
| value={sel?.level ?? "full"} | |
| onChange={(e) => | |
| setOutputStyle(id, { level: e.target.value as CavemanIntensity }) | |
| } | |
| disabled={!sel || saving} | |
| className="w-28 rounded border border-border bg-surface px-2 py-1 text-xs text-text-main" | |
| > | |
| {CAVEMAN_OUTPUT_LEVELS.map((lvl) => ( | |
| <option key={lvl} value={lvl}> | |
| {lvl} | |
| </option> | |
| ))} | |
| </select> | |
| <span data-testid={`output-style-toggle-${id}`}> | |
| <Toggle | |
| size="sm" | |
| checked={Boolean(sel)} | |
| onChange={(enabled) => setOutputStyle(id, { enabled })} | |
| disabled={saving} | |
| ariaLabel={meta.label} | |
| /> | |
| </span> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| {/* Ultra SLM tier — Phase 4 (B): pick the `ultra`-mode engine (heuristic Tier-A | |
| or the opt-in LLMLingua-2 SLM Tier-B) + best-effort pre-warm. */} | |
| <div className="mt-2 flex flex-col gap-3 border-t border-border/30 py-3"> | |
| <label className="flex items-center justify-between"> | |
| <span className="text-sm font-medium text-text-main"> | |
| {t("compressionUltraEngine")} | |
| </span> | |
| <select | |
| data-testid="ultra-engine-select" | |
| value={config.ultraEngine ?? "heuristic"} | |
| onChange={(e) => | |
| save({ ultraEngine: e.target.value === "slm" ? "slm" : "heuristic" }) | |
| } | |
| disabled={saving} | |
| className="w-44 rounded border border-border bg-surface px-2 py-1 text-sm text-text-main" | |
| > | |
| <option value="heuristic">{t("compressionUltraEngineHeuristic")}</option> | |
| <option value="slm">{t("compressionUltraEngineSlm")}</option> | |
| </select> | |
| </label> | |
| {config.ultraEngine === "slm" && ( | |
| <> | |
| <p className="text-xs text-text-muted">{t("compressionUltraSlmHint")}</p> | |
| <label className="flex items-center justify-between"> | |
| <span className="text-sm text-text-muted"> | |
| {t("compressionUltraSlmPrewarm")} | |
| </span> | |
| <span data-testid="ultra-slm-prewarm-toggle"> | |
| <Toggle | |
| size="sm" | |
| checked={config.ultraSlmPrewarm ?? false} | |
| onChange={(ultraSlmPrewarm) => save({ ultraSlmPrewarm })} | |
| disabled={saving} | |
| ariaLabel={t("compressionUltraSlmPrewarm")} | |
| /> | |
| </span> | |
| </label> | |
| </> | |
| )} | |
| </div> | |
| {/* mcpAccessibility — writes its own endpoint / separate store */} | |
| <div className="flex flex-col gap-2 border-t border-border/30 py-3 sm:flex-row sm:items-center sm:justify-between"> | |
| <div className="min-w-0 flex-1"> | |
| <p className="text-sm font-medium text-text-main">{t("mcpAccessibilityTitle")}</p> | |
| <p className="mt-0.5 text-xs text-text-muted"> | |
| Scopes MCP tool outputs (separate store). | |
| </p> | |
| </div> | |
| <span data-testid="mcp-accessibility-toggle"> | |
| <Toggle | |
| size="sm" | |
| checked={mcpAccessibility} | |
| onChange={toggleMcpAccessibility} | |
| ariaLabel={t("mcpAccessibilityTitle")} | |
| /> | |
| </span> | |
| </div> | |
| {/* General */} | |
| <div className="space-y-3 border-t border-border/30 pt-4"> | |
| <h4 className="text-sm font-medium text-text-main">{t("compressionGeneral")}</h4> | |
| <label className="flex items-center justify-between"> | |
| <span className="text-sm text-text-muted">{t("compressionAutoTrigger")}</span> | |
| <div className="flex items-center gap-2"> | |
| <input | |
| type="number" | |
| min={0} | |
| max={100000} | |
| value={config.autoTriggerTokens} | |
| onChange={(e) => save({ autoTriggerTokens: parseInt(e.target.value) || 0 })} | |
| className="w-24 rounded border border-border bg-surface px-2 py-1 text-sm text-text-main" | |
| /> | |
| <span className="text-xs text-text-muted">{t("tokens")}</span> | |
| </div> | |
| </label> | |
| <label className="flex items-center justify-between"> | |
| <span className="text-sm text-text-muted">{t("compressionPreserveSystem")}</span> | |
| <span data-testid="preserve-system-toggle"> | |
| <Toggle | |
| size="sm" | |
| checked={config.preserveSystemPrompt} | |
| onChange={(preserveSystemPrompt) => save({ preserveSystemPrompt })} | |
| disabled={saving} | |
| ariaLabel={t("compressionPreserveSystem")} | |
| /> | |
| </span> | |
| </label> | |
| </div> | |
| </Card> | |
| ); | |
| } | |