Feat: Rollen-Empfehlung - bestes installiertes Modell je Rolle (Auto-Pick)
Analog zum Auto-ctx-Button: das Rollen-Zuweisungs-Modal empfiehlt jetzt, welches
INSTALLIERTE Modell am besten auf die Rolle passt - capability-getrieben (Vision/Coder/
Tools/MoE aus services.caps) + setup-bewusster Fit (services.budget, gleiche Mathematik
wie Install-Automatik & Auto-ctx).
- services/roles.py: recommend_for_role() rankt installierte Modelle (Eignung + Fit + Tempo
+ Wissen); harte Anforderungen (Vision braucht Vision, Hirn braucht Tools) schliessen aus.
- GET /api/roles/{role}/recommend
- Cockpit-Modal: »Auto: <Modell>«-Button im Header, »Empfohlen«-Badge, Sortierung nach Score,
pro Zeile Fit + Begruendung (~t/s); ungeeignete gedimmt mit Klartext-Grund.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,25 @@ export interface Fit {
|
||||
tps: number
|
||||
}
|
||||
|
||||
export interface RoleRecModel {
|
||||
name: string
|
||||
current_role: string | null
|
||||
params_b: number
|
||||
quant: string
|
||||
fit: Fit
|
||||
suitable: boolean
|
||||
incomplete: boolean
|
||||
score: number
|
||||
reason: string
|
||||
recommended: boolean
|
||||
}
|
||||
|
||||
export interface RoleRecResp {
|
||||
role: string
|
||||
recommended: string | null
|
||||
models: RoleRecModel[]
|
||||
}
|
||||
|
||||
export interface FitResp {
|
||||
params_b: number
|
||||
fit: Fit
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useRef, useCallback } from "react"
|
||||
import { Download, Trash2, Edit3, Activity, HardDrive, X, Check, Copy, Zap, Bot } from "lucide-react"
|
||||
import { api, type ModelInfo } from "@/lib/api"
|
||||
import { api, type ModelInfo, type RoleRecResp } from "@/lib/api"
|
||||
import { useModels, useRouting, useConnect, useUpdates, useHermesBrain, useSystemStatus, useQueryClient, qk } from "@/lib/queries"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { CapsChips } from "@/components/CapsChips"
|
||||
@@ -29,6 +29,7 @@ export function Cockpit() {
|
||||
// UI state
|
||||
const [activeClient, setActiveClient] = useState<"roocode" | "cursor" | "opencode" | "zed" | "continue" | null>(null)
|
||||
const [activeRoleForAssign, setActiveRoleForAssign] = useState<string | null>(null)
|
||||
const [roleRec, setRoleRec] = useState<RoleRecResp | null>(null)
|
||||
const [specModel, setSpecModel] = useState<ModelInfo | null>(null)
|
||||
const [showBrainSwitch, setShowBrainSwitch] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
@@ -131,6 +132,15 @@ export function Cockpit() {
|
||||
}
|
||||
}
|
||||
|
||||
// Rollen-Modal öffnen + setup-bewusste Empfehlung holen (bestes installiertes Modell).
|
||||
function openRoleAssign(role: string) {
|
||||
setActiveRoleForAssign(role)
|
||||
setRoleRec(null)
|
||||
api<RoleRecResp>(`/api/roles/${encodeURIComponent(role)}/recommend`)
|
||||
.then((d) => setRoleRec(d))
|
||||
.catch(() => { /* Empfehlung optional — Modal funktioniert auch ohne */ })
|
||||
}
|
||||
|
||||
async function handleSetCtx(name: string, cur: number | null) {
|
||||
// Setup-bewussten Optimalwert holen (Rolle/Params/Quant + aktuelles Setup).
|
||||
let auto: { ctx: number; gtt_gb: number; reserved_gb: number; budget_gb: number; mode: string } | null = null
|
||||
@@ -518,7 +528,7 @@ export function Cockpit() {
|
||||
: "border-dashed border-border/40 bg-background/20"
|
||||
)}
|
||||
style={{ left: "90%", top: yPositions[indexMap] }}
|
||||
onClick={() => setActiveRoleForAssign(role)}
|
||||
onClick={() => openRoleAssign(role)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">{role}</span>
|
||||
@@ -660,7 +670,7 @@ export function Cockpit() {
|
||||
return (
|
||||
<div
|
||||
key={role}
|
||||
onClick={() => setActiveRoleForAssign(role)}
|
||||
onClick={() => openRoleAssign(role)}
|
||||
className={cn(
|
||||
"rounded-xl border p-3 flex flex-col justify-between gap-2.5 transition-all cursor-pointer select-none text-left bg-background/25 hover:border-primary/45 hover:bg-background/40 group min-h-[90px]",
|
||||
isWarm
|
||||
@@ -1125,59 +1135,86 @@ export function Cockpit() {
|
||||
</div>
|
||||
|
||||
{/* Role Assignment Modal */}
|
||||
{activeRoleForAssign && (
|
||||
{activeRoleForAssign && (() => {
|
||||
const rec = roleRec && roleRec.role === activeRoleForAssign ? roleRec : null
|
||||
const recByName: Record<string, RoleRecResp["models"][number]> = {}
|
||||
rec?.models.forEach((r) => { recByName[r.name] = r })
|
||||
// Empfohlene Reihenfolge (nach Score) wenn vorhanden, sonst Bibliotheks-Reihenfolge.
|
||||
const ordered = rec
|
||||
? rec.models.map((r) => models.find((m) => m.name === r.name)).filter(Boolean) as ModelInfo[]
|
||||
: 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">
|
||||
<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>
|
||||
<button
|
||||
<button
|
||||
onClick={() => setActiveRoleForAssign(null)}
|
||||
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Wähle ein Modell aus deiner Bibliothek für die Rolle <strong className="text-foreground">{activeRoleForAssign}</strong>:
|
||||
</p>
|
||||
|
||||
<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>:
|
||||
</p>
|
||||
{rec?.recommended && (
|
||||
<button
|
||||
onClick={() => assign(rec.recommended!)}
|
||||
title={recByName[rec.recommended]?.reason}
|
||||
className="shrink-0 h-7 px-3 text-[11px] font-semibold text-primary border border-primary/50 bg-primary/10 hover:bg-primary/20 rounded-lg cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
<Zap className="h-3 w-3" /> Auto: {rec.recommended.split("/").pop()?.replace(/\.gguf$/i, "")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 max-h-60 overflow-y-auto pr-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
handleRoleChange(activeRoleForAssign, "")
|
||||
setActiveRoleForAssign(null)
|
||||
}}
|
||||
<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>
|
||||
{models.map((m) => (
|
||||
{ordered.map((m) => {
|
||||
const r = recByName[m.name]
|
||||
const isCur = m.role === activeRoleForAssign
|
||||
const isRec = !!r?.recommended
|
||||
const unfit = !!r && !r.suitable
|
||||
return (
|
||||
<button
|
||||
key={m.name}
|
||||
onClick={() => {
|
||||
handleRoleChange(activeRoleForAssign, m.name)
|
||||
setActiveRoleForAssign(null)
|
||||
}}
|
||||
onClick={() => assign(m.name)}
|
||||
className={cn(
|
||||
"w-full text-left px-3 py-2.5 rounded-lg text-xs hover:bg-accent flex items-center justify-between font-mono cursor-pointer border border-border/30",
|
||||
m.role === activeRoleForAssign
|
||||
? "text-primary font-bold bg-primary/10 border-primary/30"
|
||||
: "text-foreground bg-background/20"
|
||||
"w-full text-left px-3 py-2.5 rounded-lg text-xs hover:bg-accent flex items-center justify-between font-mono cursor-pointer border",
|
||||
isRec ? "border-primary/50 bg-primary/10"
|
||||
: isCur ? "text-primary font-bold bg-primary/5 border-primary/30"
|
||||
: unfit ? "border-border/20 bg-background/10 opacity-60"
|
||||
: "text-foreground bg-background/20 border-border/30"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col text-left">
|
||||
<span className="truncate max-w-[280px] font-semibold">{m.name.split("/").pop()?.replace(".gguf", "")}</span>
|
||||
<span className="text-[9px] text-muted-foreground mt-0.5">{fmtSize(m.size_bytes)} · {m.quant}</span>
|
||||
<div className="flex flex-col text-left min-w-0">
|
||||
<span className="truncate max-w-[260px] font-semibold flex items-center gap-1.5">
|
||||
{m.name.split("/").pop()?.replace(/\.gguf$/i, "")}
|
||||
{isRec && <span className="text-[8px] font-bold uppercase tracking-wide text-primary bg-primary/15 px-1.5 py-0.5 rounded">Empfohlen</span>}
|
||||
</span>
|
||||
<span className="text-[9px] text-muted-foreground mt-0.5">
|
||||
{r ? `${r.params_b}B · ${m.quant} · ${r.reason}` : `${fmtSize(m.size_bytes)} · ${m.quant}`}
|
||||
</span>
|
||||
</div>
|
||||
{m.role === activeRoleForAssign && <Check className="h-4 w-4 shrink-0 text-primary" />}
|
||||
{isCur && <Check className="h-4 w-4 shrink-0 text-primary" />}
|
||||
</button>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
})()}
|
||||
|
||||
{specModel && (
|
||||
<SpecDraftModal
|
||||
|
||||
Reference in New Issue
Block a user