Spaces:
Runtime error
Runtime error
File size: 10,946 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | "use client";
// Compression Hub β the single place to understand and control compression.
//
// IMPORTANT (hydration): this component deliberately does NOT use `useTranslations`.
// The previous combos redesign failed to hydrate on the production build; the only
// structural difference from the engine pages (which hydrate fine) was a page-level
// `useTranslations("contextCombos")`. To stay on the proven-good path, strings here
// remain literal English text, exactly like `EngineConfigPage`.
//
// Phase 2: this Hub is now a thin overview. The master toggle, mode selector, and the
// reorderable per-layer pipeline live in the panel at /dashboard/context/settings and
// in the named-combo editor. Here we expose a single active-profile selector
// (Default-from-panel | a named combo) + a read-only preview.
import { useCallback, useEffect, useState } from "react";
// ββ Types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
type CompressionMode = "off" | "lite" | "standard" | "aggressive" | "ultra" | "rtk" | "stacked";
interface CompressionSettings {
enabled: boolean;
defaultMode: CompressionMode;
activeComboId?: string | null;
contextEditing?: { enabled: boolean };
[key: string]: unknown;
}
interface NamedCombo {
id: string;
name: string;
pipeline: { engine: string; intensity?: string }[];
}
// ββ Sub-components ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function Toggle({
checked,
onChange,
ariaLabel,
}: {
checked: boolean;
onChange: () => void;
ariaLabel: string;
}) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
aria-label={ariaLabel}
onClick={onChange}
className={`relative w-10 h-5 rounded-full transition-colors ${
checked ? "bg-green-500" : "bg-border"
}`}
>
<span
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
checked ? "left-5" : "left-0.5"
}`}
/>
</button>
);
}
// ββ Main component ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export default function CompressionHub() {
const [settings, setSettings] = useState<CompressionSettings | null>(null);
const [combos, setCombos] = useState<NamedCombo[]>([]);
const [loading, setLoading] = useState(true);
const [explainerOpen, setExplainerOpen] = useState(false);
const [error, setError] = useState<string | null>(null);
// ββ Initial load (parallel) ββββββββββββββββββββββββββββββββββββββββββββββββββ
useEffect(() => {
let cancelled = false;
async function load() {
setLoading(true);
const asJson = (r: Response) => (r.ok ? r.json() : null);
const [settingsData, combosData] = await Promise.all([
fetch("/api/settings/compression")
.then(asJson)
.catch(() => null),
fetch("/api/context/combos")
.then(asJson)
.catch(() => null),
]);
if (cancelled) return;
if (settingsData) {
setSettings(settingsData as CompressionSettings);
} else {
setSettings({ enabled: false, defaultMode: "off", contextEditing: { enabled: false } });
}
if (Array.isArray(combosData?.combos)) {
setCombos(combosData.combos as NamedCombo[]);
}
setLoading(false);
}
void load();
return () => {
cancelled = true;
};
}, []);
// ββ Settings mutations βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const saveSettings = useCallback(
async (patch: Partial<CompressionSettings>) => {
if (!settings) return;
const next = { ...settings, ...patch };
setSettings(next);
setError(null);
try {
const res = await fetch("/api/settings/compression", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(next),
});
if (!res.ok) {
setSettings(settings); // revert
setError("Failed to save settings.");
}
} catch {
setSettings(settings);
setError("Failed to save settings.");
}
},
[settings]
);
// ββ Derived state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (loading) {
return (
<div className="flex items-center justify-center p-10 text-sm text-text-muted">
Loading...
</div>
);
}
const activeCombo = combos.find((c) => c.id === settings?.activeComboId) ?? null;
const activePipelineText = activeCombo
? activeCombo.pipeline.map((s) => s.engine).join(" β ")
: "";
return (
<section className="flex flex-col gap-5 rounded-xl border border-primary/30 bg-surface p-5">
{/* ββ Header ββ */}
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-[26px] text-primary" aria-hidden="true">
hub
</span>
<div>
<h1 className="text-xl font-bold text-text-main">Compression Hub</h1>
<p className="text-sm text-text-muted">
Pick which compression profile runs globally.
</p>
</div>
</div>
<button
type="button"
onClick={() => setExplainerOpen((v) => !v)}
className="shrink-0 rounded-lg border border-border px-3 py-1.5 text-xs text-text-main hover:bg-bg"
>
{explainerOpen ? "Hide explanation" : "How it works"}
</button>
</div>
{error && (
<p className="rounded border border-danger/40 px-3 py-2 text-xs text-danger">{error}</p>
)}
{/* ββ Explainer ββ */}
{explainerOpen && (
<div className="rounded-lg border border-border bg-bg p-4 text-sm text-text-muted">
<p className="mb-2">
Compression reduces <strong className="text-text-main">tokens and cost</strong> by
rewriting history before it is sent to the provider while preserving meaning.
</p>
<ol className="ml-4 list-decimal space-y-1.5">
<li>
<strong className="text-text-main">Active profile</strong>: chooses which compression
profile runs globally β the panel-derived Default or one of your saved named combos.
</li>
<li>
<strong className="text-text-main">Default (from panel)</strong>: derived from the
master switch and per-engine toggles you configure in Compression Settings.
</li>
<li>
<strong className="text-text-main">Named combos</strong>: saved pipelines you build in
the named-combo editor. Selecting one makes it the active profile for every request.
</li>
<li>
<strong className="text-text-main">Preview</strong>: shows which engines the active
profile runs, in order.
</li>
</ol>
</div>
)}
{/* ββ Active profile ββ */}
<div className="flex flex-col gap-3 rounded-lg border border-border bg-bg p-4">
<div className="flex flex-col gap-1">
<label htmlFor="active-profile" className="text-sm font-semibold text-text-main">
Active profile
</label>
<p className="text-xs text-text-muted">
Pick which compression profile runs globally β the panel-derived Default or a saved named combo.
</p>
</div>
<select
id="active-profile"
data-testid="active-profile-select"
value={settings?.activeComboId ?? ""}
onChange={(e) => saveSettings({ activeComboId: e.target.value || null })}
className="rounded-lg border border-border bg-surface px-3 py-2 text-sm text-text-main"
>
<option value="">Default (from panel)</option>
{combos.map((c) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</select>
<div
data-testid="active-profile-preview"
className="rounded-lg border border-dashed border-border px-3 py-2 text-xs text-text-muted"
>
{activeCombo ? (
<span>
Runs: <span className="font-mono text-text-main">{activePipelineText}</span>
</span>
) : (
<span>
Default β configured in{" "}
<a href="/dashboard/context/settings" className="underline hover:text-text-main">
Compression Settings
</a>
.
</span>
)}
</div>
</div>
{/* ββ CompressΓ£o delegada ao provedor ββ */}
<div className="flex flex-col gap-3">
<h2 className="text-sm font-semibold text-text-main">CompressΓ£o delegada ao provedor</h2>
<div className="flex items-center gap-3 rounded-lg border border-border bg-bg p-4">
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-text-main">Context Editing (Claude)</p>
<p className="text-xs text-text-muted">
Deixa o prΓ³prio provedor limpar blocos antigos de tool-use no servidor, sem reescrever
a mensagem.
</p>
</div>
<Toggle
checked={!!settings?.contextEditing?.enabled}
onChange={() =>
saveSettings({ contextEditing: { enabled: !settings?.contextEditing?.enabled } })
}
ariaLabel="Context Editing"
/>
</div>
<div className="flex items-start gap-2 rounded-lg border border-amber-500/40 bg-amber-500/5 px-3 py-2 text-xs text-amber-500">
<span className="material-symbols-outlined text-[16px]">info</span>
<span>
Hoje disponΓvel apenas para Claude (Anthropic). Γ um modo delegado: o prΓ³prio provedor
limpa blocos antigos de tool-use no servidor β nΓ£o reescrevemos a mensagem. NΓ£o afeta
outros provedores.
</span>
</div>
</div>
</section>
);
}
|