"use client"; // Combos screen = Compression Hub (top) + named-combos manager (below). // // IMPORTANT (hydration): no `useTranslations` here. The earlier combos redesign // failed to hydrate on the production build and the only structural difference from // the engine pages was a page-level `useTranslations`. Strings are literal English, // matching `EngineConfigPage` / `CompressionHub`, both of which hydrate cleanly. import { useEffect, useState } from "react"; import { STACKED_PIPELINE_ENGINE_INTENSITIES } from "@/shared/validation/compressionConfigSchemas"; import CompressionHub from "./CompressionHub"; type PipelineStep = { engine: string; intensity?: string }; type CompressionCombo = { id: string; name: string; description: string; pipeline: PipelineStep[]; languagePacks: string[]; outputMode: boolean; outputModeIntensity: string; isDefault: boolean; }; type RoutingCombo = { id?: string; name?: string }; type LanguagePack = { language: string; ruleCount: number }; const EMPTY_PIPELINE: PipelineStep[] = [ { engine: "rtk", intensity: "standard" }, { engine: "caveman", intensity: "full" }, ]; // Engine list is sourced from the API schema so the dropdown can never offer an engine // the `PUT /api/context/combos/[id]` route would reject with HTTP 400 (#4955). const ENGINE_INTENSITIES: Record = STACKED_PIPELINE_ENGINE_INTENSITIES; function NamedCombosManager() { const [combos, setCombos] = useState([]); const [routingCombos, setRoutingCombos] = useState([]); const [languagePacks, setLanguagePacks] = useState([]); const [editingId, setEditingId] = useState(null); const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [pipeline, setPipeline] = useState(EMPTY_PIPELINE); const [selectedPacks, setSelectedPacks] = useState(["en"]); const [outputMode, setOutputMode] = useState(false); const [outputModeIntensity, setOutputModeIntensity] = useState("full"); const [assignmentIds, setAssignmentIds] = useState([]); const [saving, setSaving] = useState(false); const [activeComboId, setActiveComboId] = useState(null); const [error, setError] = useState(null); const refresh = () => { fetch("/api/context/combos") .then((res) => (res.ok ? res.json() : null)) .then((data) => setCombos(Array.isArray(data?.combos) ? data.combos : [])) .catch(() => {}); }; useEffect(() => { refresh(); fetch("/api/combos") .then((res) => (res.ok ? res.json() : null)) .then((data) => setRoutingCombos(Array.isArray(data?.combos) ? data.combos : [])) .catch(() => {}); fetch("/api/compression/language-packs") .then((res) => (res.ok ? res.json() : null)) .then((data) => setLanguagePacks(Array.isArray(data?.packs) ? data.packs : [])) .catch(() => {}); fetch("/api/settings/compression") .then((res) => (res.ok ? res.json() : null)) .then((data) => setActiveComboId(data?.activeComboId ?? null)) .catch(() => {}); }, []); const resetForm = () => { setEditingId(null); setName(""); setDescription(""); setPipeline(EMPTY_PIPELINE); setSelectedPacks(["en"]); setOutputMode(false); setOutputModeIntensity("full"); setAssignmentIds([]); setError(null); }; const loadAssignments = async (id: string) => { const res = await fetch(`/api/context/combos/${id}/assignments`); if (!res.ok) return []; const data = await res.json(); return Array.isArray(data?.assignments) ? data.assignments.map((item: { routingComboId: string }) => item.routingComboId) : []; }; const editCombo = async (combo: CompressionCombo) => { setEditingId(combo.id); setName(combo.name); setDescription(combo.description ?? ""); setPipeline(combo.pipeline.length > 0 ? combo.pipeline : EMPTY_PIPELINE); setSelectedPacks(combo.languagePacks?.length ? combo.languagePacks : ["en"]); setOutputMode(Boolean(combo.outputMode)); setOutputModeIntensity(combo.outputModeIntensity ?? "full"); setAssignmentIds(await loadAssignments(combo.id)); }; const saveCombo = async () => { const trimmed = name.trim(); if (!trimmed) { setError("Enter a combo name before saving."); return; } if (pipeline.length === 0) { setError("Add at least one pipeline step before saving."); return; } setError(null); setSaving(true); try { const payload = { name: trimmed, description, pipeline, languagePacks: selectedPacks, outputMode, outputModeIntensity, }; const res = await fetch( editingId ? `/api/context/combos/${editingId}` : "/api/context/combos", { method: editingId ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), } ); if (!res.ok) { const body = await res.json().catch(() => null); setError(body?.error || `Failed to save combo (HTTP ${res.status}).`); return; } const combo = await res.json(); await fetch(`/api/context/combos/${combo.id}/assignments`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ routingComboIds: assignmentIds }), }); resetForm(); refresh(); } finally { setSaving(false); } }; const deleteCombo = async (combo: CompressionCombo) => { if (!confirm(`Delete combo "${combo.name}"?`)) return; const res = await fetch(`/api/context/combos/${combo.id}`, { method: "DELETE" }); if (res.ok) refresh(); }; const updateStep = (index: number, patch: Partial) => { setPipeline((current) => current.map((step, stepIndex) => { if (stepIndex !== index) return step; const next = { ...step, ...patch }; const allowed = ENGINE_INTENSITIES[next.engine] ?? ["standard"]; return { ...next, intensity: allowed.includes(next.intensity ?? "") ? next.intensity : allowed[0], }; }) ); }; const togglePack = (language: string, enabled: boolean) => { setSelectedPacks((current) => enabled ? [...new Set([...current, language])] : current.filter((item) => item !== language && item !== "en") ); }; const toggleAssignment = (id: string, enabled: boolean) => { setAssignmentIds((current) => enabled ? [...new Set([...current, id])] : current.filter((item) => item !== id) ); }; return (

Named combos

Save different pipelines and assign them to specific routing combos.

setName(event.target.value)} placeholder="Combo name" className="rounded-lg border border-border bg-bg px-3 py-2 text-sm text-text-main" /> setDescription(event.target.value)} placeholder="Description" className="rounded-lg border border-border bg-bg px-3 py-2 text-sm text-text-main" />

Pipeline

{pipeline.map((step, index) => (
))}

Language packs

{languagePacks.map((pack) => ( ))}

Output mode

Assign to routing

{routingCombos.length === 0 ? (

No routing combos available.

) : ( routingCombos.map((combo) => { const id = combo.id ?? combo.name ?? ""; if (!id) return null; return ( ); }) )}
{error && (

{error}

)}
{editingId && ( )}
{combos.map((combo) => (

{combo.name}

{combo.description}

{combo.id === activeComboId && ( ● Active )}
{combo.pipeline.map((step, index) => ( {index + 1}. {step.engine} {step.intensity ? `:${step.intensity}` : ""} ))}

Language packs: {combo.languagePacks.join(", ")}

{!combo.isDefault && ( )}
))}
); } export default function CompressionCombosPageClient() { return (
); }