Frontend: Voice-Umzug in Desktop-App, MC3-Prototyp, UI-Rework-Dateien auf lucy-v2, dist konsistent
- VoiceView + components/voice + lib/voice entfernt (lebt jetzt in client/lucy-desktop)
- mc3/: Basisstation-Prototyp hinter #mc3-Weiche (non-destruktiv)
- roleMeta/useLucyHealth/useExpertMode/ExpertToggle/LucyHealthCard/WarmSetManager (UI-Rework, auf main a341d25 committet, hier nachgezogen)
- dist frisch gebaut (Asset-Stand war inkonsistent: alte geloescht, neue untracked)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,11 @@ import { useDialog } from "@/lib/useDialog"
|
||||
import { CapsChips } from "@/components/CapsChips"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { fmtSize, fmtCtx } from "@/lib/format"
|
||||
import { getBrandInfo, roleTone, ROLES } from "@/components/models/ModelBadges"
|
||||
import { getBrandInfo, ROLES, RoleLabel, roleMeta } from "@/components/models/ModelBadges"
|
||||
import { SpecDraftModal } from "@/components/models/SpecDraftModal"
|
||||
import { LaneEditor } from "@/components/models/LaneEditor"
|
||||
import { WarmSetManager } from "@/components/models/WarmSetManager"
|
||||
import { useExpertMode } from "@/lib/useExpertMode"
|
||||
|
||||
export function Cockpit() {
|
||||
const qc = useQueryClient()
|
||||
@@ -18,6 +20,10 @@ export function Cockpit() {
|
||||
const { data: sysStatus } = useSystemStatus()
|
||||
const { data: groupsResp } = useGroups()
|
||||
const { showAlert, showConfirm, showPrompt, dialogElement } = useDialog()
|
||||
const expert = useExpertMode()
|
||||
// Schutzgeländer: Modelle in einer lebenswichtigen Rolle (Hirn/Gedächtnis) dürfen
|
||||
// nicht gelöscht/entladen werden. Das `protected`-Flag kommt zentral aus roleMeta.
|
||||
const isProtected = (role?: string | null) => !!roleMeta(role).protected
|
||||
const models = modelsResp?.models ?? []
|
||||
const running = modelsResp?.running ?? []
|
||||
const error = loadErr ? String(loadErr) : ""
|
||||
@@ -88,6 +94,11 @@ export function Cockpit() {
|
||||
}
|
||||
|
||||
async function handleUnloadModel(name: string) {
|
||||
const m = models.find((x) => x.name === name)
|
||||
if (m && isProtected(m.role)) {
|
||||
showAlert("Geschützt", `„${roleMeta(m.role).label}" ist lebenswichtig für Lucy und bleibt geladen. Zum Entladen erst im Experten-Modus die Rolle einem anderen Modell geben.`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await api(`/api/models/${encodeURIComponent(name)}/unload`, { method: "POST" })
|
||||
reload()
|
||||
@@ -164,6 +175,11 @@ export function Cockpit() {
|
||||
}
|
||||
|
||||
async function handleDelete(name: string) {
|
||||
const m = models.find((x) => x.name === name)
|
||||
if (m && isProtected(m.role)) {
|
||||
showAlert("Geschützt", `„${roleMeta(m.role).label}" ist lebenswichtig für Lucy und kann nicht gelöscht werden. Weise die Rolle zuerst einem anderen Modell zu.`)
|
||||
return
|
||||
}
|
||||
showConfirm(
|
||||
"Modell löschen?",
|
||||
`Modell '${name}' und alle zugehörigen GGUF-Dateien unwiderruflich von der Box löschen?`,
|
||||
@@ -258,7 +274,7 @@ export function Cockpit() {
|
||||
<span className="text-[10px] font-mono text-muted-foreground">
|
||||
Speicher-Pool: {fmtSize(totalRunningSize)} Gewichte{gttUsed > 0 ? ` · ${fmtSize(gttUsed)} real belegt (inkl. KV)` : ""} / {fmtSize(capacity)}
|
||||
</span>
|
||||
{running.length > 0 && (
|
||||
{expert && running.length > 0 && (
|
||||
<button
|
||||
onClick={handleUnloadAll}
|
||||
className="h-6 px-2.5 rounded border border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/10 text-amber-400 text-[9px] font-bold uppercase transition-all cursor-pointer"
|
||||
@@ -295,7 +311,7 @@ export function Cockpit() {
|
||||
title={`${m.name} (${fmtSize(m.size_bytes)})`}
|
||||
>
|
||||
<span className="text-[9px] font-bold truncate max-w-[120px] font-space tracking-wide">
|
||||
{m.role ? `[${m.role}] ` : ""}{m.name.split("/").pop()?.replace(".gguf", "")}
|
||||
{m.role ? `[${roleMeta(m.role).short}] ` : ""}{m.name.split("/").pop()?.replace(".gguf", "")}
|
||||
</span>
|
||||
<span className="text-[8px] font-mono opacity-80">{fmtSize(m.size_bytes)}</span>
|
||||
</div>
|
||||
@@ -305,13 +321,16 @@ export function Cockpit() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ZONE B.4: Lane-Routing & Policy (UI-editierbar, hot-reload) */}
|
||||
<LaneEditor />
|
||||
{/* ZONE B.3: Immer-bereit-Set (Warm-Set / brains-Gruppe) — in Klartext, mit Budget */}
|
||||
<WarmSetManager />
|
||||
|
||||
{/* ZONE B.4: Lane-Routing & Policy (UI-editierbar, hot-reload) — Experten-Detail */}
|
||||
{expert && <LaneEditor />}
|
||||
|
||||
{/* ZONE B.5: Slot-Belegung */}
|
||||
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/10 space-y-3.5">
|
||||
<div className="text-xs font-bold uppercase tracking-wider text-muted-foreground px-1">
|
||||
Gateway Steckplatz-Belegung (Slot-Zuweisung)
|
||||
{expert ? "Gateway Steckplatz-Belegung (Slot-Zuweisung)" : "Wer macht was bei Lucy — zum Ändern antippen"}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
{ROLES.map((role) => {
|
||||
@@ -332,8 +351,7 @@ export function Cockpit() {
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={cn("text-[9px] font-bold uppercase tracking-wider font-mono px-1.5 py-0.5 rounded border",
|
||||
roleTone(role))}>{role}</span>
|
||||
<RoleLabel role={role} />
|
||||
{isWarm && <span className="h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" />}
|
||||
</div>
|
||||
|
||||
@@ -525,22 +543,23 @@ export function Cockpit() {
|
||||
<Activity className="h-3 w-3 animate-pulse" /> Warm
|
||||
</span>
|
||||
)}
|
||||
{m.role && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-primary/10 border border-primary/20 text-[9px] font-mono text-primary font-bold uppercase">
|
||||
{m.role}
|
||||
{m.role && <RoleLabel role={m.role} dense />}
|
||||
{isProtected(m.role) && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-background/40 border border-border/40 text-[9px] font-semibold text-muted-foreground" title="Lebenswichtig für Lucy — geschützt vor Löschen/Entladen.">
|
||||
🔒 geschützt
|
||||
</span>
|
||||
)}
|
||||
{isCoresident(m.name) && (
|
||||
{expert && isCoresident(m.name) && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-fuchsia-500/10 border border-fuchsia-500/30 text-[9px] font-mono text-fuchsia-300 font-bold uppercase" title="Ko-resident in der brains-Gruppe — bleibt mit dem Hirn gemeinsam warm, verdrängt es nicht.">
|
||||
🧠 Ko-resident
|
||||
</span>
|
||||
)}
|
||||
{m.prompt_cache && (
|
||||
{expert && m.prompt_cache && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-cyan-500/10 border border-cyan-500/20 text-[9px] font-mono text-cyan-400 font-bold uppercase" title="Prompt Caching aktiv">
|
||||
PC
|
||||
</span>
|
||||
)}
|
||||
{m.spec_active ? (
|
||||
{expert && (m.spec_active ? (
|
||||
<span className="px-1.5 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-[9px] font-mono text-emerald-400 font-bold uppercase" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>
|
||||
SPEC
|
||||
</span>
|
||||
@@ -548,8 +567,8 @@ export function Cockpit() {
|
||||
<span className="px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20 text-[9px] font-mono text-amber-400 font-bold uppercase" title={`Draft gesetzt (${m.spec_draft_model}), aber INAKTIV — --spec-type fehlt oder Vocab inkompatibel. Über 'Spec' neu konfigurieren.`}>
|
||||
SPEC?
|
||||
</span>
|
||||
) : null}
|
||||
{m.parallel_slots > 1 && (
|
||||
) : null)}
|
||||
{expert && m.parallel_slots > 1 && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-violet-500/10 border border-violet-500/20 text-[9px] font-mono text-violet-400 font-bold uppercase" title={`${m.parallel_slots} parallele Slots aktiv`}>
|
||||
SLOTS: {m.parallel_slots}
|
||||
</span>
|
||||
@@ -606,28 +625,38 @@ export function Cockpit() {
|
||||
|
||||
{/* Actions row at the bottom of Grid Card */}
|
||||
<div className="flex items-center gap-1.5 border-t border-border/20 pt-3 mt-auto">
|
||||
{(() => {
|
||||
const prot = isProtected(m.role)
|
||||
const lockUnload = isRunning && prot
|
||||
const disabled = (m.incomplete && !isRunning) || lockUnload
|
||||
return (
|
||||
<button
|
||||
onClick={() => isRunning ? handleUnloadModel(m.name) : handleLoadModel(m.name)}
|
||||
disabled={m.incomplete && !isRunning}
|
||||
disabled={disabled}
|
||||
title={lockUnload ? `„${roleMeta(m.role).label}" bleibt geladen (geschützt).` : undefined}
|
||||
className={cn(
|
||||
"flex-1 h-7 rounded-lg text-[9px] font-bold uppercase transition-all border flex items-center justify-center gap-1",
|
||||
m.incomplete && !isRunning
|
||||
disabled
|
||||
? "border-border/30 bg-background/10 text-muted-foreground/50 cursor-not-allowed"
|
||||
: isRunning
|
||||
? "border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/15 text-amber-400 cursor-pointer"
|
||||
: "border-primary/30 bg-primary/5 hover:bg-primary/15 text-primary cursor-pointer"
|
||||
)}
|
||||
>
|
||||
{isRunning ? "Entladen" : "Laden"}
|
||||
{lockUnload ? "🔒 geladen" : isRunning ? "Entladen" : "Laden"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSetCtx(m.name, m.ctx)}
|
||||
className="h-7 px-2.5 rounded-lg border border-border/40 text-[9px] font-bold uppercase hover:bg-accent text-muted-foreground transition-colors cursor-pointer"
|
||||
title="Kontextlänge anpassen"
|
||||
>
|
||||
Ctx
|
||||
</button>
|
||||
{brainsGroup && (
|
||||
)
|
||||
})()}
|
||||
{expert && (
|
||||
<button
|
||||
onClick={() => handleSetCtx(m.name, m.ctx)}
|
||||
className="h-7 px-2.5 rounded-lg border border-border/40 text-[9px] font-bold uppercase hover:bg-accent text-muted-foreground transition-colors cursor-pointer"
|
||||
title="Kontextlänge anpassen"
|
||||
>
|
||||
Ctx
|
||||
</button>
|
||||
)}
|
||||
{expert && brainsGroup && (
|
||||
<button
|
||||
onClick={() => toggleCoresident(m.name)}
|
||||
className={cn(
|
||||
@@ -643,22 +672,31 @@ export function Cockpit() {
|
||||
🧠
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setSpecModel(m)}
|
||||
className={cn(
|
||||
"h-7 px-2.5 rounded-lg border text-[9px] font-bold uppercase transition-colors cursor-pointer flex items-center gap-1",
|
||||
m.spec_active ? "border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10"
|
||||
: m.spec_draft_model ? "border-amber-500/30 text-amber-400 hover:bg-amber-500/10"
|
||||
: "border-border/40 text-muted-foreground hover:bg-accent"
|
||||
)}
|
||||
title="Speculative Draft konfigurieren (Vocab-geprüft)"
|
||||
>
|
||||
<Zap className="h-3 w-3" /> Spec
|
||||
</button>
|
||||
{expert && (
|
||||
<button
|
||||
onClick={() => setSpecModel(m)}
|
||||
className={cn(
|
||||
"h-7 px-2.5 rounded-lg border text-[9px] font-bold uppercase transition-colors cursor-pointer flex items-center gap-1",
|
||||
m.spec_active ? "border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10"
|
||||
: m.spec_draft_model ? "border-amber-500/30 text-amber-400 hover:bg-amber-500/10"
|
||||
: "border-border/40 text-muted-foreground hover:bg-accent"
|
||||
)}
|
||||
title="Speculative Draft konfigurieren (Vocab-geprüft)"
|
||||
>
|
||||
<Zap className="h-3 w-3" /> Spec
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleDelete(m.name)}
|
||||
className="h-7 w-7 rounded-lg border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 flex items-center justify-center transition-colors cursor-pointer"
|
||||
aria-label="Modell löschen" title="Modell löschen"
|
||||
disabled={isProtected(m.role)}
|
||||
className={cn(
|
||||
"h-7 w-7 rounded-lg border border-border/40 flex items-center justify-center transition-colors",
|
||||
isProtected(m.role)
|
||||
? "text-muted-foreground/40 cursor-not-allowed"
|
||||
: "text-muted-foreground hover:text-red-400 hover:bg-red-500/5 cursor-pointer"
|
||||
)}
|
||||
aria-label="Modell löschen"
|
||||
title={isProtected(m.role) ? `„${roleMeta(m.role).label}" ist geschützt und kann nicht gelöscht werden.` : "Modell löschen"}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
@@ -705,22 +743,23 @@ export function Cockpit() {
|
||||
<h4 className="text-xs font-bold text-foreground truncate max-w-[250px] sm:max-w-[400px] break-all font-mono" title={m.name}>
|
||||
{m.name.split("/").pop()}
|
||||
</h4>
|
||||
{m.role && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-primary/10 border border-primary/20 text-[8px] font-mono text-primary font-bold uppercase shrink-0">
|
||||
{m.role}
|
||||
{m.role && <RoleLabel role={m.role} dense className="shrink-0" />}
|
||||
{isProtected(m.role) && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-background/40 border border-border/40 text-[8px] font-semibold text-muted-foreground shrink-0" title="Lebenswichtig für Lucy — geschützt vor Löschen/Entladen.">
|
||||
🔒 geschützt
|
||||
</span>
|
||||
)}
|
||||
{isCoresident(m.name) && (
|
||||
{expert && isCoresident(m.name) && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-fuchsia-500/10 border border-fuchsia-500/30 text-[8px] font-mono text-fuchsia-300 font-bold uppercase shrink-0" title="Ko-resident in der brains-Gruppe — bleibt mit dem Hirn gemeinsam warm.">
|
||||
🧠 Ko-resident
|
||||
</span>
|
||||
)}
|
||||
{m.prompt_cache && (
|
||||
{expert && m.prompt_cache && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-cyan-500/10 border border-cyan-500/20 text-[8px] font-mono text-cyan-400 font-bold uppercase shrink-0" title="Prompt Caching aktiv">
|
||||
PC
|
||||
</span>
|
||||
)}
|
||||
{m.spec_active ? (
|
||||
{expert && (m.spec_active ? (
|
||||
<span className="px-1.5 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-[8px] font-mono text-emerald-400 font-bold uppercase shrink-0" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>
|
||||
SPEC
|
||||
</span>
|
||||
@@ -728,8 +767,8 @@ export function Cockpit() {
|
||||
<span className="px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20 text-[8px] font-mono text-amber-400 font-bold uppercase shrink-0" title={`Draft gesetzt (${m.spec_draft_model}), aber INAKTIV — --spec-type fehlt oder Vocab inkompatibel.`}>
|
||||
SPEC?
|
||||
</span>
|
||||
) : null}
|
||||
{m.parallel_slots > 1 && (
|
||||
) : null)}
|
||||
{expert && m.parallel_slots > 1 && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-violet-500/10 border border-violet-500/20 text-[8px] font-mono text-violet-400 font-bold uppercase shrink-0" title={`${m.parallel_slots} parallele Slots aktiv`}>
|
||||
SLOTS: {m.parallel_slots}
|
||||
</span>
|
||||
@@ -762,28 +801,38 @@ export function Cockpit() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
{(() => {
|
||||
const prot = isProtected(m.role)
|
||||
const lockUnload = isRunning && prot
|
||||
const disabled = (m.incomplete && !isRunning) || lockUnload
|
||||
return (
|
||||
<button
|
||||
onClick={() => isRunning ? handleUnloadModel(m.name) : handleLoadModel(m.name)}
|
||||
disabled={m.incomplete && !isRunning}
|
||||
disabled={disabled}
|
||||
title={lockUnload ? `„${roleMeta(m.role).label}" bleibt geladen (geschützt).` : undefined}
|
||||
className={cn(
|
||||
"h-7 px-3 rounded-lg text-[9px] font-bold uppercase transition-all border flex items-center gap-1",
|
||||
m.incomplete && !isRunning
|
||||
disabled
|
||||
? "border-border/30 bg-background/10 text-muted-foreground/50 cursor-not-allowed"
|
||||
: isRunning
|
||||
? "border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/15 text-amber-400 cursor-pointer"
|
||||
: "border-primary/30 bg-primary/5 hover:bg-primary/15 text-primary cursor-pointer"
|
||||
)}
|
||||
>
|
||||
{isRunning ? "Entladen" : "Laden"}
|
||||
{lockUnload ? "🔒 geladen" : isRunning ? "Entladen" : "Laden"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSetCtx(m.name, m.ctx)}
|
||||
className="h-7 px-2.5 rounded-lg border border-border/40 text-[9px] font-bold uppercase hover:bg-accent text-muted-foreground transition-all cursor-pointer"
|
||||
title="Kontextlänge anpassen"
|
||||
>
|
||||
Ctx
|
||||
</button>
|
||||
{brainsGroup && (
|
||||
)
|
||||
})()}
|
||||
{expert && (
|
||||
<button
|
||||
onClick={() => handleSetCtx(m.name, m.ctx)}
|
||||
className="h-7 px-2.5 rounded-lg border border-border/40 text-[9px] font-bold uppercase hover:bg-accent text-muted-foreground transition-all cursor-pointer"
|
||||
title="Kontextlänge anpassen"
|
||||
>
|
||||
Ctx
|
||||
</button>
|
||||
)}
|
||||
{expert && brainsGroup && (
|
||||
<button
|
||||
onClick={() => toggleCoresident(m.name)}
|
||||
className={cn(
|
||||
@@ -799,22 +848,31 @@ export function Cockpit() {
|
||||
🧠
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setSpecModel(m)}
|
||||
className={cn(
|
||||
"h-7 px-2.5 rounded-lg border text-[9px] font-bold uppercase transition-all cursor-pointer flex items-center gap-1",
|
||||
m.spec_active ? "border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10"
|
||||
: m.spec_draft_model ? "border-amber-500/30 text-amber-400 hover:bg-amber-500/10"
|
||||
: "border-border/40 text-muted-foreground hover:bg-accent"
|
||||
)}
|
||||
title="Speculative Draft konfigurieren (Vocab-geprüft)"
|
||||
>
|
||||
<Zap className="h-3 w-3" /> Spec
|
||||
</button>
|
||||
{expert && (
|
||||
<button
|
||||
onClick={() => setSpecModel(m)}
|
||||
className={cn(
|
||||
"h-7 px-2.5 rounded-lg border text-[9px] font-bold uppercase transition-all cursor-pointer flex items-center gap-1",
|
||||
m.spec_active ? "border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10"
|
||||
: m.spec_draft_model ? "border-amber-500/30 text-amber-400 hover:bg-amber-500/10"
|
||||
: "border-border/40 text-muted-foreground hover:bg-accent"
|
||||
)}
|
||||
title="Speculative Draft konfigurieren (Vocab-geprüft)"
|
||||
>
|
||||
<Zap className="h-3 w-3" /> Spec
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleDelete(m.name)}
|
||||
className="h-7 w-7 rounded-lg flex items-center justify-center border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 transition-all cursor-pointer"
|
||||
aria-label="Modell löschen" title="Modell löschen"
|
||||
disabled={isProtected(m.role)}
|
||||
className={cn(
|
||||
"h-7 w-7 rounded-lg flex items-center justify-center border border-border/40 transition-all",
|
||||
isProtected(m.role)
|
||||
? "text-muted-foreground/40 cursor-not-allowed"
|
||||
: "text-muted-foreground hover:text-red-400 hover:bg-red-500/5 cursor-pointer"
|
||||
)}
|
||||
aria-label="Modell löschen"
|
||||
title={isProtected(m.role) ? `„${roleMeta(m.role).label}" ist geschützt und kann nicht gelöscht werden.` : "Modell löschen"}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
@@ -839,11 +897,11 @@ export function Cockpit() {
|
||||
: models
|
||||
const assign = (name: string) => { handleRoleChange(activeRoleForAssign, name); setActiveRoleForAssign(null) }
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 overscroll-contain" role="dialog" aria-modal="true" aria-label={`Rolle ${activeRoleForAssign} konfigurieren`}>
|
||||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 overscroll-contain" role="dialog" aria-modal="true" aria-label={`${roleMeta(activeRoleForAssign).label} konfigurieren`}>
|
||||
<div className="w-full max-w-md rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="flex items-center justify-between border-b border-border/20 pb-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-primary">
|
||||
Rolle '{activeRoleForAssign}' konfigurieren
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-primary flex items-center gap-1.5">
|
||||
<RoleLabel role={activeRoleForAssign} /> festlegen
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setActiveRoleForAssign(null)}
|
||||
@@ -854,7 +912,8 @@ export function Cockpit() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Wähle ein Modell für die Rolle <strong className="text-foreground">{activeRoleForAssign}</strong>:
|
||||
Wähle ein Modell für <strong className="text-foreground">{roleMeta(activeRoleForAssign).label}</strong>
|
||||
<span className="block text-[10px] text-muted-foreground/70 mt-0.5">{roleMeta(activeRoleForAssign).desc}</span>
|
||||
</p>
|
||||
{rec?.recommended && (
|
||||
<button
|
||||
@@ -868,12 +927,19 @@ export function Cockpit() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 max-h-60 overflow-y-auto pr-1">
|
||||
<button
|
||||
onClick={() => assign("")}
|
||||
className="w-full text-left px-3 py-2 rounded-lg text-xs hover:bg-accent text-red-400 font-semibold cursor-pointer border border-red-500/20 bg-red-500/5 flex items-center justify-between"
|
||||
>
|
||||
<span>Zuweisung entfernen</span>
|
||||
</button>
|
||||
{/* Schutzgeländer: eine lebenswichtige Rolle (Hirn/Gedächtnis) lässt sich nicht leeren. */}
|
||||
{isProtected(activeRoleForAssign) ? (
|
||||
<div className="w-full px-3 py-2 rounded-lg text-[11px] text-muted-foreground border border-border/30 bg-background/20 flex items-center gap-1.5">
|
||||
🔒 Diese Rolle ist lebenswichtig und kann nicht leer bleiben — wähle stattdessen ein anderes Modell.
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => assign("")}
|
||||
className="w-full text-left px-3 py-2 rounded-lg text-xs hover:bg-accent text-red-400 font-semibold cursor-pointer border border-red-500/20 bg-red-500/5 flex items-center justify-between"
|
||||
>
|
||||
<span>Zuweisung entfernen</span>
|
||||
</button>
|
||||
)}
|
||||
{ordered.map((m) => {
|
||||
const r = recByName[m.name]
|
||||
const isCur = m.role === activeRoleForAssign
|
||||
|
||||
Reference in New Issue
Block a user