d12f356ebe
Die Diagnostic-Seite lud nicht mehr richtig: bei jedem state-Update vom
Server crashte updateState() auf null.className weil 'gw-dot' nicht mehr
existiert (Gateway-Card wurde durch Brain-Card ersetzt). Mehrere weitere
Stellen waren ebenfalls auf nicht mehr existierende Elemente geleitet.
Bereinigt:
- updateState() nutzt jetzt brain-dot/short/error (null-safe)
- openCoreTerminal entfernt (aria-core ist raus)
- closeTermModal null-safe fuer btn-core-term
- 'core_auth' WS-Event entfernt
- 'session_restarted' WS-Event entfernt (perms-status-Element war auch raus)
- 'openclaw_config' WS-Event entfernt
- rc-compact-after read/write aus loadRuntimeConfig/saveRuntimeConfig raus
(Compact-After-Messages-Setting wurde mit aria-core entfernt)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3347 lines
165 KiB
HTML
3347 lines
165 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>ARIA Diagnostic</title>
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { background: #0D0D1A; color: #E0E0F0; font-family: 'Courier New', monospace; padding: 16px; }
|
||
h1 { font-size: 20px; margin-bottom: 16px; color: #0096FF; }
|
||
h2 { font-size: 14px; margin-bottom: 8px; color: #8888AA; text-transform: uppercase; letter-spacing: 1px; }
|
||
|
||
.grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-bottom: 16px; }
|
||
.card { background: #12122A; border: 1px solid #1E1E2E; border-radius: 8px; padding: 12px; }
|
||
.card.full { grid-column: 1 / -1; }
|
||
|
||
.status-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||
.dot.connected { background: #34C759; box-shadow: 0 0 6px #34C759; }
|
||
.dot.connecting, .dot.testing { background: #FFD60A; animation: pulse 1s infinite; }
|
||
.dot.disconnected { background: #FF3B30; }
|
||
.dot.error { background: #FF3B30; box-shadow: 0 0 6px #FF3B30; }
|
||
.dot.not_configured { background: #555570; }
|
||
.dot.unknown { background: #555570; }
|
||
@keyframes pulse { 50% { opacity: 0.5; } }
|
||
|
||
.status-label { font-size: 13px; }
|
||
.error-text { color: #FF6B6B; font-size: 11px; margin-top: 4px; word-break: break-all; }
|
||
|
||
.btn { background: #0096FF; color: #fff; border: none; border-radius: 6px; padding: 8px 16px;
|
||
font-family: inherit; font-size: 13px; cursor: pointer; margin: 4px 4px 4px 0; }
|
||
.btn:hover { background: #007ADB; }
|
||
.btn:disabled { background: #333; color: #666; cursor: not-allowed; }
|
||
.btn.secondary { background: #1E1E2E; border: 1px solid #333; }
|
||
.btn.secondary:hover { background: #2A2A3E; }
|
||
|
||
/* Tabs */
|
||
.tab-bar { display: flex; gap: 2px; margin-bottom: 0; }
|
||
.tab-btn { background: #1E1E2E; color: #8888AA; border: 1px solid #1E1E2E; border-bottom: none;
|
||
border-radius: 6px 6px 0 0; padding: 6px 14px; font-family: inherit; font-size: 12px;
|
||
cursor: pointer; position: relative; top: 1px; }
|
||
.tab-btn.active { background: #080810; color: #E0E0F0; border-color: #1E1E2E; }
|
||
.tab-btn .tab-count { background: #333; color: #888; border-radius: 8px; padding: 1px 6px;
|
||
font-size: 10px; margin-left: 4px; }
|
||
|
||
.log-header { display: flex; align-items: center; justify-content: space-between; padding: 6px 8px 4px; }
|
||
.log-header h2 { margin-bottom: 0; font-size: 12px; }
|
||
.pause-hint { font-size: 11px; color: #FFD60A; display: none; }
|
||
.pause-hint.visible { display: inline; }
|
||
|
||
.log-panel { background: #080810; border: 1px solid #1E1E2E; border-radius: 0 0 6px 6px; }
|
||
.log-box { height: 280px; overflow-y: auto; padding: 8px; font-size: 11px; line-height: 1.6; }
|
||
.log-box.hidden { display: none; }
|
||
.log-entry { white-space: pre-wrap; word-break: break-all; }
|
||
.log-entry.error { color: #FF6B6B; }
|
||
.log-entry.warn { color: #FFD60A; }
|
||
.log-entry.info { color: #AAB; }
|
||
.log-entry.debug { color: #555570; }
|
||
.log-entry.trace-step { color: #0096FF; border-left: 2px solid #0096FF; padding-left: 6px; margin: 2px 0; }
|
||
.log-entry.trace-ok { color: #34C759; border-left: 2px solid #34C759; padding-left: 6px; margin: 2px 0; }
|
||
.log-entry.trace-err { color: #FF3B30; border-left: 2px solid #FF3B30; padding-left: 6px; margin: 2px 0; }
|
||
.log-entry.trace-sep { color: #333; margin: 6px 0 2px; }
|
||
|
||
.chat-box { background: #080810; border: 1px solid #1E1E2E; border-radius: 6px;
|
||
min-height: 120px; max-height: 250px; overflow-y: auto;
|
||
padding: 12px; margin-bottom: 8px; display: flex; flex-direction: column; gap: 8px; }
|
||
.chat-msg { padding: 10px 14px; border-radius: 14px; font-size: 14px; line-height: 1.5;
|
||
word-wrap: break-word; max-width: 80%; white-space: pre-wrap;
|
||
box-shadow: 0 1px 2px rgba(0,0,0,0.4); }
|
||
.chat-msg.sent { background: #0096FF; color: #fff; align-self: flex-end;
|
||
border-bottom-right-radius: 4px; }
|
||
.chat-msg.received { background: #1E1E2E; color: #E8E8F0; align-self: flex-start;
|
||
border-bottom-left-radius: 4px; }
|
||
.chat-msg.error { background: #3B1010; color: #FF6B6B; align-self: flex-start; }
|
||
.chat-msg .meta { font-size: 10px; color: rgba(255,255,255,0.4); margin-top: 4px;
|
||
display: block; }
|
||
.chat-msg a { color: #66BBFF; text-decoration: underline; }
|
||
.chat-msg.sent a { color: #CCEEFF; }
|
||
.chat-msg .chat-media { max-width: 100%; max-height: 200px; border-radius: 8px;
|
||
margin-top: 6px; cursor: pointer; display: block; }
|
||
.chat-msg .chat-media:hover { opacity: 0.85; }
|
||
.lightbox-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.92);
|
||
z-index:2000; justify-content:center; align-items:center; cursor:pointer; }
|
||
.lightbox-overlay.open { display:flex; }
|
||
.lightbox-overlay img, .lightbox-overlay video { max-width:95vw; max-height:95vh; border-radius:8px; }
|
||
|
||
.input-row { display: flex; gap: 6px; align-items: flex-end; }
|
||
.input-row input, .input-row textarea {
|
||
flex: 1; background: #1E1E2E; border: 1px solid #333; border-radius: 6px;
|
||
padding: 8px 12px; color: #E0E0F0; font-family: inherit; font-size: 13px;
|
||
}
|
||
.input-row textarea { resize: none; min-height: 38px; max-height: 200px; line-height: 1.4;
|
||
overflow-y: auto; }
|
||
|
||
/* Terminal Modal */
|
||
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.85);
|
||
z-index:1000; justify-content:center; align-items:center; }
|
||
.modal-overlay.open { display:flex; }
|
||
.modal-box { background:#0D0D1A; border:1px solid #FFD60A44; border-radius:10px; width:90vw; max-width:860px;
|
||
max-height:90vh; display:flex; flex-direction:column; overflow:hidden; }
|
||
.modal-header { display:flex; justify-content:space-between; align-items:center; padding:10px 14px;
|
||
border-bottom:1px solid #1E1E2E; }
|
||
.modal-header h3 { font-size:14px; color:#FFD60A; margin:0; }
|
||
.modal-close { background:none; border:none; color:#888; font-size:20px; cursor:pointer; padding:0 6px; }
|
||
.modal-close:hover { color:#FF3B30; }
|
||
.modal-body { flex:1; padding:4px; min-height:400px; }
|
||
.modal-footer { padding:6px 14px; border-top:1px solid #1E1E2E; font-size:11px; color:#8888AA; }
|
||
|
||
/* Haupt-Navigation */
|
||
.main-nav { display:flex; gap:0; margin-bottom:16px; border-bottom:2px solid #1E1E2E; }
|
||
.main-nav-btn { background:none; border:none; color:#8888AA; font-family:inherit; font-size:14px;
|
||
padding:10px 20px; cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-2px;
|
||
transition: color 0.2s, border-color 0.2s; }
|
||
.main-nav-btn:hover { color:#E0E0F0; }
|
||
.main-nav-btn.active { color:#0096FF; border-bottom-color:#0096FF; }
|
||
.main-tab { display:none; }
|
||
.main-tab.visible { display:block; }
|
||
|
||
/* Settings */
|
||
.settings-section { margin-bottom:20px; }
|
||
.settings-section h2 { margin-bottom:12px; }
|
||
.toggle { position:relative; width:40px; height:22px; flex-shrink:0; margin-left:8px; }
|
||
.toggle input { opacity:0; width:0; height:0; }
|
||
.toggle .slider { position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0;
|
||
background:#333; border-radius:22px; transition:0.3s; }
|
||
.toggle .slider:before { content:""; position:absolute; height:16px; width:16px; left:3px; bottom:3px;
|
||
background:#888; border-radius:50%; transition:0.3s; }
|
||
.toggle input:checked + .slider { background:#0096FF; }
|
||
.toggle input:checked + .slider:before { transform:translateX(18px); background:#fff; }
|
||
.toggle input:disabled + .slider { opacity:0.4; cursor:not-allowed; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Service-Status Banner unten rechts (Gamebox: F5-TTS / Whisper Lade-Status) -->
|
||
<div id="service-status-banner" style="display:none;position:fixed;bottom:16px;right:16px;z-index:999;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:8px;padding:10px 14px;font-size:12px;color:#fff;min-width:240px;max-width:360px;box-shadow:0 4px 14px rgba(0,0,0,0.5);">
|
||
<div style="display:flex;align-items:flex-start;gap:8px;">
|
||
<span id="service-status-icon" style="font-size:18px;line-height:1;">⏳</span>
|
||
<div id="service-status-list" style="flex:1;display:flex;flex-direction:column;gap:6px;"></div>
|
||
<button id="service-status-close" onclick="document.getElementById('service-status-banner').style.display='none'" style="background:none;border:none;color:#666680;font-size:16px;cursor:pointer;padding:0;line-height:1;display:none;">×</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Voice-Preview Modal -->
|
||
<div id="voice-preview-modal" style="display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,0.7);align-items:center;justify-content:center;">
|
||
<div style="background:#1A1A2E;border:1px solid #2A2A3E;border-radius:10px;padding:20px;max-width:560px;width:90%;display:flex;flex-direction:column;gap:12px;">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;">
|
||
<h3 style="margin:0;color:#fff;">Stimmen-Preview: <span id="voice-preview-name">—</span></h3>
|
||
<button onclick="closeVoicePreview()" style="background:none;border:none;color:#8888AA;font-size:22px;cursor:pointer;">×</button>
|
||
</div>
|
||
<textarea id="voice-preview-text" rows="4"
|
||
style="background:#0D0D1A;border:1px solid #2A2A3E;border-radius:6px;padding:10px;color:#fff;font-size:13px;resize:vertical;"></textarea>
|
||
|
||
<div style="display:flex;align-items:center;gap:10px;font-size:12px;color:#8888AA;">
|
||
<span style="min-width:120px;">Geschwindigkeit:</span>
|
||
<button onclick="adjustPreviewSpeed(-0.1)" class="btn secondary" style="padding:4px 10px;font-size:12px;">−0.1</button>
|
||
<span id="voice-preview-speed-value" style="min-width:52px;text-align:center;color:#fff;font-weight:600;">1.0 x</span>
|
||
<button onclick="adjustPreviewSpeed(0.1)" class="btn secondary" style="padding:4px 10px;font-size:12px;">+0.1</button>
|
||
<span style="color:#555570;font-size:11px;">(nur fuer dieses Modal, wird nicht gespeichert)</span>
|
||
</div>
|
||
|
||
<div style="display:flex;gap:8px;align-items:center;">
|
||
<button id="voice-preview-play" onclick="playVoicePreview()" class="btn primary" style="padding:8px 16px;">
|
||
▶ Abspielen
|
||
</button>
|
||
<span id="voice-preview-status" style="color:#8888AA;font-size:11px;flex:1;"></span>
|
||
</div>
|
||
<audio id="voice-preview-audio" controls style="width:100%;display:none;"></audio>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Disk-Space Warnung (dynamisch gesetzt) -->
|
||
<div id="disk-banner" style="display:none;position:sticky;top:0;z-index:500;padding:10px 14px;border-radius:0;margin:-16px -16px 12px -16px;font-size:13px;">
|
||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
|
||
<span id="disk-banner-icon" style="font-size:18px;">⚠️</span>
|
||
<span id="disk-banner-text" style="flex:1;min-width:200px;font-weight:600;"></span>
|
||
<button onclick="copyDiskCmd('safe')" class="btn secondary" style="padding:4px 10px;font-size:11px;" title="docker builder prune -a -f && docker image prune -a -f">
|
||
Sicher aufraeumen
|
||
</button>
|
||
<button onclick="document.getElementById('disk-banner-aggressive').style.display=(document.getElementById('disk-banner-aggressive').style.display==='none'?'flex':'none')"
|
||
class="btn secondary" style="padding:4px 10px;font-size:11px;">
|
||
Mehr ▾
|
||
</button>
|
||
<button onclick="document.getElementById('disk-banner').style.display='none'" class="btn secondary" style="padding:4px 10px;font-size:11px;">Schliessen</button>
|
||
</div>
|
||
<!-- Aggressive Variante (erst nach Klick sichtbar) -->
|
||
<div id="disk-banner-aggressive" style="display:none;margin-top:10px;padding:8px;background:rgba(0,0,0,0.25);border-radius:4px;flex-direction:column;gap:6px;font-size:12px;">
|
||
<div>
|
||
<b>Sicher</b> (empfohlen) — Build-Cache + ungenutzte Images, keine Volumes:<br>
|
||
<code style="font-family:monospace;">docker builder prune -a -f && docker image prune -a -f</code>
|
||
</div>
|
||
<div style="color:#FFAA55;">
|
||
<b>Aggressiv</b> — zusaetzlich ungenutzte Volumes. <b>Nur wenn alle ARIA-Container laufen</b>, sonst riskierst du Daten-Verlust (Sessions, SSH-Keys, Shared):<br>
|
||
<code style="font-family:monospace;">docker system prune -a --volumes -f</code>
|
||
<button onclick="copyDiskCmd('aggressive')" class="btn secondary" style="padding:2px 8px;font-size:10px;margin-left:6px;">Kopieren</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<h1>ARIA Diagnostic</h1>
|
||
|
||
<!-- Haupt-Navigation -->
|
||
<div class="main-nav">
|
||
<button class="main-nav-btn active" onclick="switchMainTab('main')">Main</button>
|
||
<button class="main-nav-btn" onclick="switchMainTab('brain')">Gehirn</button>
|
||
<button class="main-nav-btn" onclick="switchMainTab('skills')">Skills</button>
|
||
<button class="main-nav-btn" onclick="switchMainTab('files')">Dateien</button>
|
||
<button class="main-nav-btn" onclick="switchMainTab('settings')">Einstellungen</button>
|
||
</div>
|
||
|
||
<!-- ══════ TAB: Main ══════ -->
|
||
<div id="tab-main" class="main-tab visible">
|
||
|
||
<!-- Verbindungsstatus -->
|
||
<div class="grid">
|
||
<div class="card">
|
||
<h2>ARIA Brain</h2>
|
||
<div class="status-row">
|
||
<div class="dot" id="brain-dot"></div>
|
||
<span class="status-label" id="brain-status-short">Lade...</span>
|
||
</div>
|
||
<div class="error-text" id="brain-error"></div>
|
||
<button class="btn secondary" onclick="loadBrainStatus()">Status pruefen</button>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>RVS (Rendezvous)</h2>
|
||
<div class="status-row">
|
||
<div class="dot" id="rvs-dot"></div>
|
||
<span class="status-label" id="rvs-status">-</span>
|
||
</div>
|
||
<div class="error-text" id="rvs-error"></div>
|
||
<button class="btn secondary" onclick="send({action:'reconnect_rvs'})">Reconnect</button>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Claude Proxy</h2>
|
||
<div class="status-row">
|
||
<div class="dot" id="proxy-dot"></div>
|
||
<span class="status-label" id="proxy-status">Nicht getestet</span>
|
||
</div>
|
||
<div class="error-text" id="proxy-error"></div>
|
||
<div id="proxy-models" style="margin-top:6px;display:none">
|
||
<div style="font-size:11px;color:#8888AA;margin-bottom:3px">Verfuegbare Modelle:</div>
|
||
<div id="proxy-models-list" style="font-size:12px;line-height:1.6"></div>
|
||
<div style="font-size:10px;color:#555570;margin-top:4px" id="proxy-models-hint"></div>
|
||
</div>
|
||
<div id="proxy-auth" style="margin-top:6px;display:none;background:#080810;border:1px solid #1E1E2E;border-radius:4px;padding:6px 8px;font-size:10px;line-height:1.5;max-height:220px;overflow-y:auto;white-space:pre-wrap;color:#8888AA"></div>
|
||
<!-- Terminal Modal wird weiter unten definiert -->
|
||
<div id="proxy-creds-box" style="margin-top:6px;display:none">
|
||
<div style="background:#0A1A1A;border:1px solid #0096FF33;border-radius:6px;padding:8px 10px">
|
||
<div style="font-size:11px;color:#0096FF;margin-bottom:4px;font-weight:bold">Credentials einfuegen</div>
|
||
<div style="font-size:10px;color:#8888AA;margin-bottom:6px">Auf einem Rechner mit Claude Login: <code style="background:#1E1E2E;padding:1px 4px;border-radius:3px">cat ~/.config/claude/.credentials.json</code><br>Inhalt hier einfuegen:</div>
|
||
<textarea id="creds-input" rows="4" style="width:100%;background:#080810;border:1px solid #333;border-radius:4px;padding:6px;color:#E0E0F0;font-family:monospace;font-size:11px;resize:vertical" placeholder='{"claudeAiOauth":{"accessToken":"...","refreshToken":"...","expiresAt":"..."}}'></textarea>
|
||
<button class="btn" onclick="submitCredentials()" style="margin-top:4px;padding:4px 12px;font-size:11px">Speichern</button>
|
||
<div id="creds-status" style="font-size:11px;margin-top:4px"></div>
|
||
</div>
|
||
</div>
|
||
<button class="btn secondary" id="btn-proxy-test" onclick="testProxyBtn()" style="margin-top:6px">Proxy testen</button>
|
||
<button class="btn secondary" onclick="checkProxyAuth()" style="margin-top:6px">Auth pruefen</button>
|
||
<button class="btn secondary" id="btn-proxy-login" onclick="startProxyLogin()" style="margin-top:6px">Login starten</button>
|
||
<button class="btn secondary" onclick="toggleCredsBox()" style="margin-top:6px">Credentials einfuegen</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chat Test -->
|
||
<div class="grid">
|
||
<div class="card full">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
||
<h2 style="margin:0;">Chat Test</h2>
|
||
<div style="display:flex;align-items:center;gap:12px;">
|
||
<label style="color:#8888AA;font-size:11px;cursor:pointer;">
|
||
<input type="checkbox" id="tts-debug-toggle" onchange="toggleTtsDebug()" style="margin-right:4px;vertical-align:middle;">
|
||
TTS-Text einblenden
|
||
</label>
|
||
<label style="color:#8888AA;font-size:11px;cursor:pointer;">
|
||
<input type="checkbox" id="gps-debug-toggle" onchange="toggleGpsDebug()" style="margin-right:4px;vertical-align:middle;">
|
||
GPS-Position einblenden
|
||
</label>
|
||
<button class="btn secondary" onclick="toggleChatFullscreen()" id="btn-chat-fs" style="padding:4px 10px;font-size:11px;">Vollbild</button>
|
||
</div>
|
||
</div>
|
||
<div class="chat-box" id="chat-box"></div>
|
||
<div id="thinking-indicator" style="display:none;padding:6px 10px;font-size:12px;color:#FFD60A;background:#1E1E2E;border-radius:0 0 6px 6px;margin-top:-8px;margin-bottom:8px;align-items:center;justify-content:space-between;">
|
||
<span><span style="animation:pulse 1s infinite;">💭</span> <span id="thinking-text">ARIA denkt...</span></span>
|
||
<div style="display:flex;gap:6px;">
|
||
<button class="btn secondary" onclick="cancelRequest()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;">Abbrechen</button>
|
||
</div>
|
||
</div>
|
||
<div id="diag-pending-attachments" style="display:none;padding:6px 10px;background:#1E1E2E;border-radius:6px 6px 0 0;margin-bottom:-4px;display:flex;gap:6px;flex-wrap:wrap;align-items:center;">
|
||
</div>
|
||
<div class="input-row">
|
||
<label class="btn secondary" style="padding:6px 10px;cursor:pointer;font-size:14px;" title="Datei anhaengen">
|
||
📎
|
||
<input type="file" id="diag-file-input" multiple accept="image/*,application/pdf,.doc,.docx,.txt" style="display:none;" onchange="handleDiagFileSelect(this.files)">
|
||
</label>
|
||
<textarea id="chat-input" placeholder="Nachricht an ARIA... (Enter sendet, Shift+Enter neue Zeile)" rows="2" onpaste="handleDiagPaste(event)" oninput="autoResizeTextarea(this)"></textarea>
|
||
<button class="btn" id="btn-gw" onclick="testGateway()">Gateway senden</button>
|
||
<button class="btn" id="btn-rvs" onclick="testRVS()">Via RVS senden</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chat Vollbild Modal -->
|
||
<div id="chat-fullscreen" style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;background:#0D0D1A;z-index:1000;padding:16px;flex-direction:column;">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
|
||
<h2 style="margin:0;color:#0096FF;">ARIA Chat</h2>
|
||
<button class="btn secondary" onclick="toggleChatFullscreen()" style="padding:6px 14px;">Schliessen</button>
|
||
</div>
|
||
<div id="chat-box-fs" class="chat-box" style="flex:1;max-height:none;min-height:0;overflow-y:auto;"></div>
|
||
<div id="thinking-indicator-fs" style="display:none;padding:6px 10px;font-size:12px;color:#FFD60A;background:#1E1E2E;border-radius:6px;margin-top:4px;">
|
||
<span style="animation:pulse 1s infinite;">💭</span> <span id="thinking-text-fs">ARIA denkt...</span>
|
||
</div>
|
||
<div class="input-row" style="margin-top:8px;">
|
||
<textarea id="chat-input-fs" placeholder="Nachricht an ARIA... (Enter sendet, Shift+Enter neue Zeile)" rows="2" oninput="autoResizeTextarea(this)"></textarea>
|
||
<button class="btn" onclick="testGatewayFS()">Gateway senden</button>
|
||
<button class="btn" onclick="testRVSFS()">Via RVS senden</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sessions + alter Brain-Viewer entfernt — Memories laufen jetzt
|
||
komplett ueber den Gehirn-Tab gegen die Vector-DB im aria-brain. -->
|
||
|
||
<!-- Logs mit Tabs -->
|
||
<div class="card" style="margin-top:12px; padding: 8px 0 0 0;">
|
||
<div style="padding: 0 12px;">
|
||
<div class="tab-bar">
|
||
<button class="tab-btn active" data-tab="all" onclick="switchTab('all')">Alle <span class="tab-count" id="count-all">0</span></button>
|
||
<button class="tab-btn" data-tab="gateway" onclick="switchTab('gateway')">Gateway <span class="tab-count" id="count-gateway">0</span></button>
|
||
<button class="tab-btn" data-tab="rvs" onclick="switchTab('rvs')">RVS <span class="tab-count" id="count-rvs">0</span></button>
|
||
<button class="tab-btn" data-tab="proxy" onclick="switchTab('proxy')">Proxy <span class="tab-count" id="count-proxy">0</span></button>
|
||
<button class="tab-btn" data-tab="bridge" onclick="switchTab('bridge')">Bridge <span class="tab-count" id="count-bridge">0</span></button>
|
||
<button class="tab-btn" data-tab="server" onclick="switchTab('server')">Server <span class="tab-count" id="count-server">0</span></button>
|
||
<button class="tab-btn" data-tab="trace" onclick="switchTab('trace')" style="margin-left:auto;border-color:#0096FF44;color:#0096FF" title="End-to-End-Mitschnitt einer einzelnen Anfrage mit Zeitstempeln">Trace <span class="tab-count" id="count-trace">0</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="log-panel">
|
||
<div class="log-header">
|
||
<span>
|
||
<button class="btn secondary" id="btn-docker-logs" onclick="loadDockerLogs()" style="padding:4px 10px;font-size:11px;display:none">Docker Logs laden</button>
|
||
</span>
|
||
<span>
|
||
<span class="pause-hint" id="pause-hint">Autoscroll pausiert</span>
|
||
<button class="btn secondary" id="btn-scroll" onclick="resumeScroll()" style="display:none;padding:4px 10px;font-size:11px">Nach unten</button>
|
||
</span>
|
||
</div>
|
||
<div class="log-box" id="log-all"></div>
|
||
<div class="log-box hidden" id="log-gateway"></div>
|
||
<div class="log-box hidden" id="log-rvs"></div>
|
||
<div class="log-box hidden" id="log-proxy"></div>
|
||
<div class="log-box hidden" id="log-bridge"></div>
|
||
<div class="log-box hidden" id="log-server"></div>
|
||
<div class="log-box hidden" id="log-trace"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ARIA Live-Ansicht -->
|
||
<div class="card" style="margin-top:12px; padding: 8px 0 0 0;">
|
||
<div style="padding: 0 12px;">
|
||
<div class="tab-bar">
|
||
<button class="tab-btn active" id="live-tab-ssh" onclick="switchLiveTab('ssh')">SSH Terminal</button>
|
||
<button class="tab-btn" id="live-tab-desktop" onclick="switchLiveTab('desktop')">Desktop</button>
|
||
</div>
|
||
</div>
|
||
<div style="background:#080810; border:1px solid #1E1E2E; border-radius:0 0 6px 6px; position:relative;">
|
||
<!-- SSH Terminal -->
|
||
<div id="live-ssh" style="height:350px; padding:4px;">
|
||
<div id="live-ssh-bar" style="display:flex;gap:6px;align-items:center;padding:4px 4px 6px;">
|
||
<button class="btn" onclick="startLiveSSH()" id="btn-live-ssh" style="padding:4px 12px;font-size:11px;">Verbinden</button>
|
||
<span id="live-ssh-status" style="font-size:11px;color:#8888AA;">Nicht verbunden</span>
|
||
</div>
|
||
<div id="live-ssh-term" style="height:calc(100% - 32px);"></div>
|
||
</div>
|
||
<!-- Desktop Viewer -->
|
||
<div id="live-desktop" style="height:350px; display:none; position:relative;">
|
||
<div id="desktop-placeholder" style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:#555570;">
|
||
<div style="font-size:36px;margin-bottom:12px;">🖥</div>
|
||
<div style="font-size:13px;">Kein Desktop verfuegbar</div>
|
||
<div style="font-size:11px;margin-top:4px;">ARIA muss zuerst eine Desktop-Umgebung installieren</div>
|
||
<button class="btn secondary" onclick="checkDesktop()" style="margin-top:12px;padding:4px 12px;font-size:11px;">Desktop pruefen</button>
|
||
</div>
|
||
<iframe id="desktop-vnc" style="width:100%;height:100%;border:none;display:none;"></iframe>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /tab-main -->
|
||
|
||
<!-- ══════ TAB: Einstellungen ══════ -->
|
||
<div id="tab-settings" class="main-tab">
|
||
|
||
<!-- Was ist ARIA? -->
|
||
<div class="settings-section">
|
||
<div class="card" style="max-width:700px;font-size:13px;color:#AAA;border-left:3px solid #0096FF;">
|
||
<strong style="color:#0096FF;">ARIA</strong> — Autonomous Reasoning & Intelligence Assistant.
|
||
Selbst gehosteter JARVIS-artiger KI-Assistent, gebaut von Stefan / HackerSoft Oldenburg.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Reparatur & Restart -->
|
||
<div class="settings-section">
|
||
<h2>Reparatur & Restart</h2>
|
||
<div class="card" style="line-height:1.6;">
|
||
<p style="color:#8888AA;font-size:12px;margin:0 0 10px;">
|
||
Wenn ein Container haengt oder ARIA nicht mehr antwortet — hier kann man gezielt eingreifen.
|
||
</p>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
||
<button class="btn secondary" onclick="restartContainer('aria-bridge')" style="color:#FF3B30;border-color:#FF3B30;">🚨 aria-bridge neu</button>
|
||
<button class="btn secondary" onclick="restartContainer('aria-brain')" style="color:#FF3B30;border-color:#FF3B30;">🚨 aria-brain neu</button>
|
||
<button class="btn secondary" onclick="restartContainer('aria-qdrant')" style="color:#FF3B30;border-color:#FF3B30;">🚨 aria-qdrant neu</button>
|
||
</div>
|
||
<div id="restart-status" style="margin-top:10px;font-size:11px;color:#8888AA;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Komplett-Reset -->
|
||
<div class="settings-section">
|
||
<h2>Komplett-Reset</h2>
|
||
<div class="card" style="line-height:1.6;border-left:3px solid #FF3B30;">
|
||
<p style="color:#FF6B6B;font-size:12px;margin:0 0 10px;">
|
||
<strong>⚠ WIPE ALL</strong> — alle Einstellungen, Stimmen und das komplette
|
||
Gedächtnis (Vector-DB) werden gelöscht. ARIA wird leer wie nach
|
||
Erstinstallation. Bleiben tun nur:
|
||
</p>
|
||
<ul style="color:#8888AA;font-size:12px;margin:0 0 12px 16px;padding:0;">
|
||
<li><code>.env</code> (Tokens + RVS-Config)</li>
|
||
<li><code>aria-data/ssh/</code> (SSH-Keys für aria-wohnung)</li>
|
||
</ul>
|
||
<p style="color:#8888AA;font-size:12px;margin:0 0 10px;">
|
||
Vorher exportieren wenn etwas erhalten bleiben soll (Gehirn-Tab + Voice-Liste).
|
||
</p>
|
||
<button class="btn" onclick="wipeAll()" style="background:#3A1010;border:1px solid #FF3B30;color:#FF6B6B;">🗑 ALLES löschen — neue ARIA</button>
|
||
<div id="wipe-status" style="margin-top:10px;font-size:11px;color:#8888AA;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Betriebsmodus -->
|
||
<div class="settings-section">
|
||
<h2>Betriebsmodus</h2>
|
||
<div class="card" style="max-width:500px;">
|
||
<div id="mode-selector" style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
|
||
<button class="btn mode-btn" data-mode="normal" onclick="setMode('normal')" style="background:#1E1E2E;border:2px solid transparent;">
|
||
<span style="font-size:18px;">🟢</span> Normal<br><span style="font-size:10px;color:#8888AA;">Hoert zu, antwortet, spricht</span>
|
||
</button>
|
||
<button class="btn mode-btn" data-mode="nicht_stoeren" onclick="setMode('nicht_stoeren')" style="background:#1E1E2E;border:2px solid transparent;">
|
||
<span style="font-size:18px;">🔴</span> Nicht stoeren<br><span style="font-size:10px;color:#8888AA;">Nur Kritikalarme</span>
|
||
</button>
|
||
<button class="btn mode-btn" data-mode="fluester" onclick="setMode('fluester')" style="background:#1E1E2E;border:2px solid transparent;">
|
||
<span style="font-size:18px;">🟡</span> Fluestern<br><span style="font-size:10px;color:#8888AA;">Nur Text, keine Sprache</span>
|
||
</button>
|
||
<button class="btn mode-btn" data-mode="hangar" onclick="setMode('hangar')" style="background:#1E1E2E;border:2px solid transparent;">
|
||
<span style="font-size:18px;">✈️</span> Hangar<br><span style="font-size:10px;color:#8888AA;">Nur wichtige Meldungen</span>
|
||
</button>
|
||
<button class="btn mode-btn" data-mode="gaming" onclick="setMode('gaming')" style="background:#1E1E2E;border:2px solid transparent;grid-column:1/-1;">
|
||
<span style="font-size:18px;">🎮</span> Gaming<br><span style="font-size:10px;color:#8888AA;">Nur direkte Fragen</span>
|
||
</button>
|
||
</div>
|
||
<div style="margin-top:8px;font-size:11px;color:#555570;" id="mode-status">Aktueller Modus: Normal</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stimmen -->
|
||
<div class="settings-section">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||
<h2 style="margin:0;">Sprachausgabe</h2>
|
||
<div style="display:flex;gap:6px;">
|
||
<button class="btn secondary" onclick="exportVoiceSettings()" style="padding:4px 10px;font-size:11px;" title="voice_config.json + highlight_triggers herunterladen">⬇ Export</button>
|
||
<input type="file" id="voice-settings-import-file" accept=".json,application/json" style="display:none" onchange="importVoiceSettings(event)">
|
||
<button class="btn secondary" onclick="document.getElementById('voice-settings-import-file').click()" style="padding:4px 10px;font-size:11px;">⬆ Import</button>
|
||
</div>
|
||
</div>
|
||
<div class="card" style="max-width:500px;">
|
||
<!-- TTS aktiv (global) -->
|
||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
||
<label style="color:#8888AA;font-size:12px;">TTS aktiv:</label>
|
||
<label class="toggle"><input type="checkbox" id="diag-tts-enabled" checked onchange="sendVoiceConfig()"><span class="slider"></span></label>
|
||
</div>
|
||
|
||
<!-- F5-TTS Stimme (zwingend eine Voice waehlen — F5-TTS braucht eine Referenz) -->
|
||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:6px;">
|
||
<label style="color:#8888AA;font-size:12px;">F5-TTS Stimme:</label>
|
||
<select id="diag-xtts-voice" onchange="sendVoiceConfig()" style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
|
||
<option value="" disabled>(keine Stimme gewaehlt)</option>
|
||
</select>
|
||
<button class="btn secondary" onclick="loadXTTSVoices()" style="padding:4px 10px;font-size:11px;">Laden</button>
|
||
</div>
|
||
<div id="voice-status" style="font-size:11px;min-height:14px;margin-bottom:12px;color:#8888AA;"></div>
|
||
|
||
<!-- Gecloned Stimmen — Liste mit Loeschen -->
|
||
<div id="xtts-voice-list" style="margin-bottom:12px;"></div>
|
||
|
||
<!-- F5-TTS Modell-Tuning -->
|
||
<details style="background:#0D0D1A;border:1px solid #2A2A3E;border-radius:6px;padding:10px 12px;margin-bottom:12px;">
|
||
<summary style="color:#8888AA;font-size:12px;cursor:pointer;">F5-TTS Modell-Tuning (advanced)</summary>
|
||
<div style="margin-top:10px;display:flex;flex-direction:column;gap:8px;">
|
||
<div style="color:#8888AA;font-size:11px;">
|
||
Werden via RVS an die f5tts-bridge auf der Gamebox geschickt.
|
||
Modell-/Checkpoint-Wechsel triggert einen Reload (~30s).
|
||
Hardcoded Defaults: F5TTS_v1_Base, cfg_strength=2.5, nfe_step=32.
|
||
</div>
|
||
|
||
<label style="color:#8888AA;font-size:12px;">
|
||
Modell-Architektur (F5TTS_v1_Base = Default multilingual, F5TTS_Base = fuer die meisten Fine-Tunes):
|
||
</label>
|
||
<input type="text" id="diag-f5tts-model"
|
||
placeholder="F5TTS_v1_Base"
|
||
style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
|
||
|
||
<label style="color:#8888AA;font-size:12px;">
|
||
Custom Checkpoint — HF-Pfad (hf://user/repo/file) oder lokaler Container-Pfad. Leer = Default.
|
||
</label>
|
||
<input type="text" id="diag-f5tts-ckpt"
|
||
placeholder="z.B. hf://aihpi/F5-TTS-German/F5TTS_Base/model_365000.safetensors"
|
||
style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
|
||
|
||
<label style="color:#8888AA;font-size:12px;">
|
||
Custom Vocab — muss zum Checkpoint passen. Leer = Default.
|
||
</label>
|
||
<input type="text" id="diag-f5tts-vocab"
|
||
placeholder="z.B. hf://aihpi/F5-TTS-German/vocab.txt"
|
||
style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
|
||
|
||
<div style="display:flex;gap:12px;">
|
||
<div style="flex:1;">
|
||
<label style="color:#8888AA;font-size:12px;">cfg_strength (1.0 - 5.0):</label>
|
||
<input type="number" id="diag-f5tts-cfg" step="0.1" min="1" max="5"
|
||
placeholder="2.5"
|
||
style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;width:100%;box-sizing:border-box;">
|
||
<div style="color:#666680;font-size:10px;">Hoeher = klebt staerker an Referenz</div>
|
||
</div>
|
||
<div style="flex:1;">
|
||
<label style="color:#8888AA;font-size:12px;">nfe_step (8 - 64):</label>
|
||
<input type="number" id="diag-f5tts-nfe" step="1" min="8" max="64"
|
||
placeholder="32"
|
||
style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;width:100%;box-sizing:border-box;">
|
||
<div style="color:#666680;font-size:10px;">Hoeher = bessere Qualitaet, langsamer</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn primary" onclick="sendVoiceConfig()" style="padding:6px 14px;font-size:12px;align-self:flex-start;margin-top:6px;">
|
||
Anwenden
|
||
</button>
|
||
</div>
|
||
</details>
|
||
|
||
<!-- Voice Cloning -->
|
||
<div style="background:#1E1E2E;border-radius:8px;padding:12px;margin-top:8px;">
|
||
<div style="color:#0096FF;font-size:13px;font-weight:600;margin-bottom:8px;">Stimme klonen</div>
|
||
<div style="color:#8888AA;font-size:11px;margin-bottom:8px;">
|
||
Lade ein oder mehrere Audio-Samples hoch (WAV/MP3, min. 6-10 Sekunden).
|
||
Mehrere Dateien werden automatisch zusammengefuegt.
|
||
</div>
|
||
<div style="margin-bottom:8px;">
|
||
<input type="text" id="xtts-clone-name" placeholder="Name fuer die Stimme..." style="background:#0D0D1A;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;color:#fff;font-size:13px;width:100%;box-sizing:border-box;">
|
||
</div>
|
||
<div style="margin-bottom:8px;">
|
||
<input type="file" id="xtts-clone-files" accept="audio/*" multiple style="color:#8888AA;font-size:12px;">
|
||
</div>
|
||
<div style="display:flex;gap:8px;">
|
||
<button class="btn" onclick="uploadVoiceSamples()" style="flex:1;">Stimme erstellen</button>
|
||
</div>
|
||
<div id="xtts-clone-status" style="font-size:11px;color:#555570;margin-top:6px;"></div>
|
||
</div>
|
||
|
||
<!-- XTTS Status -->
|
||
<div style="margin-top:8px;font-size:11px;color:#555570;" id="xtts-status">
|
||
XTTS-Server: Nicht verbunden (starte xtts/ auf dem Gaming-PC)
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Whisper (STT) -->
|
||
<div class="settings-section">
|
||
<h2>Whisper (Spracherkennung)</h2>
|
||
<div style="font-size:11px;color:#8888AA;margin-bottom:8px;">
|
||
Aenderungen werden sofort an die Bridge gesendet und das Modell neu geladen
|
||
(kann bei medium/large 10-30s dauern — waehrend dieser Zeit ist STT kurz pausiert).
|
||
</div>
|
||
<div class="card" style="max-width:500px;">
|
||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;">
|
||
<label style="color:#8888AA;font-size:12px;min-width:80px;">Modell:</label>
|
||
<select id="diag-whisper-model" onchange="sendVoiceConfig()" style="flex:1;background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:6px 10px;font-size:13px;">
|
||
<option value="tiny">tiny (39MB, schnell, niedrige Qualitaet)</option>
|
||
<option value="base">base (74MB, schnell, ok)</option>
|
||
<option value="small">small (244MB, mittel)</option>
|
||
<option value="medium" selected>medium (769MB, gut — Empfehlung)</option>
|
||
<option value="large-v3">large-v3 (1.5GB, beste Qualitaet, langsam auf CPU)</option>
|
||
</select>
|
||
</div>
|
||
<div style="font-size:10px;color:#555570;">
|
||
Tipp: <code>medium</code> ist der beste Kompromiss fuer CPU. <code>large-v3</code> nur bei GPU sinnvoll.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Runtime-Konfiguration (migriert von .env) -->
|
||
<div class="settings-section">
|
||
<h2>Runtime-Konfiguration</h2>
|
||
<div style="font-size:11px;color:#8888AA;margin-bottom:8px;">
|
||
Werte werden in <code>/shared/config/runtime.json</code> persistiert und
|
||
ueberschreiben die ENV-Variablen aus <code>aria.env</code>. Bridge liest
|
||
sie beim naechsten Start — nach Aenderung <b>Bridge-Container neu starten</b>
|
||
(Diagnostic-Container bleibt auf ENV).
|
||
</div>
|
||
<div class="card" style="max-width:600px;">
|
||
<div style="display:grid;grid-template-columns:140px 1fr;gap:8px 10px;align-items:center;font-size:13px;">
|
||
<label style="color:#8888AA;">RVS Host:</label>
|
||
<input type="text" id="rc-rvs-host" style="width:100%;box-sizing:border-box;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:4px;padding:6px;color:#fff;">
|
||
<label style="color:#8888AA;">RVS Port:</label>
|
||
<input type="text" id="rc-rvs-port" style="width:100%;box-sizing:border-box;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:4px;padding:6px;color:#fff;">
|
||
<label style="color:#8888AA;">RVS TLS:</label>
|
||
<select id="rc-rvs-tls" style="width:100%;box-sizing:border-box;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:4px;padding:6px;color:#fff;">
|
||
<option value="true">true (wss://)</option>
|
||
<option value="false">false (ws://)</option>
|
||
</select>
|
||
<label style="color:#8888AA;">RVS Token:</label>
|
||
<div style="display:flex;gap:4px;min-width:0;">
|
||
<input type="password" id="rc-rvs-token" style="flex:1;min-width:0;box-sizing:border-box;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:4px;padding:6px;color:#fff;font-family:monospace;">
|
||
<button type="button" class="btn secondary" onclick="toggleSecret('rc-rvs-token', this)" style="padding:4px 10px;flex-shrink:0;" title="Anzeigen/Verbergen">👁</button>
|
||
</div>
|
||
<label style="color:#8888AA;">Aria Auth Token:</label>
|
||
<div style="display:flex;gap:4px;min-width:0;">
|
||
<input type="password" id="rc-auth-token" style="flex:1;min-width:0;box-sizing:border-box;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:4px;padding:6px;color:#fff;font-family:monospace;">
|
||
<button type="button" class="btn secondary" onclick="toggleSecret('rc-auth-token', this)" style="padding:4px 10px;flex-shrink:0;" title="Anzeigen/Verbergen">👁</button>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:8px;margin-top:12px;">
|
||
<button class="btn" onclick="saveRuntimeConfig()" style="flex:1;">Speichern</button>
|
||
<button class="btn secondary" onclick="loadRuntimeConfig()" style="flex:1;">Neu laden</button>
|
||
</div>
|
||
<div id="rc-status" style="font-size:11px;color:#555570;margin-top:6px;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- App-Onboarding via QR-Code -->
|
||
<div class="settings-section">
|
||
<h2>App-Onboarding (QR-Code)</h2>
|
||
<div style="font-size:11px;color:#8888AA;margin-bottom:8px;">
|
||
RVS-Credentials als QR-Code — App scannt, keine manuelle Eingabe.
|
||
Enthaelt Host, Port, TLS-Flag und Token.
|
||
</div>
|
||
<div class="card" style="max-width:500px;">
|
||
<div style="display:flex;gap:12px;align-items:flex-start;">
|
||
<div id="onboarding-qr" style="width:220px;height:220px;flex-shrink:0;background:#1E1E2E;border-radius:6px;overflow:hidden;display:flex;align-items:center;justify-content:center;color:#555570;font-size:11px;text-align:center;">
|
||
QR-Code wird geladen...
|
||
</div>
|
||
<div style="flex:1;font-size:11px;color:#8888AA;line-height:1.5;">
|
||
<div style="color:#FF9500;font-weight:bold;margin-bottom:4px;">Achtung</div>
|
||
Dieser QR enthaelt den RVS-Token im Klartext — zeige ihn niemandem,
|
||
speichere keine Screenshots davon in unsicheren Cloud-Diensten.
|
||
<button class="btn" onclick="loadOnboardingQR()" style="margin-top:10px;width:100%;">
|
||
QR neu generieren
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tool-Berechtigungen-Section entfernt — Brain steuert Tool-Use
|
||
selbst (Skills + skill_create Meta-Tool). Es gibt keine granulare
|
||
Permission-Maske, Brain weiss zur Laufzeit welche Tools es hat. -->
|
||
|
||
<!-- Brain-Model-Einstellung -->
|
||
<div class="settings-section">
|
||
<h2>Sprachmodell (Brain)</h2>
|
||
<div class="card" style="max-width:500px;">
|
||
<div style="font-size:11px;color:#8888AA;margin-bottom:10px;line-height:1.5;">
|
||
Welches Claude-Model nutzt das Brain pro Anfrage. Wert wird in
|
||
<code>/shared/config/runtime.json</code> als <code>brainModel</code> persistiert.
|
||
Bei Aenderung: <strong>aria-brain restarten</strong> (Reparatur-Section oben), damit's greift.
|
||
<br><br>
|
||
Verfuegbar via Proxy: <code>claude-sonnet-4</code> (Default — schnell, gut),
|
||
<code>claude-opus-4</code> (langsam, smarter), <code>claude-haiku-4-5</code> (sehr schnell, kleiner Kontext).
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
||
<span style="font-size:12px;color:#8888AA;white-space:nowrap;">Aktives Model:</span>
|
||
<input type="text" id="setting-model" placeholder="claude-sonnet-4" style="flex:1;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:6px 8px;color:#E0E0F0;font-family:inherit;font-size:12px;">
|
||
<button class="btn secondary" onclick="loadModel()" style="padding:4px 8px;font-size:10px;">Laden</button>
|
||
<button class="btn" onclick="saveModel()" style="padding:4px 8px;font-size:10px;">Setzen</button>
|
||
</div>
|
||
<div id="model-status" style="font-size:10px;color:#8888AA;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- OpenClaw-Config-Sektion entfernt — aria-core ist raus. -->
|
||
|
||
</div><!-- /tab-settings -->
|
||
|
||
<!-- ══════ TAB: Gehirn ══════ -->
|
||
<div id="tab-brain" class="main-tab">
|
||
|
||
<div class="settings-section">
|
||
<h2>Gehirn — Status</h2>
|
||
<div class="card">
|
||
<div id="brain-status" style="font-size:12px;color:#8888AA;margin-bottom:8px;">(Lade...)</div>
|
||
<div id="conversation-status" style="font-size:12px;color:#8888AA;margin-bottom:8px;"></div>
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap;">
|
||
<button class="btn secondary" onclick="loadBrainStatus()" style="padding:4px 12px;font-size:11px;">Aktualisieren</button>
|
||
<button class="btn secondary" onclick="distillNow()" style="padding:4px 12px;font-size:11px;color:#FFD60A;border-color:#FFD60A;" title="Destilliert die aeltesten Turns sofort zu fact-Memories">⚗ Jetzt destillieren</button>
|
||
<button class="btn secondary" onclick="resetConversation()" style="padding:4px 12px;font-size:11px;color:#FF6B6B;border-color:#FF6B6B;" title="Rolling-Window leeren — destillierte Facts bleiben in der DB">🧹 Konversation leeren</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-section">
|
||
<h2>Bootstrap & Migration</h2>
|
||
<div class="card" style="line-height:1.6;">
|
||
<p style="color:#8888AA;font-size:12px;margin:0 0 12px;">
|
||
Drei Wege ARIA mit "Grundregeln" zu füttern — von leichtgewichtig bis Voll-Wiederherstellung.
|
||
</p>
|
||
|
||
<!-- 1. Migration aus brain-import/ -->
|
||
<div style="background:#0D0D1A;border-radius:6px;padding:10px 12px;margin-bottom:8px;">
|
||
<div style="color:#0096FF;font-weight:bold;font-size:12px;margin-bottom:4px;">1. Aus brain-import/ migrieren</div>
|
||
<div style="color:#8888AA;font-size:11px;margin-bottom:8px;">
|
||
Parst <code>AGENT.md</code>/<code>USER.md</code>/<code>TOOLING.md</code> aus dem Repo und schreibt sie
|
||
als atomare pinned Memory-Punkte (identity / rule / preference / tool / skill). Idempotent — Re-Run
|
||
ersetzt nur die Migration-Punkte, eigene Memories bleiben.
|
||
</div>
|
||
<div id="brain-import-files-status" style="color:#555570;font-size:11px;margin-bottom:6px;">(Brain-Status laden für Datei-Liste)</div>
|
||
<button class="btn" onclick="runMigration()" style="background:#0096FF22;border:1px solid #0096FF;color:#0096FF;">▶ Migration starten</button>
|
||
</div>
|
||
|
||
<!-- 2. Bootstrap Snapshot -->
|
||
<div style="background:#0D0D1A;border-radius:6px;padding:10px 12px;margin-bottom:8px;">
|
||
<div style="color:#FFD60A;font-weight:bold;font-size:12px;margin-bottom:4px;">2. Bootstrap-Snapshot (nur pinned)</div>
|
||
<div style="color:#8888AA;font-size:11px;margin-bottom:8px;">
|
||
Klein und schnell: <strong>nur</strong> die pinned Memories (Identität, Regeln, Präferenzen, Tools, Skills) als JSON.
|
||
Use-Case: Wipe → Bootstrap-Import → ARIA hat Persönlichkeit zurück, sonst leer.
|
||
Cold Memory (Konversations-Fakten) bleibt beim Import unangetastet.
|
||
</div>
|
||
<button class="btn secondary" onclick="exportBootstrap()" style="color:#FFD60A;border-color:#FFD60A;">⬇ Bootstrap exportieren (JSON)</button>
|
||
<input type="file" id="bootstrap-import-file" accept=".json,application/json" style="display:none" onchange="importBootstrap(event)">
|
||
<button class="btn secondary" onclick="document.getElementById('bootstrap-import-file').click()" style="color:#FFD60A;border-color:#FFD60A;">⬆ Bootstrap importieren</button>
|
||
<div id="bootstrap-status" style="margin-top:8px;font-size:11px;color:#8888AA;"></div>
|
||
</div>
|
||
|
||
<!-- 3. Komplettes Gehirn -->
|
||
<div style="background:#0D0D1A;border-radius:6px;padding:10px 12px;">
|
||
<div style="color:#FF6B6B;font-weight:bold;font-size:12px;margin-bottom:4px;">3. Komplettes Gehirn (alles)</div>
|
||
<div style="color:#8888AA;font-size:11px;margin-bottom:8px;">
|
||
Tar.gz mit Memories + Skills + Qdrant-DB. Brain + Qdrant werden kurz angehalten.
|
||
Import überschreibt ALLES — vorher exportieren wenn etwas erhalten bleiben soll.
|
||
</div>
|
||
<button class="btn" onclick="brainExport()">⬇ Gehirn exportieren (tar.gz)</button>
|
||
<input type="file" id="brain-import-file" accept=".tar.gz,.tgz,application/gzip" style="display:none" onchange="brainImport(event)">
|
||
<button class="btn secondary" onclick="document.getElementById('brain-import-file').click()" style="background:#3A1010;border-color:#FF6B6B;color:#FF6B6B;">⬆ Gehirn importieren</button>
|
||
<div id="brain-import-status" style="margin-top:8px;font-size:11px;color:#8888AA;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Alte Sessions-Sicherung entfernt — aria-core ist raus. -->
|
||
|
||
|
||
<div class="settings-section">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
||
<h2 style="margin:0;">Memories</h2>
|
||
<div>
|
||
<button class="btn secondary" onclick="resetBrainFilters();loadBrainMemoryList()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
|
||
<button class="btn" onclick="openMemoryModal()" style="padding:4px 10px;font-size:11px;">+ Neu</button>
|
||
</div>
|
||
</div>
|
||
<div class="card" style="margin-bottom:8px;">
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
||
<input type="text" id="brain-search" placeholder="Semantische Suche (z.B. 'Stefan Persönlichkeit')..."
|
||
style="flex:1;min-width:200px;background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;"
|
||
onkeydown="if(event.key==='Enter') runBrainSearch()">
|
||
<button class="btn secondary" onclick="runBrainSearch()" style="padding:4px 12px;font-size:11px;">Suchen</button>
|
||
<select id="brain-filter-type" onchange="loadBrainMemoryList()"
|
||
style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
|
||
<option value="">Alle Typen</option>
|
||
<option value="identity">Identität</option>
|
||
<option value="rule">Regeln / Werte</option>
|
||
<option value="preference">Praeferenzen</option>
|
||
<option value="tool">Tools</option>
|
||
<option value="skill">Skills</option>
|
||
<option value="fact">Fakten</option>
|
||
<option value="conversation">Konversation</option>
|
||
<option value="reminder">Reminder</option>
|
||
</select>
|
||
<select id="brain-filter-pinned" onchange="loadBrainMemoryList()"
|
||
style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
|
||
<option value="all">Pinned + Cold</option>
|
||
<option value="pinned">📌 Nur Pinned</option>
|
||
<option value="cold">Nur Cold</option>
|
||
</select>
|
||
<button class="btn secondary" onclick="resetBrainFilters();loadBrainMemoryList()" style="padding:4px 8px;font-size:11px;color:#8888AA;" title="Suche + Filter zurücksetzen">✕</button>
|
||
</div>
|
||
<div id="brain-search-info" style="margin-top:6px;font-size:10px;color:#8888AA;display:none;"></div>
|
||
</div>
|
||
<div class="card">
|
||
<div id="brain-memory-list" style="font-size:12px;color:#8888AA;">(Brain-Container nicht erreichbar oder leer)</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /tab-brain -->
|
||
|
||
<!-- ══════ TAB: Dateien ══════ -->
|
||
<div id="tab-files" class="main-tab">
|
||
<div class="settings-section">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
||
<h2 style="margin:0;">Dateien</h2>
|
||
<button class="btn secondary" onclick="loadFiles()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
|
||
</div>
|
||
<div class="card" style="margin-bottom:8px;">
|
||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||
<input type="text" id="files-search" placeholder="Suche (Dateiname)..." oninput="renderFilesList()"
|
||
style="flex:1;min-width:200px;background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;">
|
||
<select id="files-filter" onchange="renderFilesList()" style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
|
||
<option value="all">Alle</option>
|
||
<option value="aria">Von ARIA (aria_*)</option>
|
||
<option value="user">Vom Benutzer</option>
|
||
</select>
|
||
</div>
|
||
<div id="files-info" style="margin-top:6px;font-size:10px;color:#8888AA;"></div>
|
||
</div>
|
||
<div class="card">
|
||
<div id="files-list" style="font-size:12px;color:#8888AA;">(Lade...)</div>
|
||
</div>
|
||
</div>
|
||
</div><!-- /tab-files -->
|
||
|
||
<!-- ══════ TAB: Skills ══════ -->
|
||
<div id="tab-skills" class="main-tab">
|
||
<div class="settings-section">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
||
<h2 style="margin:0;">Skills</h2>
|
||
<div style="display:flex;gap:6px;">
|
||
<button class="btn secondary" onclick="loadSkills()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
|
||
<input type="file" id="skill-import-file" accept=".tar.gz,.tgz,application/gzip" style="display:none" onchange="importSkillFile(event)">
|
||
<button class="btn secondary" onclick="document.getElementById('skill-import-file').click()" style="padding:4px 10px;font-size:11px;">⬆ Import</button>
|
||
</div>
|
||
</div>
|
||
<div class="card" style="margin-bottom:8px;">
|
||
<p style="color:#8888AA;font-size:12px;margin:0;">
|
||
Skills sind ARIAs Faehigkeiten. ARIA legt sie selbst an wenn sie ein wiederkehrendes
|
||
Problem geloest hat. Du kannst sie hier verwalten — ansehen, ausfuehren, deaktivieren,
|
||
exportieren, importieren. Ein deaktivierter Skill bleibt ARIA bekannt (sie baut ihn
|
||
nicht doppelt), kann aber nicht aufgerufen werden.
|
||
</p>
|
||
</div>
|
||
<div class="card">
|
||
<div id="skills-list" style="font-size:12px;color:#8888AA;">(Lade...)</div>
|
||
</div>
|
||
</div>
|
||
</div><!-- /tab-skills -->
|
||
|
||
<!-- Memory-Modal (Neu + Editieren) -->
|
||
<div class="modal-overlay" id="memory-modal">
|
||
<div class="modal-box" style="max-width:640px;">
|
||
<div class="modal-header">
|
||
<h3 id="memory-modal-title">Neue Memory</h3>
|
||
<button class="modal-close" onclick="closeMemoryModal()">×</button>
|
||
</div>
|
||
<div class="modal-body" style="padding:16px;">
|
||
<input type="hidden" id="memory-edit-id" value="">
|
||
<label style="display:block;font-size:11px;color:#8888AA;margin-bottom:4px;">Typ</label>
|
||
<select id="memory-type" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;margin-bottom:10px;">
|
||
<option value="identity">identity — Wer ARIA ist</option>
|
||
<option value="rule">rule — Sicherheit / Werte / Normen</option>
|
||
<option value="preference">preference — Benutzer-Praeferenzen</option>
|
||
<option value="tool">tool — Tool-Freigaben</option>
|
||
<option value="skill">skill — Faehigkeit / Workflow</option>
|
||
<option value="fact" selected>fact — Wissens-Fakt</option>
|
||
<option value="conversation">conversation — Aus Gespraech destilliert</option>
|
||
<option value="reminder">reminder — Termin / Aufgabe</option>
|
||
</select>
|
||
<label style="display:block;font-size:11px;color:#8888AA;margin-bottom:4px;">Titel</label>
|
||
<input type="text" id="memory-title" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;margin-bottom:10px;" placeholder="Kurze Ueberschrift">
|
||
<label style="display:block;font-size:11px;color:#8888AA;margin-bottom:4px;">Inhalt</label>
|
||
<textarea id="memory-content" rows="8" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;resize:vertical;margin-bottom:10px;" placeholder="Der eigentliche Text — das wird embedded und durchsucht."></textarea>
|
||
<label style="display:block;font-size:11px;color:#8888AA;margin-bottom:4px;">Kategorie (frei, optional)</label>
|
||
<input type="text" id="memory-category" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;margin-bottom:10px;" placeholder="z.B. persoenlichkeit, sicherheit, infrastruktur">
|
||
<label style="display:block;font-size:11px;color:#8888AA;margin-bottom:4px;">Tags (komma-getrennt)</label>
|
||
<input type="text" id="memory-tags" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;margin-bottom:10px;" placeholder="rvs, voice, bug">
|
||
<label style="display:flex;align-items:center;gap:8px;color:#E0E0F0;font-size:13px;cursor:pointer;">
|
||
<input type="checkbox" id="memory-pinned">
|
||
<span>📌 Pinned (Hot Memory — IMMER im System-Prompt)</span>
|
||
</label>
|
||
<div id="memory-modal-error" style="color:#FF6B6B;font-size:11px;margin-top:10px;display:none;"></div>
|
||
</div>
|
||
<div class="modal-footer" style="padding:10px 16px;border-top:1px solid #1E1E2E;display:flex;justify-content:flex-end;gap:8px;">
|
||
<button class="btn secondary" onclick="closeMemoryModal()">Abbrechen</button>
|
||
<button class="btn" onclick="saveMemory()">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Terminal Modal -->
|
||
<div class="modal-overlay" id="term-modal">
|
||
<div class="modal-box">
|
||
<div class="modal-header">
|
||
<h3>Claude Login Terminal</h3>
|
||
<button class="modal-close" onclick="closeTermModal()">×</button>
|
||
</div>
|
||
<div class="modal-body" id="terminal-container"></div>
|
||
<div class="modal-footer" id="term-status"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Lightbox fuer Medien -->
|
||
<div class="lightbox-overlay" id="lightbox" onclick="closeLightbox()"></div>
|
||
|
||
<script>
|
||
const chatBox = document.getElementById('chat-box');
|
||
const pauseHint = document.getElementById('pause-hint');
|
||
const btnScroll = document.getElementById('btn-scroll');
|
||
let ws;
|
||
let activeTab = 'all';
|
||
const DOCKER_TABS = ['gateway', 'proxy', 'bridge'];
|
||
const autoScroll = { all: true, gateway: true, rvs: true, proxy: true, bridge: true, server: true, trace: true };
|
||
const logCounts = { all: 0, gateway: 0, rvs: 0, proxy: 0, bridge: 0, server: 0, trace: 0 };
|
||
|
||
const logBoxes = {
|
||
all: document.getElementById('log-all'),
|
||
gateway: document.getElementById('log-gateway'),
|
||
rvs: document.getElementById('log-rvs'),
|
||
proxy: document.getElementById('log-proxy'),
|
||
bridge: document.getElementById('log-bridge'),
|
||
server: document.getElementById('log-server'),
|
||
trace: document.getElementById('log-trace'),
|
||
};
|
||
|
||
// Scroll-Pause pro aktivem Tab
|
||
Object.entries(logBoxes).forEach(([tab, box]) => {
|
||
box.addEventListener('scroll', () => {
|
||
const atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 30;
|
||
autoScroll[tab] = atBottom;
|
||
if (tab === activeTab) {
|
||
pauseHint.classList.toggle('visible', !atBottom);
|
||
btnScroll.style.display = atBottom ? 'none' : 'inline';
|
||
}
|
||
});
|
||
});
|
||
|
||
function resumeScroll() {
|
||
autoScroll[activeTab] = true;
|
||
const box = logBoxes[activeTab];
|
||
box.scrollTop = box.scrollHeight;
|
||
pauseHint.classList.remove('visible');
|
||
btnScroll.style.display = 'none';
|
||
}
|
||
|
||
function switchTab(tab) {
|
||
activeTab = tab;
|
||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
|
||
Object.entries(logBoxes).forEach(([t, box]) => box.classList.toggle('hidden', t !== tab));
|
||
// Docker Logs Button nur bei Tabs mit Containern
|
||
document.getElementById('btn-docker-logs').style.display = DOCKER_TABS.includes(tab) ? 'inline' : 'none';
|
||
// Pause-Hinweis aktualisieren
|
||
const box = logBoxes[tab];
|
||
const atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 30;
|
||
pauseHint.classList.toggle('visible', !atBottom);
|
||
btnScroll.style.display = atBottom ? 'none' : 'inline';
|
||
if (autoScroll[tab]) box.scrollTop = box.scrollHeight;
|
||
}
|
||
|
||
function updateCount(source) {
|
||
logCounts.all++;
|
||
document.getElementById('count-all').textContent = logCounts.all;
|
||
// source → tab mapping
|
||
const tab = mapSourceToTab(source);
|
||
if (tab && logCounts[tab] !== undefined) {
|
||
logCounts[tab]++;
|
||
document.getElementById(`count-${tab}`).textContent = logCounts[tab];
|
||
}
|
||
}
|
||
|
||
function mapSourceToTab(source) {
|
||
if (source === 'gateway') return 'gateway';
|
||
if (source === 'rvs') return 'rvs';
|
||
if (source === 'proxy') return 'proxy';
|
||
if (source === 'bridge') return 'bridge';
|
||
if (source === 'server' || source === 'browser') return 'server';
|
||
if (source === 'trace') return 'trace';
|
||
return null;
|
||
}
|
||
|
||
const STATUS_LABELS = {
|
||
connected: 'Verbunden',
|
||
connecting: 'Verbinde...',
|
||
testing: 'Teste...',
|
||
disconnected: 'Getrennt',
|
||
error: 'Fehler',
|
||
not_configured: 'Nicht konfiguriert',
|
||
unknown: 'Nicht getestet',
|
||
};
|
||
|
||
function connectWS() {
|
||
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
||
ws = new WebSocket(`${proto}://${location.host}`);
|
||
|
||
ws.onopen = () => {
|
||
addLog('info', 'browser', 'Verbunden mit Diagnostic Server');
|
||
send({ action: 'load_chat_history' });
|
||
};
|
||
ws.onclose = () => {
|
||
addLog('warn', 'browser', 'Verbindung zum Diagnostic Server verloren — Reconnect in 2s');
|
||
setTimeout(connectWS, 2000);
|
||
};
|
||
|
||
ws.onmessage = (e) => {
|
||
const msg = JSON.parse(e.data);
|
||
|
||
if (msg.type === 'init') {
|
||
updateState(msg.state);
|
||
msg.logs.forEach(entry => addLog(entry.level, entry.source, entry.message, entry.ts));
|
||
return;
|
||
}
|
||
|
||
if (msg.type === 'state') { updateState(msg.state); return; }
|
||
if (msg.type === 'log') { addLog(msg.entry.level, msg.entry.source, msg.entry.message, msg.entry.ts); return; }
|
||
|
||
if (msg.type === 'agent_activity') {
|
||
updateThinkingIndicator(msg);
|
||
return;
|
||
}
|
||
|
||
if (msg.type === 'disk_status') {
|
||
updateDiskBanner(msg);
|
||
return;
|
||
}
|
||
|
||
if (msg.type === 'mode' && msg.payload) {
|
||
// Bridge hat den Modus geaendert (evtl. von anderer App/Diagnostic) — UI syncen
|
||
const mode = (msg.payload.mode || '').toLowerCase();
|
||
if (MODE_LABELS[mode]) {
|
||
updateModeUI(mode);
|
||
log("info", "server", `Mode-Sync: ${mode}`);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (msg.type === 'xtts_voices_list') {
|
||
const select = document.getElementById('diag-xtts-voice');
|
||
// Aktuelle Auswahl merken damit Rebuild sie nicht zerstoert
|
||
const previouslySelected = select.value;
|
||
while (select.options.length > 1) select.remove(1);
|
||
const voices = msg.payload?.voices || [];
|
||
for (const v of voices) {
|
||
const opt = document.createElement('option');
|
||
opt.value = v.name;
|
||
opt.textContent = `${v.name} (${(v.size / 1024).toFixed(0)}KB)`;
|
||
select.appendChild(opt);
|
||
}
|
||
// Wenn die vorherige Auswahl weiter existiert → wiederherstellen
|
||
if (previouslySelected && voices.some(v => v.name === previouslySelected)) {
|
||
select.value = previouslySelected;
|
||
}
|
||
document.getElementById('xtts-status').textContent = `XTTS: ${voices.length} Stimme(n) verfuegbar`;
|
||
document.getElementById('xtts-status').style.color = '#34C759';
|
||
renderVoiceList(voices);
|
||
return;
|
||
}
|
||
if (msg.type === 'xtts_voice_saved') {
|
||
document.getElementById('xtts-clone-status').textContent = `Stimme "${msg.payload?.name}" gespeichert!`;
|
||
document.getElementById('xtts-clone-status').style.color = '#34C759';
|
||
loadXTTSVoices(); // Liste neu laden
|
||
return;
|
||
}
|
||
if (msg.type === 'xtts_voice_exported') {
|
||
const p = msg.payload || {};
|
||
const status = document.getElementById('voice-status');
|
||
if (!p.ok || !p.data) {
|
||
if (status) status.textContent = '✗ Export fehlgeschlagen: ' + (p.error || 'unbekannt');
|
||
return;
|
||
}
|
||
// base64 → Blob → Download
|
||
try {
|
||
const bin = atob(p.data);
|
||
const bytes = new Uint8Array(bin.length);
|
||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||
const blob = new Blob([bytes], { type: 'application/gzip' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = (p.name || 'voice') + '.tar.gz';
|
||
document.body.appendChild(a); a.click();
|
||
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
|
||
if (status) status.textContent = '✓ ' + p.name + ' exportiert (' + (bytes.length/1024).toFixed(0) + ' KB)';
|
||
} catch (e) {
|
||
if (status) status.textContent = '✗ Decode fehlgeschlagen: ' + e.message;
|
||
}
|
||
return;
|
||
}
|
||
if (msg.type === 'xtts_voice_imported') {
|
||
const p = msg.payload || {};
|
||
const status = document.getElementById('voice-status');
|
||
if (p.ok) {
|
||
if (status) status.textContent = '✓ Stimme "' + p.name + '" importiert';
|
||
loadXTTSVoices();
|
||
} else {
|
||
if (status) status.textContent = '✗ Import fehlgeschlagen: ' + (p.error || 'unbekannt');
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (msg.type === 'voice_config') {
|
||
document.getElementById('diag-tts-enabled').checked = msg.ttsEnabled !== false;
|
||
// XTTS-Voice setzen — Option hinzufuegen falls nicht vorhanden
|
||
const xttsSelect = document.getElementById('diag-xtts-voice');
|
||
const xttsVoice = msg.xttsVoice || '';
|
||
if (xttsVoice && !Array.from(xttsSelect.options).some(o => o.value === xttsVoice)) {
|
||
const opt = document.createElement('option');
|
||
opt.value = xttsVoice;
|
||
opt.textContent = xttsVoice;
|
||
xttsSelect.appendChild(opt);
|
||
}
|
||
xttsSelect.value = xttsVoice;
|
||
// Whisper-Modell wiederherstellen (falls gesetzt)
|
||
if (msg.whisperModel) {
|
||
const wSel = document.getElementById('diag-whisper-model');
|
||
if (wSel) wSel.value = msg.whisperModel;
|
||
}
|
||
// F5-TTS Tuning-Felder wiederherstellen (falls gesetzt)
|
||
const setIfPresent = (id, val) => {
|
||
const el = document.getElementById(id);
|
||
if (el && val !== undefined && val !== null && val !== '') el.value = val;
|
||
};
|
||
setIfPresent('diag-f5tts-model', msg.f5ttsModel);
|
||
setIfPresent('diag-f5tts-ckpt', msg.f5ttsCkptFile);
|
||
setIfPresent('diag-f5tts-vocab', msg.f5ttsVocabFile);
|
||
setIfPresent('diag-f5tts-cfg', msg.f5ttsCfgStrength);
|
||
setIfPresent('diag-f5tts-nfe', msg.f5ttsNfeStep);
|
||
return;
|
||
}
|
||
|
||
if (msg.type === 'service_status') {
|
||
updateServiceStatus(msg.payload || {});
|
||
return;
|
||
}
|
||
|
||
if (msg.type === 'voice_preview_audio') {
|
||
const statusEl = document.getElementById('voice-preview-status');
|
||
const audio = document.getElementById('voice-preview-audio');
|
||
const playBtn = document.getElementById('voice-preview-play');
|
||
if (playBtn) playBtn.disabled = false;
|
||
if (msg.error) {
|
||
if (statusEl) statusEl.textContent = '❌ Fehler: ' + msg.error;
|
||
return;
|
||
}
|
||
if (msg.base64 && audio) {
|
||
audio.src = 'data:audio/wav;base64,' + msg.base64;
|
||
audio.style.display = 'block';
|
||
audio.play().catch(() => {});
|
||
if (statusEl) statusEl.textContent = '✅ fertig';
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (msg.type === 'voice_ready') {
|
||
const v = msg.payload?.voice || '';
|
||
const err = msg.payload?.error;
|
||
const ms = msg.payload?.loadMs;
|
||
const statusEl = document.getElementById('voice-status');
|
||
if (statusEl) {
|
||
if (err) {
|
||
statusEl.textContent = `⚠️ Stimme "${v}" Fehler: ${err}`;
|
||
statusEl.style.color = '#FF3B30';
|
||
} else {
|
||
statusEl.textContent = `✅ Stimme "${v || 'Standard'}" bereit${ms ? ` (${(ms/1000).toFixed(1)}s)` : ''}`;
|
||
statusEl.style.color = '#34C759';
|
||
}
|
||
setTimeout(() => { if (statusEl) statusEl.textContent = ''; }, 5000);
|
||
}
|
||
addLog('info', 'xtts', err ? `Voice "${v}": ${err}` : `Voice "${v || 'Standard'}" bereit`);
|
||
return;
|
||
}
|
||
|
||
if (msg.type === 'watchdog') {
|
||
const colors = { warning: '#FFD60A', fixing: '#FF9500', fixed: '#34C759', error: '#FF3B30' };
|
||
const color = colors[msg.status] || '#FFD60A';
|
||
addChat('error', `\u26A0\uFE0F Watchdog: ${msg.message}`, `system — ${msg.status}`);
|
||
addLog('warn', 'server', `Watchdog: ${msg.message}`);
|
||
return;
|
||
}
|
||
|
||
if (msg.type === 'chat_final') {
|
||
addChat('received', msg.text || '', 'chat:final');
|
||
return;
|
||
}
|
||
if (msg.type === 'file_from_aria') {
|
||
const p = msg.payload || {};
|
||
addAriaFile(p);
|
||
return;
|
||
}
|
||
if (msg.type === 'file_deleted') {
|
||
if (msg.path) markFileDeletedInChat(msg.path);
|
||
// Falls Datei-Manager grade offen ist: Liste refreshen
|
||
if (typeof loadFiles === 'function' && document.getElementById('tab-files') && document.getElementById('tab-files').classList.contains('visible')) {
|
||
loadFiles();
|
||
}
|
||
return;
|
||
}
|
||
if (msg.type === 'skill_created') {
|
||
addSkillCreatedBubble(msg.payload || {});
|
||
// Falls Skills-Tab offen: refreshen
|
||
if (document.getElementById('tab-skills') && document.getElementById('tab-skills').classList.contains('visible')) {
|
||
loadSkills();
|
||
}
|
||
return;
|
||
}
|
||
if (msg.type === 'chat_delta') { return; }
|
||
if (msg.type === 'chat_error') {
|
||
addChat('error', msg.error, 'chat:error');
|
||
return;
|
||
}
|
||
if (msg.type === 'rvs_chat') {
|
||
const p = msg.msg.payload || {};
|
||
const sender = p.sender || '?';
|
||
// ARIA-Antworten kommen schon via Gateway (chat:final) — nicht nochmal via RVS anzeigen
|
||
if (sender === 'aria') return;
|
||
const chatType = 'sent';
|
||
const label = sender === 'stt' ? '\uD83C\uDFA4 Spracheingabe' : `via RVS (${sender})`;
|
||
addChat(chatType, p.text || '?', label, { location: p.location });
|
||
return;
|
||
}
|
||
if (msg.type === 'proxy_result') {
|
||
if (msg.ok) {
|
||
addChat('received', msg.reply, 'Claude Proxy direkt');
|
||
} else {
|
||
addChat('error', msg.error, 'Claude Proxy Fehler');
|
||
}
|
||
if (msg.models && msg.models.length) showProxyModels(msg.models);
|
||
return;
|
||
}
|
||
if (msg.type === 'proxy_auth') {
|
||
const el = document.getElementById('proxy-auth');
|
||
el.style.display = 'block';
|
||
el.textContent = msg.error ? `Fehler: ${msg.error}` : msg.info;
|
||
return;
|
||
}
|
||
// core_auth WS-Event entfernt — aria-core ist raus.
|
||
// Live SSH + Desktop
|
||
if (msg.type?.startsWith('live_ssh_')) { handleLiveSSH(msg); return; }
|
||
if (msg.type === 'desktop_status') { handleDesktop(msg); return; }
|
||
|
||
if (msg.type === 'term_ready') {
|
||
document.getElementById('term-status').textContent = 'Verbunden — interaktives Terminal';
|
||
if (term) term.writeln('\x1b[32mVerbunden!\x1b[0m\r\n');
|
||
return;
|
||
}
|
||
if (msg.type === 'term_data') {
|
||
if (term) {
|
||
// base64 → Uint8Array (UTF-8 safe)
|
||
const raw = atob(msg.data);
|
||
const bytes = new Uint8Array(raw.length);
|
||
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
|
||
term.write(bytes);
|
||
}
|
||
return;
|
||
}
|
||
if (msg.type === 'term_exit') {
|
||
if (term) term.writeln('\r\n\x1b[33m--- Session beendet ---\x1b[0m');
|
||
document.getElementById('term-status').textContent = 'Session beendet — Fenster schliessen oder erneut einloggen';
|
||
return;
|
||
}
|
||
if (msg.type === 'term_error') {
|
||
if (term) term.writeln('\r\n\x1b[31mFehler: ' + msg.error + '\x1b[0m');
|
||
document.getElementById('btn-proxy-login').disabled = false;
|
||
return;
|
||
}
|
||
if (msg.type === 'login_status') {
|
||
if (msg.status === 'done') {
|
||
const cs = document.getElementById('creds-status');
|
||
if (cs) { cs.textContent = 'Erfolgreich!'; cs.style.color = '#34C759'; }
|
||
} else if (msg.status === 'error') {
|
||
const cs = document.getElementById('creds-status');
|
||
if (cs) { cs.textContent = msg.error; cs.style.color = '#FF6B6B'; }
|
||
}
|
||
return;
|
||
}
|
||
if (msg.type === 'docker_logs') {
|
||
showDockerLogs(msg);
|
||
return;
|
||
}
|
||
// Chat-History (nach F5 / Reconnect)
|
||
if (msg.type === 'chat_history') {
|
||
chatBox.innerHTML = '';
|
||
if (msg.messages && msg.messages.length > 0) {
|
||
for (const m of msg.messages) {
|
||
if (m.type === 'aria_file') {
|
||
// ARIA-Datei-Bubble rekonstruieren (statt addAriaFile damit
|
||
// kein Auto-Scroll-Race waehrend des Bulk-Loads)
|
||
addAriaFile({ serverPath: m.serverPath, name: m.name, mimeType: m.mimeType, size: m.size });
|
||
continue;
|
||
}
|
||
const el = document.createElement('div');
|
||
el.className = `chat-msg ${m.type}`;
|
||
// [FILE: ...]-Marker rausfiltern (gleicher Filter wie addChat)
|
||
const cleaned = (m.text || '').replace(/\[FILE:\s*\/shared\/uploads\/[^\]]+\]/gi, '').replace(/\n{3,}/g, '\n\n').trim();
|
||
const escaped = escapeHtml(cleaned);
|
||
let linked = linkifyText(escaped);
|
||
// /shared/uploads/-Bildpfade auch im History inline rendern
|
||
// (gleicher Replace wie in addChat — sonst sieht man nach F5 nur Text-Pfade)
|
||
linked = linked.replace(/\/shared\/uploads\/[^\s<"]+\.(jpg|jpeg|png|gif|webp|svg|bmp)/gi, (match) => {
|
||
return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`;
|
||
});
|
||
const time = m.ts ? new Date(m.ts).toLocaleTimeString('de-DE') : '?';
|
||
el.innerHTML = `${linked}<div class="meta">${escapeHtml(m.meta)} — ${time}</div>`;
|
||
chatBox.appendChild(el);
|
||
}
|
||
chatBox.scrollTop = chatBox.scrollHeight;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// session_restarted / openclaw_config WS-Events entfernt — aria-core ist raus.
|
||
if (msg.type === 'model_info') {
|
||
const el = document.getElementById('setting-model');
|
||
const st = document.getElementById('model-status');
|
||
if (el && msg.model) el.value = msg.model;
|
||
if (st) {
|
||
st.textContent = msg.info || msg.error || '';
|
||
st.style.color = msg.error ? '#FF6B6B' : '#34C759';
|
||
}
|
||
return;
|
||
}
|
||
if (msg.type === 'response') { return; }
|
||
};
|
||
}
|
||
|
||
function send(obj) {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify(obj));
|
||
}
|
||
}
|
||
|
||
function sendDiagAttachments() {
|
||
// Alle pending Dateien an RVS senden
|
||
for (const f of diagPendingFiles) {
|
||
send({ action: 'send_file', name: f.name, type: f.type, size: f.size, base64: f.base64 });
|
||
}
|
||
if (diagPendingFiles.length > 0) {
|
||
addChat('sent', `${diagPendingFiles.length} Anhang/Anhaenge`, 'Datei');
|
||
}
|
||
diagPendingFiles = [];
|
||
renderDiagPending();
|
||
}
|
||
|
||
function testGateway() {
|
||
const input = document.getElementById('chat-input');
|
||
const text = input.value.trim();
|
||
if (!text && diagPendingFiles.length === 0) return;
|
||
if (diagPendingFiles.length > 0) sendDiagAttachments();
|
||
if (text) {
|
||
addChat('sent', text, 'Gateway direkt');
|
||
send({ action: 'test_gateway', text });
|
||
}
|
||
input.value = '';
|
||
}
|
||
|
||
function testRVS() {
|
||
const input = document.getElementById('chat-input');
|
||
const text = input.value.trim();
|
||
if (!text && diagPendingFiles.length === 0) return;
|
||
if (diagPendingFiles.length > 0) sendDiagAttachments();
|
||
if (text) {
|
||
addChat('sent', text, 'via RVS');
|
||
send({ action: 'test_rvs', text });
|
||
}
|
||
input.value = '';
|
||
}
|
||
|
||
function testProxyBtn() {
|
||
send({ action: 'test_proxy', text: 'Antworte mit genau einem Wort: Ping' });
|
||
}
|
||
|
||
function checkProxyAuth() {
|
||
send({ action: 'check_proxy_auth' });
|
||
}
|
||
|
||
let term = null;
|
||
let termFitAddon = null;
|
||
let termAction = null; // Welche Aktion beim Terminal-Start gesendet wird
|
||
|
||
function openTermModal(title, action) {
|
||
termAction = action;
|
||
document.querySelector('#term-modal .modal-header h3').textContent = title;
|
||
document.getElementById('term-modal').classList.add('open');
|
||
document.getElementById('term-status').textContent = 'Starte Terminal...';
|
||
|
||
if (typeof Terminal === 'undefined') {
|
||
const s = document.createElement('script');
|
||
s.src = 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js';
|
||
s.onload = () => {
|
||
const s2 = document.createElement('script');
|
||
s2.src = 'https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js';
|
||
s2.onload = () => initTerminal();
|
||
document.head.appendChild(s2);
|
||
};
|
||
document.head.appendChild(s);
|
||
} else {
|
||
initTerminal();
|
||
}
|
||
}
|
||
|
||
function startProxyLogin() {
|
||
document.getElementById('btn-proxy-login').disabled = true;
|
||
openTermModal('Claude Login Terminal (aria-proxy)', { action: 'proxy_login' });
|
||
}
|
||
|
||
// openCoreTerminal entfernt — aria-core ist raus.
|
||
|
||
function closeTermModal() {
|
||
document.getElementById('term-modal').classList.remove('open');
|
||
const proxyBtn = document.getElementById('btn-proxy-login');
|
||
if (proxyBtn) proxyBtn.disabled = false;
|
||
// Terminal aufraeumen
|
||
if (term) { term.dispose(); term = null; }
|
||
}
|
||
|
||
function initTerminal() {
|
||
const container = document.getElementById('terminal-container');
|
||
container.innerHTML = '';
|
||
|
||
term = new Terminal({
|
||
cursorBlink: true,
|
||
fontSize: 14,
|
||
fontFamily: "'DejaVu Sans Mono', 'Courier New', monospace",
|
||
theme: { background: '#000000', foreground: '#E0E0F0' },
|
||
});
|
||
|
||
if (window.FitAddon) {
|
||
termFitAddon = new FitAddon.FitAddon();
|
||
term.loadAddon(termFitAddon);
|
||
term.open(container);
|
||
termFitAddon.fit();
|
||
} else {
|
||
term.open(container);
|
||
}
|
||
|
||
// Tastatureingabe → Server (UTF-8 safe)
|
||
term.onData((data) => {
|
||
const encoder = new TextEncoder();
|
||
const bytes = encoder.encode(data);
|
||
let b64 = '';
|
||
for (let i = 0; i < bytes.length; i++) b64 += String.fromCharCode(bytes[i]);
|
||
send({ action: 'term_input', data: btoa(b64) });
|
||
});
|
||
|
||
const containerName = termAction?.action === 'core_terminal' ? 'aria-core' : 'aria-proxy';
|
||
term.writeln('\x1b[33mVerbinde mit ' + containerName + '...\x1b[0m');
|
||
send(termAction);
|
||
}
|
||
|
||
// Resize bei Fensteraenderung
|
||
window.addEventListener('resize', () => { if (termFitAddon && term) termFitAddon.fit(); });
|
||
|
||
function toggleCredsBox() {
|
||
const box = document.getElementById('proxy-creds-box');
|
||
box.style.display = box.style.display === 'none' ? 'block' : 'none';
|
||
}
|
||
|
||
function submitCredentials() {
|
||
const input = document.getElementById('creds-input').value.trim();
|
||
const status = document.getElementById('creds-status');
|
||
if (!input) { status.textContent = 'Bitte JSON einfuegen'; status.style.color = '#FF6B6B'; return; }
|
||
try {
|
||
JSON.parse(input);
|
||
} catch (e) {
|
||
status.textContent = 'Ungueltig: Kein gueltiges JSON'; status.style.color = '#FF6B6B'; return;
|
||
}
|
||
status.textContent = 'Speichere...'; status.style.color = '#FFD60A';
|
||
send({ action: 'write_credentials', credentials: input });
|
||
}
|
||
|
||
function loadDockerLogs() {
|
||
if (!DOCKER_TABS.includes(activeTab)) return;
|
||
send({ action: 'docker_logs', tab: activeTab, tail: 200 });
|
||
}
|
||
|
||
function updateState(state) {
|
||
// Brain-Status (loest die alte gw-* Gateway-Card ab — wenn loadBrainStatus
|
||
// gleich danach laeuft, kriegt es die korrekten Werte; hier nur Default)
|
||
const brainDot = document.getElementById('brain-dot');
|
||
const brainShort = document.getElementById('brain-status-short');
|
||
const brainErr = document.getElementById('brain-error');
|
||
const gw = state.gateway || {};
|
||
if (brainDot) brainDot.className = `dot ${gw.status === 'connected' ? 'connected' : 'disconnected'}`;
|
||
if (brainShort) brainShort.textContent = gw.status === 'disabled' ? 'aria-core entfernt — Brain-Loop' : (STATUS_LABELS[gw.status] || gw.status || '-');
|
||
if (brainErr) brainErr.textContent = gw.lastError || '';
|
||
|
||
// RVS
|
||
const rvs = state.rvs || {};
|
||
document.getElementById('rvs-dot').className = `dot ${rvs.status || 'disconnected'}`;
|
||
document.getElementById('rvs-status').textContent = STATUS_LABELS[rvs.status] || rvs.status;
|
||
document.getElementById('rvs-error').textContent = rvs.lastError || '';
|
||
|
||
// Proxy
|
||
const proxy = state.proxy || {};
|
||
document.getElementById('proxy-dot').className = `dot ${proxy.status || 'unknown'}`;
|
||
document.getElementById('proxy-status').textContent = STATUS_LABELS[proxy.status] || proxy.status;
|
||
document.getElementById('proxy-error').textContent = proxy.lastError || '';
|
||
document.getElementById('btn-proxy-test').disabled = proxy.status === 'testing';
|
||
if (proxy.models && proxy.models.length) showProxyModels(proxy.models);
|
||
|
||
// Buttons
|
||
document.getElementById('btn-gw').disabled = gw.status !== 'connected';
|
||
document.getElementById('btn-rvs').disabled = rvs.status !== 'connected';
|
||
}
|
||
|
||
function addLog(level, source, message, ts) {
|
||
const time = ts ? new Date(ts).toLocaleTimeString('de-DE') : new Date().toLocaleTimeString('de-DE');
|
||
const line = `${time} [${source}] ${message}`;
|
||
|
||
// Trace-Eintraege nur in Trace-Tab (nicht in Alle)
|
||
if (source === 'trace') {
|
||
const pipeLevel = level === 'error' ? 'trace-err' : level === 'info' && message.includes('>>>') ? 'trace-ok' : 'trace-step';
|
||
appendToLog('trace', pipeLevel, `${time} ${message}`);
|
||
logCounts.trace++;
|
||
document.getElementById('count-trace').textContent = logCounts.trace;
|
||
return;
|
||
}
|
||
|
||
// In "Alle" und in den passenden Tab schreiben
|
||
appendToLog('all', level, line);
|
||
const tab = mapSourceToTab(source);
|
||
if (tab) appendToLog(tab, level, line);
|
||
|
||
updateCount(source);
|
||
}
|
||
|
||
function appendToLog(tab, level, line) {
|
||
const box = logBoxes[tab];
|
||
const el = document.createElement('div');
|
||
el.className = `log-entry ${level}`;
|
||
el.textContent = line;
|
||
box.appendChild(el);
|
||
if (autoScroll[tab]) box.scrollTop = box.scrollHeight;
|
||
}
|
||
|
||
const MEDIA_IMG = /\.(jpe?g|png|gif|webp|svg|bmp|ico)$/i;
|
||
const MEDIA_VID = /\.(mp4|webm|ogg|mov)$/i;
|
||
const FILE_PDF = /\.pdf$/i;
|
||
|
||
function linkifyText(escaped) {
|
||
// URLs in escaped HTML erkennen und ersetzen
|
||
return escaped.replace(/(https?:\/\/[^\s<&]+)/g, function(url) {
|
||
const decoded = url.replace(/&/g, '&');
|
||
const pathname = new URL(decoded).pathname;
|
||
if (MEDIA_IMG.test(pathname)) {
|
||
return `<a href="${url}" target="_blank">${url}</a><img src="${url}" class="chat-media" onclick="event.stopPropagation();openLightbox('img','${url}')">`;
|
||
}
|
||
if (MEDIA_VID.test(pathname)) {
|
||
return `<a href="${url}" target="_blank">${url}</a><video src="${url}" class="chat-media" controls onclick="event.stopPropagation();openLightbox('video','${url}')"></video>`;
|
||
}
|
||
return `<a href="${url}" target="_blank" rel="noopener">${url}</a>`;
|
||
});
|
||
}
|
||
|
||
// Debug-Toggle: TTS-aufbereitete Variante unter ARIA-Nachrichten einblenden
|
||
let showTtsDebug = localStorage.getItem('aria-show-tts-debug') === '1';
|
||
function toggleTtsDebug() {
|
||
showTtsDebug = !showTtsDebug;
|
||
localStorage.setItem('aria-show-tts-debug', showTtsDebug ? '1' : '0');
|
||
const el = document.getElementById('tts-debug-toggle');
|
||
if (el) el.checked = showTtsDebug;
|
||
}
|
||
|
||
// Debug-Toggle: GPS-Position unter User-Nachrichten einblenden (nur Diagnostic).
|
||
// App zeigt's bewusst nicht — die Position geht nur an aria-core.
|
||
let showGpsDebug = localStorage.getItem('aria-show-gps-debug') === '1';
|
||
function toggleGpsDebug() {
|
||
showGpsDebug = !showGpsDebug;
|
||
localStorage.setItem('aria-show-gps-debug', showGpsDebug ? '1' : '0');
|
||
const el = document.getElementById('gps-debug-toggle');
|
||
if (el) el.checked = showGpsDebug;
|
||
}
|
||
|
||
// Minimal-JS-Port von clean_text_for_tts() (Bridge) — reine Anzeige
|
||
function previewTtsText(text) {
|
||
if (!text) return '';
|
||
// <voice>...</voice>
|
||
const vm = text.match(/<voice>([\s\S]*?)<\/voice>/i);
|
||
if (vm) text = vm[1];
|
||
let t = text;
|
||
t = t.replace(/```[\s\S]*?```/g, '. ');
|
||
t = t.replace(/`[^`]+`/g, '');
|
||
t = t.replace(/\*\*([^*]+)\*\*/g, '$1');
|
||
t = t.replace(/\*([^*]+)\*/g, '$1');
|
||
t = t.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
||
t = t.replace(/https?:\/\/\S+/g, 'ein Link');
|
||
t = t.replace(/^#{1,6}\s*/gm, '');
|
||
t = t.replace(/^>\s*/gm, '');
|
||
t = t.replace(/^[\-\*]\s+/gm, '');
|
||
t = t.replace(/(\d+)GB\b/g, '$1 Gigabyte');
|
||
t = t.replace(/(\d+)MB\b/g, '$1 Megabyte');
|
||
t = t.replace(/%/g, ' Prozent');
|
||
t = t.replace(/\bCPU\b/g, 'C P U').replace(/\bAPI\b/g, 'A P I').replace(/\bRAM\b/g, 'R A M');
|
||
t = t.replace(/\n{2,}/g, '. ').replace(/\n/g, ', ').replace(/\s{2,}/g, ' ');
|
||
return t.trim();
|
||
}
|
||
|
||
function addChat(type, text, meta, options) {
|
||
// [FILE: /shared/uploads/aria_xxx.ext]-Marker aus dem Antworttext entfernen —
|
||
// die Datei kommt separat via file_from_aria-Event als eigene Bubble.
|
||
// /gi entfernt mehrere Marker, falls ARIA mehrere Dateien in einer Antwort liefert.
|
||
if (text) text = text.replace(/\[FILE:\s*\/shared\/uploads\/[^\]]+\]/gi, '').replace(/\n{3,}/g, '\n\n').trim();
|
||
const escaped = escapeHtml(text);
|
||
let linked = linkifyText(escaped);
|
||
// /shared/uploads/ Pfade als Inline-Bilder anzeigen
|
||
linked = linked.replace(/\/shared\/uploads\/[^\s<"]+\.(jpg|jpeg|png|gif)/gi, (match) => {
|
||
return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`;
|
||
});
|
||
// Optional: TTS-Variante als zusaetzliches Block unter der Nachricht
|
||
let ttsBlock = '';
|
||
if (showTtsDebug && type === 'received') {
|
||
const ttsText = (options && options.ttsText) || previewTtsText(text);
|
||
if (ttsText && ttsText !== text) {
|
||
ttsBlock = `<div style="margin-top:6px;padding:4px 8px;background:rgba(0,150,255,0.08);border-left:2px solid #0096FF;font-size:11px;color:#88AACC;"><span style="color:#0096FF;font-weight:bold;">TTS:</span> ${escapeHtml(ttsText)}</div>`;
|
||
}
|
||
}
|
||
// Optional: GPS-Position als Block unter User-Nachrichten (nur Diagnostic)
|
||
let gpsBlock = '';
|
||
if (showGpsDebug && options && options.location) {
|
||
const loc = options.location;
|
||
const lat = typeof loc.lat === 'number' ? loc.lat.toFixed(6) : '?';
|
||
const lon = typeof loc.lon === 'number' ? loc.lon.toFixed(6) : (typeof loc.lng === 'number' ? loc.lng.toFixed(6) : '?');
|
||
if (lat !== '?' && lon !== '?') {
|
||
const mapLink = `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=16/${lat}/${lon}`;
|
||
gpsBlock = `<div style="margin-top:6px;padding:4px 8px;background:rgba(52,199,89,0.08);border-left:2px solid #34C759;font-size:11px;color:#88BB99;"><span style="color:#34C759;font-weight:bold;">📍 GPS:</span> <a href="${mapLink}" target="_blank" rel="noopener" style="color:#88BB99;text-decoration:underline;">${lat}, ${lon}</a></div>`;
|
||
}
|
||
}
|
||
const html = `${linked}${ttsBlock}${gpsBlock}<div class="meta">${escapeHtml(meta)} — ${new Date().toLocaleTimeString('de-DE')}</div>`;
|
||
|
||
// Thinking-Indikator ausblenden bei neuer Nachricht
|
||
updateThinkingIndicator({ activity: 'idle' });
|
||
|
||
// In beide Chat-Boxen schreiben (normal + Vollbild)
|
||
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
|
||
if (!box) continue;
|
||
const el = document.createElement('div');
|
||
el.className = `chat-msg ${type}`;
|
||
el.innerHTML = html;
|
||
box.appendChild(el);
|
||
box.scrollTop = box.scrollHeight;
|
||
}
|
||
}
|
||
|
||
/** ARIA hat eine Datei rausgegeben — als eigene Bubble mit Klick-Handler. */
|
||
function addAriaFile(p) {
|
||
const name = p.name || 'datei';
|
||
const serverPath = p.serverPath || '';
|
||
const mimeType = p.mimeType || '';
|
||
const sizeKB = p.size ? Math.round(p.size / 1024) : 0;
|
||
const isImage = mimeType.startsWith('image/');
|
||
const isPdf = mimeType === 'application/pdf';
|
||
const url = serverPath; // Diagnostic-Server liefert /shared/* aus
|
||
const sizeStr = sizeKB > 1024 ? `${(sizeKB/1024).toFixed(1)}MB` : `${sizeKB}KB`;
|
||
const icon = isImage ? '🖼️' : isPdf ? '📄' : '📎';
|
||
const deleted = !!p.deleted;
|
||
let html;
|
||
if (deleted) {
|
||
html = `<div style="font-weight:bold;color:#FF9500;">${icon} ${escapeHtml(name)} — vom Benutzer gelöscht</div>` +
|
||
` <span style="color:#888;font-size:11px;">(${escapeHtml(mimeType)})</span>` +
|
||
`<div style="margin-top:4px;font-size:10px;color:#666;font-family:monospace;text-decoration:line-through;">${escapeHtml(serverPath)}</div>` +
|
||
`<div class="meta">ARIA-Datei (gelöscht)</div>`;
|
||
} else {
|
||
const linkAttrs = (isImage || isPdf)
|
||
? `href="${url}" target="_blank" rel="noopener"`
|
||
: `href="${url}" download="${escapeHtml(name)}"`;
|
||
let preview = '';
|
||
if (isImage) {
|
||
preview = `<img src="${url}" class="chat-media" onclick="openLightbox('image','${url}')" onerror="this.style.display='none'" style="margin-top:6px;">`;
|
||
}
|
||
html = `<div style="font-weight:bold;">${icon} ARIA hat eine Datei erstellt</div>` +
|
||
`<a ${linkAttrs} style="color:#0096FF;text-decoration:underline;">${escapeHtml(name)}</a>` +
|
||
` <span style="color:#888;font-size:11px;">(${escapeHtml(mimeType)}, ${sizeStr})</span>` +
|
||
preview +
|
||
`<div style="margin-top:4px;font-size:10px;color:#666;font-family:monospace;">${escapeHtml(serverPath)}</div>` +
|
||
`<div class="meta">ARIA-Datei — ${new Date().toLocaleTimeString('de-DE')}</div>`;
|
||
}
|
||
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
|
||
if (!box) continue;
|
||
const el = document.createElement('div');
|
||
el.className = 'chat-msg received';
|
||
el.dataset.ariaFilePath = serverPath;
|
||
if (deleted) el.dataset.deleted = '1';
|
||
el.innerHTML = html;
|
||
box.appendChild(el);
|
||
box.scrollTop = box.scrollHeight;
|
||
}
|
||
}
|
||
|
||
/** ARIA hat einen Skill erstellt — als auffaellige Bubble anzeigen. */
|
||
function addSkillCreatedBubble(skill) {
|
||
const name = skill.name || '(unbenannt)';
|
||
const desc = skill.description || '(ohne Beschreibung)';
|
||
const execMode = skill.execution || 'bash';
|
||
const active = skill.active !== false;
|
||
const setupErr = skill.setup_error
|
||
? `<div style="color:#FF6B6B;font-size:11px;margin-top:4px;">⚠ Setup-Fehler: ${escapeHtml(skill.setup_error.slice(0,200))}</div>`
|
||
: '';
|
||
const statusBadge = active
|
||
? '<span style="color:#3FFF3F;font-size:11px;">aktiv</span>'
|
||
: '<span style="color:#FF9500;font-size:11px;">deaktiviert</span>';
|
||
const html = `
|
||
<div style="font-weight:bold;color:#FFD60A;">🛠 ARIA hat einen neuen Skill erstellt</div>
|
||
<div style="margin-top:4px;color:#E0E0F0;">
|
||
<strong>${escapeHtml(name)}</strong>
|
||
<span style="color:#8888AA;font-size:11px;margin-left:6px;">(${escapeHtml(execMode)}, ${statusBadge})</span>
|
||
</div>
|
||
<div style="color:#8888AA;font-size:12px;margin-top:2px;">${escapeHtml(desc)}</div>
|
||
${setupErr}
|
||
<div class="meta">
|
||
ARIA-Skill — ${new Date().toLocaleTimeString('de-DE')} ·
|
||
<a href="#" onclick="event.preventDefault();switchMainTab('skills');" style="color:#FFD60A;">im Skills-Tab ansehen</a>
|
||
</div>`;
|
||
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
|
||
if (!box) continue;
|
||
const el = document.createElement('div');
|
||
el.className = 'chat-msg received';
|
||
el.style.borderLeft = '3px solid #FFD60A';
|
||
el.innerHTML = html;
|
||
box.appendChild(el);
|
||
box.scrollTop = box.scrollHeight;
|
||
}
|
||
}
|
||
|
||
/** Wenn der Server file_deleted broadcastet: alle Bubbles mit
|
||
diesem serverPath rerendern als "geloescht" markieren. */
|
||
function markFileDeletedInChat(serverPath) {
|
||
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
|
||
if (!box) continue;
|
||
const bubbles = box.querySelectorAll(`[data-aria-file-path="${CSS.escape(serverPath)}"]`);
|
||
bubbles.forEach(el => {
|
||
if (el.dataset.deleted === '1') return;
|
||
el.dataset.deleted = '1';
|
||
const name = (serverPath.split('/').pop()) || 'datei';
|
||
el.innerHTML = `<div style="font-weight:bold;color:#FF9500;">📎 ${escapeHtml(name)} — vom Benutzer gelöscht</div>` +
|
||
`<div style="margin-top:4px;font-size:10px;color:#666;font-family:monospace;text-decoration:line-through;">${escapeHtml(serverPath)}</div>` +
|
||
`<div class="meta">ARIA-Datei (gelöscht)</div>`;
|
||
});
|
||
}
|
||
}
|
||
|
||
let chatFullscreen = false;
|
||
function toggleChatFullscreen() {
|
||
const modal = document.getElementById('chat-fullscreen');
|
||
chatFullscreen = !chatFullscreen;
|
||
if (chatFullscreen) {
|
||
modal.style.display = 'flex';
|
||
// Chat-Inhalt synchronisieren
|
||
const fsBox = document.getElementById('chat-box-fs');
|
||
fsBox.innerHTML = chatBox.innerHTML;
|
||
fsBox.scrollTop = fsBox.scrollHeight;
|
||
document.getElementById('chat-input-fs').focus();
|
||
} else {
|
||
modal.style.display = 'none';
|
||
}
|
||
}
|
||
function testGatewayFS() {
|
||
const input = document.getElementById('chat-input-fs');
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
addChat('sent', text, 'Gateway direkt');
|
||
send({ action: 'test_gateway', text });
|
||
input.value = '';
|
||
}
|
||
function testRVSFS() {
|
||
const input = document.getElementById('chat-input-fs');
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
addChat('sent', text, 'via RVS');
|
||
send({ action: 'test_rvs', text });
|
||
input.value = '';
|
||
}
|
||
// Escape schliesst Vollbild-Chat
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && chatFullscreen) toggleChatFullscreen();
|
||
});
|
||
|
||
// ── Thinking-Indikator ─────────────────────────────
|
||
let thinkingTimeout = null;
|
||
const TOOL_LABELS = {
|
||
'Bash': '\uD83D\uDDA5\uFE0F Shell-Befehl',
|
||
'WebFetch': '\uD83C\uDF10 Webseite abrufen',
|
||
'WebSearch': '\uD83D\uDD0D Suche',
|
||
'Read': '\uD83D\uDCC4 Datei lesen',
|
||
'Write': '\u270D\uFE0F Datei schreiben',
|
||
'Edit': '\u270D\uFE0F Datei bearbeiten',
|
||
'Grep': '\uD83D\uDD0D Code durchsuchen',
|
||
'Glob': '\uD83D\uDCC1 Dateien suchen',
|
||
'Agent': '\uD83E\uDD16 Sub-Agent',
|
||
};
|
||
// ── Service-Status Banner (Gamebox: F5-TTS / Whisper Lade-Status) ──
|
||
// Aggregiert die Status-Infos der Bridges. Wenn irgendwas am Laden
|
||
// ist, zeigt das Banner unten rechts. Sobald alles auf 'ready' ist,
|
||
// bleibt's einen Moment und wird dann vom User weggeklickt (oder
|
||
// nach 8s automatisch).
|
||
const _serviceState = {}; // { f5tts: {state, model, ...}, whisper: {...} }
|
||
let _serviceFadeTimer = null;
|
||
function updateServiceStatus(p) {
|
||
const svc = p.service || '?';
|
||
_serviceState[svc] = p;
|
||
|
||
const banner = document.getElementById('service-status-banner');
|
||
const list = document.getElementById('service-status-list');
|
||
const icon = document.getElementById('service-status-icon');
|
||
const closeBtn = document.getElementById('service-status-close');
|
||
|
||
// Liste neu aufbauen
|
||
list.innerHTML = '';
|
||
let anyLoading = false, anyError = false;
|
||
const labels = { f5tts: 'F5-TTS', whisper: 'Whisper STT' };
|
||
for (const [s, info] of Object.entries(_serviceState)) {
|
||
const row = document.createElement('div');
|
||
row.style.cssText = 'display:flex;align-items:center;gap:6px;';
|
||
let dot = '⚫', color = '#666680', text = '';
|
||
if (info.state === 'loading') {
|
||
dot = '⏳'; color = '#FFD60A'; anyLoading = true;
|
||
text = `${labels[s] || s}: laedt${info.model ? ' ' + info.model : ''}...`;
|
||
} else if (info.state === 'ready') {
|
||
dot = '✅'; color = '#34C759';
|
||
const sec = info.loadSeconds ? ` (${info.loadSeconds.toFixed(1)}s)` : '';
|
||
text = `${labels[s] || s}: bereit${info.model ? ' ' + info.model : ''}${sec}`;
|
||
} else if (info.state === 'error') {
|
||
dot = '❌'; color = '#FF3B30'; anyError = true;
|
||
text = `${labels[s] || s}: Fehler ${info.error || ''}`;
|
||
} else {
|
||
text = `${labels[s] || s}: ${info.state}`;
|
||
}
|
||
row.innerHTML = `<span style="color:${color}">${dot}</span><span>${text}</span>`;
|
||
list.appendChild(row);
|
||
}
|
||
|
||
// Icon spiegelt Gesamt-Status
|
||
if (anyError) icon.innerHTML = '❌';
|
||
else if (anyLoading) icon.innerHTML = '⏳';
|
||
else icon.innerHTML = '✅';
|
||
|
||
banner.style.display = 'block';
|
||
|
||
// Wenn alles ready (kein Loading, kein Error): X-Button anzeigen
|
||
// + nach 8s automatisch wegfaden
|
||
if (!anyLoading && !anyError) {
|
||
closeBtn.style.display = 'block';
|
||
clearTimeout(_serviceFadeTimer);
|
||
_serviceFadeTimer = setTimeout(() => {
|
||
banner.style.display = 'none';
|
||
}, 8000);
|
||
} else {
|
||
closeBtn.style.display = 'none';
|
||
clearTimeout(_serviceFadeTimer);
|
||
}
|
||
}
|
||
|
||
function updateThinkingIndicator(msg) {
|
||
const indicators = [
|
||
document.getElementById('thinking-indicator'),
|
||
document.getElementById('thinking-indicator-fs'),
|
||
];
|
||
const texts = [
|
||
document.getElementById('thinking-text'),
|
||
document.getElementById('thinking-text-fs'),
|
||
];
|
||
|
||
if (msg.activity === 'idle') {
|
||
indicators.forEach(el => { if (el) el.style.display = 'none'; });
|
||
if (thinkingTimeout) { clearTimeout(thinkingTimeout); thinkingTimeout = null; }
|
||
return;
|
||
}
|
||
|
||
let label = 'ARIA denkt...';
|
||
if (msg.activity === 'tool' && msg.tool) {
|
||
label = TOOL_LABELS[msg.tool] || `\uD83D\uDD27 ${msg.tool}`;
|
||
} else if (msg.activity === 'assistant') {
|
||
label = 'ARIA schreibt...';
|
||
}
|
||
|
||
indicators.forEach((el, i) => {
|
||
if (!el) return;
|
||
// Haupt-Indicator ist flex (Abbrechen-Button rechts), Vollbild-Variante block
|
||
el.style.display = i === 0 ? 'flex' : 'block';
|
||
});
|
||
texts.forEach(el => { if (el) el.textContent = label; });
|
||
|
||
// Auto-Hide nach 2min (falls idle Event verpasst wird — ARIA arbeitet max 15min)
|
||
if (thinkingTimeout) clearTimeout(thinkingTimeout);
|
||
thinkingTimeout = setTimeout(() => {
|
||
indicators.forEach(el => { if (el) el.style.display = 'none'; });
|
||
}, 120000);
|
||
}
|
||
|
||
// ── XTTS Panel ─────────────────────────────
|
||
function renderVoiceList(voices) {
|
||
const box = document.getElementById('xtts-voice-list');
|
||
if (!box) return;
|
||
if (!voices || voices.length === 0) {
|
||
box.innerHTML = '<div style="color:#555570;font-size:11px;">Noch keine eigenen Stimmen vorhanden.</div>';
|
||
return;
|
||
}
|
||
let html = '<div style="color:#8888AA;font-size:11px;margin-bottom:4px;">Geclonte Stimmen:</div>';
|
||
html += '<div style="display:flex;flex-direction:column;gap:4px;">';
|
||
for (const v of voices) {
|
||
const esc = (s) => String(s).replace(/[&<>"']/g, c => ({ "&":"&", "<":"<", ">":">", '"':""", "'":"'" }[c]));
|
||
const jsName = esc(v.name).replace(/'/g, "\\'");
|
||
html += `<div style="display:flex;align-items:center;gap:8px;background:#1E1E2E;border-radius:4px;padding:4px 8px;font-size:12px;">`
|
||
+ `<span style="flex:1;color:#E0E0F0;">${esc(v.name)}</span>`
|
||
+ `<span style="color:#555570;font-size:10px;">${(v.size/1024).toFixed(0)}KB</span>`
|
||
+ `<button class="btn secondary" onclick="openVoicePreview('${jsName}')" style="padding:2px 8px;font-size:12px;" title="Stimme anhoeren">▶</button>`
|
||
+ `<button class="btn secondary" onclick="exportXttsVoice('${jsName}')" style="padding:2px 8px;font-size:10px;color:#0096FF;" title="Stimme exportieren">⬇</button>`
|
||
+ `<button class="btn secondary" onclick="deleteXttsVoice('${jsName}')" style="padding:2px 8px;font-size:10px;color:#FF6B6B;" title="Stimme loeschen">X</button>`
|
||
+ `</div>`;
|
||
}
|
||
html += '</div>';
|
||
html += '<div style="margin-top:6px;display:flex;gap:6px;">';
|
||
html += '<input type="file" id="xtts-voice-import-file" accept=".tar.gz,.tgz,application/gzip" style="display:none" onchange="importXttsVoice(event)">';
|
||
html += '<button class="btn secondary" onclick="document.getElementById(\'xtts-voice-import-file\').click()" style="padding:2px 10px;font-size:11px;">⬆ Stimme importieren</button>';
|
||
html += '</div>';
|
||
box.innerHTML = html;
|
||
}
|
||
|
||
// ── Voice Export/Import ────────────────────────────
|
||
function exportXttsVoice(name) {
|
||
const status = document.getElementById('voice-status');
|
||
if (status) status.textContent = '⏳ Exportiere ' + name + ' ...';
|
||
// RVS request/response — Antwort kommt via 'xtts_voice_exported'
|
||
send({ action: 'xtts_export_voice', name });
|
||
}
|
||
|
||
async function importXttsVoice(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
const status = document.getElementById('voice-status');
|
||
try {
|
||
// Datei als base64 lesen (kann mehrere MB sein)
|
||
const buf = await file.arrayBuffer();
|
||
const bytes = new Uint8Array(buf);
|
||
let bin = '';
|
||
const chunk = 8192;
|
||
for (let i = 0; i < bytes.length; i += chunk) {
|
||
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
||
}
|
||
const b64 = btoa(bin);
|
||
// Name aus Dateinamen ableiten — z.B. "maia.tar.gz" → "maia"
|
||
const baseName = file.name.replace(/\.tar\.gz$|\.tgz$/i, '');
|
||
if (!confirm(`Stimme "${baseName}" importieren?\n\nFalls schon vorhanden, wird sie überschrieben.`)) {
|
||
event.target.value = '';
|
||
return;
|
||
}
|
||
if (status) status.textContent = '⏳ Lade hoch (' + (file.size/1024).toFixed(0) + ' KB)...';
|
||
send({ action: 'xtts_import_voice', name: baseName, data: b64 });
|
||
} catch (e) {
|
||
if (status) status.textContent = '✗ ' + e.message;
|
||
} finally {
|
||
event.target.value = '';
|
||
}
|
||
}
|
||
|
||
// ── Voice Preview Modal ─────────────────────────
|
||
const VOICE_PREVIEW_DEFAULT = 'Hallo, ich bin ARIA. Das hier ist ein kleiner Test damit du meine Stimme beurteilen kannst.';
|
||
const PREVIEW_SPEED_DEFAULT = 1.0;
|
||
const PREVIEW_SPEED_MIN = 0.1;
|
||
const PREVIEW_SPEED_MAX = 5.0;
|
||
let currentPreviewVoice = '';
|
||
let currentPreviewSpeed = PREVIEW_SPEED_DEFAULT;
|
||
|
||
function _refreshPreviewSpeedLabel() {
|
||
const el = document.getElementById('voice-preview-speed-value');
|
||
if (el) el.textContent = currentPreviewSpeed.toFixed(1) + ' x';
|
||
}
|
||
|
||
function adjustPreviewSpeed(delta) {
|
||
const next = Math.round((currentPreviewSpeed + delta) * 10) / 10;
|
||
if (next < PREVIEW_SPEED_MIN || next > PREVIEW_SPEED_MAX) return;
|
||
currentPreviewSpeed = next;
|
||
_refreshPreviewSpeedLabel();
|
||
}
|
||
|
||
function openVoicePreview(name) {
|
||
currentPreviewVoice = name;
|
||
// Speed bei jedem Oeffnen zuruecksetzen — bewusst kein persist
|
||
currentPreviewSpeed = PREVIEW_SPEED_DEFAULT;
|
||
_refreshPreviewSpeedLabel();
|
||
document.getElementById('voice-preview-name').textContent = name;
|
||
// Text bei jedem Oeffnen zuruecksetzen
|
||
document.getElementById('voice-preview-text').value = VOICE_PREVIEW_DEFAULT;
|
||
document.getElementById('voice-preview-status').textContent = '';
|
||
const audio = document.getElementById('voice-preview-audio');
|
||
audio.style.display = 'none';
|
||
audio.src = '';
|
||
document.getElementById('voice-preview-modal').style.display = 'flex';
|
||
}
|
||
|
||
function closeVoicePreview() {
|
||
document.getElementById('voice-preview-modal').style.display = 'none';
|
||
const audio = document.getElementById('voice-preview-audio');
|
||
try { audio.pause(); } catch {}
|
||
}
|
||
|
||
function playVoicePreview() {
|
||
const text = (document.getElementById('voice-preview-text').value || '').trim();
|
||
if (!text) {
|
||
document.getElementById('voice-preview-status').textContent = 'Text leer';
|
||
return;
|
||
}
|
||
document.getElementById('voice-preview-status').textContent = '⏳ Rendere...';
|
||
document.getElementById('voice-preview-play').disabled = true;
|
||
send({
|
||
action: 'preview_voice',
|
||
voice: currentPreviewVoice,
|
||
text,
|
||
speed: currentPreviewSpeed,
|
||
});
|
||
}
|
||
|
||
function deleteXttsVoice(name) {
|
||
if (!confirm(`Stimme "${name}" endgueltig loeschen?`)) return;
|
||
send({ action: 'xtts_delete_voice', name });
|
||
// Bei aktueller Auswahl: auf Default zuruecksetzen
|
||
const sel = document.getElementById('diag-xtts-voice');
|
||
if (sel.value === name) { sel.value = ''; sendVoiceConfig(); }
|
||
}
|
||
|
||
// Legacy no-op (XTTS ist jetzt die einzige Engine, kein Panel-Toggle noetig)
|
||
function toggleXTTSPanel() {
|
||
void 0;
|
||
if (engine === 'xtts') loadXTTSVoices();
|
||
}
|
||
|
||
function loadXTTSVoices() {
|
||
send({ action: 'xtts_list_voices' });
|
||
}
|
||
|
||
function arrayBufferToBase64(buffer) {
|
||
const bytes = new Uint8Array(buffer);
|
||
let binary = '';
|
||
for (let i = 0; i < bytes.length; i += 8192) {
|
||
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192));
|
||
}
|
||
return btoa(binary);
|
||
}
|
||
|
||
async function uploadVoiceSamples() {
|
||
const name = document.getElementById('xtts-clone-name').value.trim();
|
||
const files = document.getElementById('xtts-clone-files').files;
|
||
if (!name) { alert('Bitte einen Namen eingeben'); return; }
|
||
if (!files || files.length === 0) { alert('Bitte Audio-Dateien auswaehlen'); return; }
|
||
if (files.length > 10) { alert('Maximal 10 Dateien'); return; }
|
||
|
||
const status = document.getElementById('xtts-clone-status');
|
||
status.textContent = `Lade ${files.length} Datei(en)...`;
|
||
status.style.color = '#FFD60A';
|
||
|
||
try {
|
||
const samples = [];
|
||
for (let i = 0; i < files.length; i++) {
|
||
status.textContent = `Lese Datei ${i + 1}/${files.length}: ${files[i].name}...`;
|
||
const buffer = await files[i].arrayBuffer();
|
||
const base64 = arrayBufferToBase64(buffer);
|
||
samples.push({ base64, name: files[i].name, size: files[i].size });
|
||
}
|
||
|
||
const totalSize = samples.reduce((s, f) => s + f.size, 0);
|
||
status.textContent = `Sende ${samples.length} Sample(s) (${(totalSize / 1024).toFixed(0)}KB)...`;
|
||
|
||
send({ action: 'voice_upload', name, samples });
|
||
|
||
status.textContent = `Gesendet — warte auf Bestaetigung vom XTTS-Server...`;
|
||
} catch (err) {
|
||
status.textContent = `Fehler: ${err.message}`;
|
||
status.style.color = '#FF3B30';
|
||
}
|
||
}
|
||
|
||
// ── Diagnostic Anhang-Handling ─────────────
|
||
let diagPendingFiles = [];
|
||
|
||
function handleDiagFileSelect(files) {
|
||
for (const file of files) {
|
||
const reader = new FileReader();
|
||
reader.onload = () => {
|
||
const base64 = reader.result.split(',')[1];
|
||
diagPendingFiles.push({ name: file.name, type: file.type, size: file.size, base64 });
|
||
renderDiagPending();
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
}
|
||
|
||
function handleDiagPaste(event) {
|
||
const items = event.clipboardData?.items;
|
||
if (!items) return;
|
||
for (const item of items) {
|
||
if (item.kind === 'file') {
|
||
event.preventDefault();
|
||
const file = item.getAsFile();
|
||
if (file) handleDiagFileSelect([file]);
|
||
}
|
||
}
|
||
}
|
||
|
||
function renderDiagPending() {
|
||
const container = document.getElementById('diag-pending-attachments');
|
||
if (diagPendingFiles.length === 0) {
|
||
container.style.display = 'none';
|
||
return;
|
||
}
|
||
container.style.display = 'flex';
|
||
container.innerHTML = diagPendingFiles.map((f, i) => {
|
||
const isImage = f.type.startsWith('image/');
|
||
const preview = isImage ? `<img src="data:${f.type};base64,${f.base64}" style="width:40px;height:40px;border-radius:4px;object-fit:cover;">` : `<span style="font-size:20px;">📄</span>`;
|
||
return `<div style="position:relative;display:inline-block;">
|
||
${preview}
|
||
<span onclick="removeDiagPending(${i})" style="position:absolute;top:-4px;right:-4px;width:16px;height:16px;border-radius:8px;background:#FF3B30;color:#fff;font-size:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;">X</span>
|
||
</div>`;
|
||
}).join('') + `<span style="color:#8888AA;font-size:11px;margin-left:4px;">${diagPendingFiles.length} Datei(en)</span>
|
||
<span onclick="diagPendingFiles=[];renderDiagPending();" style="color:#FF3B30;font-size:11px;cursor:pointer;margin-left:8px;">Alle X</span>`;
|
||
}
|
||
|
||
function removeDiagPending(idx) {
|
||
diagPendingFiles.splice(idx, 1);
|
||
renderDiagPending();
|
||
}
|
||
|
||
// doctorFix + ariaRestart entfernt — aria-core ist raus.
|
||
// Fuer Container-Restarts: restartContainer('aria-bridge'|'aria-brain'|'aria-qdrant').
|
||
|
||
// ── Voice Settings Export/Import ──────────────────────
|
||
function exportVoiceSettings() {
|
||
window.location.href = '/api/voice-config-export';
|
||
}
|
||
|
||
async function importVoiceSettings(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
try {
|
||
const text = await file.text();
|
||
// Sanity-Check: muss JSON sein
|
||
JSON.parse(text);
|
||
if (!confirm(`Voice-Settings aus "${file.name}" importieren?\n\nAktuelle voice_config + highlight_triggers werden überschrieben.`)) {
|
||
event.target.value = '';
|
||
return;
|
||
}
|
||
const r = await fetch('/api/voice-config-import', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: text,
|
||
});
|
||
const d = await r.json();
|
||
if (d.ok) {
|
||
alert('✓ ' + d.message);
|
||
send({ action: 'get_voice_config' });
|
||
} else {
|
||
alert('Import fehlgeschlagen: ' + d.error);
|
||
}
|
||
} catch (e) {
|
||
alert('Ungültige Datei: ' + e.message);
|
||
} finally {
|
||
event.target.value = '';
|
||
}
|
||
}
|
||
|
||
// ── Wipe All — Komplett-Reset ──────────────────────────
|
||
async function wipeAll() {
|
||
const statusEl = document.getElementById('wipe-status');
|
||
if (!confirm('WIRKLICH alles löschen?\n\nGedächtnis + Stimmen + Settings → komplett weg.\n.env + SSH-Keys bleiben.\n\nVorher exportieren wenn was erhalten bleiben soll.')) return;
|
||
if (!confirm('Letzte Warnung: Das KANN NICHT rückgängig gemacht werden.\n\nFortfahren?')) return;
|
||
statusEl.innerHTML = '⏳ Stoppe Container, lösche Daten...';
|
||
try {
|
||
const r = await fetch('/api/wipe-all', { method: 'POST' });
|
||
const d = await r.json();
|
||
if (d.ok) {
|
||
statusEl.innerHTML = `<span style="color:#3FFF3F;">✓ ${d.message}</span>`;
|
||
setTimeout(() => location.reload(), 3000);
|
||
} else {
|
||
statusEl.innerHTML = `<span style="color:#FF6B6B;">✗ ${d.error}</span>`;
|
||
}
|
||
} catch (e) {
|
||
statusEl.innerHTML = `<span style="color:#FF6B6B;">✗ ${e.message}</span>`;
|
||
}
|
||
}
|
||
|
||
// ── Generischer Container-Restart (Bridge/Brain/Qdrant) ──────
|
||
async function restartContainer(name) {
|
||
const statusEl = document.getElementById('restart-status');
|
||
if (!confirm(`Container "${name}" wirklich neu starten?\n\nLaufende Anfragen gehen verloren.`)) return;
|
||
if (statusEl) statusEl.innerHTML = `⏳ Restart ${name}...`;
|
||
try {
|
||
const r = await fetch('/api/container-restart', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name }),
|
||
});
|
||
const d = await r.json();
|
||
if (d.ok) {
|
||
if (statusEl) statusEl.innerHTML = `<span style="color:#3FFF3F;">✓ ${name} neu gestartet</span>`;
|
||
addLog('info', 'server', `${name} neu gestartet`);
|
||
} else {
|
||
if (statusEl) statusEl.innerHTML = `<span style="color:#FF6B6B;">✗ ${d.error}</span>`;
|
||
addLog('error', 'server', `${name} Restart: ${d.error}`);
|
||
}
|
||
} catch (e) {
|
||
if (statusEl) statusEl.innerHTML = `<span style="color:#FF6B6B;">✗ ${e.message}</span>`;
|
||
}
|
||
}
|
||
|
||
// ariaSessionReset entfernt — aria-core ist raus.
|
||
|
||
// ── Abbrechen ──────────────────────────────
|
||
function cancelRequest() {
|
||
send({ action: 'cancel_request' });
|
||
updateThinkingIndicator({ activity: 'idle' });
|
||
addChat('error', 'Anfrage abgebrochen', 'system');
|
||
}
|
||
|
||
// ── Stimmen-Config ──────────────────────────
|
||
function sendVoiceConfig() {
|
||
const ttsEnabled = document.getElementById('diag-tts-enabled').checked;
|
||
const xttsVoice = document.getElementById('diag-xtts-voice').value;
|
||
const whisperModel = document.getElementById('diag-whisper-model').value;
|
||
const f5ttsModel = document.getElementById('diag-f5tts-model')?.value || '';
|
||
const f5ttsCkptFile = document.getElementById('diag-f5tts-ckpt')?.value || '';
|
||
const f5ttsVocabFile = document.getElementById('diag-f5tts-vocab')?.value || '';
|
||
const f5ttsCfgRaw = document.getElementById('diag-f5tts-cfg')?.value || '';
|
||
const f5ttsNfeRaw = document.getElementById('diag-f5tts-nfe')?.value || '';
|
||
const f5ttsCfgStrength = f5ttsCfgRaw ? parseFloat(f5ttsCfgRaw) : undefined;
|
||
const f5ttsNfeStep = f5ttsNfeRaw ? parseInt(f5ttsNfeRaw, 10) : undefined;
|
||
send({
|
||
action: 'send_voice_config',
|
||
ttsEnabled, xttsVoice, whisperModel,
|
||
f5ttsModel, f5ttsCkptFile, f5ttsVocabFile,
|
||
f5ttsCfgStrength, f5ttsNfeStep,
|
||
});
|
||
const statusEl = document.getElementById('voice-status');
|
||
if (statusEl && xttsVoice) {
|
||
statusEl.textContent = `⏳ Stimme "${xttsVoice}" wird geladen...`;
|
||
statusEl.style.color = '#FFD60A';
|
||
}
|
||
}
|
||
|
||
// ── Passwort-Feld Anzeigen/Verbergen ─────────────────────
|
||
function toggleSecret(inputId, btn) {
|
||
const el = document.getElementById(inputId);
|
||
if (!el) return;
|
||
if (el.type === 'password') {
|
||
el.type = 'text';
|
||
btn.innerHTML = '👀'; // 👀
|
||
btn.title = 'Verbergen';
|
||
} else {
|
||
el.type = 'password';
|
||
btn.innerHTML = '👁'; // 👁
|
||
btn.title = 'Anzeigen';
|
||
}
|
||
}
|
||
|
||
// ── Runtime-Konfiguration ─────────────────────
|
||
async function loadRuntimeConfig() {
|
||
const statusEl = document.getElementById('rc-status');
|
||
statusEl.textContent = 'Lade...';
|
||
try {
|
||
const resp = await fetch('/api/runtime-config');
|
||
const cfg = await resp.json();
|
||
document.getElementById('rc-rvs-host').value = cfg.RVS_HOST || '';
|
||
document.getElementById('rc-rvs-port').value = cfg.RVS_PORT || '443';
|
||
document.getElementById('rc-rvs-tls').value = String(cfg.RVS_TLS) === 'false' ? 'false' : 'true';
|
||
document.getElementById('rc-rvs-token').value = cfg.RVS_TOKEN || '';
|
||
document.getElementById('rc-auth-token').value = cfg.ARIA_AUTH_TOKEN || '';
|
||
statusEl.textContent = 'Geladen.';
|
||
statusEl.style.color = '#34C759';
|
||
loadOnboardingQR(); // QR bei Config-Wechsel neu generieren
|
||
} catch (e) {
|
||
statusEl.textContent = 'Fehler: ' + e.message;
|
||
statusEl.style.color = '#FF6B6B';
|
||
}
|
||
}
|
||
|
||
async function saveRuntimeConfig() {
|
||
const statusEl = document.getElementById('rc-status');
|
||
statusEl.textContent = 'Speichere...';
|
||
const patch = {
|
||
RVS_HOST: document.getElementById('rc-rvs-host').value.trim(),
|
||
RVS_PORT: document.getElementById('rc-rvs-port').value.trim(),
|
||
RVS_TLS: document.getElementById('rc-rvs-tls').value,
|
||
RVS_TOKEN: document.getElementById('rc-rvs-token').value.trim(),
|
||
ARIA_AUTH_TOKEN: document.getElementById('rc-auth-token').value.trim(),
|
||
};
|
||
try {
|
||
const resp = await fetch('/api/runtime-config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(patch),
|
||
});
|
||
const data = await resp.json();
|
||
if (data.ok) {
|
||
statusEl.textContent = 'Gespeichert — Bridge-Container fuer Uebernahme neu starten.';
|
||
statusEl.style.color = '#FFD60A';
|
||
loadOnboardingQR(); // QR mit neuem Token
|
||
} else {
|
||
throw new Error(data.error || 'Unbekannt');
|
||
}
|
||
} catch (e) {
|
||
statusEl.textContent = 'Fehler: ' + e.message;
|
||
statusEl.style.color = '#FF6B6B';
|
||
}
|
||
}
|
||
|
||
// ── App-Onboarding QR-Code ────────────────────
|
||
let qrLibReady = false;
|
||
function ensureQRLib() {
|
||
return new Promise((resolve) => {
|
||
if (qrLibReady || window.qrcode) { qrLibReady = true; resolve(); return; }
|
||
const s = document.createElement('script');
|
||
s.src = 'https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js';
|
||
s.onload = () => { qrLibReady = true; resolve(); };
|
||
s.onerror = () => resolve(); // silent fail
|
||
document.head.appendChild(s);
|
||
});
|
||
}
|
||
|
||
async function loadOnboardingQR() {
|
||
const box = document.getElementById('onboarding-qr');
|
||
box.textContent = 'Lade...';
|
||
try {
|
||
await ensureQRLib();
|
||
if (!window.qrcode) throw new Error('QR-Library nicht geladen');
|
||
const resp = await fetch('/api/onboarding');
|
||
const cfg = await resp.json();
|
||
if (!cfg.rvsHost || !cfg.rvsToken) {
|
||
box.innerHTML = '<div style="color:#FF6B6B;">RVS nicht konfiguriert (ENV Variablen fehlen)</div>';
|
||
return;
|
||
}
|
||
// Format kompatibel mit android/src/components/QRScanner.tsx parseQRData()
|
||
const payload = JSON.stringify({
|
||
host: cfg.rvsHost,
|
||
port: Number(cfg.rvsPort) || 443,
|
||
tls: cfg.rvsTLS !== false,
|
||
token: cfg.rvsToken,
|
||
});
|
||
const qr = window.qrcode(0, 'M');
|
||
qr.addData(payload);
|
||
qr.make();
|
||
// Als SVG rendern — skaliert sauber auf Container-Groesse
|
||
box.innerHTML = qr.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
|
||
const svg = box.querySelector('svg');
|
||
if (svg) {
|
||
svg.style.cssText = 'width:100%;height:100%;background:#fff;border-radius:4px;padding:6px;box-sizing:border-box;display:block;';
|
||
}
|
||
} catch (e) {
|
||
box.innerHTML = `<div style="color:#FF6B6B;">Fehler: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
// ── Modus-Wechsel ────────────────────────────
|
||
// Kanonische IDs (matchen bridge/modes.py canonical_id + android ModeSelector)
|
||
const MODE_LABELS = { normal: 'Normal', nicht_stoeren: 'Nicht stoeren', fluester: 'Fluestern', hangar: 'Hangar', gaming: 'Gaming' };
|
||
let currentMode = 'normal';
|
||
|
||
function updateModeUI(mode) {
|
||
currentMode = mode;
|
||
document.querySelectorAll('.mode-btn').forEach(btn => {
|
||
btn.style.borderColor = btn.dataset.mode === mode ? '#0096FF' : 'transparent';
|
||
});
|
||
const label = MODE_LABELS[mode] || mode;
|
||
document.getElementById('mode-status').textContent = `Aktueller Modus: ${label}`;
|
||
}
|
||
|
||
function setMode(mode) {
|
||
// Optimistic UI-Update — Bridge bestaetigt per Broadcast
|
||
updateModeUI(mode);
|
||
// Sauberer Weg: type=mode via RVS an Bridge — die broadcastet an alle Clients
|
||
send({ action: 'set_mode', mode });
|
||
}
|
||
|
||
function openLightbox(mediaType, url) {
|
||
const lb = document.getElementById('lightbox');
|
||
if (mediaType === 'video') {
|
||
lb.innerHTML = `<video src="${url}" controls autoplay style="max-width:95vw;max-height:95vh;border-radius:8px;" onclick="event.stopPropagation()"></video>`;
|
||
} else {
|
||
lb.innerHTML = `<img src="${url}" style="max-width:95vw;max-height:95vh;border-radius:8px;">`;
|
||
}
|
||
lb.classList.add('open');
|
||
}
|
||
|
||
function closeLightbox() {
|
||
const lb = document.getElementById('lightbox');
|
||
lb.classList.remove('open');
|
||
lb.innerHTML = '';
|
||
}
|
||
|
||
function showDockerLogs(msg) {
|
||
const tab = msg.tab;
|
||
const box = logBoxes[tab];
|
||
if (!box) return;
|
||
|
||
if (msg.error) {
|
||
appendToLog(tab, 'error', `Docker Logs Fehler: ${msg.error}`);
|
||
return;
|
||
}
|
||
|
||
// Bestehende Eintraege leeren und Docker-Logs einfuegen
|
||
box.innerHTML = '';
|
||
const header = document.createElement('div');
|
||
header.className = 'log-entry info';
|
||
header.textContent = `── Docker Logs: ${msg.container} (letzte ${msg.lines.length} Zeilen) ──`;
|
||
box.appendChild(header);
|
||
|
||
for (const line of msg.lines) {
|
||
const el = document.createElement('div');
|
||
el.className = 'log-entry ' + (line.match(/\b(error|Error|ERROR)\b/) ? 'error' :
|
||
line.match(/\b(warn|Warning|WARN)\b/) ? 'warn' : 'info');
|
||
el.textContent = line;
|
||
box.appendChild(el);
|
||
}
|
||
|
||
// Zähler aktualisieren
|
||
logCounts[tab] = msg.lines.length;
|
||
const countEl = document.getElementById(`count-${tab}`);
|
||
if (countEl) countEl.textContent = logCounts[tab];
|
||
|
||
box.scrollTop = box.scrollHeight;
|
||
autoScroll[tab] = true;
|
||
}
|
||
|
||
function showProxyModels(models) {
|
||
const container = document.getElementById('proxy-models');
|
||
const list = document.getElementById('proxy-models-list');
|
||
const hint = document.getElementById('proxy-models-hint');
|
||
container.style.display = 'block';
|
||
list.innerHTML = models.map(m => {
|
||
const clean = m.replace('openai/', '');
|
||
return `<div style="display:inline-block;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:2px 8px;margin:2px;font-size:11px">${escapeHtml(m)}</div>`;
|
||
}).join('');
|
||
const cleanNames = models.map(m => m.replace('openai/', ''));
|
||
hint.textContent = `DEFAULT_MODEL fuer docker-compose.yml: ${cleanNames.join(' | ')}`;
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
// Auto-Resize fuer Textarea — wuchst mit dem Inhalt bis zum max-height
|
||
function autoResizeTextarea(el) {
|
||
el.style.height = 'auto';
|
||
el.style.height = Math.min(el.scrollHeight, 200) + 'px';
|
||
}
|
||
|
||
// Enter sendet, Shift+Enter macht neue Zeile (chat-Standard).
|
||
function chatInputKeydown(e, sendFn) {
|
||
if (e.key === 'Enter' && !e.shiftKey) {
|
||
e.preventDefault();
|
||
sendFn();
|
||
// Textarea zurueck auf 2 rows setzen
|
||
e.target.style.height = 'auto';
|
||
}
|
||
}
|
||
document.getElementById('chat-input').addEventListener('keydown', (e) => chatInputKeydown(e, testRVS));
|
||
document.getElementById('chat-input-fs').addEventListener('keydown', (e) => chatInputKeydown(e, testRVSFS));
|
||
|
||
// Escape schliesst Lightbox
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') closeLightbox();
|
||
});
|
||
|
||
// ── ARIA Live-Ansicht (SSH + Desktop) ──────────────────
|
||
|
||
let liveSshTerm = null;
|
||
let liveSshFit = null;
|
||
|
||
function switchLiveTab(tab) {
|
||
document.getElementById('live-ssh').style.display = tab === 'ssh' ? 'block' : 'none';
|
||
document.getElementById('live-desktop').style.display = tab === 'desktop' ? 'block' : 'none';
|
||
document.getElementById('live-tab-ssh').className = 'tab-btn' + (tab === 'ssh' ? ' active' : '');
|
||
document.getElementById('live-tab-desktop').className = 'tab-btn' + (tab === 'desktop' ? ' active' : '');
|
||
if (tab === 'ssh' && liveSshTerm && liveSshFit) {
|
||
setTimeout(() => liveSshFit.fit(), 50);
|
||
}
|
||
}
|
||
|
||
function startLiveSSH() {
|
||
const statusEl = document.getElementById('live-ssh-status');
|
||
const btn = document.getElementById('btn-live-ssh');
|
||
|
||
// Wenn schon verbunden, trennen
|
||
if (liveSshTerm && liveSshTerm._sshConnected) {
|
||
send({ action: 'live_ssh_close' });
|
||
statusEl.textContent = 'Getrennt';
|
||
statusEl.style.color = '#FF6B6B';
|
||
btn.textContent = 'Verbinden';
|
||
liveSshTerm._sshConnected = false;
|
||
return;
|
||
}
|
||
|
||
statusEl.textContent = 'Verbinde...';
|
||
statusEl.style.color = '#FFD60A';
|
||
|
||
function initSSHTerm() {
|
||
const container = document.getElementById('live-ssh-term');
|
||
if (!liveSshTerm) {
|
||
liveSshTerm = new Terminal({
|
||
theme: { background: '#080810', foreground: '#E0E0F0', cursor: '#0096FF' },
|
||
fontFamily: 'Courier New, monospace',
|
||
fontSize: 12,
|
||
cursorBlink: true,
|
||
});
|
||
liveSshFit = new FitAddon.FitAddon();
|
||
liveSshTerm.loadAddon(liveSshFit);
|
||
liveSshTerm.open(container);
|
||
liveSshFit.fit();
|
||
liveSshTerm.onData((data) => {
|
||
send({ action: 'live_ssh_input', data });
|
||
});
|
||
}
|
||
liveSshTerm.clear();
|
||
send({ action: 'live_ssh_start' });
|
||
}
|
||
|
||
if (typeof Terminal === 'undefined') {
|
||
const s = document.createElement('script');
|
||
s.src = 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js';
|
||
s.onload = () => {
|
||
const s2 = document.createElement('script');
|
||
s2.src = 'https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js';
|
||
s2.onload = () => initSSHTerm();
|
||
document.head.appendChild(s2);
|
||
};
|
||
document.head.appendChild(s);
|
||
} else {
|
||
initSSHTerm();
|
||
}
|
||
}
|
||
|
||
function handleLiveSSH(msg) {
|
||
const statusEl = document.getElementById('live-ssh-status');
|
||
const btn = document.getElementById('btn-live-ssh');
|
||
if (msg.type === 'live_ssh_data' && liveSshTerm) {
|
||
const raw = atob(msg.data);
|
||
const bytes = new Uint8Array(raw.length);
|
||
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
|
||
liveSshTerm.write(bytes);
|
||
} else if (msg.type === 'live_ssh_connected') {
|
||
statusEl.textContent = 'Verbunden mit aria-wohnung';
|
||
statusEl.style.color = '#34C759';
|
||
btn.textContent = 'Trennen';
|
||
if (liveSshTerm) liveSshTerm._sshConnected = true;
|
||
} else if (msg.type === 'live_ssh_error') {
|
||
statusEl.textContent = msg.error || 'Fehler';
|
||
statusEl.style.color = '#FF6B6B';
|
||
btn.textContent = 'Verbinden';
|
||
if (liveSshTerm) liveSshTerm._sshConnected = false;
|
||
} else if (msg.type === 'live_ssh_closed') {
|
||
statusEl.textContent = 'Getrennt';
|
||
statusEl.style.color = '#8888AA';
|
||
btn.textContent = 'Verbinden';
|
||
if (liveSshTerm) liveSshTerm._sshConnected = false;
|
||
}
|
||
}
|
||
|
||
function checkDesktop() {
|
||
send({ action: 'check_desktop' });
|
||
}
|
||
|
||
function handleDesktop(msg) {
|
||
if (msg.type === 'desktop_status') {
|
||
const placeholder = document.getElementById('desktop-placeholder');
|
||
const vnc = document.getElementById('desktop-vnc');
|
||
if (msg.available && msg.url) {
|
||
placeholder.style.display = 'none';
|
||
vnc.style.display = 'block';
|
||
vnc.src = msg.url;
|
||
} else {
|
||
placeholder.style.display = 'flex';
|
||
vnc.style.display = 'none';
|
||
placeholder.querySelector('div:nth-child(2)').textContent = msg.message || 'Kein Desktop verfuegbar';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Sessions- und alter Brain-Viewer wurden entfernt — Memories laufen
|
||
// jetzt komplett ueber den Gehirn-Tab + Vector-DB im aria-brain.
|
||
|
||
// ── Haupt-Tab Navigation ──────────────────────────────────
|
||
|
||
function switchMainTab(tab) {
|
||
document.querySelectorAll('.main-tab').forEach(el => el.classList.remove('visible'));
|
||
document.querySelectorAll('.main-nav-btn').forEach(b => b.classList.remove('active'));
|
||
const target = document.getElementById('tab-' + tab);
|
||
if (target) target.classList.add('visible');
|
||
// Button aktivieren — Match per onclick-Attribut (robust gegen Beschriftungs-Aenderungen)
|
||
document.querySelectorAll('.main-nav-btn').forEach(b => {
|
||
const oc = b.getAttribute('onclick') || '';
|
||
if (oc.includes(`'${tab}'`)) b.classList.add('active');
|
||
});
|
||
// Einstellungen: Config + QR laden
|
||
if (tab === 'settings') {
|
||
send({ action: 'get_voice_config' });
|
||
loadRuntimeConfig();
|
||
loadOnboardingQR();
|
||
} else if (tab === 'brain') {
|
||
loadBrainStatus();
|
||
loadBrainMemoryList();
|
||
refreshImportFiles();
|
||
} else if (tab === 'files') {
|
||
loadFiles();
|
||
} else if (tab === 'skills') {
|
||
loadSkills();
|
||
}
|
||
}
|
||
|
||
// ── Skills-Verwaltung ────────────────────────────
|
||
let skillsCache = [];
|
||
const skillExpanded = new Set();
|
||
|
||
async function loadSkills() {
|
||
const el = document.getElementById('skills-list');
|
||
if (!el) return;
|
||
try {
|
||
const r = await fetch('/api/brain/skills/list');
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
const d = await r.json();
|
||
skillsCache = d.skills || [];
|
||
renderSkillsList();
|
||
} catch (e) {
|
||
el.innerHTML = `🔴 Brain nicht erreichbar (${e.message})`;
|
||
}
|
||
}
|
||
|
||
function renderSkillsList() {
|
||
const el = document.getElementById('skills-list');
|
||
if (!el) return;
|
||
if (!skillsCache.length) {
|
||
el.innerHTML = '<div style="padding:8px;color:#555570;">Keine Skills vorhanden. ARIA legt welche an wenn sie ein wiederkehrendes Problem löst — oder importiere einen.</div>';
|
||
return;
|
||
}
|
||
const fmtDate = (iso) => iso ? new Date(iso).toLocaleString('de-DE') : '–';
|
||
el.innerHTML = skillsCache.map(s => {
|
||
const active = s.active !== false;
|
||
const expanded = skillExpanded.has(s.name);
|
||
const statusBadge = active
|
||
? '<span style="background:#34C75922;color:#34C759;padding:1px 6px;border-radius:3px;font-size:10px;">aktiv</span>'
|
||
: '<span style="background:#55557022;color:#888;padding:1px 6px;border-radius:3px;font-size:10px;">DEAKTIVIERT</span>';
|
||
const execBadge = `<span style="background:#0096FF22;color:#0096FF;padding:1px 6px;border-radius:3px;font-size:10px;">${escapeHtml(s.execution || 'bash')}</span>`;
|
||
const authorBadge = s.author === 'aria'
|
||
? '<span style="background:#FFD60A22;color:#FFD60A;padding:1px 6px;border-radius:3px;font-size:10px;">von ARIA</span>'
|
||
: '';
|
||
const setupErr = s.setup_error
|
||
? `<div style="color:#FF6B6B;font-size:10px;margin-top:4px;">⚠ Setup-Fehler: ${escapeHtml(s.setup_error.slice(0,200))}</div>`
|
||
: '';
|
||
let expandedSection = '';
|
||
if (expanded) {
|
||
expandedSection = `
|
||
<div style="margin-top:8px;padding-top:8px;border-top:1px solid #1E1E2E;">
|
||
<div id="skill-readme-${escapeHtml(s.name)}" style="font-size:11px;color:#8888AA;margin-bottom:8px;">(README lädt...)</div>
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px;">
|
||
<button class="btn secondary" onclick="runSkillPrompt('${escapeHtml(s.name)}')" style="padding:2px 10px;font-size:11px;color:#3FFF3F;border-color:#3FFF3F;">▶ Ausführen</button>
|
||
<button class="btn secondary" onclick="toggleSkillActive('${escapeHtml(s.name)}', ${!active})" style="padding:2px 10px;font-size:11px;color:#FF9500;border-color:#FF9500;">${active ? '⏸ Deaktivieren' : '▶ Aktivieren'}</button>
|
||
<button class="btn secondary" onclick="exportSkill('${escapeHtml(s.name)}')" style="padding:2px 10px;font-size:11px;color:#0096FF;border-color:#0096FF;">⬇ Export</button>
|
||
<button class="btn secondary" onclick="deleteSkill('${escapeHtml(s.name)}')" style="padding:2px 10px;font-size:11px;color:#FF6B6B;border-color:#FF6B6B;">🗑 Löschen</button>
|
||
</div>
|
||
<div style="color:#0096FF;font-size:11px;font-weight:bold;margin:6px 0 4px;">Logs (letzte 20)</div>
|
||
<div id="skill-logs-${escapeHtml(s.name)}" style="font-size:11px;color:#8888AA;">(Logs lädt...)</div>
|
||
</div>
|
||
`;
|
||
}
|
||
return `
|
||
<div style="border-bottom:1px solid #1E1E2E;padding:8px 0;">
|
||
<div style="display:flex;align-items:center;gap:8px;cursor:pointer;" onclick="toggleSkillExpand('${escapeHtml(s.name)}')">
|
||
<span style="font-size:14px;color:#E0E0F0;">${expanded ? '▼' : '▶'}</span>
|
||
<span style="flex:1;color:#E0E0F0;font-weight:bold;">${escapeHtml(s.name)}</span>
|
||
${statusBadge} ${execBadge} ${authorBadge}
|
||
<span style="color:#555570;font-size:10px;">${s.use_count || 0}× · zuletzt ${fmtDate(s.last_used)}</span>
|
||
</div>
|
||
<div style="color:#8888AA;font-size:11px;margin-top:2px;margin-left:20px;">${escapeHtml(s.description || '(ohne Beschreibung)')}</div>
|
||
${setupErr}
|
||
${expandedSection}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
async function toggleSkillExpand(name) {
|
||
if (skillExpanded.has(name)) skillExpanded.delete(name);
|
||
else skillExpanded.add(name);
|
||
renderSkillsList();
|
||
if (skillExpanded.has(name)) {
|
||
// README + Logs lazy laden
|
||
try {
|
||
const r = await fetch('/api/brain/skills/' + encodeURIComponent(name));
|
||
const d = await r.json();
|
||
const el = document.getElementById('skill-readme-' + name);
|
||
if (el && d.readme) el.innerHTML = '<pre style="margin:0;font-family:inherit;white-space:pre-wrap;">' + escapeHtml(d.readme) + '</pre>';
|
||
} catch {}
|
||
try {
|
||
const r2 = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/logs');
|
||
const d2 = await r2.json();
|
||
const el2 = document.getElementById('skill-logs-' + name);
|
||
if (!el2) return;
|
||
if (!d2.logs.length) { el2.innerHTML = '(keine Runs)'; return; }
|
||
el2.innerHTML = d2.logs.map(l => {
|
||
const okBadge = l.exit_code === 0 ? '<span style="color:#3FFF3F;">OK</span>' : `<span style="color:#FF6B6B;">FEHLER (${l.exit_code})</span>`;
|
||
return `<div style="padding:4px 6px;border:1px solid #1E1E2E;border-radius:3px;margin-bottom:4px;">
|
||
<div style="font-size:10px;color:#555570;">${escapeHtml(l.ts)} · ${l.duration_sec}s · ${okBadge}</div>
|
||
${l.stdout ? `<pre style="margin:2px 0;font-size:10px;color:#E0E0F0;white-space:pre-wrap;max-height:120px;overflow:auto;">${escapeHtml(l.stdout)}</pre>` : ''}
|
||
${l.stderr ? `<pre style="margin:2px 0;font-size:10px;color:#FF9500;white-space:pre-wrap;max-height:80px;overflow:auto;">${escapeHtml(l.stderr)}</pre>` : ''}
|
||
</div>`;
|
||
}).join('');
|
||
} catch {}
|
||
}
|
||
}
|
||
|
||
async function toggleSkillActive(name, newActive) {
|
||
try {
|
||
await fetch('/api/brain/skills/' + encodeURIComponent(name), {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ active: newActive }),
|
||
});
|
||
loadSkills();
|
||
} catch (e) {
|
||
alert('Toggle fehlgeschlagen: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function deleteSkill(name) {
|
||
if (!confirm(`Skill "${name}" wirklich löschen?\n\nFolder, venv, Logs — alles weg.`)) return;
|
||
try {
|
||
await fetch('/api/brain/skills/' + encodeURIComponent(name), { method: 'DELETE' });
|
||
skillExpanded.delete(name);
|
||
loadSkills();
|
||
} catch (e) {
|
||
alert('Löschen fehlgeschlagen: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function runSkillPrompt(name) {
|
||
const argsStr = prompt(`Skill "${name}" ausführen.\n\nArgs als JSON (oder leer):`, '{}');
|
||
if (argsStr === null) return;
|
||
let args = {};
|
||
try {
|
||
args = JSON.parse(argsStr || '{}');
|
||
} catch (e) {
|
||
alert('Ungültiges JSON: ' + e.message);
|
||
return;
|
||
}
|
||
try {
|
||
const r = await fetch('/api/brain/skills/run', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name, args }),
|
||
});
|
||
const d = await r.json();
|
||
const summary = `${d.ok ? '✓ OK' : '✗ FEHLER'} · ${d.duration_sec}s · exit=${d.exit_code}\n\nstdout:\n${(d.stdout||'').slice(0,1500)}\n\nstderr:\n${(d.stderr||'').slice(0,500)}`;
|
||
alert(summary);
|
||
if (skillExpanded.has(name)) toggleSkillExpand(name); // refresh logs by collapse+expand
|
||
loadSkills();
|
||
} catch (e) {
|
||
alert('Run fehlgeschlagen: ' + e.message);
|
||
}
|
||
}
|
||
|
||
function exportSkill(name) {
|
||
window.location.href = '/api/brain/skills/' + encodeURIComponent(name) + '/export';
|
||
}
|
||
|
||
async function importSkillFile(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
if (!confirm(`Skill aus "${file.name}" importieren?\n\nFalls schon vorhanden mit gleichem Namen, wird er überschrieben.`)) {
|
||
event.target.value = ''; return;
|
||
}
|
||
try {
|
||
const r = await fetch('/api/brain/skills/import?overwrite=true', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/gzip' },
|
||
body: file,
|
||
});
|
||
const d = await r.json();
|
||
if (r.ok) {
|
||
alert(`✓ Skill "${d.imported?.name}" importiert.`);
|
||
loadSkills();
|
||
} else {
|
||
alert('Import fehlgeschlagen: ' + (d.detail || JSON.stringify(d)));
|
||
}
|
||
} catch (e) {
|
||
alert('Import fehlgeschlagen: ' + e.message);
|
||
} finally {
|
||
event.target.value = '';
|
||
}
|
||
}
|
||
|
||
// ── Datei-Manager ──────────────────────────────────────
|
||
let filesCache = [];
|
||
|
||
async function loadFiles() {
|
||
const listEl = document.getElementById('files-list');
|
||
if (listEl) listEl.innerHTML = '(Lade...)';
|
||
try {
|
||
const r = await fetch('/api/files-list');
|
||
const d = await r.json();
|
||
if (!d.ok) throw new Error(d.error || 'Unbekannter Fehler');
|
||
filesCache = d.files || [];
|
||
renderFilesList();
|
||
} catch (e) {
|
||
if (listEl) listEl.innerHTML = `🔴 ${e.message}`;
|
||
}
|
||
}
|
||
|
||
function renderFilesList() {
|
||
const listEl = document.getElementById('files-list');
|
||
const infoEl = document.getElementById('files-info');
|
||
if (!listEl) return;
|
||
const q = (document.getElementById('files-search').value || '').toLowerCase();
|
||
const filter = document.getElementById('files-filter').value;
|
||
let files = filesCache.slice();
|
||
if (filter === 'aria') files = files.filter(f => f.fromAria);
|
||
else if (filter === 'user') files = files.filter(f => !f.fromAria);
|
||
if (q) files = files.filter(f => f.name.toLowerCase().includes(q));
|
||
if (infoEl) infoEl.textContent = `${files.length} von ${filesCache.length} Dateien`;
|
||
if (!files.length) {
|
||
listEl.innerHTML = '(Keine Dateien gefunden)';
|
||
return;
|
||
}
|
||
const fmtSize = (b) => b < 1024 ? `${b} B` : b < 1024*1024 ? `${(b/1024).toFixed(1)} KB` : `${(b/1024/1024).toFixed(1)} MB`;
|
||
const fmtDate = (ms) => new Date(ms).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||
listEl.innerHTML = files.map(f => {
|
||
const badge = f.fromAria
|
||
? '<span style="background:#0096FF22;color:#0096FF;padding:1px 6px;border-radius:3px;font-size:10px;margin-right:6px;">ARIA</span>'
|
||
: '<span style="background:#34C75922;color:#34C759;padding:1px 6px;border-radius:3px;font-size:10px;margin-right:6px;">User</span>';
|
||
return `<div style="padding:8px 0;border-bottom:1px solid #1E1E2E;display:flex;gap:6px;align-items:center;">
|
||
<div style="flex:1;min-width:0;">
|
||
<div style="color:#E0E0F0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${badge}<strong>${escapeHtml(f.name)}</strong></div>
|
||
<div style="color:#555570;font-size:10px;">${fmtSize(f.size)} · ${fmtDate(f.mtime)}</div>
|
||
</div>
|
||
<button class="btn secondary" onclick="downloadFile('${encodeURIComponent(f.path)}')" style="padding:2px 8px;font-size:10px;" title="Herunterladen">⬇</button>
|
||
<button class="btn secondary" onclick="deleteFile('${escapeHtml(f.path)}','${escapeHtml(f.name)}')" style="padding:2px 8px;font-size:10px;color:#FF6B6B;border-color:#FF6B6B;" title="Loeschen">🗑</button>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function downloadFile(encPath) {
|
||
window.location.href = '/api/files-download?path=' + encPath;
|
||
}
|
||
|
||
async function deleteFile(p, name) {
|
||
if (!confirm(`Datei "${name}" wirklich löschen?\n\nIn allen Chat-Bubbles wird sie als gelöscht markiert.`)) return;
|
||
try {
|
||
const r = await fetch('/api/files-delete', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ path: p }),
|
||
});
|
||
const d = await r.json();
|
||
if (d.ok) {
|
||
loadFiles();
|
||
// Server broadcastet file_deleted Event → markFileDeletedInChat() updated bubbles
|
||
} else {
|
||
alert('Löschen fehlgeschlagen: ' + d.error);
|
||
}
|
||
} catch (e) {
|
||
alert('Löschen fehlgeschlagen: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ── Gehirn-Tab ────────────────────────────────────────────
|
||
|
||
async function loadBrainStatus() {
|
||
const el = document.getElementById('brain-status');
|
||
if (!el) return;
|
||
try {
|
||
const r = await fetch('/api/brain/health');
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
const d = await r.json();
|
||
const st = d.status === 'ok' ? '🟢 online' : '🟡 ' + (d.status || 'unknown');
|
||
el.innerHTML = `${st} · ${d.memory_count ?? '?'} Memories · Qdrant: ${d.qdrant || '-'}`;
|
||
} catch (e) {
|
||
el.innerHTML = `🔴 Brain nicht erreichbar (${e.message})`;
|
||
}
|
||
// Conversation-Stats (separater Endpoint)
|
||
const conv = document.getElementById('conversation-status');
|
||
if (!conv) return;
|
||
try {
|
||
const r2 = await fetch('/api/brain/conversation/stats');
|
||
if (!r2.ok) throw new Error('HTTP ' + r2.status);
|
||
const d2 = await r2.json();
|
||
const distillIcon = d2.needs_distill ? ' ⚠ Destillat bald fällig' : '';
|
||
conv.innerHTML = `Konversation: <strong>${d2.turns}</strong> Turns · Window: ${d2.max_window} · Schwelle: ${d2.distill_threshold}${distillIcon}`;
|
||
} catch (e) {
|
||
conv.innerHTML = `Konversation: <span style="color:#555570;">${e.message}</span>`;
|
||
}
|
||
}
|
||
|
||
async function distillNow() {
|
||
if (!confirm('Destillat manuell auslösen?\n\nDie ältesten Turns werden zu fact-Memories verdichtet — kostet einen Claude-Call.')) return;
|
||
try {
|
||
const r = await fetch('/api/brain/conversation/distill', { method: 'POST' });
|
||
const d = await r.json();
|
||
alert(`Destillat: ${d.distilled || 0} Facts geschrieben, ${d.removed_turns || 0} Turns entfernt.${d.error ? '\nFehler: ' + d.error : ''}`);
|
||
loadBrainStatus();
|
||
loadBrainMemoryList();
|
||
} catch (e) {
|
||
alert('Destillat fehlgeschlagen: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function resetConversation() {
|
||
if (!confirm('Konversation leeren?\n\nDer Rolling-Window-Verlauf wird komplett verworfen. Destillierte Facts bleiben in der DB.')) return;
|
||
try {
|
||
const r = await fetch('/api/brain/conversation/reset', { method: 'POST' });
|
||
const d = await r.json();
|
||
if (d.ok) {
|
||
loadBrainStatus();
|
||
} else {
|
||
alert('Reset fehlgeschlagen');
|
||
}
|
||
} catch (e) {
|
||
alert('Reset fehlgeschlagen: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Cache aller geladenen Memories — fuer Edit-Lookup
|
||
let brainMemoryCache = {};
|
||
// Aktuelle Search-Treffer (IDs in Reihenfolge); leer = normale Liste
|
||
let brainSearchIds = null;
|
||
|
||
function resetBrainFilters() {
|
||
const s = document.getElementById('brain-search'); if (s) s.value = '';
|
||
const t = document.getElementById('brain-filter-type'); if (t) t.value = '';
|
||
const p = document.getElementById('brain-filter-pinned'); if (p) p.value = 'all';
|
||
const info = document.getElementById('brain-search-info'); if (info) info.style.display = 'none';
|
||
brainSearchIds = null;
|
||
}
|
||
|
||
async function runBrainSearch() {
|
||
const q = (document.getElementById('brain-search').value || '').trim();
|
||
const info = document.getElementById('brain-search-info');
|
||
if (!q) {
|
||
brainSearchIds = null;
|
||
if (info) info.style.display = 'none';
|
||
loadBrainMemoryList();
|
||
return;
|
||
}
|
||
const typeFilter = document.getElementById('brain-filter-type').value;
|
||
const params = new URLSearchParams({ q, k: '20', include_pinned: 'true' });
|
||
if (typeFilter) params.set('type', typeFilter);
|
||
try {
|
||
const r = await fetch('/api/brain/memory/search?' + params.toString());
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
const hits = await r.json();
|
||
hits.forEach(m => { brainMemoryCache[m.id] = m; });
|
||
brainSearchIds = hits.map(m => m.id);
|
||
if (info) {
|
||
info.style.display = 'block';
|
||
info.innerHTML = `🔍 ${hits.length} Treffer für "${escapeHtml(q)}"` +
|
||
(typeFilter ? ` · Typ=${escapeHtml(typeFilter)}` : '') +
|
||
` · sortiert nach Aehnlichkeit`;
|
||
}
|
||
renderBrainList(hits, true);
|
||
} catch (e) {
|
||
if (info) {
|
||
info.style.display = 'block';
|
||
info.innerHTML = `🔴 Suche fehlgeschlagen: ${escapeHtml(e.message)}`;
|
||
}
|
||
}
|
||
}
|
||
|
||
async function loadBrainMemoryList() {
|
||
const el = document.getElementById('brain-memory-list');
|
||
if (!el) return;
|
||
// Wenn aktive Search-Treffer da sind: die anzeigen, nicht neu laden
|
||
if (brainSearchIds && brainSearchIds.length) {
|
||
const items = brainSearchIds.map(id => brainMemoryCache[id]).filter(Boolean);
|
||
renderBrainList(items, true);
|
||
return;
|
||
}
|
||
try {
|
||
const typeFilter = document.getElementById('brain-filter-type').value;
|
||
const pinnedFilter = document.getElementById('brain-filter-pinned').value;
|
||
const params = new URLSearchParams({ limit: '500' });
|
||
if (typeFilter) params.set('type', typeFilter);
|
||
const r = await fetch('/api/brain/memory/list?' + params.toString());
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
let items = await r.json();
|
||
brainMemoryCache = {};
|
||
items.forEach(m => { brainMemoryCache[m.id] = m; });
|
||
if (pinnedFilter === 'pinned') items = items.filter(m => m.pinned);
|
||
else if (pinnedFilter === 'cold') items = items.filter(m => !m.pinned);
|
||
if (!items.length) {
|
||
el.innerHTML = '(Keine Memories — leere DB oder Filter zu eng)';
|
||
return;
|
||
}
|
||
renderBrainList(items, false);
|
||
} catch (e) {
|
||
el.innerHTML = `🔴 Brain nicht erreichbar (${e.message})`;
|
||
}
|
||
}
|
||
|
||
const BRAIN_TYPE_LABELS = {
|
||
identity: 'Identität', rule: 'Regeln / Werte', preference: 'Praeferenzen',
|
||
tool: 'Tools', skill: 'Skills', fact: 'Fakten',
|
||
conversation: 'Konversation', reminder: 'Reminder'
|
||
};
|
||
const BRAIN_TYPE_ORDER = ['identity','rule','preference','tool','skill','fact','conversation','reminder'];
|
||
|
||
function renderMemoryRow(m, withScore) {
|
||
const pin = m.pinned ? '📌 ' : '';
|
||
const preview = (m.content || '').slice(0, 140).replace(/\n/g, ' ');
|
||
const score = withScore && typeof m.score === 'number' ? `<span style="color:#FFD60A;font-size:10px;margin-left:6px;">${m.score.toFixed(2)}</span>` : '';
|
||
const typeBadge = withScore ? `<span style="color:#0096FF;font-size:10px;margin-right:6px;">${escapeHtml(BRAIN_TYPE_LABELS[m.type] || m.type)}</span>` : '';
|
||
return `<div style="padding:6px 0;border-bottom:1px solid #1E1E2E;display:flex;gap:6px;align-items:flex-start;">
|
||
<div style="flex:1;min-width:0;cursor:pointer;" onclick="openMemoryModal('${m.id}')">
|
||
<div style="color:#E0E0F0;font-size:12px;">${typeBadge}${pin}<strong>${escapeHtml(m.title || '(ohne Titel)')}</strong>${score}
|
||
${m.category ? `<span style="color:#555570;font-weight:normal;font-size:10px;margin-left:6px;">[${escapeHtml(m.category)}]</span>` : ''}
|
||
</div>
|
||
<div style="color:#888;font-size:11px;line-height:1.4;">${escapeHtml(preview)}${m.content && m.content.length > 140 ? '...' : ''}</div>
|
||
</div>
|
||
<button class="btn secondary" onclick="event.stopPropagation();openMemoryModal('${m.id}')" style="padding:2px 8px;font-size:10px;flex-shrink:0;" title="Bearbeiten">✎</button>
|
||
<button class="btn secondary" onclick="event.stopPropagation();deleteMemory('${m.id}')" style="padding:2px 8px;font-size:10px;flex-shrink:0;color:#FF6B6B;border-color:#FF6B6B;" title="Loeschen">🗑</button>
|
||
</div>`;
|
||
}
|
||
|
||
function renderBrainList(items, isSearchResult) {
|
||
const el = document.getElementById('brain-memory-list');
|
||
if (!el) return;
|
||
if (isSearchResult) {
|
||
// Such-Treffer: in Aehnlichkeits-Reihenfolge, kein Type-Gruppieren
|
||
const html = items.map(m => renderMemoryRow(m, true)).join('');
|
||
el.innerHTML = html || '(Keine Treffer)';
|
||
return;
|
||
}
|
||
// Normale Liste: nach Type gruppieren
|
||
const byType = {};
|
||
items.forEach(m => { (byType[m.type] = byType[m.type] || []).push(m); });
|
||
const html = BRAIN_TYPE_ORDER.flatMap(t => {
|
||
if (!byType[t]) return [];
|
||
const heading = `<div style="margin-top:14px;color:#0096FF;font-weight:bold;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;">${BRAIN_TYPE_LABELS[t] || t} (${byType[t].length})</div>`;
|
||
const rows = byType[t].map(m => renderMemoryRow(m, false)).join('');
|
||
return [heading, rows];
|
||
}).join('');
|
||
// Falls Items mit unbekannten Types existieren, hinten dranhaengen
|
||
const extraTypes = Object.keys(byType).filter(t => !BRAIN_TYPE_ORDER.includes(t));
|
||
let extra = '';
|
||
for (const t of extraTypes) {
|
||
extra += `<div style="margin-top:14px;color:#0096FF;font-weight:bold;font-size:11px;text-transform:uppercase;">${escapeHtml(t)} (${byType[t].length})</div>`;
|
||
extra += byType[t].map(m => renderMemoryRow(m, false)).join('');
|
||
}
|
||
el.innerHTML = (html + extra) || '(Keine bekannten Typen gefunden)';
|
||
}
|
||
|
||
// ── Memory CRUD ───────────────────────────────────
|
||
|
||
function openMemoryModal(id) {
|
||
const modal = document.getElementById('memory-modal');
|
||
const titleEl = document.getElementById('memory-modal-title');
|
||
const idEl = document.getElementById('memory-edit-id');
|
||
const errEl = document.getElementById('memory-modal-error');
|
||
errEl.style.display = 'none';
|
||
|
||
if (id && brainMemoryCache[id]) {
|
||
const m = brainMemoryCache[id];
|
||
titleEl.textContent = 'Memory bearbeiten';
|
||
idEl.value = id;
|
||
document.getElementById('memory-type').value = m.type || 'fact';
|
||
document.getElementById('memory-title').value = m.title || '';
|
||
document.getElementById('memory-content').value = m.content || '';
|
||
document.getElementById('memory-category').value = m.category || '';
|
||
document.getElementById('memory-tags').value = (m.tags || []).join(', ');
|
||
document.getElementById('memory-pinned').checked = !!m.pinned;
|
||
} else {
|
||
titleEl.textContent = 'Neue Memory';
|
||
idEl.value = '';
|
||
document.getElementById('memory-type').value = 'fact';
|
||
document.getElementById('memory-title').value = '';
|
||
document.getElementById('memory-content').value = '';
|
||
document.getElementById('memory-category').value = '';
|
||
document.getElementById('memory-tags').value = '';
|
||
document.getElementById('memory-pinned').checked = false;
|
||
}
|
||
modal.classList.add('open');
|
||
}
|
||
|
||
function closeMemoryModal() {
|
||
document.getElementById('memory-modal').classList.remove('open');
|
||
}
|
||
|
||
async function saveMemory() {
|
||
const errEl = document.getElementById('memory-modal-error');
|
||
errEl.style.display = 'none';
|
||
const id = document.getElementById('memory-edit-id').value;
|
||
const type = document.getElementById('memory-type').value;
|
||
const title = document.getElementById('memory-title').value.trim();
|
||
const content = document.getElementById('memory-content').value.trim();
|
||
const category = document.getElementById('memory-category').value.trim();
|
||
const tags = document.getElementById('memory-tags').value.split(',').map(t => t.trim()).filter(Boolean);
|
||
const pinned = document.getElementById('memory-pinned').checked;
|
||
|
||
if (!title) { errEl.textContent = 'Titel fehlt.'; errEl.style.display = 'block'; return; }
|
||
if (!content) { errEl.textContent = 'Inhalt fehlt.'; errEl.style.display = 'block'; return; }
|
||
|
||
try {
|
||
let r;
|
||
if (id) {
|
||
r = await fetch('/api/brain/memory/update/' + encodeURIComponent(id), {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ title, content, pinned, category, tags }),
|
||
});
|
||
} else {
|
||
r = await fetch('/api/brain/memory/save', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ type, title, content, pinned, category, tags, source: 'manual' }),
|
||
});
|
||
}
|
||
if (!r.ok) {
|
||
const txt = await r.text();
|
||
throw new Error('HTTP ' + r.status + ': ' + txt.slice(0, 200));
|
||
}
|
||
closeMemoryModal();
|
||
loadBrainMemoryList();
|
||
loadBrainStatus();
|
||
} catch (e) {
|
||
errEl.textContent = e.message;
|
||
errEl.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
async function deleteMemory(id) {
|
||
const m = brainMemoryCache[id];
|
||
const label = m ? `"${m.title}" (${m.type})` : id;
|
||
if (!confirm(`Memory ${label} wirklich löschen?`)) return;
|
||
try {
|
||
const r = await fetch('/api/brain/memory/delete/' + encodeURIComponent(id), { method: 'DELETE' });
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
loadBrainMemoryList();
|
||
loadBrainStatus();
|
||
} catch (e) {
|
||
alert('Löschen fehlgeschlagen: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// snapshotSessions entfernt — aria-core ist raus.
|
||
|
||
function escapeHtml(s) {
|
||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
}
|
||
|
||
function brainExport() {
|
||
// Browser folgt der Download-Header-Antwort automatisch
|
||
window.location.href = '/api/brain-export';
|
||
}
|
||
|
||
// ── Migration aus brain-import/ ────────────────────────
|
||
async function runMigration() {
|
||
const status = document.getElementById('brain-import-files-status');
|
||
if (!confirm('Migration starten?\n\nParst die Markdown-Dateien aus brain-import/ und schreibt sie als pinned Memory-Punkte in die DB. Bei Re-Run werden nur die Migration-Punkte ersetzt — eigene Memories bleiben.')) return;
|
||
if (status) status.innerHTML = '⏳ Migriere...';
|
||
try {
|
||
const r = await fetch('/api/brain/memory/migrate', { method: 'POST' });
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
const d = await r.json();
|
||
if (d.error) throw new Error(d.error);
|
||
const files = (d.files || []).join(', ');
|
||
if (status) status.innerHTML = `<span style="color:#3FFF3F;">✓ ${d.created} Memories erstellt aus: ${files}</span>`;
|
||
loadBrainMemoryList();
|
||
loadBrainStatus();
|
||
} catch (e) {
|
||
if (status) status.innerHTML = `<span style="color:#FF6B6B;">✗ ${e.message}</span>`;
|
||
}
|
||
}
|
||
|
||
async function refreshImportFiles() {
|
||
const el = document.getElementById('brain-import-files-status');
|
||
if (!el) return;
|
||
try {
|
||
const r = await fetch('/api/brain/memory/import-files');
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
const d = await r.json();
|
||
if (!d.exists) {
|
||
el.innerHTML = `<span style="color:#FF9500;">brain-import/ nicht gemountet (${d.import_dir})</span>`;
|
||
return;
|
||
}
|
||
if (!d.files.length) {
|
||
el.innerHTML = '<span style="color:#FF9500;">Keine .md-Dateien in brain-import/</span>';
|
||
return;
|
||
}
|
||
const fmt = d.files.map(f => `${f.name} (${(f.size/1024).toFixed(1)}KB)`).join(', ');
|
||
el.innerHTML = `<span style="color:#555570;">Verfügbar: ${escapeHtml(fmt)}</span>`;
|
||
} catch (e) {
|
||
el.innerHTML = `<span style="color:#555570;">Brain nicht erreichbar</span>`;
|
||
}
|
||
}
|
||
|
||
// ── Bootstrap Export / Import ──────────────────────────
|
||
async function exportBootstrap() {
|
||
const status = document.getElementById('bootstrap-status');
|
||
if (status) status.innerHTML = '⏳ Lade...';
|
||
try {
|
||
const r = await fetch('/api/brain/memory/export-bootstrap');
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
const data = await r.json();
|
||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `aria-bootstrap-${ts}.json`;
|
||
document.body.appendChild(a); a.click();
|
||
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
|
||
if (status) status.innerHTML = `<span style="color:#3FFF3F;">✓ ${data.count} pinned Memories exportiert</span>`;
|
||
} catch (e) {
|
||
if (status) status.innerHTML = `<span style="color:#FF6B6B;">✗ ${e.message}</span>`;
|
||
}
|
||
}
|
||
|
||
async function importBootstrap(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
const status = document.getElementById('bootstrap-status');
|
||
try {
|
||
const text = await file.text();
|
||
const bundle = JSON.parse(text);
|
||
if (!Array.isArray(bundle.memories)) throw new Error('Datei hat kein "memories"-Array');
|
||
if (!confirm(`Bootstrap importieren?\n\n${bundle.memories.length} pinned Memories aus "${file.name}".\n\nALLE aktuell pinned Memories werden überschrieben. Cold Memory bleibt unverändert.`)) {
|
||
event.target.value = '';
|
||
return;
|
||
}
|
||
if (status) status.innerHTML = '⏳ Importiere...';
|
||
const r = await fetch('/api/brain/memory/import-bootstrap', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: text,
|
||
});
|
||
if (!r.ok) {
|
||
const errText = await r.text();
|
||
throw new Error('HTTP ' + r.status + ': ' + errText.slice(0, 200));
|
||
}
|
||
const d = await r.json();
|
||
if (status) status.innerHTML = `<span style="color:#3FFF3F;">✓ ${d.created} Memories importiert</span>`;
|
||
loadBrainMemoryList();
|
||
loadBrainStatus();
|
||
} catch (e) {
|
||
if (status) status.innerHTML = `<span style="color:#FF6B6B;">✗ ${e.message}</span>`;
|
||
} finally {
|
||
event.target.value = '';
|
||
}
|
||
}
|
||
|
||
async function brainImport(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
const statusEl = document.getElementById('brain-import-status');
|
||
if (!confirm(`Wirklich importieren? ALLE aktuellen Memories + Skills gehen verloren.\nDatei: ${file.name} (${(file.size/1024/1024).toFixed(1)} MB)`)) {
|
||
event.target.value = '';
|
||
return;
|
||
}
|
||
statusEl.innerHTML = `⏳ Lade hoch (${(file.size/1024/1024).toFixed(1)} MB) — Container werden gestoppt...`;
|
||
try {
|
||
const r = await fetch('/api/brain-import', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/gzip' },
|
||
body: file,
|
||
});
|
||
const d = await r.json();
|
||
if (d.ok) {
|
||
statusEl.innerHTML = `<span style="color:#3FFF3F;">✓ ${d.message}</span>`;
|
||
setTimeout(() => { loadBrainStatus(); loadBrainMemoryList(); }, 3000);
|
||
} else {
|
||
statusEl.innerHTML = `<span style="color:#FF6B6B;">✗ Fehler: ${d.error}</span>`;
|
||
}
|
||
} catch (e) {
|
||
statusEl.innerHTML = `<span style="color:#FF6B6B;">✗ Upload fehlgeschlagen: ${e.message}</span>`;
|
||
} finally {
|
||
event.target.value = '';
|
||
}
|
||
}
|
||
|
||
// ── Einstellungen: Tool-Berechtigungen ──────────────────
|
||
// Granulare Permissions entfernt — Claude Code laeuft mit
|
||
// --dangerously-skip-permissions (Alles oder Nichts).
|
||
// Siehe docker-compose.yml: CLAUDE_CODE_BUBBLEWRAP=1
|
||
|
||
// ── Einstellungen: Model ────────────────────────────────
|
||
|
||
function loadModel() {
|
||
send({ action: 'get_model' });
|
||
}
|
||
|
||
function saveModel() {
|
||
const model = document.getElementById('setting-model').value.trim();
|
||
if (!model) return;
|
||
send({ action: 'set_model', model });
|
||
}
|
||
|
||
// ── Einstellungen: OpenClaw Config ──────────────────────
|
||
|
||
// loadOpenClawConfig entfernt — aria-core ist raus.
|
||
|
||
// Toggle-Checkbox initial korrekt setzen
|
||
const ttsToggleEl = document.getElementById('tts-debug-toggle');
|
||
if (ttsToggleEl) ttsToggleEl.checked = showTtsDebug;
|
||
const gpsToggleEl = document.getElementById('gps-debug-toggle');
|
||
if (gpsToggleEl) gpsToggleEl.checked = showGpsDebug;
|
||
|
||
// Disk-Space Banner aktualisieren (wird vom Server via disk_status gepusht)
|
||
function updateDiskBanner(status) {
|
||
const banner = document.getElementById('disk-banner');
|
||
const icon = document.getElementById('disk-banner-icon');
|
||
const text = document.getElementById('disk-banner-text');
|
||
if (!banner) return;
|
||
if (!status || status.level === 'ok') {
|
||
banner.style.display = 'none';
|
||
return;
|
||
}
|
||
const gb = (n) => (n / 1024 / 1024 / 1024).toFixed(1);
|
||
const pct = status.percent;
|
||
const used = gb(status.usedBytes);
|
||
const total = gb(status.totalBytes);
|
||
const avail = gb(status.availBytes);
|
||
let bg, col, msg;
|
||
if (status.level === 'critical') {
|
||
bg = '#5C1A1A'; col = '#FF6B6B'; icon.innerHTML = '🚨'; // 🚨
|
||
msg = `KRITISCH: Platte ${pct}% voll (${used}GB von ${total}GB, nur noch ${avail}GB frei). aria-core kann bald nicht mehr schreiben — sofort aufraeumen!`;
|
||
} else if (status.level === 'warn') {
|
||
bg = '#5C3A1A'; col = '#FFAA55'; icon.innerHTML = '⚠️'; // ⚠️
|
||
msg = `Warnung: Platte ${pct}% voll (${avail}GB frei). Bald aufraeumen.`;
|
||
} else {
|
||
bg = '#4A3A1A'; col = '#FFD60A'; icon.innerHTML = 'ℹ️'; // ℹ️
|
||
msg = `Hinweis: Platte ${pct}% voll (${avail}GB frei).`;
|
||
}
|
||
banner.style.background = bg;
|
||
banner.style.color = col;
|
||
banner.style.borderBottom = `2px solid ${col}`;
|
||
text.textContent = msg;
|
||
banner.style.display = 'block';
|
||
}
|
||
|
||
function copyDiskCmd(variant) {
|
||
const cmd = variant === 'aggressive'
|
||
? 'docker system prune -a --volumes -f'
|
||
: 'docker builder prune -a -f && docker image prune -a -f';
|
||
navigator.clipboard.writeText(cmd).then(() => {
|
||
const btn = event.target;
|
||
const old = btn.textContent;
|
||
btn.textContent = 'Kopiert!';
|
||
setTimeout(() => { btn.textContent = old; }, 1500);
|
||
}).catch(() => {
|
||
alert('Kopieren fehlgeschlagen — Befehl: ' + cmd);
|
||
});
|
||
}
|
||
|
||
connectWS();
|
||
</script>
|
||
</body>
|
||
</html>
|