94444f4dcb
- Fix: 'Interaktives Terminal' im Cockpit führte ins Leere (#terminal) → Konsole - Mobile: Off-Canvas-Sidebar mit Hamburger unter md, Inhalt volle Breite (vorher: 240px starre Sidebar, 135px Rest mit Quer-Scroll auf dem Handy) - Dead Weight raus: src/mc3/ (verworfener Prototyp), DashboardView (alte Zentrale) + 6 nur dort genutzte Karten, 8 ungenutzte npm-Pakete (three, three-vrm, react-three fiber/drei, sigma, graphology ×2, @types/three) - Code-Splitting: alle Views außer Cockpit per React.lazy → Haupt-Bundle 983 → 739 KB, jede View ein eigenes Chunk - Fonts lokal (@fontsource) statt Google-CDN → UI offline identisch - Polling harmonisiert: EINE Takt-Tabelle in queries.ts (Graphen 3s, Rest 8-60s), keine Override-Intervalle mehr in Komponenten; Updates-Check (apt/git!) von 4s auf 60s - CSS-Hack-Layer aufgelöst: !important-Klassen-Sniffing → explizite .mc-card/.mc-card-sm/.nav-active-Klassen (Look identisch, ungeschichtete Regeln statt Utility-Raten); totes .light-Theme raus; Scrollbars 1× definiert - Aurora: animierte Fullscreen-Blur-Filter → statische radial-gradients (gleicher Look, keine Dauer-GPU-Last) - Sicherheit: Sudo-Passwort/HF-Token nur noch bei mutierenden Requests, nicht mehr auf jedem GET-Poll - Strg+K statt ⌘K auf Windows; dist neu gebaut Live gegen die Box verifiziert: alle 10 Views + Mobile-Flow, 0 Konsolen-Fehler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
182 lines
7.9 KiB
TypeScript
182 lines
7.9 KiB
TypeScript
import { useEffect, useState } from "react"
|
|
import { Sliders, RotateCcw, Check, Loader2, MessageSquare, Code2 } from "lucide-react"
|
|
import { updateRoutingPolicy, type RoutingPolicy } from "@/lib/api"
|
|
import { useRoutingPolicy, useQueryClient, qk } from "@/lib/queries"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
// Welche Policy-Felder zu welcher Lane gehören (Rest ist global).
|
|
const CHAT_FIELDS: (keyof RoutingPolicy)[] = ["fast", "heavy", "heavy_chars"]
|
|
const CODING_FIELDS: (keyof RoutingPolicy)[] = ["coder_lite", "coder", "coding_escalate_chars"]
|
|
|
|
export function LaneEditor() {
|
|
const qc = useQueryClient()
|
|
const { data: meta, isLoading } = useRoutingPolicy()
|
|
const [draft, setDraft] = useState<RoutingPolicy | null>(null)
|
|
const [saving, setSaving] = useState(false)
|
|
const [err, setErr] = useState("")
|
|
const [savedAt, setSavedAt] = useState(0)
|
|
|
|
// Draft initialisieren, sobald die Policy geladen ist (und nicht überschreiben, wenn schon editiert).
|
|
useEffect(() => {
|
|
if (meta?.policy && !draft) setDraft({ ...meta.policy })
|
|
}, [meta, draft])
|
|
|
|
if (isLoading || !meta || !draft) {
|
|
return (
|
|
<div className="mc-card p-5 text-xs text-muted-foreground">
|
|
Lade Routing-Policy…
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const fieldSpec = (key: keyof RoutingPolicy) => meta.fields.find((f) => f.key === key)!
|
|
const dirty = (Object.keys(draft) as (keyof RoutingPolicy)[]).some((k) => draft[k] !== meta.policy[k])
|
|
|
|
const set = <K extends keyof RoutingPolicy>(key: K, value: RoutingPolicy[K]) => {
|
|
setDraft((d) => (d ? { ...d, [key]: value } : d))
|
|
setErr("")
|
|
}
|
|
const resetField = (key: keyof RoutingPolicy) => set(key, meta.defaults[key])
|
|
|
|
async function save() {
|
|
if (!draft) return
|
|
const patch: Partial<RoutingPolicy> = {}
|
|
for (const k of Object.keys(draft) as (keyof RoutingPolicy)[]) {
|
|
if (draft[k] !== meta!.policy[k]) (patch as any)[k] = draft[k]
|
|
}
|
|
if (Object.keys(patch).length === 0) return
|
|
setSaving(true)
|
|
setErr("")
|
|
try {
|
|
const { policy } = await updateRoutingPolicy(patch)
|
|
setDraft({ ...policy })
|
|
qc.invalidateQueries({ queryKey: qk.routingPolicy })
|
|
qc.invalidateQueries({ queryKey: qk.routing })
|
|
setSavedAt(Date.now())
|
|
setTimeout(() => setSavedAt(0), 2000)
|
|
} catch (e: any) {
|
|
setErr(e.message || String(e))
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
function Field({ k }: { k: keyof RoutingPolicy }) {
|
|
const spec = fieldSpec(k)
|
|
const val = draft![k]
|
|
const isDefault = draft![k] === meta!.defaults[k]
|
|
return (
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<label className="text-[10px] font-semibold text-muted-foreground">{spec.label}</label>
|
|
{!isDefault && (
|
|
<button
|
|
onClick={() => resetField(k)}
|
|
className="flex items-center gap-0.5 text-[9px] text-muted-foreground/60 hover:text-foreground transition-colors cursor-pointer"
|
|
title={`Auf Default zurücksetzen (${String(meta!.defaults[k]) || "leer"})`}
|
|
>
|
|
<RotateCcw className="h-2.5 w-2.5" /> Default
|
|
</button>
|
|
)}
|
|
</div>
|
|
{spec.type === "bool" ? (
|
|
<button
|
|
onClick={() => set(k, !val as any)}
|
|
className={cn(
|
|
"flex h-8 w-full items-center justify-between rounded-lg border px-3 text-[11px] font-semibold transition-all cursor-pointer",
|
|
val ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-300" : "border-border/40 bg-background/40 text-muted-foreground",
|
|
)}
|
|
>
|
|
<span>{val ? "An" : "Aus"}</span>
|
|
<span className={cn("h-3.5 w-3.5 rounded-full transition-colors", val ? "bg-emerald-400" : "bg-muted-foreground/40")} />
|
|
</button>
|
|
) : spec.type === "int" ? (
|
|
<input
|
|
type="number"
|
|
value={val as number}
|
|
min={spec.min}
|
|
max={spec.max}
|
|
aria-label={spec.label}
|
|
onChange={(e) => set(k, Number(e.target.value) as any)}
|
|
className="h-8 w-full rounded-lg border border-border/40 bg-background/40 px-3 font-mono text-[11px] text-foreground outline-none focus:border-primary/50"
|
|
/>
|
|
) : (
|
|
<input
|
|
type="text"
|
|
value={val as string}
|
|
aria-label={spec.label}
|
|
spellCheck={false}
|
|
placeholder={k === "coder_lite" ? "(leer = aus)" : ""}
|
|
onChange={(e) => set(k, e.target.value as any)}
|
|
className="h-8 w-full rounded-lg border border-border/40 bg-background/40 px-3 font-mono text-[11px] text-foreground outline-none focus:border-primary/50"
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="mc-card p-5 space-y-4">
|
|
<div className="flex items-center justify-between gap-3 flex-wrap">
|
|
<div className="flex items-center gap-2">
|
|
<Sliders className="h-4.5 w-4.5 text-primary" />
|
|
<span className="text-xs font-bold uppercase tracking-wider text-foreground">Lane-Routing & Policy</span>
|
|
<span className="text-[9px] font-mono px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-300 border border-emerald-500/20">hot-reload</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{err && <span className="text-[10px] text-red-400 max-w-[280px] truncate" title={err}>{err}</span>}
|
|
{savedAt > 0 && (
|
|
<span className="flex items-center gap-1 text-[10px] text-emerald-400"><Check className="h-3.5 w-3.5" /> gespeichert</span>
|
|
)}
|
|
<button
|
|
onClick={save}
|
|
disabled={!dirty || saving}
|
|
className={cn(
|
|
"h-8 px-4 rounded-lg text-[10px] font-bold uppercase tracking-wide transition-all flex items-center gap-1.5",
|
|
dirty && !saving
|
|
? "bg-primary text-primary-foreground hover:opacity-90 cursor-pointer shadow-md shadow-primary/10"
|
|
: "bg-background/40 text-muted-foreground/50 border border-border/40 cursor-not-allowed",
|
|
)}
|
|
>
|
|
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : null}
|
|
Speichern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-[10px] text-muted-foreground/70 leading-relaxed -mt-1">
|
|
Welches echte Modell hinter den virtuellen Lanes <code className="text-cyan-300">chat</code> und{" "}
|
|
<code className="text-cyan-300">coding</code> steckt. Änderungen greifen sofort (kein Neustart). Die
|
|
Keyword-Heuristiken bleiben im Code.
|
|
</p>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{/* chat-Lane */}
|
|
<div className="rounded-xl border border-border/40 bg-background/25 p-4 space-y-3">
|
|
<div className="flex items-center gap-2 border-b border-border/30 pb-2">
|
|
<MessageSquare className="h-4 w-4 text-teal-400" />
|
|
<span className="text-[11px] font-bold uppercase tracking-wider text-foreground">chat</span>
|
|
<span className="text-[9px] text-muted-foreground/60 font-mono">(= auto)</span>
|
|
</div>
|
|
{CHAT_FIELDS.map((k) => <Field key={k} k={k} />)}
|
|
</div>
|
|
|
|
{/* coding-Lane */}
|
|
<div className="rounded-xl border border-border/40 bg-background/25 p-4 space-y-3">
|
|
<div className="flex items-center gap-2 border-b border-border/30 pb-2">
|
|
<Code2 className="h-4 w-4 text-indigo-400" />
|
|
<span className="text-[11px] font-bold uppercase tracking-wider text-foreground">coding</span>
|
|
<span className="text-[9px] text-muted-foreground/60 font-mono">(agentisch → immer Coder)</span>
|
|
</div>
|
|
{CODING_FIELDS.map((k) => <Field key={k} k={k} />)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Globale Schalter */}
|
|
<div className="rounded-xl border border-border/40 bg-background/25 p-4">
|
|
<div className="max-w-xs"><Field k="fast_no_think" /></div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|