feat: lower memory dedupe threshold for more aggressive cleaning
This commit is contained in:
+480
-480
@@ -1,480 +1,480 @@
|
||||
// Schmaler Fetch-Helfer gegen das MC-2-Backend (/api/*).
|
||||
|
||||
export async function api<T = unknown>(path: string, init?: RequestInit): Promise<T> {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...init?.headers,
|
||||
} as Record<string, string>
|
||||
|
||||
const sudoPassword = localStorage.getItem("mc_sudo_password")
|
||||
const hfToken = localStorage.getItem("mc_hf_token")
|
||||
|
||||
if (sudoPassword) {
|
||||
headers["X-Sudo-Password"] = sudoPassword
|
||||
}
|
||||
|
||||
let body = init?.body
|
||||
const method = init?.method?.toUpperCase() || "GET"
|
||||
if (method === "POST") {
|
||||
if (typeof body === "string") {
|
||||
try {
|
||||
const data = JSON.parse(body)
|
||||
let changed = false
|
||||
if (sudoPassword && !("sudo_password" in data)) {
|
||||
data["sudo_password"] = sudoPassword
|
||||
changed = true
|
||||
}
|
||||
if (hfToken && !("hf_token" in data)) {
|
||||
data["hf_token"] = hfToken
|
||||
changed = true
|
||||
}
|
||||
if (changed) {
|
||||
body = JSON.stringify(data)
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore JSON parse errors
|
||||
}
|
||||
} else if (!body) {
|
||||
const data: Record<string, any> = {}
|
||||
if (sudoPassword) data["sudo_password"] = sudoPassword
|
||||
if (hfToken) data["hf_token"] = hfToken
|
||||
if (Object.keys(data).length > 0) {
|
||||
body = JSON.stringify(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(path, {
|
||||
...init,
|
||||
headers,
|
||||
body,
|
||||
})
|
||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
||||
return res.json() as Promise<T>
|
||||
}
|
||||
|
||||
// llama-swap-`groups`: Mitglieder mit swap=false dürfen GLEICHZEITIG resident sein (Ko-Residenz).
|
||||
// Die `brains`-Gruppe hält Hirn (fast) + Augen (vision) gemeinsam warm, statt sich zu verdrängen.
|
||||
export interface GroupSpec {
|
||||
swap: boolean
|
||||
persist: boolean
|
||||
members: string[]
|
||||
}
|
||||
export interface GroupsResp {
|
||||
groups: Record<string, GroupSpec>
|
||||
}
|
||||
export const getGroups = () => api<GroupsResp>("/api/groups")
|
||||
export const setGroup = (group: string, members: string[], swap = false, persist = true) =>
|
||||
api("/api/groups", { method: "PUT", body: JSON.stringify({ group, members, swap, persist }) })
|
||||
|
||||
export interface Capabilities {
|
||||
moe: boolean
|
||||
active_b: number | null
|
||||
tools: "yes" | "likely" | "no"
|
||||
vision: boolean
|
||||
coder: boolean
|
||||
reasoning: boolean
|
||||
embedding: boolean
|
||||
ctx: number | null
|
||||
params_b: number | null
|
||||
arch: string | null
|
||||
}
|
||||
|
||||
export interface Fit {
|
||||
level: "perfect" | "marginal" | "too_tight"
|
||||
text: string
|
||||
req_gb: number
|
||||
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
|
||||
optimal_ctx: number
|
||||
assigned_ctx: number
|
||||
budget: { gtt_gb: number; reserved_gb: number; budget_gb: number; mode: string }
|
||||
sys_ram_gb: number
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
name: string
|
||||
role: string | null
|
||||
aliases: string[]
|
||||
api_ids: string[]
|
||||
ctx: number | null
|
||||
ttl: number | null
|
||||
cmd: string
|
||||
gguf_path: string
|
||||
filename: string
|
||||
quant: string
|
||||
size_bytes: number | null
|
||||
incomplete: boolean
|
||||
prompt_cache: boolean
|
||||
spec_draft_model: string | null
|
||||
spec_type: string | null
|
||||
spec_active: boolean
|
||||
parallel_slots: number
|
||||
capabilities: Capabilities
|
||||
}
|
||||
|
||||
export interface VocabFingerprint {
|
||||
model: string | null
|
||||
pre: string | null
|
||||
n_vocab: number | null
|
||||
arch: string | null
|
||||
}
|
||||
|
||||
export interface DraftInfo {
|
||||
path: string
|
||||
filename: string
|
||||
size_bytes: number | null
|
||||
vocab: VocabFingerprint | null
|
||||
compatible: boolean | null // null = nicht prüfbar (Ziel-GGUF fehlt)
|
||||
mtp?: boolean // MTP-Kopf (Multi-Token-Prediction) statt klassischem Draft
|
||||
}
|
||||
|
||||
export interface DraftsResp {
|
||||
target_path: string
|
||||
target_exists: boolean
|
||||
target_vocab: VocabFingerprint | null
|
||||
drafts: DraftInfo[]
|
||||
}
|
||||
|
||||
export interface DiscoverModel {
|
||||
name: string
|
||||
author: string
|
||||
repo: string
|
||||
role: string
|
||||
params_b: number
|
||||
quant: string
|
||||
tags: string[]
|
||||
downloads: number
|
||||
fit: Fit
|
||||
optimal_ctx: number
|
||||
caps: Capabilities
|
||||
}
|
||||
|
||||
export interface DiscoverCategory {
|
||||
role: string
|
||||
title: string
|
||||
icon: string
|
||||
models: DiscoverModel[]
|
||||
recommended: string | null
|
||||
}
|
||||
|
||||
export interface DiscoverResp {
|
||||
updated: number
|
||||
categories: DiscoverCategory[]
|
||||
sys_ram_gb: number
|
||||
}
|
||||
|
||||
export interface RoutingResp {
|
||||
mode?: string
|
||||
endpoint?: string
|
||||
heavy_threshold_chars?: number
|
||||
routes: { name: string; target: string }[]
|
||||
lanes?: { name: string; target: string; threshold_chars?: number; escalate_chars?: number; aka?: string }[]
|
||||
fallbacks: Record<string, string[]>[]
|
||||
context_window_fallbacks: Record<string, string[]>[]
|
||||
gateway_reachable: boolean
|
||||
}
|
||||
|
||||
// UI-editierbare Routing-Policy (GET/PUT /api/routing/policy). Aliase + Zeichen-Schwellen
|
||||
// hinter den Lanes; hot-reload im Backend (kein Restart).
|
||||
export interface RoutingPolicy {
|
||||
fast: string
|
||||
heavy: string
|
||||
coder: string
|
||||
coder_lite: string
|
||||
heavy_chars: number
|
||||
coding_escalate_chars: number
|
||||
fast_no_think: boolean
|
||||
}
|
||||
export interface RoutingPolicyField {
|
||||
key: keyof RoutingPolicy
|
||||
label: string
|
||||
type: "str" | "int" | "bool"
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
export interface RoutingPolicyMeta {
|
||||
policy: RoutingPolicy
|
||||
defaults: RoutingPolicy
|
||||
fields: RoutingPolicyField[]
|
||||
}
|
||||
export const getRoutingPolicy = () => api<RoutingPolicyMeta>("/api/routing/policy")
|
||||
export const updateRoutingPolicy = (patch: Partial<RoutingPolicy>) =>
|
||||
api<{ policy: RoutingPolicy }>("/api/routing/policy", { method: "PUT", body: JSON.stringify(patch) })
|
||||
|
||||
export interface GitInfo {
|
||||
hash: string
|
||||
date: string
|
||||
subject: string
|
||||
branch: string
|
||||
dirty: boolean
|
||||
path: string
|
||||
}
|
||||
|
||||
// Engine kann git-, binary- oder unbekannte Version sein — Felder je nach `type`.
|
||||
export interface ComponentVersion {
|
||||
type: "git" | "binary" | "unknown"
|
||||
hash?: string
|
||||
date?: string
|
||||
subject?: string
|
||||
branch?: string
|
||||
dirty?: boolean
|
||||
path?: string
|
||||
version_text?: string
|
||||
}
|
||||
|
||||
export interface Versions {
|
||||
mc2: GitInfo | null
|
||||
engine: ComponentVersion
|
||||
hermes_ui: GitInfo | null
|
||||
hermes_agent: GitInfo | null
|
||||
}
|
||||
|
||||
export interface SystemStatus {
|
||||
cpu: { percent: number; cores: number | null }
|
||||
ram: { total: number; used: number; percent: number }
|
||||
gpu: {
|
||||
busy_percent: number | null
|
||||
vram_used: number | null
|
||||
vram_total: number | null
|
||||
gtt_used?: number | null
|
||||
gtt_total?: number | null
|
||||
} | null
|
||||
temp: { cpu?: number; gpu?: number } | null
|
||||
disk: { total: number; used: number; percent: number } | null
|
||||
versions?: Versions
|
||||
}
|
||||
|
||||
export interface ServicesResp {
|
||||
services: { name: string; unit: string; url: string; ok: boolean }[]
|
||||
links: { engine_ui: string; gateway: string; hermes_terminal: string }
|
||||
}
|
||||
|
||||
export interface ComponentUpdate {
|
||||
key: string // z.B. "hermes_agent"
|
||||
name: string
|
||||
current: string | null
|
||||
latest: string | null
|
||||
update: boolean | null // null = unbestimmbar (installierte Version remote nicht abfragbar)
|
||||
reachable: boolean | null
|
||||
}
|
||||
|
||||
export interface UpdatesResp {
|
||||
os: number
|
||||
engine: number
|
||||
swap: number
|
||||
models: number
|
||||
model_list: { role: string; title: string; repo: string }[]
|
||||
last_check?: number | null
|
||||
components?: ComponentUpdate[]
|
||||
}
|
||||
|
||||
export interface UpdateDetails {
|
||||
kind: "os" | "engine" | "swap" | "hermes"
|
||||
error?: string
|
||||
// LLM-Zusammenfassung in Lucys Stimme (hermes/engine/swap; Breaking Changes zuerst)
|
||||
summary?: string
|
||||
// Aktions-Verdikt: muss der Besitzer selbst etwas tun? (null = kein Verdikt/keine Summary)
|
||||
action_needed?: boolean | null
|
||||
action_text?: string
|
||||
// os
|
||||
count?: number
|
||||
packages?: { name: string; current: string; candidate: string }[]
|
||||
// os: vom System zurückgestellte Pakete (Phasen-Rollout / kept back) — ehrlich statt "hängt"
|
||||
held_back?: { name: string; reason: "phasing" | "kept_back" }[]
|
||||
// engine
|
||||
installed_build?: number | null
|
||||
latest_build?: number | null
|
||||
latest_tag?: string | null
|
||||
name?: string | null
|
||||
url?: string | null
|
||||
body?: string | null
|
||||
// hermes
|
||||
branch?: string | null
|
||||
behind?: number
|
||||
commits?: { hash: string; subject: string; when: string }[]
|
||||
}
|
||||
|
||||
export interface ConnectTool {
|
||||
label: string
|
||||
lang: string
|
||||
snippet: string
|
||||
note: string
|
||||
}
|
||||
export interface ConnectResp {
|
||||
host: string
|
||||
gateway_url: string
|
||||
mc_url: string
|
||||
tools: Record<string, ConnectTool> // Leitung 1 — Modell (IDEs/Agenten → Gateway)
|
||||
memory: ConnectTool // Leitung 2 — Gedächtnis (separater MCP-Server)
|
||||
}
|
||||
|
||||
export interface ConnectLine {
|
||||
ok: boolean
|
||||
detail: string
|
||||
}
|
||||
export interface ConnectHealth {
|
||||
gateway: ConnectLine
|
||||
memory: ConnectLine
|
||||
}
|
||||
|
||||
export interface Memory {
|
||||
id: string
|
||||
content: string
|
||||
category: string
|
||||
source: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
score?: number // Relevanz bei semantischer Suche (q gesetzt); sonst undefined
|
||||
}
|
||||
|
||||
export interface MemoryGraphNode {
|
||||
id: string
|
||||
content: string
|
||||
category: string
|
||||
source: string
|
||||
}
|
||||
export interface MemoryGraph {
|
||||
nodes: MemoryGraphNode[]
|
||||
edges: { source: string; target: string; weight: number }[]
|
||||
}
|
||||
|
||||
export interface DedupeResult {
|
||||
groups: { keep: { id: string; content: string; category: string }; remove: { id: string; content: string }[] }[]
|
||||
duplicate_count: number
|
||||
removed: number
|
||||
applied: boolean
|
||||
}
|
||||
|
||||
export interface AgentStatus {
|
||||
gateway_url: string
|
||||
terminal_url: string
|
||||
box_console_url?: string
|
||||
hermes_ui_url?: string
|
||||
gateway_reachable: boolean
|
||||
terminal_reachable: boolean
|
||||
box_console_reachable?: boolean
|
||||
hermes_ui_reachable?: boolean
|
||||
home_exists: boolean
|
||||
brain_model?: string
|
||||
has_config: boolean
|
||||
has_skills: boolean
|
||||
has_memories: boolean
|
||||
telegram_enabled?: boolean
|
||||
mcp_server_count?: number
|
||||
pc_executor_reachable?: boolean
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
id: string
|
||||
label: string
|
||||
group?: string | null // "maintenance" = system-veränderndes Update (Wartungs-Riegel)
|
||||
state: "queued" | "running" | "done" | "failed" | "canceled"
|
||||
progress?: number
|
||||
total_bytes?: number
|
||||
done_bytes?: number
|
||||
rate_bps?: number
|
||||
eta_s?: number
|
||||
}
|
||||
|
||||
export interface Health {
|
||||
status: string
|
||||
version: string
|
||||
engine_reachable: boolean
|
||||
gateway_reachable: boolean
|
||||
brain?: { role: string; model: string | null; ready: boolean }
|
||||
}
|
||||
|
||||
export interface TokenStats {
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
total_tokens: number
|
||||
saved_usd: number
|
||||
saved_eur: number
|
||||
pricing?: Record<string, { in: number; out: number }>
|
||||
}
|
||||
|
||||
// Per-Turn-Latenz-Trace (GET /api/voice/trace). Balken-Stufen (zeitlich disjunkt):
|
||||
// stt · vision · hirn · gen. mem0 = Unter-Detail INNERHALB von hirn (nicht zum Balken addieren).
|
||||
export interface VoiceTurn {
|
||||
id: string
|
||||
ts: number // Epoch-Sekunden
|
||||
session: string
|
||||
kind: string
|
||||
had_images: boolean
|
||||
stt_ms: number | null
|
||||
vision_ms: number | null
|
||||
hirn_ms: number | null // Zeit bis erstes Inhalts-Token (Agent + Mem0 + LLM-TTFT)
|
||||
gen_ms: number | null // Generierung nach dem ersten Token bis Stream-Ende
|
||||
mem0_ms: number | null // Teil VON hirn (Mem0-Retrieve), best-effort
|
||||
total_ms: number
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export interface VoiceTraceResp {
|
||||
turns: VoiceTurn[]
|
||||
}
|
||||
|
||||
export interface ModelsResp {
|
||||
models: ModelInfo[]
|
||||
running?: string[]
|
||||
}
|
||||
|
||||
export interface HermesBrainModel {
|
||||
name: string
|
||||
filename?: string
|
||||
params_b: number | null
|
||||
quant?: string
|
||||
size_bytes?: number | null
|
||||
version?: number | null
|
||||
gguf_path?: string
|
||||
incomplete?: boolean
|
||||
}
|
||||
|
||||
export interface HermesBrainCandidate {
|
||||
repo: string
|
||||
name: string
|
||||
version: number
|
||||
params_b: number
|
||||
downloads: number
|
||||
fit: Fit
|
||||
}
|
||||
|
||||
export interface HermesBrainBudget {
|
||||
gtt_gb: number
|
||||
brain_gb: number // Footprint des (empfohlenen) Brains, das immer resident bleibt
|
||||
warm_projected_gb: number // alle persist-Modelle warm (Info)
|
||||
free_after_gb: number // frei nach Brain + größtem on-demand
|
||||
largest_ondemand_gb: number // größtes on-demand-Modell (z.B. heavy/coder)
|
||||
fits: boolean // passt Brain + größtes on-demand zusammen ins Budget?
|
||||
}
|
||||
|
||||
export interface HermesBrainResp {
|
||||
current: HermesBrainModel | null
|
||||
recommended: HermesBrainCandidate | null
|
||||
update_available: boolean
|
||||
budget?: HermesBrainBudget | null
|
||||
}
|
||||
// Schmaler Fetch-Helfer gegen das MC-2-Backend (/api/*).
|
||||
|
||||
export async function api<T = unknown>(path: string, init?: RequestInit): Promise<T> {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...init?.headers,
|
||||
} as Record<string, string>
|
||||
|
||||
const sudoPassword = localStorage.getItem("mc_sudo_password")
|
||||
const hfToken = localStorage.getItem("mc_hf_token")
|
||||
|
||||
if (sudoPassword) {
|
||||
headers["X-Sudo-Password"] = sudoPassword
|
||||
}
|
||||
|
||||
let body = init?.body
|
||||
const method = init?.method?.toUpperCase() || "GET"
|
||||
if (method === "POST") {
|
||||
if (typeof body === "string") {
|
||||
try {
|
||||
const data = JSON.parse(body)
|
||||
let changed = false
|
||||
if (sudoPassword && !("sudo_password" in data)) {
|
||||
data["sudo_password"] = sudoPassword
|
||||
changed = true
|
||||
}
|
||||
if (hfToken && !("hf_token" in data)) {
|
||||
data["hf_token"] = hfToken
|
||||
changed = true
|
||||
}
|
||||
if (changed) {
|
||||
body = JSON.stringify(data)
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore JSON parse errors
|
||||
}
|
||||
} else if (!body) {
|
||||
const data: Record<string, any> = {}
|
||||
if (sudoPassword) data["sudo_password"] = sudoPassword
|
||||
if (hfToken) data["hf_token"] = hfToken
|
||||
if (Object.keys(data).length > 0) {
|
||||
body = JSON.stringify(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(path, {
|
||||
...init,
|
||||
headers,
|
||||
body,
|
||||
})
|
||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
||||
return res.json() as Promise<T>
|
||||
}
|
||||
|
||||
// llama-swap-`groups`: Mitglieder mit swap=false dürfen GLEICHZEITIG resident sein (Ko-Residenz).
|
||||
// Die `brains`-Gruppe hält Hirn (fast) + Augen (vision) gemeinsam warm, statt sich zu verdrängen.
|
||||
export interface GroupSpec {
|
||||
swap: boolean
|
||||
persist: boolean
|
||||
members: string[]
|
||||
}
|
||||
export interface GroupsResp {
|
||||
groups: Record<string, GroupSpec>
|
||||
}
|
||||
export const getGroups = () => api<GroupsResp>("/api/groups")
|
||||
export const setGroup = (group: string, members: string[], swap = false, persist = true) =>
|
||||
api("/api/groups", { method: "PUT", body: JSON.stringify({ group, members, swap, persist }) })
|
||||
|
||||
export interface Capabilities {
|
||||
moe: boolean
|
||||
active_b: number | null
|
||||
tools: "yes" | "likely" | "no"
|
||||
vision: boolean
|
||||
coder: boolean
|
||||
reasoning: boolean
|
||||
embedding: boolean
|
||||
ctx: number | null
|
||||
params_b: number | null
|
||||
arch: string | null
|
||||
}
|
||||
|
||||
export interface Fit {
|
||||
level: "perfect" | "marginal" | "too_tight"
|
||||
text: string
|
||||
req_gb: number
|
||||
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
|
||||
optimal_ctx: number
|
||||
assigned_ctx: number
|
||||
budget: { gtt_gb: number; reserved_gb: number; budget_gb: number; mode: string }
|
||||
sys_ram_gb: number
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
name: string
|
||||
role: string | null
|
||||
aliases: string[]
|
||||
api_ids: string[]
|
||||
ctx: number | null
|
||||
ttl: number | null
|
||||
cmd: string
|
||||
gguf_path: string
|
||||
filename: string
|
||||
quant: string
|
||||
size_bytes: number | null
|
||||
incomplete: boolean
|
||||
prompt_cache: boolean
|
||||
spec_draft_model: string | null
|
||||
spec_type: string | null
|
||||
spec_active: boolean
|
||||
parallel_slots: number
|
||||
capabilities: Capabilities
|
||||
}
|
||||
|
||||
export interface VocabFingerprint {
|
||||
model: string | null
|
||||
pre: string | null
|
||||
n_vocab: number | null
|
||||
arch: string | null
|
||||
}
|
||||
|
||||
export interface DraftInfo {
|
||||
path: string
|
||||
filename: string
|
||||
size_bytes: number | null
|
||||
vocab: VocabFingerprint | null
|
||||
compatible: boolean | null // null = nicht prüfbar (Ziel-GGUF fehlt)
|
||||
mtp?: boolean // MTP-Kopf (Multi-Token-Prediction) statt klassischem Draft
|
||||
}
|
||||
|
||||
export interface DraftsResp {
|
||||
target_path: string
|
||||
target_exists: boolean
|
||||
target_vocab: VocabFingerprint | null
|
||||
drafts: DraftInfo[]
|
||||
}
|
||||
|
||||
export interface DiscoverModel {
|
||||
name: string
|
||||
author: string
|
||||
repo: string
|
||||
role: string
|
||||
params_b: number
|
||||
quant: string
|
||||
tags: string[]
|
||||
downloads: number
|
||||
fit: Fit
|
||||
optimal_ctx: number
|
||||
caps: Capabilities
|
||||
}
|
||||
|
||||
export interface DiscoverCategory {
|
||||
role: string
|
||||
title: string
|
||||
icon: string
|
||||
models: DiscoverModel[]
|
||||
recommended: string | null
|
||||
}
|
||||
|
||||
export interface DiscoverResp {
|
||||
updated: number
|
||||
categories: DiscoverCategory[]
|
||||
sys_ram_gb: number
|
||||
}
|
||||
|
||||
export interface RoutingResp {
|
||||
mode?: string
|
||||
endpoint?: string
|
||||
heavy_threshold_chars?: number
|
||||
routes: { name: string; target: string }[]
|
||||
lanes?: { name: string; target: string; threshold_chars?: number; escalate_chars?: number; aka?: string }[]
|
||||
fallbacks: Record<string, string[]>[]
|
||||
context_window_fallbacks: Record<string, string[]>[]
|
||||
gateway_reachable: boolean
|
||||
}
|
||||
|
||||
// UI-editierbare Routing-Policy (GET/PUT /api/routing/policy). Aliase + Zeichen-Schwellen
|
||||
// hinter den Lanes; hot-reload im Backend (kein Restart).
|
||||
export interface RoutingPolicy {
|
||||
fast: string
|
||||
heavy: string
|
||||
coder: string
|
||||
coder_lite: string
|
||||
heavy_chars: number
|
||||
coding_escalate_chars: number
|
||||
fast_no_think: boolean
|
||||
}
|
||||
export interface RoutingPolicyField {
|
||||
key: keyof RoutingPolicy
|
||||
label: string
|
||||
type: "str" | "int" | "bool"
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
export interface RoutingPolicyMeta {
|
||||
policy: RoutingPolicy
|
||||
defaults: RoutingPolicy
|
||||
fields: RoutingPolicyField[]
|
||||
}
|
||||
export const getRoutingPolicy = () => api<RoutingPolicyMeta>("/api/routing/policy")
|
||||
export const updateRoutingPolicy = (patch: Partial<RoutingPolicy>) =>
|
||||
api<{ policy: RoutingPolicy }>("/api/routing/policy", { method: "PUT", body: JSON.stringify(patch) })
|
||||
|
||||
export interface GitInfo {
|
||||
hash: string
|
||||
date: string
|
||||
subject: string
|
||||
branch: string
|
||||
dirty: boolean
|
||||
path: string
|
||||
}
|
||||
|
||||
// Engine kann git-, binary- oder unbekannte Version sein — Felder je nach `type`.
|
||||
export interface ComponentVersion {
|
||||
type: "git" | "binary" | "unknown"
|
||||
hash?: string
|
||||
date?: string
|
||||
subject?: string
|
||||
branch?: string
|
||||
dirty?: boolean
|
||||
path?: string
|
||||
version_text?: string
|
||||
}
|
||||
|
||||
export interface Versions {
|
||||
mc2: GitInfo | null
|
||||
engine: ComponentVersion
|
||||
hermes_ui: GitInfo | null
|
||||
hermes_agent: GitInfo | null
|
||||
}
|
||||
|
||||
export interface SystemStatus {
|
||||
cpu: { percent: number; cores: number | null }
|
||||
ram: { total: number; used: number; percent: number }
|
||||
gpu: {
|
||||
busy_percent: number | null
|
||||
vram_used: number | null
|
||||
vram_total: number | null
|
||||
gtt_used?: number | null
|
||||
gtt_total?: number | null
|
||||
} | null
|
||||
temp: { cpu?: number; gpu?: number } | null
|
||||
disk: { total: number; used: number; percent: number } | null
|
||||
versions?: Versions
|
||||
}
|
||||
|
||||
export interface ServicesResp {
|
||||
services: { name: string; unit: string; url: string; ok: boolean }[]
|
||||
links: { engine_ui: string; gateway: string; hermes_terminal: string }
|
||||
}
|
||||
|
||||
export interface ComponentUpdate {
|
||||
key: string // z.B. "hermes_agent"
|
||||
name: string
|
||||
current: string | null
|
||||
latest: string | null
|
||||
update: boolean | null // null = unbestimmbar (installierte Version remote nicht abfragbar)
|
||||
reachable: boolean | null
|
||||
}
|
||||
|
||||
export interface UpdatesResp {
|
||||
os: number
|
||||
engine: number
|
||||
swap: number
|
||||
models: number
|
||||
model_list: { role: string; title: string; repo: string }[]
|
||||
last_check?: number | null
|
||||
components?: ComponentUpdate[]
|
||||
}
|
||||
|
||||
export interface UpdateDetails {
|
||||
kind: "os" | "engine" | "swap" | "hermes"
|
||||
error?: string
|
||||
// LLM-Zusammenfassung in Lucys Stimme (hermes/engine/swap; Breaking Changes zuerst)
|
||||
summary?: string
|
||||
// Aktions-Verdikt: muss der Besitzer selbst etwas tun? (null = kein Verdikt/keine Summary)
|
||||
action_needed?: boolean | null
|
||||
action_text?: string
|
||||
// os
|
||||
count?: number
|
||||
packages?: { name: string; current: string; candidate: string }[]
|
||||
// os: vom System zurückgestellte Pakete (Phasen-Rollout / kept back) — ehrlich statt "hängt"
|
||||
held_back?: { name: string; reason: "phasing" | "kept_back" }[]
|
||||
// engine
|
||||
installed_build?: number | null
|
||||
latest_build?: number | null
|
||||
latest_tag?: string | null
|
||||
name?: string | null
|
||||
url?: string | null
|
||||
body?: string | null
|
||||
// hermes
|
||||
branch?: string | null
|
||||
behind?: number
|
||||
commits?: { hash: string; subject: string; when: string }[]
|
||||
}
|
||||
|
||||
export interface ConnectTool {
|
||||
label: string
|
||||
lang: string
|
||||
snippet: string
|
||||
note: string
|
||||
}
|
||||
export interface ConnectResp {
|
||||
host: string
|
||||
gateway_url: string
|
||||
mc_url: string
|
||||
tools: Record<string, ConnectTool> // Leitung 1 — Modell (IDEs/Agenten → Gateway)
|
||||
memory: ConnectTool // Leitung 2 — Gedächtnis (separater MCP-Server)
|
||||
}
|
||||
|
||||
export interface ConnectLine {
|
||||
ok: boolean
|
||||
detail: string
|
||||
}
|
||||
export interface ConnectHealth {
|
||||
gateway: ConnectLine
|
||||
memory: ConnectLine
|
||||
}
|
||||
|
||||
export interface Memory {
|
||||
id: string
|
||||
content: string
|
||||
category: string
|
||||
source: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
score?: number // Relevanz bei semantischer Suche (q gesetzt); sonst undefined
|
||||
}
|
||||
|
||||
export interface MemoryGraphNode {
|
||||
id: string
|
||||
content: string
|
||||
category: string
|
||||
source: string
|
||||
}
|
||||
export interface MemoryGraph {
|
||||
nodes: MemoryGraphNode[]
|
||||
edges: { source: string; target: string; weight: number }[]
|
||||
}
|
||||
|
||||
export interface DedupeResult {
|
||||
groups: { keep: { id: string; content: string; category: string }; remove: { id: string; content: string }[] }[]
|
||||
duplicate_count: number
|
||||
removed: number
|
||||
applied: boolean
|
||||
}
|
||||
|
||||
export interface AgentStatus {
|
||||
gateway_url: string
|
||||
terminal_url: string
|
||||
box_console_url?: string
|
||||
hermes_ui_url?: string
|
||||
gateway_reachable: boolean
|
||||
terminal_reachable: boolean
|
||||
box_console_reachable?: boolean
|
||||
hermes_ui_reachable?: boolean
|
||||
home_exists: boolean
|
||||
brain_model?: string
|
||||
has_config: boolean
|
||||
has_skills: boolean
|
||||
has_memories: boolean
|
||||
telegram_enabled?: boolean
|
||||
mcp_server_count?: number
|
||||
pc_executor_reachable?: boolean
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
id: string
|
||||
label: string
|
||||
group?: string | null // "maintenance" = system-veränderndes Update (Wartungs-Riegel)
|
||||
state: "queued" | "running" | "done" | "failed" | "canceled"
|
||||
progress?: number
|
||||
total_bytes?: number
|
||||
done_bytes?: number
|
||||
rate_bps?: number
|
||||
eta_s?: number
|
||||
}
|
||||
|
||||
export interface Health {
|
||||
status: string
|
||||
version: string
|
||||
engine_reachable: boolean
|
||||
gateway_reachable: boolean
|
||||
brain?: { role: string; model: string | null; ready: boolean }
|
||||
}
|
||||
|
||||
export interface TokenStats {
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
total_tokens: number
|
||||
saved_usd: number
|
||||
saved_eur: number
|
||||
pricing?: Record<string, { in: number; out: number }>
|
||||
}
|
||||
|
||||
// Per-Turn-Latenz-Trace (GET /api/voice/trace). Balken-Stufen (zeitlich disjunkt):
|
||||
// stt · vision · hirn · gen. mem0 = Unter-Detail INNERHALB von hirn (nicht zum Balken addieren).
|
||||
export interface VoiceTurn {
|
||||
id: string
|
||||
ts: number // Epoch-Sekunden
|
||||
session: string
|
||||
kind: string
|
||||
had_images: boolean
|
||||
stt_ms: number | null
|
||||
vision_ms: number | null
|
||||
hirn_ms: number | null // Zeit bis erstes Inhalts-Token (Agent + Mem0 + LLM-TTFT)
|
||||
gen_ms: number | null // Generierung nach dem ersten Token bis Stream-Ende
|
||||
mem0_ms: number | null // Teil VON hirn (Mem0-Retrieve), best-effort
|
||||
total_ms: number
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export interface VoiceTraceResp {
|
||||
turns: VoiceTurn[]
|
||||
}
|
||||
|
||||
export interface ModelsResp {
|
||||
models: ModelInfo[]
|
||||
running?: string[]
|
||||
}
|
||||
|
||||
export interface HermesBrainModel {
|
||||
name: string
|
||||
filename?: string
|
||||
params_b: number | null
|
||||
quant?: string
|
||||
size_bytes?: number | null
|
||||
version?: number | null
|
||||
gguf_path?: string
|
||||
incomplete?: boolean
|
||||
}
|
||||
|
||||
export interface HermesBrainCandidate {
|
||||
repo: string
|
||||
name: string
|
||||
version: number
|
||||
params_b: number
|
||||
downloads: number
|
||||
fit: Fit
|
||||
}
|
||||
|
||||
export interface HermesBrainBudget {
|
||||
gtt_gb: number
|
||||
brain_gb: number // Footprint des (empfohlenen) Brains, das immer resident bleibt
|
||||
warm_projected_gb: number // alle persist-Modelle warm (Info)
|
||||
free_after_gb: number // frei nach Brain + größtem on-demand
|
||||
largest_ondemand_gb: number // größtes on-demand-Modell (z.B. heavy/coder)
|
||||
fits: boolean // passt Brain + größtes on-demand zusammen ins Budget?
|
||||
}
|
||||
|
||||
export interface HermesBrainResp {
|
||||
current: HermesBrainModel | null
|
||||
recommended: HermesBrainCandidate | null
|
||||
update_available: boolean
|
||||
budget?: HermesBrainBudget | null
|
||||
}
|
||||
|
||||
+32
-32
@@ -1,32 +1,32 @@
|
||||
// Zentrale Formatierungs-Helfer (vorher in einzelnen Views dupliziert).
|
||||
|
||||
/** Bytes → GB als String mit einer Nachkommastelle (z.B. "14.1"). */
|
||||
export function gb(b: number): string {
|
||||
return (b / 1024 ** 3).toFixed(1)
|
||||
}
|
||||
|
||||
/** Bytes → "1.2 GB" / "512 MB"; leer bei 0/undefined. */
|
||||
export function fmtBytes(b?: number): string {
|
||||
if (!b) return ""
|
||||
if (b > 1024 ** 3) return `${(b / 1024 ** 3).toFixed(1)} GB`
|
||||
return `${(b / 1024 ** 2).toFixed(0)} MB`
|
||||
}
|
||||
|
||||
/** Bytes → "1.2 GB" / "512 MB"; "—" bei 0/undefined/null. */
|
||||
export function fmtSize(b?: number | null): string {
|
||||
if (!b) return "—"
|
||||
const g = b / 1024 ** 3
|
||||
return g >= 1 ? `${g.toFixed(1)} GB` : `${(b / 1024 ** 2).toFixed(0)} MB`
|
||||
}
|
||||
|
||||
/** Sekunden → "3 min" / "45 s"; leer bei 0/undefined. */
|
||||
export function fmtEta(s?: number): string {
|
||||
if (!s) return ""
|
||||
const m = Math.floor(s / 60)
|
||||
return m > 0 ? `${m} min` : `${s} s`
|
||||
}
|
||||
|
||||
/** Kontextlänge → "32k"; "—" bei null. */
|
||||
export function fmtCtx(c: number | null): string {
|
||||
return c ? `${Math.round(c / 1024)}k` : "—"
|
||||
}
|
||||
// Zentrale Formatierungs-Helfer (vorher in einzelnen Views dupliziert).
|
||||
|
||||
/** Bytes → GB als String mit einer Nachkommastelle (z.B. "14.1"). */
|
||||
export function gb(b: number): string {
|
||||
return (b / 1024 ** 3).toFixed(1)
|
||||
}
|
||||
|
||||
/** Bytes → "1.2 GB" / "512 MB"; leer bei 0/undefined. */
|
||||
export function fmtBytes(b?: number): string {
|
||||
if (!b) return ""
|
||||
if (b > 1024 ** 3) return `${(b / 1024 ** 3).toFixed(1)} GB`
|
||||
return `${(b / 1024 ** 2).toFixed(0)} MB`
|
||||
}
|
||||
|
||||
/** Bytes → "1.2 GB" / "512 MB"; "—" bei 0/undefined/null. */
|
||||
export function fmtSize(b?: number | null): string {
|
||||
if (!b) return "—"
|
||||
const g = b / 1024 ** 3
|
||||
return g >= 1 ? `${g.toFixed(1)} GB` : `${(b / 1024 ** 2).toFixed(0)} MB`
|
||||
}
|
||||
|
||||
/** Sekunden → "3 min" / "45 s"; leer bei 0/undefined. */
|
||||
export function fmtEta(s?: number): string {
|
||||
if (!s) return ""
|
||||
const m = Math.floor(s / 60)
|
||||
return m > 0 ? `${m} min` : `${s} s`
|
||||
}
|
||||
|
||||
/** Kontextlänge → "32k"; "—" bei null. */
|
||||
export function fmtCtx(c: number | null): string {
|
||||
return c ? `${Math.round(c / 1024)}k` : "—"
|
||||
}
|
||||
|
||||
+152
-152
@@ -1,152 +1,152 @@
|
||||
// Zentrale Daten-Hooks (TanStack Query). Kapseln lib/api.ts und liefern Caching,
|
||||
// Dedup (gleicher queryKey = eine Anfrage über alle Views), Retry und Polling.
|
||||
// Mutations invalidieren gezielt die betroffenen Keys, statt manuell neu zu laden.
|
||||
|
||||
import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query"
|
||||
import {
|
||||
api,
|
||||
type AgentStatus,
|
||||
type ConnectResp,
|
||||
type ConnectHealth,
|
||||
type DiscoverResp,
|
||||
type DraftsResp,
|
||||
type GroupsResp,
|
||||
type HermesBrainResp,
|
||||
type Health,
|
||||
type Job,
|
||||
type Memory,
|
||||
type MemoryGraph,
|
||||
type ModelsResp,
|
||||
type RoutingResp,
|
||||
type RoutingPolicyMeta,
|
||||
type ServicesResp,
|
||||
type SystemStatus,
|
||||
type TokenStats,
|
||||
type UpdatesResp,
|
||||
type VoiceTraceResp,
|
||||
} from "./api"
|
||||
|
||||
// Zentrale Query-Keys (eine Quelle der Wahrheit für invalidate).
|
||||
export const qk = {
|
||||
health: ["health"] as const,
|
||||
systemStatus: ["system-status"] as const,
|
||||
services: ["services"] as const,
|
||||
models: ["models"] as const,
|
||||
groups: ["groups"] as const,
|
||||
routing: ["routing"] as const,
|
||||
routingPolicy: ["routing-policy"] as const,
|
||||
jobs: ["jobs"] as const,
|
||||
tokenStats: ["token-stats"] as const,
|
||||
agentStatus: ["agent-status"] as const,
|
||||
hermesBrain: ["hermes-brain"] as const,
|
||||
updates: ["updates"] as const,
|
||||
discover: ["discover"] as const,
|
||||
drafts: (target?: string) => ["drafts", target ?? ""] as const,
|
||||
connect: (params?: string) => ["connect", params ?? ""] as const,
|
||||
connectHealth: ["connect-health"] as const,
|
||||
memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const,
|
||||
memoryGraph: ["memory-graph"] as const,
|
||||
voiceTrace: ["voice-trace"] as const,
|
||||
}
|
||||
|
||||
// Per-Turn-Latenz-Trace (letzte N Voice/Lucy-Turns mit Stufen-Breakdown).
|
||||
export const useVoiceTrace = (limit = 12, refetchInterval = 4_000) =>
|
||||
useQuery({
|
||||
queryKey: qk.voiceTrace,
|
||||
queryFn: () => api<VoiceTraceResp>(`/api/voice/trace?limit=${limit}`),
|
||||
refetchInterval,
|
||||
select: (d) => d.turns ?? [],
|
||||
})
|
||||
|
||||
export const useMemoryGraph = (enabled = true) =>
|
||||
useQuery({
|
||||
queryKey: qk.memoryGraph,
|
||||
queryFn: () => api<MemoryGraph>("/api/memory/graph"),
|
||||
enabled,
|
||||
})
|
||||
|
||||
export const useHealth = () =>
|
||||
useQuery({ queryKey: qk.health, queryFn: () => api<Health>("/api/health"), refetchInterval: 10_000 })
|
||||
|
||||
export const useSystemStatus = (refetchInterval = 5_000) =>
|
||||
useQuery({ queryKey: qk.systemStatus, queryFn: () => api<SystemStatus>("/api/system/status"), refetchInterval })
|
||||
|
||||
export const useServices = (refetchInterval = 3_000) =>
|
||||
useQuery({ queryKey: qk.services, queryFn: () => api<ServicesResp>("/api/system/services"), refetchInterval })
|
||||
|
||||
export const useModels = (refetchInterval = 4_000) =>
|
||||
useQuery({ queryKey: qk.models, queryFn: () => api<ModelsResp>("/api/models"), refetchInterval })
|
||||
|
||||
export const useGroups = (refetchInterval = 8_000) =>
|
||||
useQuery({ queryKey: qk.groups, queryFn: () => api<GroupsResp>("/api/groups"), refetchInterval })
|
||||
|
||||
export const useRouting = (refetchInterval = 4_000) =>
|
||||
useQuery({ queryKey: qk.routing, queryFn: () => api<RoutingResp>("/api/routing"), refetchInterval })
|
||||
|
||||
export const useRoutingPolicy = () =>
|
||||
useQuery({ queryKey: qk.routingPolicy, queryFn: () => api<RoutingPolicyMeta>("/api/routing/policy") })
|
||||
|
||||
export const useJobs = (refetchInterval = 2_000) =>
|
||||
useQuery({
|
||||
queryKey: qk.jobs,
|
||||
queryFn: () => api<{ jobs: Job[] }>("/api/jobs"),
|
||||
refetchInterval,
|
||||
select: (d) => d.jobs ?? [],
|
||||
})
|
||||
|
||||
export const useTokenStats = (refetchInterval = 3_000) =>
|
||||
useQuery({ queryKey: qk.tokenStats, queryFn: () => api<TokenStats>("/api/system/token-stats"), refetchInterval })
|
||||
|
||||
export const useAgentStatus = (refetchInterval = 5_000) =>
|
||||
useQuery({ queryKey: qk.agentStatus, queryFn: () => api<AgentStatus>("/api/agent/status"), refetchInterval })
|
||||
|
||||
// Agent-Hirn (Hermes) + bestes NousResearch-Update. Selten pollen (HF-Abfrage).
|
||||
export const useHermesBrain = (refetchInterval = 60_000) =>
|
||||
useQuery({ queryKey: qk.hermesBrain, queryFn: () => api<HermesBrainResp>("/api/agent/brain"), refetchInterval })
|
||||
|
||||
export const useUpdates = (refetchInterval?: number) =>
|
||||
useQuery({ queryKey: qk.updates, queryFn: () => api<UpdatesResp>("/api/maintenance/updates"), refetchInterval })
|
||||
|
||||
export const useDiscover = () =>
|
||||
useQuery({ queryKey: qk.discover, queryFn: () => api<DiscoverResp>("/api/discover") })
|
||||
|
||||
// Verfügbare Spec-Draft-Modelle + Vocab-Kompatibilität zum Ziel-Modell (GGUF-Pfad).
|
||||
export const useDrafts = (target?: string) =>
|
||||
useQuery({
|
||||
queryKey: qk.drafts(target),
|
||||
queryFn: () => api<DraftsResp>(`/api/models/drafts?target=${encodeURIComponent(target ?? "")}`),
|
||||
enabled: !!target,
|
||||
})
|
||||
|
||||
export const useConnect = (params?: string) =>
|
||||
useQuery({
|
||||
queryKey: qk.connect(params),
|
||||
queryFn: () => api<ConnectResp>(params ? `/api/connect?${params}` : "/api/connect"),
|
||||
})
|
||||
|
||||
// Live-Status der zwei Leitungen (Gateway + Gedächtnis) — alle 15s aktualisiert.
|
||||
export const useConnectHealth = () =>
|
||||
useQuery({
|
||||
queryKey: qk.connectHealth,
|
||||
queryFn: () => api<ConnectHealth>("/api/connect/health"),
|
||||
refetchInterval: 15000,
|
||||
})
|
||||
|
||||
export const useMemory = (opts?: { q?: string; category?: string; limit?: number }) =>
|
||||
useQuery({
|
||||
queryKey: qk.memory(opts?.q, opts?.category),
|
||||
queryFn: () => {
|
||||
const p = new URLSearchParams()
|
||||
if (opts?.q) p.set("q", opts.q)
|
||||
if (opts?.category) p.set("category", opts.category)
|
||||
return api<Memory[]>(`/api/memory?${p}`)
|
||||
},
|
||||
select: (d) => (opts?.limit ? d.slice(0, opts.limit) : d),
|
||||
})
|
||||
|
||||
/** Nach Mutationen die betroffenen Listen-Queries neu ziehen. */
|
||||
export function invalidate(qc: QueryClient, ...keys: readonly unknown[][]) {
|
||||
for (const key of keys) qc.invalidateQueries({ queryKey: key })
|
||||
}
|
||||
|
||||
export { useQueryClient }
|
||||
// Zentrale Daten-Hooks (TanStack Query). Kapseln lib/api.ts und liefern Caching,
|
||||
// Dedup (gleicher queryKey = eine Anfrage über alle Views), Retry und Polling.
|
||||
// Mutations invalidieren gezielt die betroffenen Keys, statt manuell neu zu laden.
|
||||
|
||||
import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query"
|
||||
import {
|
||||
api,
|
||||
type AgentStatus,
|
||||
type ConnectResp,
|
||||
type ConnectHealth,
|
||||
type DiscoverResp,
|
||||
type DraftsResp,
|
||||
type GroupsResp,
|
||||
type HermesBrainResp,
|
||||
type Health,
|
||||
type Job,
|
||||
type Memory,
|
||||
type MemoryGraph,
|
||||
type ModelsResp,
|
||||
type RoutingResp,
|
||||
type RoutingPolicyMeta,
|
||||
type ServicesResp,
|
||||
type SystemStatus,
|
||||
type TokenStats,
|
||||
type UpdatesResp,
|
||||
type VoiceTraceResp,
|
||||
} from "./api"
|
||||
|
||||
// Zentrale Query-Keys (eine Quelle der Wahrheit für invalidate).
|
||||
export const qk = {
|
||||
health: ["health"] as const,
|
||||
systemStatus: ["system-status"] as const,
|
||||
services: ["services"] as const,
|
||||
models: ["models"] as const,
|
||||
groups: ["groups"] as const,
|
||||
routing: ["routing"] as const,
|
||||
routingPolicy: ["routing-policy"] as const,
|
||||
jobs: ["jobs"] as const,
|
||||
tokenStats: ["token-stats"] as const,
|
||||
agentStatus: ["agent-status"] as const,
|
||||
hermesBrain: ["hermes-brain"] as const,
|
||||
updates: ["updates"] as const,
|
||||
discover: ["discover"] as const,
|
||||
drafts: (target?: string) => ["drafts", target ?? ""] as const,
|
||||
connect: (params?: string) => ["connect", params ?? ""] as const,
|
||||
connectHealth: ["connect-health"] as const,
|
||||
memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const,
|
||||
memoryGraph: ["memory-graph"] as const,
|
||||
voiceTrace: ["voice-trace"] as const,
|
||||
}
|
||||
|
||||
// Per-Turn-Latenz-Trace (letzte N Voice/Lucy-Turns mit Stufen-Breakdown).
|
||||
export const useVoiceTrace = (limit = 12, refetchInterval = 4_000) =>
|
||||
useQuery({
|
||||
queryKey: qk.voiceTrace,
|
||||
queryFn: () => api<VoiceTraceResp>(`/api/voice/trace?limit=${limit}`),
|
||||
refetchInterval,
|
||||
select: (d) => d.turns ?? [],
|
||||
})
|
||||
|
||||
export const useMemoryGraph = (enabled = true) =>
|
||||
useQuery({
|
||||
queryKey: qk.memoryGraph,
|
||||
queryFn: () => api<MemoryGraph>("/api/memory/graph"),
|
||||
enabled,
|
||||
})
|
||||
|
||||
export const useHealth = () =>
|
||||
useQuery({ queryKey: qk.health, queryFn: () => api<Health>("/api/health"), refetchInterval: 10_000 })
|
||||
|
||||
export const useSystemStatus = (refetchInterval = 5_000) =>
|
||||
useQuery({ queryKey: qk.systemStatus, queryFn: () => api<SystemStatus>("/api/system/status"), refetchInterval })
|
||||
|
||||
export const useServices = (refetchInterval = 3_000) =>
|
||||
useQuery({ queryKey: qk.services, queryFn: () => api<ServicesResp>("/api/system/services"), refetchInterval })
|
||||
|
||||
export const useModels = (refetchInterval = 4_000) =>
|
||||
useQuery({ queryKey: qk.models, queryFn: () => api<ModelsResp>("/api/models"), refetchInterval })
|
||||
|
||||
export const useGroups = (refetchInterval = 8_000) =>
|
||||
useQuery({ queryKey: qk.groups, queryFn: () => api<GroupsResp>("/api/groups"), refetchInterval })
|
||||
|
||||
export const useRouting = (refetchInterval = 4_000) =>
|
||||
useQuery({ queryKey: qk.routing, queryFn: () => api<RoutingResp>("/api/routing"), refetchInterval })
|
||||
|
||||
export const useRoutingPolicy = () =>
|
||||
useQuery({ queryKey: qk.routingPolicy, queryFn: () => api<RoutingPolicyMeta>("/api/routing/policy") })
|
||||
|
||||
export const useJobs = (refetchInterval = 2_000) =>
|
||||
useQuery({
|
||||
queryKey: qk.jobs,
|
||||
queryFn: () => api<{ jobs: Job[] }>("/api/jobs"),
|
||||
refetchInterval,
|
||||
select: (d) => d.jobs ?? [],
|
||||
})
|
||||
|
||||
export const useTokenStats = (refetchInterval = 3_000) =>
|
||||
useQuery({ queryKey: qk.tokenStats, queryFn: () => api<TokenStats>("/api/system/token-stats"), refetchInterval })
|
||||
|
||||
export const useAgentStatus = (refetchInterval = 5_000) =>
|
||||
useQuery({ queryKey: qk.agentStatus, queryFn: () => api<AgentStatus>("/api/agent/status"), refetchInterval })
|
||||
|
||||
// Agent-Hirn (Hermes) + bestes NousResearch-Update. Selten pollen (HF-Abfrage).
|
||||
export const useHermesBrain = (refetchInterval = 60_000) =>
|
||||
useQuery({ queryKey: qk.hermesBrain, queryFn: () => api<HermesBrainResp>("/api/agent/brain"), refetchInterval })
|
||||
|
||||
export const useUpdates = (refetchInterval?: number) =>
|
||||
useQuery({ queryKey: qk.updates, queryFn: () => api<UpdatesResp>("/api/maintenance/updates"), refetchInterval })
|
||||
|
||||
export const useDiscover = () =>
|
||||
useQuery({ queryKey: qk.discover, queryFn: () => api<DiscoverResp>("/api/discover") })
|
||||
|
||||
// Verfügbare Spec-Draft-Modelle + Vocab-Kompatibilität zum Ziel-Modell (GGUF-Pfad).
|
||||
export const useDrafts = (target?: string) =>
|
||||
useQuery({
|
||||
queryKey: qk.drafts(target),
|
||||
queryFn: () => api<DraftsResp>(`/api/models/drafts?target=${encodeURIComponent(target ?? "")}`),
|
||||
enabled: !!target,
|
||||
})
|
||||
|
||||
export const useConnect = (params?: string) =>
|
||||
useQuery({
|
||||
queryKey: qk.connect(params),
|
||||
queryFn: () => api<ConnectResp>(params ? `/api/connect?${params}` : "/api/connect"),
|
||||
})
|
||||
|
||||
// Live-Status der zwei Leitungen (Gateway + Gedächtnis) — alle 15s aktualisiert.
|
||||
export const useConnectHealth = () =>
|
||||
useQuery({
|
||||
queryKey: qk.connectHealth,
|
||||
queryFn: () => api<ConnectHealth>("/api/connect/health"),
|
||||
refetchInterval: 15000,
|
||||
})
|
||||
|
||||
export const useMemory = (opts?: { q?: string; category?: string; limit?: number }) =>
|
||||
useQuery({
|
||||
queryKey: qk.memory(opts?.q, opts?.category),
|
||||
queryFn: () => {
|
||||
const p = new URLSearchParams()
|
||||
if (opts?.q) p.set("q", opts.q)
|
||||
if (opts?.category) p.set("category", opts.category)
|
||||
return api<Memory[]>(`/api/memory?${p}`)
|
||||
},
|
||||
select: (d) => (opts?.limit ? d.slice(0, opts.limit) : d),
|
||||
})
|
||||
|
||||
/** Nach Mutationen die betroffenen Listen-Queries neu ziehen. */
|
||||
export function invalidate(qc: QueryClient, ...keys: readonly unknown[][]) {
|
||||
for (const key of keys) qc.invalidateQueries({ queryKey: key })
|
||||
}
|
||||
|
||||
export { useQueryClient }
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
// Ein Hook für Alert/Confirm/Prompt-Dialoge — ersetzt die zuvor in jeder View
|
||||
// duplizierte showAlert/showConfirm-Logik + den lokalen Dialog-State.
|
||||
//
|
||||
// Nutzung:
|
||||
// const { showAlert, showConfirm, showPrompt, dialogElement } = useDialog()
|
||||
// ...
|
||||
// showConfirm("Titel", "Wirklich?", () => doIt())
|
||||
// return (<>{dialogElement}...</>)
|
||||
|
||||
import { useCallback, useState } from "react"
|
||||
import { CustomDialog } from "@/components/CustomDialog"
|
||||
|
||||
interface DialogState {
|
||||
type: "alert" | "confirm" | "prompt"
|
||||
title: string
|
||||
message: string
|
||||
defaultValue?: string
|
||||
autoValue?: string
|
||||
autoLabel?: string
|
||||
onConfirm: (val?: string) => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
export function useDialog() {
|
||||
const [dialog, setDialog] = useState<DialogState | null>(null)
|
||||
const close = useCallback(() => setDialog(null), [])
|
||||
|
||||
const showAlert = useCallback((title: string, message: string, onConfirm?: () => void) => {
|
||||
setDialog({
|
||||
type: "alert", title, message,
|
||||
onConfirm: () => { setDialog(null); onConfirm?.() },
|
||||
})
|
||||
}, [])
|
||||
|
||||
const showConfirm = useCallback(
|
||||
(title: string, message: string, onConfirm: () => void, onCancel?: () => void) => {
|
||||
setDialog({
|
||||
type: "confirm", title, message,
|
||||
onConfirm: () => { setDialog(null); onConfirm() },
|
||||
onCancel: () => { setDialog(null); onCancel?.() },
|
||||
})
|
||||
}, [])
|
||||
|
||||
const showPrompt = useCallback(
|
||||
(title: string, message: string, defaultValue: string,
|
||||
onConfirm: (val?: string) => void, onCancel?: () => void,
|
||||
opts?: { autoValue?: string; autoLabel?: string }) => {
|
||||
setDialog({
|
||||
type: "prompt", title, message, defaultValue,
|
||||
autoValue: opts?.autoValue, autoLabel: opts?.autoLabel,
|
||||
onConfirm: (val) => { setDialog(null); onConfirm(val) },
|
||||
onCancel: () => { setDialog(null); onCancel?.() },
|
||||
})
|
||||
}, [])
|
||||
|
||||
const dialogElement = dialog ? <CustomDialog {...dialog} /> : null
|
||||
|
||||
return { showAlert, showConfirm, showPrompt, close, dialogElement }
|
||||
}
|
||||
// Ein Hook für Alert/Confirm/Prompt-Dialoge — ersetzt die zuvor in jeder View
|
||||
// duplizierte showAlert/showConfirm-Logik + den lokalen Dialog-State.
|
||||
//
|
||||
// Nutzung:
|
||||
// const { showAlert, showConfirm, showPrompt, dialogElement } = useDialog()
|
||||
// ...
|
||||
// showConfirm("Titel", "Wirklich?", () => doIt())
|
||||
// return (<>{dialogElement}...</>)
|
||||
|
||||
import { useCallback, useState } from "react"
|
||||
import { CustomDialog } from "@/components/CustomDialog"
|
||||
|
||||
interface DialogState {
|
||||
type: "alert" | "confirm" | "prompt"
|
||||
title: string
|
||||
message: string
|
||||
defaultValue?: string
|
||||
autoValue?: string
|
||||
autoLabel?: string
|
||||
onConfirm: (val?: string) => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
export function useDialog() {
|
||||
const [dialog, setDialog] = useState<DialogState | null>(null)
|
||||
const close = useCallback(() => setDialog(null), [])
|
||||
|
||||
const showAlert = useCallback((title: string, message: string, onConfirm?: () => void) => {
|
||||
setDialog({
|
||||
type: "alert", title, message,
|
||||
onConfirm: () => { setDialog(null); onConfirm?.() },
|
||||
})
|
||||
}, [])
|
||||
|
||||
const showConfirm = useCallback(
|
||||
(title: string, message: string, onConfirm: () => void, onCancel?: () => void) => {
|
||||
setDialog({
|
||||
type: "confirm", title, message,
|
||||
onConfirm: () => { setDialog(null); onConfirm() },
|
||||
onCancel: () => { setDialog(null); onCancel?.() },
|
||||
})
|
||||
}, [])
|
||||
|
||||
const showPrompt = useCallback(
|
||||
(title: string, message: string, defaultValue: string,
|
||||
onConfirm: (val?: string) => void, onCancel?: () => void,
|
||||
opts?: { autoValue?: string; autoLabel?: string }) => {
|
||||
setDialog({
|
||||
type: "prompt", title, message, defaultValue,
|
||||
autoValue: opts?.autoValue, autoLabel: opts?.autoLabel,
|
||||
onConfirm: (val) => { setDialog(null); onConfirm(val) },
|
||||
onCancel: () => { setDialog(null); onCancel?.() },
|
||||
})
|
||||
}, [])
|
||||
|
||||
const dialogElement = dialog ? <CustomDialog {...dialog} /> : null
|
||||
|
||||
return { showAlert, showConfirm, showPrompt, close, dialogElement }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user