fix: Chat-Isolation, Tab-Besitz und eigene Seiten für Einstellungen/Verlauf
Ereignisse landeten im falschen Chat: der Broker markiert jedes Ereignis mit chatId, das Panel filtert aber auf sessionId — handleBrokerMessage reichte die Nachricht unverändert weiter, der Filter griff also nie und alles landete im gerade sichtbaren Chat. chatId wird jetzt beim Weiterreichen übersetzt. Tab-Isolation war reine Optik — die Gruppen wurden nirgends durchgesetzt: - getActiveTab() nahm sich den gerade sichtbaren Tab (auch den eines anderen Chats oder einen losen Tab des Nutzers) und zog ihn in die eigene Gruppe. Fremde Tabs werden jetzt nie adoptiert; der Chat öffnet sich einen eigenen. - browser_tabs_create legte den neuen Tab in die Gruppe der SICHTBAREN statt der aufrufenden Konversation (sessionId fehlte). - browser_tabs_select/close prüften den Besitz gar nicht — der Agent konnte jeden Tab übernehmen oder schließen, auch die des Nutzers. - browser_tabs_list zeigte jeden offenen Tab; Tabs anderer Chats sind jetzt unsichtbar, Nutzer-Tabs klar als "nicht anfassen" markiert. Besitz wird über Chromes Tab-Gruppen bestimmt — überlebt einen Neustart des Service Workers, anders als reiner Speicher. Session wird explizit durch runTool/dispatchTool/getActiveTab gereicht. Der bisherige Trick, activeSessionId kurz umzubiegen, hielt nur ohne Parallelität: zwischen Setzen und Zurücksetzen liegt ein await. Einstellungen und Verlauf nehmen dem Chat keinen Platz mehr weg: Einstellungen sind eine eigene Seite (vorher ausklappbar bis 45vh), der Verlauf ein Overlay über dem Header-Knopf. Abschnitte als eigene Module mit Icon und Akzentkante. Nebenbei repariert: nach der Umbenennung .settings -> .settings-panel zeigten mehrere Regeln ins Leere. Kimi K2.5, GLM-5 und Gemini 3.1 Flash Lite im Nexus-Katalog; Kimi/GLM laufen über /moonshot bzw. /zai statt über den Azure-Deployment-Pfad. Ollama als lokaler Anbieter wählbar (kein Key nötig). Enthält außerdem ältere, noch nicht committete Arbeit, die nicht von diesem Durchgang stammt und sich nicht sauber abtrennen ließ: kräftigerer Glow mit Farbverlauf (spotlight.ts), Raster-Overlay für Screenshots (computer.ts) und ein Ein/Aus-Schalter für CDP (cdp.ts).
This commit is contained in:
+250
-23
@@ -76,7 +76,7 @@ const modeBadge = need<HTMLSpanElement>('modeBadge');
|
||||
const headerModelSel = need<HTMLSelectElement>('headerModel');
|
||||
const modeOffBtn = need<HTMLButtonElement>('modeOff');
|
||||
const modeConfirmBtn = need<HTMLButtonElement>('modeConfirm');
|
||||
const settingsPanel = need<HTMLDetailsElement>('settingsPanel');
|
||||
const settingsPanel = need<HTMLDivElement>('settingsPanel');
|
||||
const settingsToggle = need<HTMLButtonElement>('settingsToggle');
|
||||
const directGroup = need<HTMLDivElement>('directGroup');
|
||||
|
||||
@@ -110,11 +110,32 @@ const nexusModelSel = need<HTMLSelectElement>('nexusModel');
|
||||
const nexusModelRefresh = need<HTMLButtonElement>('nexusModelRefresh');
|
||||
const nexusModelProbe = need<HTMLButtonElement>('nexusModelProbe');
|
||||
const nexusModelHint = need<HTMLDivElement>('nexusModelHint');
|
||||
const convBar = need<HTMLDivElement>('convBar');
|
||||
const historyToggle = need<HTMLButtonElement>('historyToggle');
|
||||
const convSelect = need<HTMLSelectElement>('convSelect');
|
||||
const newConvBtn = need<HTMLButtonElement>('newConvBtn');
|
||||
const syncMemoryBtn = need<HTMLButtonElement>('syncMemoryBtn');
|
||||
const diagToggle = need<HTMLButtonElement>('diagToggle');
|
||||
const diagPanel = need<HTMLDivElement>('diagPanel');
|
||||
const statsToggle = need<HTMLButtonElement>('statsToggle');
|
||||
const statsPanel = need<HTMLElement>('statsPanel');
|
||||
const statsRefresh = need<HTMLButtonElement>('statsRefresh');
|
||||
const statCalls = need<HTMLDivElement>('statCalls');
|
||||
const statRate = need<HTMLDivElement>('statRate');
|
||||
const statChanges = need<HTMLDivElement>('statChanges');
|
||||
const statRetries = need<HTMLDivElement>('statRetries');
|
||||
const statErrors = need<HTMLDivElement>('statErrors');
|
||||
const statTokens = need<HTMLDivElement>('statTokens');
|
||||
const statsTools = need<HTMLDivElement>('statsTools');
|
||||
const statsHistory = need<HTMLDivElement>('statsHistory');
|
||||
// Insgesamt (alle Sitzungen) — persistiert am Broker, siehe agent/ext_bridge.py cumulative_summary()
|
||||
const atCalls = need<HTMLDivElement>('atCalls');
|
||||
const atTurns = need<HTMLDivElement>('atTurns');
|
||||
const atChanges = need<HTMLDivElement>('atChanges');
|
||||
const atRetries = need<HTMLDivElement>('atRetries');
|
||||
const atErrors = need<HTMLDivElement>('atErrors');
|
||||
const atTokens = need<HTMLDivElement>('atTokens');
|
||||
const atTools = need<HTMLDivElement>('atTools');
|
||||
const diagVersion = need<HTMLSpanElement>('diagVersion');
|
||||
const diagReloadBtn = need<HTMLButtonElement>('diagReloadBtn');
|
||||
const diagBroker = need<HTMLSpanElement>('diagBroker');
|
||||
@@ -153,10 +174,22 @@ let config: ExtensionConfig = { ...DEFAULT_CONFIG };
|
||||
let brokerConnected = false;
|
||||
let brokerUnreachable = false;
|
||||
let isRunning = false;
|
||||
/** IDs der Sessions, die gerade laufen — für die ●-Markierung im Dropdown. */
|
||||
let runningSessions: Set<string> = new Set();
|
||||
let currentStreamEl: HTMLElement | null = null;
|
||||
|
||||
/** Kumulative Token-Nutzung über die gesamte Session. */
|
||||
let sessionUsage = { inputTokens: 0, outputTokens: 0, turns: 0 };
|
||||
let localStats = { toolCalls: 0, toolErrors: 0, stateChanges: 0, startedAt: Date.now(), tools: new Map<string, number>() };
|
||||
|
||||
/** Stats pro Konversation — damit man sieht, was jeder Agent verbraucht hat. */
|
||||
const perSessionStats = new Map<string, { inputTokens: number; outputTokens: number; turns: number; toolCalls: number; toolErrors: number }>();
|
||||
|
||||
function getSessionStats(sid: string) {
|
||||
let s = perSessionStats.get(sid);
|
||||
if (!s) { s = { inputTokens: 0, outputTokens: 0, turns: 0, toolCalls: 0, toolErrors: 0 }; perSessionStats.set(sid, s); }
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Offene Rueckfragen, damit sie bei Verbindungsverlust ehrlich entwertet werden koennen. */
|
||||
const openRequests = new Map<string, { stale: () => void }>();
|
||||
@@ -228,10 +261,19 @@ function send(msg: PanelMessage): boolean {
|
||||
// ─── Nachrichten vom Hintergrund ──────────────────────────────────────────────
|
||||
|
||||
function handleMessage(msg: BackgroundMessage): void {
|
||||
// Events mit sessionId nur verarbeiten, wenn sie zur aktiven Konversation gehören.
|
||||
// Ausnahme: state, sync_start, connection_status — die sind global.
|
||||
const msgSessionId = (msg as any).sessionId as string | undefined;
|
||||
const isForOtherSession = msgSessionId && msgSessionId !== activeConvId;
|
||||
|
||||
switch (msg.type) {
|
||||
case 'state':
|
||||
applyState(msg.config, msg.connected, msg.running);
|
||||
applyWorkerVersion(msg.version);
|
||||
if ((msg as any).runningSessions) {
|
||||
runningSessions = new Set((msg as any).runningSessions as string[]);
|
||||
refreshConvSelect(convCache, activeConvId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sync_start':
|
||||
@@ -256,29 +298,44 @@ function handleMessage(msg: BackgroundMessage): void {
|
||||
break;
|
||||
|
||||
case 'text_delta':
|
||||
if (isForOtherSession) break;
|
||||
appendStreamingText(msg.text);
|
||||
break;
|
||||
|
||||
case 'round':
|
||||
if (isForOtherSession) break;
|
||||
roundCounter.textContent = `${msg.n}/${msg.maxRounds}`;
|
||||
setRunning(true);
|
||||
break;
|
||||
|
||||
case 'tool_executing':
|
||||
if (isForOtherSession) break;
|
||||
finalizeStreaming();
|
||||
lastToolName = msg.name;
|
||||
addToolEvent(`▸ ${msg.name}`, previewOf(msg.input), false);
|
||||
break;
|
||||
|
||||
case 'tool_done': {
|
||||
if (isForOtherSession) break;
|
||||
const failure = msg.error ?? errorMessageOf(msg.result);
|
||||
localStats.toolCalls++;
|
||||
localStats.tools.set(msg.name, (localStats.tools.get(msg.name) ?? 0) + 1);
|
||||
if (failure) localStats.toolErrors++;
|
||||
const resultObj = msg.result && typeof msg.result === 'object' ? msg.result as any : null;
|
||||
if (resultObj?.afterState || resultObj?.stateChange) localStats.stateChanges++;
|
||||
// Per-Session-Stats
|
||||
const ss = getSessionStats(activeConvId);
|
||||
ss.toolCalls++;
|
||||
if (failure) ss.toolErrors++;
|
||||
const meta = `${msg.durationMs} ms · ${msg.method}`;
|
||||
if (failure) addToolEvent(`✗ ${msg.name} — ${meta}`, failure, true);
|
||||
else addToolEvent(`✓ ${msg.name} — ${meta}`, previewOf(msg.result), false);
|
||||
if (statsPanel.classList.contains('open')) renderLocalStats();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'tool_result_event':
|
||||
if (isForOtherSession) break;
|
||||
addToolEvent(
|
||||
`${msg.isError ? '✗' : '✓'} ${msg.name} — Broker`,
|
||||
previewOf(msg.result),
|
||||
@@ -287,10 +344,12 @@ function handleMessage(msg: BackgroundMessage): void {
|
||||
break;
|
||||
|
||||
case 'screenshot':
|
||||
if (isForOtherSession) break;
|
||||
addScreenshot(msg.callId, msg.dataUrl);
|
||||
break;
|
||||
|
||||
case 'risk_request':
|
||||
if (isForOtherSession) break;
|
||||
finalizeStreaming();
|
||||
addRiskCard(msg.requestId, msg.name, msg.verdict, msg.timeoutMs);
|
||||
break;
|
||||
@@ -302,6 +361,7 @@ function handleMessage(msg: BackgroundMessage): void {
|
||||
break;
|
||||
|
||||
case 'done':
|
||||
if (isForOtherSession) break;
|
||||
finalizeStreaming();
|
||||
setRunning(false);
|
||||
roundCounter.textContent = '';
|
||||
@@ -312,22 +372,30 @@ function handleMessage(msg: BackgroundMessage): void {
|
||||
sessionUsage.inputTokens += msg.usage.inputTokens;
|
||||
sessionUsage.outputTokens += msg.usage.outputTokens;
|
||||
sessionUsage.turns++;
|
||||
// Per-Session-Stats
|
||||
const ss = getSessionStats(activeConvId);
|
||||
ss.inputTokens += msg.usage.inputTokens;
|
||||
ss.outputTokens += msg.usage.outputTokens;
|
||||
ss.turns++;
|
||||
updateUsageBar();
|
||||
const cost = estimateCost(sessionUsage.inputTokens, sessionUsage.outputTokens);
|
||||
addSystemMessage(
|
||||
`Tokens: ${fmtNum(msg.usage.inputTokens)} ein / ${fmtNum(msg.usage.outputTokens)} aus` +
|
||||
` · Session: ${fmtNum(sessionUsage.inputTokens)}/${fmtNum(sessionUsage.outputTokens)} (${sessionUsage.turns} Turns${cost ? ', ~$' + cost : ''})`,
|
||||
` · Konv: ${fmtNum(ss.inputTokens)}/${fmtNum(ss.outputTokens)} (${ss.turns} Turns)` +
|
||||
` · Gesamt: ${fmtNum(sessionUsage.inputTokens)}/${fmtNum(sessionUsage.outputTokens)}${cost ? ' ~$' + cost : ''}`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
if (isForOtherSession) break;
|
||||
finalizeStreaming();
|
||||
addErrorMessage(msg.text);
|
||||
if (msg.fatal) { setRunning(false); roundCounter.textContent = ''; }
|
||||
break;
|
||||
|
||||
case 'log':
|
||||
if (isForOtherSession) break;
|
||||
if (msg.level === 'error') addErrorMessage(msg.text);
|
||||
else addSystemMessage(msg.text, msg.level === 'warn');
|
||||
break;
|
||||
@@ -624,6 +692,8 @@ function wireSettings(): void {
|
||||
anthropic: 'https://api.anthropic.com/v1',
|
||||
openai: 'https://api.openai.com/v1',
|
||||
requesty: 'https://router.requesty.ai/v1',
|
||||
kimi: 'https://api.moonshot.ai/v1',
|
||||
ollama: 'http://localhost:11434/v1',
|
||||
nexus: NEXUS_BASE_URL,
|
||||
};
|
||||
// Nexus setzt zusaetzlich das Modell: eine Anthropic-ID wie 'claude-sonnet-5'
|
||||
@@ -639,8 +709,12 @@ function wireSettings(): void {
|
||||
|
||||
const preset = presets[provider];
|
||||
if (preset && (!config.baseUrl || Object.values(presets).includes(config.baseUrl))) {
|
||||
patchConfigValue({ provider, baseUrl: preset });
|
||||
// Ollama laeuft lokal ohne echten Key — Platzhalter, damit das Feld nicht
|
||||
// faelschlich als "fehlt" erscheint (der Server prueft ihn ohnehin nicht).
|
||||
const apiKey = provider === 'ollama' && !config.apiKey ? 'ollama' : config.apiKey;
|
||||
patchConfigValue({ provider, baseUrl: preset, apiKey });
|
||||
setFieldValue(baseUrlInput, preset);
|
||||
setFieldValue(apiKeyInput, apiKey);
|
||||
applyNexusUi(config);
|
||||
return null; // patchConfigValue schon aufgerufen
|
||||
}
|
||||
@@ -698,13 +772,7 @@ function wireSettings(): void {
|
||||
}
|
||||
});
|
||||
|
||||
settingsToggle.addEventListener('click', () => {
|
||||
settingsPanel.open = !settingsPanel.open;
|
||||
settingsToggle.setAttribute('aria-expanded', String(settingsPanel.open));
|
||||
});
|
||||
settingsPanel.addEventListener('toggle', () => {
|
||||
settingsToggle.setAttribute('aria-expanded', String(settingsPanel.open));
|
||||
});
|
||||
settingsToggle.addEventListener('click', () => toggleSettings());
|
||||
|
||||
// Das Panel kann jederzeit geschlossen werden — offene Eingaben vorher wegschreiben.
|
||||
window.addEventListener('pagehide', flushAllFields);
|
||||
@@ -721,7 +789,7 @@ function wireSettings(): void {
|
||||
* Brokers und schickt die Auswahl zurück.
|
||||
*
|
||||
* Nur im Broker-Modus sinnvoll: bei driveMode 'direct' spricht der Service Worker
|
||||
* direkt mit Anthropic/OpenAI/Requesty, dort gilt das Feld "Modell" weiter unten.
|
||||
* direkt mit Anthropic/OpenAI/Requesty/Kimi, dort gilt das Feld "Modell" weiter unten.
|
||||
*/
|
||||
|
||||
interface NexusModel {
|
||||
@@ -755,8 +823,129 @@ function brokerHttpBase(): string {
|
||||
}
|
||||
}
|
||||
|
||||
interface StatsSession {
|
||||
ts?: string;
|
||||
duration_sec?: number;
|
||||
turns?: number;
|
||||
tool_calls?: number;
|
||||
tool_errors?: number;
|
||||
tools_per_min?: number;
|
||||
state_changes?: number;
|
||||
throttle_retries?: number;
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
top_tools?: Array<[string, number]>;
|
||||
}
|
||||
|
||||
function safeText(value: unknown): string {
|
||||
const el = document.createElement('span');
|
||||
el.textContent = String(value ?? '');
|
||||
return el.innerHTML;
|
||||
}
|
||||
|
||||
function renderStatsData(current?: StatsSession | null, history: StatsSession[] = []): void {
|
||||
const elapsedMin = Math.max((Date.now() - localStats.startedAt) / 60000, 1 / 60);
|
||||
const localTop = [...localStats.tools.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8);
|
||||
const data: StatsSession = current ?? {
|
||||
tool_calls: localStats.toolCalls,
|
||||
tool_errors: localStats.toolErrors,
|
||||
tools_per_min: localStats.toolCalls / elapsedMin,
|
||||
state_changes: localStats.stateChanges,
|
||||
throttle_retries: 0,
|
||||
input_tokens: sessionUsage.inputTokens,
|
||||
output_tokens: sessionUsage.outputTokens,
|
||||
top_tools: localTop,
|
||||
};
|
||||
statCalls.textContent = fmtNum(data.tool_calls ?? 0);
|
||||
statRate.textContent = (data.tools_per_min ?? 0).toFixed(1);
|
||||
statChanges.textContent = fmtNum(data.state_changes ?? 0);
|
||||
statRetries.textContent = fmtNum(data.throttle_retries ?? 0);
|
||||
statErrors.textContent = fmtNum(data.tool_errors ?? 0);
|
||||
statTokens.textContent = fmtNum((data.input_tokens ?? 0) + (data.output_tokens ?? 0));
|
||||
|
||||
const tools = data.top_tools ?? localTop;
|
||||
statsTools.innerHTML = tools.length
|
||||
? `<table class="stats-table">${tools.map(([name, count]) => `<tr><td>${safeText(name)}</td><td>${count}</td></tr>`).join('')}</table>`
|
||||
: '<div class="stats-empty">Noch keine Tool-Aufrufe</div>';
|
||||
statsHistory.innerHTML = history.length
|
||||
? `<table class="stats-table">${history.slice().reverse().map(s => `<tr><td>${safeText(s.ts?.slice(0, 16).replace('T', ' ') || 'Session')}</td><td>${s.tool_calls ?? 0} Calls · ${s.tools_per_min ?? 0}/min</td></tr>`).join('')}</table>`
|
||||
: '<div class="stats-empty">Historie entsteht nach dem ersten Broker-Disconnect.</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* "Insgesamt"-Kacheln — kommen NUR vom Broker (agent/ext_bridge.py
|
||||
* cumulative_summary()), der sie persistiert und bei jedem Tool-Aufruf sofort
|
||||
* weiterschreibt. Es gibt keinen lokalen Fallback: anders als die
|
||||
* Session-Werte oben ist das hier die eine Zahl, die tatsächlich wächst statt
|
||||
* bei jedem Neustart/Reconnect bei null anzufangen — ohne Broker also lieber
|
||||
* "—" zeigen als eine falsche, session-lokale Notlösung.
|
||||
*/
|
||||
function renderAllTimeData(allTime?: StatsSession | null): void {
|
||||
if (!allTime) return;
|
||||
atCalls.textContent = fmtNum(allTime.tool_calls ?? 0);
|
||||
atTurns.textContent = fmtNum(allTime.turns ?? 0);
|
||||
atChanges.textContent = fmtNum(allTime.state_changes ?? 0);
|
||||
atRetries.textContent = fmtNum(allTime.throttle_retries ?? 0);
|
||||
atErrors.textContent = fmtNum(allTime.tool_errors ?? 0);
|
||||
atTokens.textContent = fmtNum((allTime.input_tokens ?? 0) + (allTime.output_tokens ?? 0));
|
||||
|
||||
const tools = allTime.top_tools ?? [];
|
||||
atTools.innerHTML = tools.length
|
||||
? `<table class="stats-table">${tools.map(([name, count]) => `<tr><td>${safeText(name)}</td><td>${count}</td></tr>`).join('')}</table>`
|
||||
: '<div class="stats-empty">Noch keine Tool-Aufrufe</div>';
|
||||
}
|
||||
|
||||
function renderLocalStats(): void { renderStatsData(null, []); }
|
||||
|
||||
async function loadStats(): Promise<void> {
|
||||
renderLocalStats();
|
||||
if (config.driveMode !== 'broker') return;
|
||||
statsRefresh.disabled = true;
|
||||
try {
|
||||
const res = await fetch(`${brokerHttpBase()}/ext/v1/stats`, { signal: AbortSignal.timeout(4000) });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const body = await res.json();
|
||||
renderStatsData(body.currentSession ?? null, body.recentSessions ?? []);
|
||||
renderAllTimeData(body.allTime ?? null);
|
||||
} catch {
|
||||
// Lokale Live-Werte bleiben sichtbar; Broker-Historie/Insgesamt sind optional.
|
||||
} finally {
|
||||
statsRefresh.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleStats(): void {
|
||||
const open = !statsPanel.classList.contains('open');
|
||||
statsPanel.classList.toggle('open', open);
|
||||
if (open) toggleSettings(false); // beide sind volle Seiten — nur eine auf einmal
|
||||
chatContainer.style.display = open ? 'none' : '';
|
||||
inputArea.style.display = open ? 'none' : '';
|
||||
attachStrip.style.display = open ? 'none' : '';
|
||||
statsToggle.setAttribute('aria-pressed', String(open));
|
||||
if (open) loadStats();
|
||||
}
|
||||
|
||||
function toggleSettings(force?: boolean): void {
|
||||
const open = force ?? !settingsPanel.classList.contains('open');
|
||||
settingsPanel.classList.toggle('open', open);
|
||||
if (open) {
|
||||
statsPanel.classList.remove('open'); // beide sind volle Seiten — nur eine auf einmal
|
||||
statsToggle.setAttribute('aria-pressed', 'false');
|
||||
}
|
||||
chatContainer.style.display = open ? 'none' : '';
|
||||
inputArea.style.display = open ? 'none' : '';
|
||||
attachStrip.style.display = open ? 'none' : '';
|
||||
settingsToggle.setAttribute('aria-expanded', String(open));
|
||||
}
|
||||
|
||||
let nexusModels: NexusModel[] = [];
|
||||
let nexusPollTimer: number | null = null;
|
||||
// Das Modell IST pro Konversation (siehe agent/ext_bridge.py _model_for) — der
|
||||
// per-Modell `.active`-Flag aus dem Katalog ist dagegen global (models.py
|
||||
// _active) und würde beim Rendern das GLOBALE zuletzt gewählte Modell zeigen,
|
||||
// nicht das dieser Konversation. Deshalb eigene, chat-lokale Variable statt
|
||||
// nexusModels.find(m => m.active).
|
||||
let activeNexusModelId = '';
|
||||
|
||||
function setNexusHint(text: string, kind: '' | 'ok' | 'err' | 'throttled'): void {
|
||||
nexusModelHint.textContent = text;
|
||||
@@ -775,7 +964,7 @@ function stateMark(m: NexusModel): string {
|
||||
}
|
||||
|
||||
function renderNexusModels(): void {
|
||||
const active = nexusModels.find(m => m.active);
|
||||
const active = nexusModels.find(m => m.id === activeNexusModelId);
|
||||
nexusModelSel.innerHTML = '';
|
||||
|
||||
if (!nexusModels.length) {
|
||||
@@ -795,7 +984,7 @@ function renderNexusModels(): void {
|
||||
// Nicht vorhandene oder gesperrte Modelle nicht wählbar machen — ein Wechsel
|
||||
// dorthin würde jeden Aufruf scheitern lassen.
|
||||
if (m.state === 'missing' || m.state === 'unauthorized') opt.disabled = true;
|
||||
if (m.active) opt.selected = true;
|
||||
if (m.id === activeNexusModelId) opt.selected = true;
|
||||
nexusModelSel.appendChild(opt);
|
||||
}
|
||||
|
||||
@@ -827,12 +1016,14 @@ function renderNexusModels(): void {
|
||||
async function loadNexusModels(silent = true): Promise<void> {
|
||||
if (config.driveMode !== 'broker') return;
|
||||
try {
|
||||
const res = await fetch(`${brokerHttpBase()}/api/models`, {
|
||||
const q = activeConvId ? `?ext_chat_id=${encodeURIComponent(activeConvId)}` : '';
|
||||
const res = await fetch(`${brokerHttpBase()}/api/models${q}`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
nexusModels = Array.isArray(data.models) ? data.models : [];
|
||||
activeNexusModelId = data.active || '';
|
||||
renderNexusModels();
|
||||
refreshHeaderModel(config);
|
||||
} catch (e: unknown) {
|
||||
@@ -851,19 +1042,22 @@ async function setNexusModel(modelId: string): Promise<void> {
|
||||
const res = await fetch(`${brokerHttpBase()}/api/models/active`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model: modelId }),
|
||||
// extChatId: nur DIESE Konversation bekommt das neue Modell — andere,
|
||||
// parallel laufende Chats behalten ihres (siehe web.py api_models_active).
|
||||
body: JSON.stringify({ model: modelId, extChatId: activeConvId }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
// 409: der Broker arbeitet gerade — ein Wechsel würde den Verlauf zerreißen.
|
||||
// 409: DIESER Chat arbeitet gerade — ein Wechsel würde seinen Verlauf zerreißen.
|
||||
setNexusHint(data.error || `Wechsel abgelehnt (HTTP ${res.status})`, 'err');
|
||||
await loadNexusModels();
|
||||
return;
|
||||
}
|
||||
nexusModels = Array.isArray(data.models) ? data.models : nexusModels;
|
||||
activeNexusModelId = data.active || modelId;
|
||||
renderNexusModels();
|
||||
const m = nexusModels.find(x => x.active);
|
||||
const m = nexusModels.find(x => x.id === activeNexusModelId);
|
||||
addSystemMessage(`Modell gewechselt: ${m?.label ?? modelId}`);
|
||||
} catch (e: unknown) {
|
||||
setNexusHint(`Wechsel fehlgeschlagen: ${e instanceof Error ? e.message : String(e)}`, 'err');
|
||||
@@ -1110,7 +1304,7 @@ function sendUserMessage(): void {
|
||||
// Lauf läuft — Nachricht puffern und visuell kennzeichnen
|
||||
addUserMessage(`📥 ${text}`);
|
||||
addSystemMessage('Nachricht gepuffert — wird nach aktuellem Turn ausgeführt.');
|
||||
send({ type: 'user_message', text: payloadText, images, maxRounds: config.maxRounds, isContinue });
|
||||
send({ type: 'user_message', text: payloadText, images, maxRounds: config.maxRounds, isContinue, sessionId: activeConvId });
|
||||
chatInput.value = '';
|
||||
clearAttachments();
|
||||
autoResize();
|
||||
@@ -1123,7 +1317,7 @@ function sendUserMessage(): void {
|
||||
addUserMessage(text, images);
|
||||
}
|
||||
|
||||
if (!send({ type: 'user_message', text: payloadText, images, maxRounds: config.maxRounds, isContinue })) {
|
||||
if (!send({ type: 'user_message', text: payloadText, images, maxRounds: config.maxRounds, isContinue, sessionId: activeConvId })) {
|
||||
addErrorMessage('Keine Verbindung zum Hintergrund — die Nachricht wurde nicht gesendet.');
|
||||
return;
|
||||
}
|
||||
@@ -1259,7 +1453,7 @@ function wireAttachments(): void {
|
||||
}
|
||||
|
||||
function abortRun(): void {
|
||||
send({ type: 'abort' });
|
||||
send({ type: 'abort', sessionId: activeConvId });
|
||||
finalizeStreaming();
|
||||
setRunning(false);
|
||||
roundCounter.textContent = '';
|
||||
@@ -1839,7 +2033,8 @@ function refreshConvSelect(convs: StoredConversation[], activeId: string): void
|
||||
for (const c of convs) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = c.id;
|
||||
opt.textContent = c.title || 'Unbenannt';
|
||||
const running = runningSessions.has(c.id);
|
||||
opt.textContent = (running ? '● ' : '') + (c.title || 'Unbenannt');
|
||||
if (c.id === activeId) opt.selected = true;
|
||||
convSelect.appendChild(opt);
|
||||
}
|
||||
@@ -1854,6 +2049,10 @@ async function loadAndDisplayConversation(id: string): Promise<void> {
|
||||
scheduleStorageWrite();
|
||||
chatContainer.innerHTML = '';
|
||||
|
||||
// Worker über Session-Wechsel informieren — Target-Tab und Running-Status wechseln mit.
|
||||
send({ type: 'switch_session', sessionId: id });
|
||||
loadNexusModels(); // Modell-Pille auf das Modell DIESER Konversation umschalten
|
||||
|
||||
for (const msg of conv.messages) {
|
||||
if (msg.role === 'user') {
|
||||
chatContainer.appendChild(makeEl('div', 'message user', msg.content));
|
||||
@@ -1884,7 +2083,10 @@ async function startNewConversation(): Promise<void> {
|
||||
scheduleStorageWrite();
|
||||
chatContainer.innerHTML = '';
|
||||
refreshConvSelect(convCache, conv.id);
|
||||
send({ type: 'new_conversation' });
|
||||
send({ type: 'new_conversation', sessionId: conv.id });
|
||||
// Worker über neue aktive Session informieren
|
||||
send({ type: 'switch_session', sessionId: conv.id });
|
||||
loadNexusModels(); // frische Konversation erbt den globalen Default
|
||||
}
|
||||
|
||||
async function syncCurrentToMemory(): Promise<void> {
|
||||
@@ -2117,11 +2319,34 @@ function wireDiag(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function toggleHistoryOverlay(open?: boolean): void {
|
||||
const next = open ?? !convBar.classList.contains('open');
|
||||
convBar.classList.toggle('open', next);
|
||||
historyToggle.setAttribute('aria-expanded', String(next));
|
||||
}
|
||||
|
||||
function wireConversations(): void {
|
||||
newConvBtn.addEventListener('click', startNewConversation);
|
||||
historyToggle.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleHistoryOverlay();
|
||||
});
|
||||
// Klick außerhalb schließt — Overlay soll nicht offen im Weg stehen, sobald
|
||||
// man die Auswahl getroffen hat oder woanders weiterarbeitet.
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!convBar.classList.contains('open')) return;
|
||||
const target = e.target as Node;
|
||||
if (!convBar.contains(target) && !historyToggle.contains(target)) {
|
||||
toggleHistoryOverlay(false);
|
||||
}
|
||||
});
|
||||
newConvBtn.addEventListener('click', () => {
|
||||
startNewConversation();
|
||||
toggleHistoryOverlay(false);
|
||||
});
|
||||
syncMemoryBtn.addEventListener('click', syncCurrentToMemory);
|
||||
convSelect.addEventListener('change', () => {
|
||||
loadAndDisplayConversation(convSelect.value);
|
||||
toggleHistoryOverlay(false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2139,6 +2364,8 @@ async function init(): Promise<void> {
|
||||
wireMemoryLogin();
|
||||
wireConversations();
|
||||
wireDiag();
|
||||
statsToggle.addEventListener('click', toggleStats);
|
||||
statsRefresh.addEventListener('click', loadStats);
|
||||
|
||||
sendBtn.addEventListener('click', sendUserMessage);
|
||||
stopBtn.addEventListener('click', abortRun);
|
||||
|
||||
+185
-57
@@ -212,61 +212,84 @@
|
||||
.mode-btn[aria-pressed="true"] .s { color: var(--text-dim); }
|
||||
|
||||
/* ── Einstellungen ───────────────────────────────────────────────────── */
|
||||
.settings {
|
||||
/* Eigene Seite statt ausklappbarer Leiste (war <details>, klappte bis zu
|
||||
45vh in den Chatbereich rein) — derselbe Ein/Aus-Ansatz wie .stats-panel:
|
||||
ersetzt den Chat komplett, statt ihm dauerhaft Höhe wegzunehmen. */
|
||||
.settings-panel {
|
||||
display: none; flex: 1 1 auto; min-height: 0; flex-direction: column;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex: 0 0 auto;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.settings > summary {
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
list-style: none;
|
||||
.settings-panel.open { display: flex; }
|
||||
.settings-head {
|
||||
display: flex; align-items: baseline; gap: 8px;
|
||||
padding: 14px 14px 12px; border-bottom: 1px solid var(--border); flex: 0 0 auto;
|
||||
}
|
||||
.settings > summary::-webkit-details-marker { display: none; }
|
||||
.settings > summary::before { content: '▸ '; color: var(--text-mute); }
|
||||
.settings[open] > summary::before { content: '▾ '; }
|
||||
.settings > summary:hover { color: var(--text); }
|
||||
.settings-body { padding: 2px 10px 10px; }
|
||||
.settings-head h2 {
|
||||
font-size: 15px; font-weight: 650; letter-spacing: -0.01em; color: var(--text);
|
||||
}
|
||||
.settings-head .settings-sub {
|
||||
font-size: 10.5px; color: var(--text-mute); letter-spacing: 0.2px;
|
||||
}
|
||||
.settings-head .spacer { flex: 1; }
|
||||
.settings-body { padding: 10px; overflow-y: auto; flex: 1 1 auto; min-height: 0; }
|
||||
|
||||
/* Klappbare Abschnitte: 18 Zeilen auf einer Ebene sind keine Bedienung,
|
||||
sondern eine Liste. Was man taeglich anfasst, steht offen; der Rest
|
||||
liegt zugeklappt darunter. */
|
||||
.sgroup { border-top: 1px solid var(--border); margin-top: 6px; }
|
||||
/* Klappbare Abschnitte als eigenständige Module (Cockpit-Sprache der
|
||||
restlichen UI weitergedacht: jedes Modul hat eine eigene Fläche und
|
||||
"leuchtet" auf, sobald es offen ist — statt einer reinen Textliste
|
||||
mit Trennlinien, wie es vorher war). Was man taeglich anfasst, steht
|
||||
offen; der Rest liegt zugeklappt darunter. */
|
||||
.sgroup {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 9px;
|
||||
margin-top: 7px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.sgroup:first-child { margin-top: 0; }
|
||||
.sgroup[open] { border-color: var(--border-lt); }
|
||||
.sgroup > summary {
|
||||
padding: 6px 0 5px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-mute);
|
||||
padding: 9px 11px;
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.15px;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
list-style: none;
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
}
|
||||
/* Linke Akzentkante: aus statt lackiert, an statt lackiert — "Modul aktiv"
|
||||
statt reiner Dekoration; einzige Farbstelle in einer sonst ruhigen Liste. */
|
||||
.sgroup > summary::after {
|
||||
content: '';
|
||||
position: absolute; left: 0; top: 0; bottom: 0; width: 2.5px;
|
||||
background: var(--accent);
|
||||
opacity: 0; transition: opacity 0.15s;
|
||||
}
|
||||
.sgroup[open] > summary::after { opacity: 1; }
|
||||
.sgroup-icon { flex: 0 0 auto; font-size: 12px; opacity: 0.85; line-height: 1; }
|
||||
.sgroup-sub {
|
||||
font-size: 9.5px; font-weight: 500; letter-spacing: 0; text-transform: none;
|
||||
color: var(--text-mute); font-family: ui-monospace, 'Cascadia Code', 'Consolas', monospace;
|
||||
}
|
||||
.sgroup > summary::-webkit-details-marker { display: none; }
|
||||
.sgroup > summary::before {
|
||||
content: '';
|
||||
.sgroup-chevron {
|
||||
margin-left: auto; flex: 0 0 auto;
|
||||
width: 0; height: 0;
|
||||
border-left: 4px solid currentColor;
|
||||
border-top: 3px solid transparent;
|
||||
border-bottom: 3px solid transparent;
|
||||
color: var(--text-mute);
|
||||
transition: transform 0.15s;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.sgroup[open] > summary::before { transform: rotate(90deg); }
|
||||
.sgroup[open] .sgroup-chevron { transform: rotate(90deg); color: var(--accent); }
|
||||
.sgroup > summary:hover { color: var(--text); }
|
||||
.sgroup-body { padding: 1px 0 7px; }
|
||||
.sgroup-body { padding: 2px 11px 11px; border-top: 1px solid var(--border); }
|
||||
|
||||
/* Modellwahl in der Kopfzeile — der am haeufigsten benutzte Schalter
|
||||
gehoert nicht ans Ende einer Einstellungsliste. */
|
||||
@@ -286,7 +309,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 3px 0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.row > label {
|
||||
flex: 0 0 88px;
|
||||
@@ -302,15 +325,19 @@
|
||||
min-width: 0;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border-lt);
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
color: var(--text);
|
||||
padding: 4px 7px;
|
||||
padding: 5px 8px;
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.12s, box-shadow 0.12s;
|
||||
}
|
||||
.row input[type="text"], .row input[type="password"] { font-family: ui-monospace, 'Cascadia Code', 'Consolas', monospace; }
|
||||
.row input:focus, .row select:focus, .settings textarea:focus { border-color: var(--accent); }
|
||||
.row input:focus, .row select:focus, .settings-panel textarea:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(78, 163, 200, 0.16);
|
||||
}
|
||||
.row.check { gap: 6px; }
|
||||
.row.check > label { flex: 1 1 auto; order: 2; }
|
||||
.row input[type="checkbox"] {
|
||||
@@ -320,14 +347,14 @@
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.settings .field-block { padding: 4px 0; }
|
||||
.settings .field-block > label {
|
||||
.settings-panel .field-block { padding: 4px 0; }
|
||||
.settings-panel .field-block > label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.settings textarea {
|
||||
.settings-panel textarea {
|
||||
width: 100%;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border-lt);
|
||||
@@ -434,15 +461,35 @@
|
||||
margin-top: 2px;
|
||||
}
|
||||
.diag-section:first-child { border-top: none; padding-top: 0; margin-top: 0; }
|
||||
/* Overlay statt fester Zeile: schwebt ÜBER dem Chat, kostet ihm also nie
|
||||
dauerhaft Höhe — anders als vorher (fixe Leiste, immer sichtbar, immer
|
||||
ein Stück Chatbereich weg). Öffnen/Schließen über #historyToggle im
|
||||
Header, Klick daneben schließt (siehe wireHistoryOverlay() in app.ts). */
|
||||
.conv-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
padding: 5px 10px;
|
||||
padding: 8px;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
border: 1px solid var(--border-lt);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 32px rgba(0,0,0,0.4), 0 0 0 1px rgba(0,0,0,0.08);
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
z-index: 50;
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
.conv-bar.open {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
.conv-bar .conv-row { display: flex; gap: 5px; align-items: center; }
|
||||
.conv-bar select {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
@@ -929,6 +976,47 @@
|
||||
#sendBtn:disabled { opacity: 0.4; cursor: not-allowed; filter: none; }
|
||||
#stopBtn { background: var(--error); color: #fff; }
|
||||
#stopBtn:hover { filter: brightness(1.1); }
|
||||
|
||||
/* ── Statistik-Ansicht ───────────────────────────────────────────────── */
|
||||
.stats-panel {
|
||||
display: none; flex: 1 1 auto; min-height: 0; overflow: auto;
|
||||
padding: 12px; background: var(--bg);
|
||||
}
|
||||
.stats-panel.open { display: block; }
|
||||
.stats-head { display:flex; align-items:center; gap:8px; margin-bottom:12px; }
|
||||
.stats-head h2 { font-size:15px; }
|
||||
.stats-head .spacer { flex:1; }
|
||||
.stats-grid {
|
||||
display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr));
|
||||
gap:8px; margin-bottom:12px;
|
||||
}
|
||||
.stat-card { background:var(--surface); border:1px solid var(--border); border-radius:9px; padding:10px; }
|
||||
.stat-value { font-size:20px; font-weight:700; color:var(--accent); font-variant-numeric:tabular-nums; }
|
||||
.stat-label { font-size:10px; color:var(--text-dim); margin-top:2px; }
|
||||
.stats-section { background:var(--surface); border:1px solid var(--border); border-radius:9px; padding:10px; margin-top:8px; }
|
||||
.stats-section h3 { font-size:12px; margin-bottom:8px; color:var(--text); }
|
||||
.stats-table { width:100%; border-collapse:collapse; font-size:11px; }
|
||||
.stats-table td { padding:5px 3px; border-top:1px solid var(--border); color:var(--text-dim); }
|
||||
.stats-table td:last-child { text-align:right; color:var(--text); font-variant-numeric:tabular-nums; }
|
||||
.stats-empty { color:var(--text-mute); padding:16px 0; text-align:center; }
|
||||
|
||||
/* Schmale Panels: Header entrümpeln, Eingabe bleibt bedienbar. */
|
||||
@media (max-width: 520px) {
|
||||
.header { flex-wrap:wrap; }
|
||||
.header h1 { flex:1; }
|
||||
.header .spacer { display:none; }
|
||||
.target-tab { order:10; max-width:100%; flex:1 1 100%; }
|
||||
.mcp-status, .usage-bar-wrap { display:none; }
|
||||
.mode-bar { padding:6px; }
|
||||
.input-area { gap:5px; padding:7px; }
|
||||
#sendBtn, #stopBtn { padding:8px 10px; }
|
||||
.stats-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
||||
}
|
||||
@media (min-width: 760px) {
|
||||
body { font-size:14px; }
|
||||
.chat-container { padding-left:16px; padding-right:16px; }
|
||||
.message { max-width:900px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -946,6 +1034,8 @@
|
||||
<span class="mcp-status" id="mcpStatus"></span>
|
||||
<select id="headerModel" title="Aktives Modell"></select>
|
||||
<span class="status-text" id="statusText">getrennt</span>
|
||||
<button class="icon-btn" id="historyToggle" type="button" title="Konversationen" aria-label="Konversationen" aria-expanded="false">🕘</button>
|
||||
<button class="icon-btn" id="statsToggle" type="button" title="Statistiken" aria-label="Statistiken">▥</button>
|
||||
<button class="icon-btn" id="settingsToggle" type="button" title="Einstellungen ein-/ausklappen" aria-label="Einstellungen">⚙</button>
|
||||
<button class="icon-btn" id="diagToggle" type="button" title="Diagnose / Tests" aria-label="Diagnose">🔬</button>
|
||||
</div>
|
||||
@@ -1021,16 +1111,22 @@
|
||||
</div>
|
||||
|
||||
<div class="conv-bar" id="convBar">
|
||||
<button class="conv-btn" id="newConvBtn" type="button" title="Neue Konversation">+</button>
|
||||
<select id="convSelect" title="Konversation wählen"></select>
|
||||
<button class="conv-btn" id="syncMemoryBtn" type="button" title="Zu Memory synchronisieren">☁</button>
|
||||
<div class="conv-row">
|
||||
<button class="conv-btn" id="newConvBtn" type="button" title="Neue Konversation">+</button>
|
||||
<select id="convSelect" title="Konversation wählen"></select>
|
||||
<button class="conv-btn" id="syncMemoryBtn" type="button" title="Zu Memory synchronisieren">☁</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details class="settings" id="settingsPanel">
|
||||
<summary>Einstellungen</summary>
|
||||
<div class="settings-panel" id="settingsPanel">
|
||||
<div class="settings-head">
|
||||
<h2>Einstellungen</h2>
|
||||
<span class="settings-sub">Änderungen wirken sofort</span>
|
||||
<span class="spacer"></span>
|
||||
</div>
|
||||
<div class="settings-body">
|
||||
|
||||
<details class="sgroup"><summary>Verbindung & Antrieb</summary><div class="sgroup-body">
|
||||
<details class="sgroup"><summary><span class="sgroup-icon">🔌</span>Verbindung & Antrieb<span class="sgroup-chevron"></span></summary><div class="sgroup-body">
|
||||
<div class="row">
|
||||
<label for="brokerUrl">Broker</label>
|
||||
<input type="text" id="brokerUrl" spellcheck="false" autocomplete="off" placeholder="ws://127.0.0.1:8765/ext/ws">
|
||||
@@ -1066,7 +1162,7 @@
|
||||
</div>
|
||||
</div></details>
|
||||
|
||||
<details class="sgroup"><summary>Verhalten</summary><div class="sgroup-body">
|
||||
<details class="sgroup"><summary><span class="sgroup-icon">🎚</span>Verhalten<span class="sgroup-chevron"></span></summary><div class="sgroup-body">
|
||||
<div class="row">
|
||||
<label for="inputMode">Eingabe</label>
|
||||
<select id="inputMode">
|
||||
@@ -1085,7 +1181,7 @@
|
||||
</div>
|
||||
</div></details>
|
||||
|
||||
<details class="sgroup"><summary>Anzeige & Sicherheit</summary><div class="sgroup-body">
|
||||
<details class="sgroup"><summary><span class="sgroup-icon">🛡</span>Anzeige & Sicherheit<span class="sgroup-chevron"></span></summary><div class="sgroup-body">
|
||||
<div class="row">
|
||||
<label for="pageOverlay">Anzeige</label>
|
||||
<select id="pageOverlay" title="Sichtbare Rückmeldung auf der Seite, an der der Agent arbeitet.">
|
||||
@@ -1110,7 +1206,7 @@
|
||||
|
||||
</div></details>
|
||||
|
||||
<details class="sgroup" open id="directSection"><summary>Modell & Zugang</summary><div class="sgroup-body">
|
||||
<details class="sgroup" open id="directSection"><summary><span class="sgroup-icon">🧠</span>Modell & Zugang<span class="sgroup-chevron"></span></summary><div class="sgroup-body">
|
||||
<div id="directGroup" hidden>
|
||||
<div class="row">
|
||||
<label for="provider">Anbieter</label>
|
||||
@@ -1119,6 +1215,8 @@
|
||||
<option value="anthropic">anthropic</option>
|
||||
<option value="openai">openai</option>
|
||||
<option value="requesty">requesty</option>
|
||||
<option value="kimi">kimi (Moonshot)</option>
|
||||
<option value="ollama">ollama (lokal)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -1150,7 +1248,7 @@
|
||||
|
||||
</div></details>
|
||||
|
||||
<details class="sgroup"><summary>Memory (memory.cnull.net)</summary><div class="sgroup-body">
|
||||
<details class="sgroup"><summary><span class="sgroup-icon">🗂</span>Memory<span class="sgroup-sub">memory.cnull.net</span><span class="sgroup-chevron"></span></summary><div class="sgroup-body">
|
||||
<div class="row check">
|
||||
<input type="checkbox" id="memoryEnabled">
|
||||
<label for="memoryEnabled">Memory-Server anbinden</label>
|
||||
@@ -1196,7 +1294,7 @@
|
||||
</div></details>
|
||||
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Spotlight: zeigt was die KI gerade sieht -->
|
||||
<div id="spotlight">
|
||||
@@ -1211,6 +1309,36 @@
|
||||
<div class="spotlight-action" id="spotlightAction"></div>
|
||||
</div>
|
||||
|
||||
<section class="stats-panel" id="statsPanel" aria-label="Statistiken">
|
||||
<div class="stats-head">
|
||||
<h2>Statistiken</h2><span class="spacer"></span>
|
||||
<button class="icon-btn" id="statsRefresh" type="button">Aktualisieren</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-section"><h3>Insgesamt (alle Sitzungen, übersteht Neustarts)</h3></div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card"><div class="stat-value" id="atCalls">—</div><div class="stat-label">Tool-Aufrufe</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="atTurns">—</div><div class="stat-label">Turns</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="atChanges">—</div><div class="stat-label">State-Änderungen</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="atRetries">—</div><div class="stat-label">Gateway-Retries</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="atErrors">—</div><div class="stat-label">Tool-Fehler</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="atTokens">—</div><div class="stat-label">Tokens gesamt</div></div>
|
||||
</div>
|
||||
<div class="stats-section"><h3>Top-Tools (insgesamt)</h3><div id="atTools" class="stats-empty">Noch keine Daten</div></div>
|
||||
|
||||
<div class="stats-section"><h3>Diese Sitzung (seit letztem Broker-Connect)</h3></div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card"><div class="stat-value" id="statCalls">—</div><div class="stat-label">Tool-Aufrufe</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="statRate">—</div><div class="stat-label">Tools / Minute</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="statChanges">—</div><div class="stat-label">State-Änderungen</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="statRetries">—</div><div class="stat-label">Gateway-Retries</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="statErrors">—</div><div class="stat-label">Tool-Fehler</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="statTokens">—</div><div class="stat-label">Tokens gesamt</div></div>
|
||||
</div>
|
||||
<div class="stats-section"><h3>Top-Tools (diese Sitzung)</h3><div id="statsTools" class="stats-empty">Noch keine Daten</div></div>
|
||||
<div class="stats-section"><h3>Letzte Sessions</h3><div id="statsHistory" class="stats-empty">Noch keine Daten</div></div>
|
||||
</section>
|
||||
|
||||
<div class="chat-container" id="chatContainer"></div>
|
||||
|
||||
<div class="attach-strip" id="attachStrip" hidden></div>
|
||||
|
||||
Reference in New Issue
Block a user