Polish: A11y-, Motion- & Deep-Linking-Sweep (Web Interface Guidelines)

Aus dem web-design-guidelines-Audit über das ganze Frontend:

P1 — index.css: prefers-reduced-motion-Block (Animationen/Transitions aus),
color-scheme dark/light, zwei `transition: all` → explizite Properties,
globaler :focus-visible-Ring. Formular-aria-labels (SystemDrawer-Passwörter
inkl. name/autocomplete/spellCheck, Memory, Connect, ModelBrowse, LaneEditor,
CustomDialog). Icon-Button-aria-labels + dekorative Icons aria-hidden.

P2 — Memory-Lösch-Bestätigung (war sofort), overscroll-behavior:contain auf
Modals (CustomDialog/Agent/Cockpit/CommandPalette), ModelBrowse fmtN → Intl.NumberFormat.

P3 — Deep-Linking: top-level View im URL-Hash (#models), Reload/Teilen/Cmd-Klick
funktionieren; Sidebar-Nav als <a href="#id"> (echte Links, aria-current),
hashchange-Sync; index.css-Aktiv-Selektoren button→a nachgezogen.

Verifiziert: npm run build (tsc strict) clean; live gegen die Box (Hash-Nav,
Reload-Persistenz, aria-current, Aktiv-Styling teal+Akzent, keine Konsolenfehler).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-29 20:25:08 +02:00
parent 8108d3fd28
commit 087eb2259d
17 changed files with 378 additions and 306 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-BLpIFZ-c.js"></script> <script type="module" crossorigin src="/assets/index-B3e8J_PY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-MrCHFz7_.css"> <link rel="stylesheet" crossorigin href="/assets/index-DoqQd4Zi.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+29 -9
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react" import { useEffect, useState, useCallback } from "react"
import { Command as CommandIcon, ChevronLeft, ChevronRight } from "lucide-react" import { Command as CommandIcon, ChevronLeft, ChevronRight } from "lucide-react"
import { NAV, type ViewId } from "@/nav" import { NAV, type ViewId } from "@/nav"
import { CommandPalette } from "@/components/CommandPalette" import { CommandPalette } from "@/components/CommandPalette"
@@ -18,7 +18,15 @@ import { cn } from "@/lib/utils"
export default function App() { export default function App() {
useMetricsFeeder() // sammelt Live-Verlauf global, unabhängig vom aktiven Tab useMetricsFeeder() // sammelt Live-Verlauf global, unabhängig vom aktiven Tab
const [view, setView] = useState<ViewId>("dashboard") // View ist im URL-Hash (#models) verankert → Reload/Teilen/Cmd-Klick funktionieren.
const [view, setView] = useState<ViewId>(() => {
const h = window.location.hash.slice(1) as ViewId
return NAV.some((n) => n.id === h) ? h : "dashboard"
})
const navigate = useCallback((v: ViewId) => {
if (window.location.hash.slice(1) === v) setView(v)
else window.location.hash = v
}, [])
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem("mc_sidebar_collapsed") === "true") const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem("mc_sidebar_collapsed") === "true")
const [drawerOpen, setDrawerOpen] = useState(false) const [drawerOpen, setDrawerOpen] = useState(false)
const [drawerTab, setDrawerTab] = useState<"maintenance" | "logs">("maintenance") const [drawerTab, setDrawerTab] = useState<"maintenance" | "logs">("maintenance")
@@ -43,10 +51,19 @@ export default function App() {
useEffect(() => { useEffect(() => {
const handleNav = (e: Event) => { const handleNav = (e: Event) => {
const v = (e as CustomEvent).detail?.view as ViewId | undefined const v = (e as CustomEvent).detail?.view as ViewId | undefined
if (v) setView(v) if (v) navigate(v)
} }
window.addEventListener("mc-navigate", handleNav) window.addEventListener("mc-navigate", handleNav)
return () => window.removeEventListener("mc-navigate", handleNav) return () => window.removeEventListener("mc-navigate", handleNav)
}, [navigate])
useEffect(() => {
const onHash = () => {
const h = window.location.hash.slice(1) as ViewId
if (NAV.some((n) => n.id === h)) setView(h)
}
window.addEventListener("hashchange", onHash)
return () => window.removeEventListener("hashchange", onHash)
}, []) }, [])
const active = NAV.find((n) => n.id === view)! const active = NAV.find((n) => n.id === view)!
@@ -61,7 +78,7 @@ export default function App() {
<div className="absolute top-[30%] right-[20%] h-[40%] w-[40%] rounded-full bg-indigo-500/10 blur-[140px]" /> <div className="absolute top-[30%] right-[20%] h-[40%] w-[40%] rounded-full bg-indigo-500/10 blur-[140px]" />
</div> </div>
<CommandPalette onNavigate={setView} /> <CommandPalette onNavigate={navigate} />
<SystemDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} defaultTab={drawerTab} /> <SystemDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} defaultTab={drawerTab} />
{/* Sidebar */} {/* Sidebar */}
@@ -88,17 +105,19 @@ export default function App() {
}) })
}} }}
className="p-1 rounded-md hover:bg-accent text-muted-foreground transition-colors cursor-pointer" className="p-1 rounded-md hover:bg-accent text-muted-foreground transition-colors cursor-pointer"
aria-label={sidebarCollapsed ? "Seitenleiste ausklappen" : "Seitenleiste einklappen"}
title={sidebarCollapsed ? "Maximieren" : "Minimieren"} title={sidebarCollapsed ? "Maximieren" : "Minimieren"}
> >
{sidebarCollapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />} {sidebarCollapsed ? <ChevronRight className="h-4 w-4" aria-hidden="true" /> : <ChevronLeft className="h-4 w-4" aria-hidden="true" />}
</button> </button>
</div> </div>
<nav className="flex-1 space-y-1 px-3 py-4 overflow-y-auto"> <nav className="flex-1 space-y-1 px-3 py-4 overflow-y-auto">
{NAV.map((item) => ( {NAV.map((item) => (
<button <a
key={item.id} key={item.id}
onClick={() => setView(item.id)} href={`#${item.id}`}
aria-current={view === item.id ? "page" : undefined}
className={cn( className={cn(
"flex w-full items-center rounded-md text-sm transition-all cursor-pointer", "flex w-full items-center rounded-md text-sm transition-all cursor-pointer",
sidebarCollapsed ? "justify-center p-2.5" : "gap-3 px-3 py-2", sidebarCollapsed ? "justify-center p-2.5" : "gap-3 px-3 py-2",
@@ -107,10 +126,11 @@ export default function App() {
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground", : "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
)} )}
title={sidebarCollapsed ? item.label : undefined} title={sidebarCollapsed ? item.label : undefined}
aria-label={sidebarCollapsed ? item.label : undefined}
> >
<item.icon className="h-4.5 w-4.5 shrink-0" /> <item.icon className="h-4.5 w-4.5 shrink-0" aria-hidden="true" />
{!sidebarCollapsed && <span className="truncate">{item.label}</span>} {!sidebarCollapsed && <span className="truncate">{item.label}</span>}
</button> </a>
))} ))}
</nav> </nav>
+1 -1
View File
@@ -21,7 +21,7 @@ export function CommandPalette({ onNavigate }: { onNavigate: (v: ViewId) => void
open={open} open={open}
onOpenChange={setOpen} onOpenChange={setOpen}
label="Befehlspalette" label="Befehlspalette"
className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 p-4 pt-[12vh]" className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 p-4 pt-[12vh] overscroll-contain"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
> >
<div <div
+5 -3
View File
@@ -15,15 +15,16 @@ export interface CustomDialogProps {
export function CustomDialog({ type, title, message, defaultValue, autoValue, autoLabel, onConfirm, onCancel }: CustomDialogProps) { export function CustomDialog({ type, title, message, defaultValue, autoValue, autoLabel, onConfirm, onCancel }: CustomDialogProps) {
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
return ( return (
<div className="fixed inset-0 z-[99] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4"> <div className="fixed inset-0 z-[99] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 overscroll-contain" role="dialog" aria-modal="true" aria-label={title}>
<div className="w-full max-w-sm 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="w-full max-w-sm 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"> <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">{title}</h3> <h3 className="text-xs font-bold uppercase tracking-wider text-primary">{title}</h3>
<button <button
onClick={onCancel || (() => onConfirm())} onClick={onCancel || (() => onConfirm())}
aria-label="Schließen"
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer" className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer"
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" aria-hidden="true" />
</button> </button>
</div> </div>
<p className="text-xs text-muted-foreground leading-relaxed">{message}</p> <p className="text-xs text-muted-foreground leading-relaxed">{message}</p>
@@ -34,6 +35,7 @@ export function CustomDialog({ type, title, message, defaultValue, autoValue, au
ref={inputRef} ref={inputRef}
type="text" type="text"
defaultValue={defaultValue} defaultValue={defaultValue}
aria-label={title}
className="flex-1 h-9 px-3 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground" className="flex-1 h-9 px-3 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
autoFocus autoFocus
onKeyDown={(e) => { onKeyDown={(e) => {
+13 -3
View File
@@ -722,15 +722,20 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
type={showSudo ? "text" : "password"} type={showSudo ? "text" : "password"}
value={sudoPasswordInput} value={sudoPasswordInput}
onChange={(e) => setSudoPasswordInput(e.target.value)} onChange={(e) => setSudoPasswordInput(e.target.value)}
aria-label="Host Sudo-Passwort"
name="sudo-password"
autoComplete="off"
spellCheck={false}
placeholder="Sudo-Passwort für System-Operationen" placeholder="Sudo-Passwort für System-Operationen"
className="w-full h-10 pl-3 pr-10 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground" className="w-full h-10 pl-3 pr-10 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
/> />
<button <button
type="button" type="button"
onClick={() => setShowSudo(!showSudo)} onClick={() => setShowSudo(!showSudo)}
aria-label={showSudo ? "Passwort verbergen" : "Passwort anzeigen"}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground transition-colors" className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground transition-colors"
> >
{showSudo ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} {showSudo ? <EyeOff className="h-4 w-4" aria-hidden="true" /> : <Eye className="h-4 w-4" aria-hidden="true" />}
</button> </button>
</div> </div>
<p className="text-[9px] text-muted-foreground leading-normal"> <p className="text-[9px] text-muted-foreground leading-normal">
@@ -749,15 +754,20 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
type={showHf ? "text" : "password"} type={showHf ? "text" : "password"}
value={hfTokenInput} value={hfTokenInput}
onChange={(e) => setHfTokenInput(e.target.value)} onChange={(e) => setHfTokenInput(e.target.value)}
placeholder="hf_..." aria-label="HuggingFace API Token"
name="hf-token"
autoComplete="off"
spellCheck={false}
placeholder="hf_…"
className="w-full h-10 pl-3 pr-10 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground" className="w-full h-10 pl-3 pr-10 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
/> />
<button <button
type="button" type="button"
onClick={() => setShowHf(!showHf)} onClick={() => setShowHf(!showHf)}
aria-label={showHf ? "Token verbergen" : "Token anzeigen"}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground transition-colors" className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground transition-colors"
> >
{showHf ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} {showHf ? <EyeOff className="h-4 w-4" aria-hidden="true" /> : <Eye className="h-4 w-4" aria-hidden="true" />}
</button> </button>
</div> </div>
<p className="text-[9px] text-muted-foreground leading-normal"> <p className="text-[9px] text-muted-foreground leading-normal">
@@ -39,7 +39,8 @@ export function MemoryInputCard() {
<textarea <textarea
value={memContent} value={memContent}
onChange={(e) => setMemContent(e.target.value)} onChange={(e) => setMemContent(e.target.value)}
placeholder="Fakt / Regel im Pool speichern..." aria-label="Fakt oder Regel im Gedächtnis speichern"
placeholder="Fakt / Regel im Pool speichern…"
rows={2} rows={2}
className="w-full resize-none rounded-xl border border-border/50 bg-background/30 p-2 text-[10px] outline-none focus:ring-1 focus:ring-primary transition-all leading-normal" className="w-full resize-none rounded-xl border border-border/50 bg-background/30 p-2 text-[10px] outline-none focus:ring-1 focus:ring-primary transition-all leading-normal"
/> />
@@ -47,6 +48,7 @@ export function MemoryInputCard() {
<select <select
value={memCat} value={memCat}
onChange={(e) => setMemCat(e.target.value)} onChange={(e) => setMemCat(e.target.value)}
aria-label="Kategorie"
className="h-7 rounded-lg border border-border/50 bg-background/50 px-2 text-[10px] outline-none cursor-pointer" className="h-7 rounded-lg border border-border/50 bg-background/50 px-2 text-[10px] outline-none cursor-pointer"
> >
<option value="stable">🔵 Fakt</option> <option value="stable">🔵 Fakt</option>
@@ -96,6 +96,7 @@ export function LaneEditor() {
value={val as number} value={val as number}
min={spec.min} min={spec.min}
max={spec.max} max={spec.max}
aria-label={spec.label}
onChange={(e) => set(k, Number(e.target.value) as any)} 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" 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"
/> />
@@ -103,6 +104,8 @@ export function LaneEditor() {
<input <input
type="text" type="text"
value={val as string} value={val as string}
aria-label={spec.label}
spellCheck={false}
placeholder={k === "coder_lite" ? "(leer = aus)" : ""} placeholder={k === "coder_lite" ? "(leer = aus)" : ""}
onChange={(e) => set(k, e.target.value as any)} 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" 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"
+23 -4
View File
@@ -21,6 +21,7 @@
--input: 222 18% 23%; --input: 222 18% 23%;
--ring: 172 72% 50%; --ring: 172 72% 50%;
--radius: 0.75rem; --radius: 0.75rem;
color-scheme: dark;
} }
.light { .light {
@@ -39,6 +40,7 @@
--border: 220 13% 88%; --border: 220 13% 88%;
--input: 220 13% 88%; --input: 220 13% 88%;
--ring: 172 66% 50%; --ring: 172 66% 50%;
color-scheme: light;
} }
@theme inline { @theme inline {
@@ -97,6 +99,23 @@
outline-color: hsl(var(--primary) / 0.5); outline-color: hsl(var(--primary) / 0.5);
} }
/* Sichtbarer Fokus für Tastatur-Nutzer auf allen interaktiven Elementen
(Inputs haben ihren eigenen Ring unten via box-shadow). */
:focus-visible {
outline: 2px solid hsl(var(--primary) / 0.7);
outline-offset: 2px;
}
/* Bewegungs-Sensibilität respektieren: Animationen/Transitions praktisch aus. */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
html, html,
body, body,
#root { #root {
@@ -139,7 +158,7 @@ body {
box-shadow: 0 10px 30px -15px rgba(0, 0, 0, 0.45), inset 0 1px 0 0 rgba(255, 255, 255, 0.05) !important; box-shadow: 0 10px 30px -15px rgba(0, 0, 0, 0.45), inset 0 1px 0 0 rgba(255, 255, 255, 0.05) !important;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
} }
/* Gorgeous gradient top accent line to all main cards to inject color and structure */ /* Gorgeous gradient top accent line to all main cards to inject color and structure */
@@ -171,7 +190,7 @@ body {
.rounded-xl.border.bg-card\/45 { .rounded-xl.border.bg-card\/45 {
background-color: hsl(var(--card) / 0.88) !important; background-color: hsl(var(--card) / 0.88) !important;
border-color: hsl(var(--border) / 0.9) !important; border-color: hsl(var(--border) / 0.9) !important;
transition: all 0.2s ease !important; transition: background-color 0.2s ease, border-color 0.2s ease !important;
} }
.rounded-xl.border.bg-card\/45:hover { .rounded-xl.border.bg-card\/45:hover {
background-color: hsl(var(--card) / 0.95) !important; background-color: hsl(var(--card) / 0.95) !important;
@@ -179,7 +198,7 @@ body {
} }
/* Sidebar active link styling */ /* Sidebar active link styling */
aside nav button[class*="bg-primary/15"]:not([class*="justify-center"]) { aside nav a[class*="bg-primary/15"]:not([class*="justify-center"]) {
background: linear-gradient(90deg, hsl(var(--primary) / 0.18), rgba(99, 102, 241, 0.12)) !important; background: linear-gradient(90deg, hsl(var(--primary) / 0.18), rgba(99, 102, 241, 0.12)) !important;
color: hsl(var(--primary)) !important; color: hsl(var(--primary)) !important;
border-left: 3.5px solid hsl(var(--primary)) !important; border-left: 3.5px solid hsl(var(--primary)) !important;
@@ -188,7 +207,7 @@ aside nav button[class*="bg-primary/15"]:not([class*="justify-center"]) {
box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.02) !important; box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.02) !important;
} }
aside nav button[class*="bg-primary/15"][class*="justify-center"] { aside nav a[class*="bg-primary/15"][class*="justify-center"] {
background: hsl(var(--primary) / 0.18) !important; background: hsl(var(--primary) / 0.18) !important;
color: hsl(var(--primary)) !important; color: hsl(var(--primary)) !important;
box-shadow: 0 0 12px 1px hsl(var(--primary) / 0.1) !important; box-shadow: 0 0 12px 1px hsl(var(--primary) / 0.1) !important;
+1 -1
View File
@@ -250,7 +250,7 @@ export function AgentView() {
{/* Brain Selection Modal — eine Stelle für den Hirn-Wechsel (warm-bewusst + Alias) */} {/* Brain Selection Modal — eine Stelle für den Hirn-Wechsel (warm-bewusst + Alias) */}
{s && showBrainSelect && ( {s && showBrainSelect && (
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4"> <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="Hermes-Hirn 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="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"> <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 flex items-center gap-1.5"> <h3 className="text-xs font-bold uppercase tracking-wider text-primary flex items-center gap-1.5">
+4
View File
@@ -178,6 +178,8 @@ export function ConnectView() {
<input <input
value={host} value={host}
onChange={(e) => saveHost(e.target.value)} onChange={(e) => saveHost(e.target.value)}
aria-label="Box LAN IP-Adresse"
spellCheck={false}
placeholder="z.B. 192.168.178.151" placeholder="z.B. 192.168.178.151"
className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground" className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground"
/> />
@@ -191,6 +193,8 @@ export function ConnectView() {
<input <input
value={mcpPath} value={mcpPath}
onChange={(e) => saveMcpPath(e.target.value)} onChange={(e) => saveMcpPath(e.target.value)}
aria-label="Lokaler MCP-Scriptpfad"
spellCheck={false}
placeholder="z.B. F:\Coding Stuff\mission-control-2\mcp\mcp_memory.py" placeholder="z.B. F:\Coding Stuff\mission-control-2\mcp\mcp_memory.py"
className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-violet-500/50 text-foreground" className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-violet-500/50 text-foreground"
/> />
+14 -8
View File
@@ -94,9 +94,11 @@ export function MemoryView() {
setContent(""); setShowAdd(false); reloadMemory() setContent(""); setShowAdd(false); reloadMemory()
} }
async function del(id: string) { function del(id: string) {
await api(`/api/memory/${id}`, { method: "DELETE" }) showConfirm("Eintrag löschen?", "Diesen Gedächtnis-Eintrag dauerhaft entfernen?", async () => {
reloadMemory() try { await api(`/api/memory/${id}`, { method: "DELETE" }); reloadMemory() }
catch (e: any) { showAlert("Fehler", `Löschen fehlgeschlagen: ${e.message || e}`) }
})
} }
async function copyPrompt() { async function copyPrompt() {
@@ -156,6 +158,8 @@ export function MemoryView() {
<input <input
value={q} value={q}
onChange={(e) => setQ(e.target.value)} onChange={(e) => setQ(e.target.value)}
aria-label="Gedächtnis durchsuchen"
type="search"
placeholder="Semantisch suchen…" placeholder="Semantisch suchen…"
className="w-52 h-9 pl-8 pr-3 rounded-lg border border-border/60 bg-card/45 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground" className="w-52 h-9 pl-8 pr-3 rounded-lg border border-border/60 bg-card/45 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground"
/> />
@@ -176,8 +180,8 @@ export function MemoryView() {
</div> </div>
<button onClick={cleanup} disabled={deduping} <button onClick={cleanup} disabled={deduping}
className="flex h-9 w-9 items-center justify-center rounded-lg border border-border/60 bg-card hover:border-primary/50 transition-all cursor-pointer shadow-md disabled:opacity-50" className="flex h-9 w-9 items-center justify-center rounded-lg border border-border/60 bg-card hover:border-primary/50 transition-all cursor-pointer shadow-md disabled:opacity-50"
title="Deduplizieren"> aria-label="Duplikate bereinigen" title="Deduplizieren">
<Sparkles className="h-4 w-4 text-primary" /> <Sparkles className="h-4 w-4 text-primary" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
@@ -200,13 +204,15 @@ export function MemoryView() {
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-4 space-y-3"> <div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-4 space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Neuen Eintrag anlegen</div> <div className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Neuen Eintrag anlegen</div>
<button onClick={() => setShowAdd(false)} className="text-muted-foreground hover:text-foreground"><X className="h-4 w-4" /></button> <button onClick={() => setShowAdd(false)} aria-label="Schließen" className="text-muted-foreground hover:text-foreground"><X className="h-4 w-4" aria-hidden="true" /></button>
</div> </div>
<textarea value={content} onChange={(e) => setContent(e.target.value)} rows={2} <textarea value={content} onChange={(e) => setContent(e.target.value)} rows={2}
aria-label="Eintragstext"
placeholder="Eine Regel, Vorliebe oder einen stabilen Fakt über dich oder das Projekt…" placeholder="Eine Regel, Vorliebe oder einen stabilen Fakt über dich oder das Projekt…"
className="w-full resize-none rounded-xl border border-border/60 bg-background/30 p-3 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground leading-relaxed" /> className="w-full resize-none rounded-xl border border-border/60 bg-background/30 p-3 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground leading-relaxed" />
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<select value={category} onChange={(e) => setCategory(e.target.value)} <select value={category} onChange={(e) => setCategory(e.target.value)}
aria-label="Kategorie"
className="h-8 rounded-lg border border-border/60 bg-background/50 px-2 text-xs outline-none font-semibold text-foreground cursor-pointer"> className="h-8 rounded-lg border border-border/60 bg-background/50 px-2 text-xs outline-none font-semibold text-foreground cursor-pointer">
{CATEGORIES.map((c) => <option key={c} value={c} className="bg-popover text-foreground">{CAT_CONFIG[c]?.label || c}</option>)} {CATEGORIES.map((c) => <option key={c} value={c} className="bg-popover text-foreground">{CAT_CONFIG[c]?.label || c}</option>)}
</select> </select>
@@ -321,9 +327,9 @@ export function MemoryView() {
title={isAuto ? "Automatisch gelernt" : "Manuell angelegt"}> title={isAuto ? "Automatisch gelernt" : "Manuell angelegt"}>
{isAuto && <Sparkles className="h-2.5 w-2.5" />}{m.source} {isAuto && <Sparkles className="h-2.5 w-2.5" />}{m.source}
</span> </span>
<button onClick={() => del(m.id)} title="Eintrag löschen" <button onClick={() => del(m.id)} aria-label="Eintrag löschen" title="Eintrag löschen"
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 opacity-0 group-hover:opacity-100"> 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 opacity-0 group-hover:opacity-100">
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
+5 -5
View File
@@ -655,9 +655,9 @@ export function Cockpit() {
<button <button
onClick={() => handleDelete(m.name)} 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" 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"
title="Modell löschen" aria-label="Modell löschen" title="Modell löschen"
> >
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
@@ -811,9 +811,9 @@ export function Cockpit() {
<button <button
onClick={() => handleDelete(m.name)} 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" 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"
title="Modell löschen" aria-label="Modell löschen" title="Modell löschen"
> >
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
@@ -836,7 +836,7 @@ export function Cockpit() {
: models : models
const assign = (name: string) => { handleRoleChange(activeRoleForAssign, name); setActiveRoleForAssign(null) } const assign = (name: string) => { handleRoleChange(activeRoleForAssign, name); setActiveRoleForAssign(null) }
return ( return (
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4"> <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="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="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"> <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"> <h3 className="text-xs font-bold uppercase tracking-wider text-primary">
+9 -3
View File
@@ -15,10 +15,9 @@ const CHIPS: { key: string; label: string; q: string }[] = [
] ]
const ROLE_OPTS = ["fast", "heavy", "coder", "vision", "scout"] const ROLE_OPTS = ["fast", "heavy", "coder", "vision", "scout"]
const _nf = new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 })
function fmtN(n: number): string { function fmtN(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` return _nf.format(n)
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}k`
return String(n)
} }
export function ModelBrowse() { export function ModelBrowse() {
@@ -143,6 +142,9 @@ export function ModelBrowse() {
value={q} value={q}
onChange={(e) => setQ(e.target.value)} onChange={(e) => setQ(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && submitSearch()} onKeyDown={(e) => e.key === "Enter" && submitSearch()}
aria-label="HuggingFace durchsuchen"
type="search"
spellCheck={false}
placeholder="HuggingFace durchsuchen (z.B. Qwen Coder, Llama-3.1, gemma)…" placeholder="HuggingFace durchsuchen (z.B. Qwen Coder, Llama-3.1, gemma)…"
className="w-full h-10 pl-9 pr-3 rounded-xl border border-border/60 bg-background/40 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground" className="w-full h-10 pl-9 pr-3 rounded-xl border border-border/60 bg-background/40 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground"
/> />
@@ -227,6 +229,7 @@ export function ModelBrowse() {
<select <select
value={quant} value={quant}
onChange={(e) => { setQuant(e.target.value); refreshFit(r.repo, e.target.value, role) }} onChange={(e) => { setQuant(e.target.value); refreshFit(r.repo, e.target.value, role) }}
aria-label="Quantisierung"
className="h-8 rounded-lg border border-border/60 bg-background/40 px-2 text-xs text-foreground font-semibold outline-none" className="h-8 rounded-lg border border-border/60 bg-background/40 px-2 text-xs text-foreground font-semibold outline-none"
> >
{quants.map((qq) => <option key={qq} value={qq} className="bg-popover text-foreground">{qq}</option>)} {quants.map((qq) => <option key={qq} value={qq} className="bg-popover text-foreground">{qq}</option>)}
@@ -235,6 +238,7 @@ export function ModelBrowse() {
<select <select
value={role} value={role}
onChange={(e) => { setRole(e.target.value); refreshFit(r.repo, quant, e.target.value) }} onChange={(e) => { setRole(e.target.value); refreshFit(r.repo, quant, e.target.value) }}
aria-label="Rolle"
title="Optional — bestimmt Auto-Konfiguration + setup-bewussten Kontext" title="Optional — bestimmt Auto-Konfiguration + setup-bewussten Kontext"
className="h-8 rounded-lg border border-border/60 bg-background/40 px-2 text-xs text-foreground font-semibold outline-none" className="h-8 rounded-lg border border-border/60 bg-background/40 px-2 text-xs text-foreground font-semibold outline-none"
> >
@@ -291,6 +295,8 @@ export function ModelBrowse() {
value={direct} value={direct}
onChange={(e) => setDirect(e.target.value)} onChange={(e) => setDirect(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && loadDirect()} onKeyDown={(e) => e.key === "Enter" && loadDirect()}
aria-label="Repository oder URL direkt eingeben"
spellCheck={false}
placeholder="org/repo oder HF-URL (z.B. unsloth/Qwen2.5-Coder-7B-Instruct-GGUF)" placeholder="org/repo oder HF-URL (z.B. unsloth/Qwen2.5-Coder-7B-Instruct-GGUF)"
className="flex-1 h-9 rounded-lg border border-border/60 bg-background/40 px-3 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground font-mono" className="flex-1 h-9 rounded-lg border border-border/60 bg-background/40 px-3 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground font-mono"
/> />