2162 lines
103 KiB
HTML
2162 lines
103 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.pipeline-step { color: #0096FF; border-left: 2px solid #0096FF; padding-left: 6px; margin: 2px 0; }
|
|
.log-entry.pipeline-ok { color: #34C759; border-left: 2px solid #34C759; padding-left: 6px; margin: 2px 0; }
|
|
.log-entry.pipeline-err { color: #FF3B30; border-left: 2px solid #FF3B30; padding-left: 6px; margin: 2px 0; }
|
|
.log-entry.pipeline-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: 8px; margin-bottom: 8px; }
|
|
.chat-msg { margin-bottom: 6px; padding: 6px 10px; border-radius: 6px; font-size: 13px; line-height: 1.5; word-wrap: break-word; }
|
|
.chat-msg.sent { background: #0096FF; color: #fff; margin-left: 20%; text-align: right; }
|
|
.chat-msg.received { background: #1E1E2E; margin-right: 20%; }
|
|
.chat-msg.error { background: #3B1010; color: #FF6B6B; }
|
|
.chat-msg .meta { font-size: 10px; color: rgba(255,255,255,0.4); margin-top: 2px; }
|
|
.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: 4px; margin-top: 4px; 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; }
|
|
.input-row input { flex: 1; background: #1E1E2E; border: 1px solid #333; border-radius: 6px;
|
|
padding: 8px 12px; color: #E0E0F0; font-family: inherit; font-size: 13px; }
|
|
|
|
/* 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; }
|
|
.perm-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:8px; }
|
|
.perm-item { display:flex; align-items:center; justify-content:space-between; background:#12122A;
|
|
border:1px solid #1E1E2E; border-radius:6px; padding:8px 12px; }
|
|
.perm-item .perm-info { flex:1; min-width:0; }
|
|
.perm-item .perm-name { font-size:13px; color:#E0E0F0; font-weight:bold; }
|
|
.perm-item .perm-desc { font-size:10px; color:#8888AA; margin-top:2px; }
|
|
.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>
|
|
<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('settings')">Einstellungen</button>
|
|
</div>
|
|
|
|
<!-- ══════ TAB: Main ══════ -->
|
|
<div id="tab-main" class="main-tab visible">
|
|
|
|
<!-- Verbindungsstatus -->
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h2>OpenClaw Gateway</h2>
|
|
<div class="status-row">
|
|
<div class="dot" id="gw-dot"></div>
|
|
<span class="status-label" id="gw-status">-</span>
|
|
</div>
|
|
<div class="error-text" id="gw-error"></div>
|
|
<div id="core-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>
|
|
<button class="btn secondary" onclick="send({action:'reconnect_gateway'})">Reconnect</button>
|
|
<button class="btn secondary" onclick="send({action:'check_core_auth'})">Agent-Auth pruefen</button>
|
|
<button class="btn secondary" id="btn-core-term" onclick="openCoreTerminal()">Core Terminal</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>
|
|
<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>
|
|
<button class="btn secondary" onclick="cancelRequest()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;">Abbrechen</button>
|
|
</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>
|
|
<input type="text" id="chat-input" placeholder="Nachricht an ARIA..." onpaste="handleDiagPaste(event)">
|
|
<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;">
|
|
<input type="text" id="chat-input-fs" placeholder="Nachricht an ARIA..." onkeydown="if(event.key==='Enter'){testRVSFS();event.preventDefault();}">
|
|
<button class="btn" onclick="testGatewayFS()">Gateway senden</button>
|
|
<button class="btn" onclick="testRVSFS()">Via RVS senden</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Session + Brain Viewer -->
|
|
<div class="grid" style="grid-template-columns: 1fr 1fr;">
|
|
<div class="card">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
|
<h2>Sessions</h2>
|
|
<div style="display:flex;gap:4px;">
|
|
<button class="btn secondary" onclick="createSession()" style="padding:4px 10px;font-size:11px;">+ Neu</button>
|
|
<button class="btn secondary" onclick="loadSessions()" style="padding:4px 10px;font-size:11px;">Laden</button>
|
|
</div>
|
|
</div>
|
|
<div id="active-session-bar" style="font-size:10px;color:#34C759;margin-bottom:6px;padding:4px 6px;background:#0D0D1A;border-radius:4px;display:none;">
|
|
Aktiv: <span id="active-session-name" style="font-weight:bold;"></span>
|
|
</div>
|
|
<div id="sessions-list" style="max-height:300px;overflow-y:auto;font-size:12px;"></div>
|
|
<div id="session-detail" style="display:none;margin-top:8px;background:#080810;border:1px solid #1E1E2E;border-radius:4px;padding:8px;max-height:300px;overflow-y:auto;">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
|
|
<span style="font-size:11px;color:#0096FF;font-weight:bold;" id="session-detail-title"></span>
|
|
<button class="btn secondary" onclick="closeSessionDetail()" style="padding:2px 8px;font-size:10px;">Schliessen</button>
|
|
</div>
|
|
<div id="session-detail-content" style="font-size:11px;line-height:1.5;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
|
<h2>Brain / Memory</h2>
|
|
<button class="btn secondary" onclick="loadBrain()" style="padding:4px 10px;font-size:11px;">Laden</button>
|
|
</div>
|
|
<div id="brain-list" style="max-height:200px;overflow-y:auto;font-size:12px;"></div>
|
|
<div id="brain-content" style="display:none;margin-top:8px;background:#080810;border:1px solid #1E1E2E;border-radius:4px;padding:8px;max-height:300px;overflow-y:auto;">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
|
|
<span style="font-size:11px;color:#0096FF;font-weight:bold;" id="brain-content-title"></span>
|
|
<button class="btn secondary" onclick="closeBrainContent()" style="padding:2px 8px;font-size:10px;">Schliessen</button>
|
|
</div>
|
|
<pre id="brain-content-text" style="font-size:11px;line-height:1.5;white-space:pre-wrap;color:#E0E0F0;margin:0;"></pre>
|
|
</div>
|
|
<div id="brain-empty" style="display:none;text-align:center;padding:20px;color:#555570;">
|
|
<div style="font-size:24px;margin-bottom:8px;">🧠</div>
|
|
<div style="font-size:12px;">Gehirn ist leer</div>
|
|
<div style="font-size:10px;margin-top:4px;">ARIA speichert Erinnerungen wenn sie etwas Wichtiges lernt</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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="pipeline" onclick="switchTab('pipeline')" style="margin-left:auto;border-color:#0096FF44;color:#0096FF">Pipeline <span class="tab-count" id="count-pipeline">0</span></button>
|
|
<button class="tab-btn" data-tab="tts" onclick="switchTab('tts')" style="border-color:#34C75944;color:#34C759">TTS</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-pipeline"></div>
|
|
<div class="log-box hidden" id="log-tts" style="padding:12px;">
|
|
<h3 style="color:#34C759;margin:0 0 12px;">TTS Diagnose (XTTS)</h3>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px;">
|
|
<div style="background:#1E1E2E;padding:8px;border-radius:6px;">
|
|
<div style="color:#8888AA;font-size:10px;text-transform:uppercase;">Status</div>
|
|
<div style="font-size:14px;margin-top:4px;" id="tts-status">Unbekannt</div>
|
|
</div>
|
|
<div style="background:#1E1E2E;padding:8px;border-radius:6px;">
|
|
<div style="color:#8888AA;font-size:10px;text-transform:uppercase;">Letzter Fehler</div>
|
|
<div style="color:#FF6B6B;font-size:12px;margin-top:4px;word-break:break-all;" id="tts-last-error">-</div>
|
|
</div>
|
|
</div>
|
|
<div style="margin-bottom:8px;">
|
|
<input type="text" id="tts-test-text" value="Hallo Stefan, ich bin ARIA." placeholder="Test-Text..." style="background:#1E1E2E;border:1px solid #2A2A3E;border-radius:6px;padding:8px;color:#fff;font-size:13px;width:100%;box-sizing:border-box;">
|
|
</div>
|
|
<div style="display:flex;gap:8px;">
|
|
<button class="btn" onclick="testTTS('')" style="flex:1;">XTTS testen</button>
|
|
<button class="btn secondary" onclick="checkTTSStatus()" style="flex:1;">Status pruefen</button>
|
|
</div>
|
|
<div id="tts-log" style="margin-top:12px;max-height:200px;overflow-y:auto;font-size:11px;font-family:monospace;color:#8888AA;"></div>
|
|
</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">
|
|
|
|
<!-- 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">
|
|
<h2>Sprachausgabe</h2>
|
|
<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>
|
|
|
|
<!-- XTTS Stimme -->
|
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
|
<label style="color:#8888AA;font-size:12px;">XTTS 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="">Standard (XTTS Default)</option>
|
|
</select>
|
|
<button class="btn secondary" onclick="loadXTTSVoices()" style="padding:4px 10px;font-size:11px;">Laden</button>
|
|
</div>
|
|
|
|
<!-- Gecloned Stimmen — Liste mit Loeschen -->
|
|
<div id="xtts-voice-list" style="margin-bottom:12px;"></div>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- Highlight-Trigger -->
|
|
<div class="settings-section">
|
|
<h2>Highlight-Trigger</h2>
|
|
<div style="font-size:11px;color:#8888AA;margin-bottom:8px;">
|
|
Woerter die automatisch die Highlight-Stimme (Thorsten) ausloesen.
|
|
Eines pro Zeile. Aenderungen werden in der Bridge gespeichert.
|
|
</div>
|
|
<div class="card" style="max-width:500px;">
|
|
<textarea id="highlight-triggers" rows="8" style="width:100%;box-sizing:border-box;background:#1E1E2E;border:1px solid #2A2A3E;border-radius:6px;padding:8px;color:#fff;font-size:13px;font-family:monospace;resize:vertical;"
|
|
placeholder="Lade..."></textarea>
|
|
<div style="display:flex;gap:8px;margin-top:8px;">
|
|
<button class="btn" onclick="saveHighlightTriggers()" style="flex:1;">Speichern</button>
|
|
<button class="btn secondary" onclick="loadHighlightTriggers()" style="flex:1;">Neu laden</button>
|
|
</div>
|
|
<div id="trigger-status" style="font-size:11px;color:#555570;margin-top:6px;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tool-Berechtigungen -->
|
|
<div class="settings-section">
|
|
<h2>Tool-Berechtigungen</h2>
|
|
<div style="font-size:11px;color:#8888AA;margin-bottom:12px;">
|
|
Claude Code laeuft mit <code>--dangerously-skip-permissions</code> im Proxy — Alles oder Nichts.
|
|
Granulare Tool-Kontrolle ist in dieser Architektur nicht moeglich.
|
|
</div>
|
|
<div class="card" style="max-width:500px;">
|
|
<div class="perm-item" style="border:none;padding:0;">
|
|
<div class="perm-info">
|
|
<div class="perm-name">Claude darf alle Tools benutzen</div>
|
|
<div class="perm-desc">WebFetch, Bash, Read/Write/Edit, WebSearch, Agent, etc.</div>
|
|
</div>
|
|
<label class="toggle"><input type="checkbox" checked disabled><span class="slider"></span></label>
|
|
</div>
|
|
<div style="font-size:10px;color:#555570;margin-top:8px;">
|
|
Deaktivieren = <code>--dangerously-skip-permissions</code> aus docker-compose.yml entfernen.
|
|
Dann kann ARIA keine Tools mehr benutzen (auch kein WebFetch fuer Wetter etc.).
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Model-Einstellungen -->
|
|
<div class="settings-section">
|
|
<h2>Model</h2>
|
|
<div class="card" style="max-width:500px;">
|
|
<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="z.B. proxy/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 Info -->
|
|
<div class="settings-section">
|
|
<h2>OpenClaw Config</h2>
|
|
<div class="card">
|
|
<button class="btn secondary" onclick="loadOpenClawConfig()" style="padding:4px 12px;font-size:11px;margin-bottom:8px;">Config laden</button>
|
|
<pre id="openclaw-config" style="font-size:10px;color:#8888AA;white-space:pre-wrap;max-height:300px;overflow-y:auto;margin:0;">(Noch nicht geladen)</pre>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /tab-settings -->
|
|
|
|
<!-- 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, pipeline: true };
|
|
const logCounts = { all: 0, gateway: 0, rvs: 0, proxy: 0, bridge: 0, server: 0, pipeline: 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'),
|
|
pipeline: document.getElementById('log-pipeline'),
|
|
tts: document.getElementById('log-tts'),
|
|
};
|
|
|
|
// 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 === 'pipeline') return 'pipeline';
|
|
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: 'get_active_session' });
|
|
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 === 'tts_result') {
|
|
if (msg.ok) {
|
|
ttsLog(`\u2705 ${msg.voice}: ${msg.duration}ms, ${msg.size} bytes`);
|
|
document.getElementById('tts-status').textContent = 'OK';
|
|
document.getElementById('tts-status').style.color = '#34C759';
|
|
} else {
|
|
ttsLog(`\u274C Fehler: ${msg.error}`);
|
|
document.getElementById('tts-status').textContent = 'Fehler';
|
|
document.getElementById('tts-status').style.color = '#FF3B30';
|
|
document.getElementById('tts-last-error').textContent = msg.error;
|
|
}
|
|
return;
|
|
}
|
|
if (msg.type === 'tts_status') {
|
|
document.getElementById('tts-status').textContent = msg.ok ? 'OK' : 'Fehler';
|
|
document.getElementById('tts-status').style.color = msg.ok ? '#34C759' : '#FF3B30';
|
|
if (msg.error) { document.getElementById('tts-last-error').textContent = msg.error; ttsLog(`Fehler: ${msg.error}`); }
|
|
else { document.getElementById('tts-last-error').textContent = '-'; ttsLog('TTS OK'); }
|
|
return;
|
|
}
|
|
|
|
if (msg.type === 'agent_activity') {
|
|
updateThinkingIndicator(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 === '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;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (msg.type === 'trigger_list') {
|
|
const textarea = document.getElementById('highlight-triggers');
|
|
textarea.value = (msg.triggers || []).join('\n');
|
|
document.getElementById('trigger-status').textContent = msg.triggers.length + ' Trigger geladen';
|
|
document.getElementById('trigger-status').style.color = '#8888AA';
|
|
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 === '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);
|
|
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;
|
|
}
|
|
if (msg.type === 'core_auth') {
|
|
const el = document.getElementById('core-auth');
|
|
el.style.display = 'block';
|
|
el.textContent = msg.error ? `Fehler: ${msg.error}` : msg.info;
|
|
return;
|
|
}
|
|
// 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) {
|
|
const el = document.createElement('div');
|
|
el.className = `chat-msg ${m.type}`;
|
|
const escaped = escapeHtml(m.text);
|
|
const linked = linkifyText(escaped);
|
|
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 + Brain Viewer
|
|
if (msg.type === 'sessions_list') { renderSessions(msg); return; }
|
|
if (msg.type === 'session_detail') { renderSessionDetail(msg); return; }
|
|
if (msg.type === 'session_deleted') {
|
|
if (msg.ok) loadSessions();
|
|
else alert('Loeschen fehlgeschlagen: ' + (msg.error || '?'));
|
|
return;
|
|
}
|
|
if (msg.type === 'session_export') {
|
|
if (!msg.ok) { alert('Export fehlgeschlagen: ' + (msg.error || '?')); return; }
|
|
const blob = new Blob([msg.markdown], { type: 'text/markdown;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = msg.filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
|
|
return;
|
|
}
|
|
if (msg.type === 'active_session') {
|
|
updateActiveSessionBar(msg.sessionKey);
|
|
loadSessions(); // Tabelle neu rendern
|
|
send({ action: 'load_chat_history' }); // Chat-Verlauf der neuen Session laden
|
|
return;
|
|
}
|
|
if (msg.type === 'session_created') {
|
|
if (msg.ok) {
|
|
loadSessions();
|
|
} else {
|
|
alert('Session erstellen fehlgeschlagen: ' + (msg.error || '?'));
|
|
}
|
|
return;
|
|
}
|
|
if (msg.type === 'brain_list') { renderBrainList(msg); return; }
|
|
if (msg.type === 'brain_content') { renderBrainContent(msg); return; }
|
|
// Settings (permissions_list/permissions_saved entfernt — Alles-oder-Nichts via --dangerously-skip-permissions)
|
|
if (msg.type === 'session_restarted') {
|
|
const s = document.getElementById('perms-status');
|
|
s.style.display = 'block';
|
|
if (msg.status === 'restarting') {
|
|
s.style.color = '#FFD60A';
|
|
s.textContent = 'aria-core wird neu gestartet...';
|
|
} else if (msg.status === 'ok') {
|
|
s.style.color = '#34C759';
|
|
s.textContent = msg.info || 'Session neu gestartet!';
|
|
} else {
|
|
s.style.color = '#FF6B6B';
|
|
s.textContent = 'Restart fehlgeschlagen: ' + (msg.error || '?');
|
|
}
|
|
return;
|
|
}
|
|
if (msg.type === 'model_info') {
|
|
const el = document.getElementById('setting-model');
|
|
const st = document.getElementById('model-status');
|
|
if (msg.model) el.value = msg.model;
|
|
st.textContent = msg.info || '';
|
|
st.style.color = msg.error ? '#FF6B6B' : '#34C759';
|
|
return;
|
|
}
|
|
if (msg.type === 'openclaw_config') {
|
|
document.getElementById('openclaw-config').textContent = msg.config || msg.error || '(leer)';
|
|
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' });
|
|
}
|
|
|
|
function openCoreTerminal() {
|
|
document.getElementById('btn-core-term').disabled = true;
|
|
openTermModal('aria-core Shell', { action: 'core_terminal' });
|
|
}
|
|
|
|
function closeTermModal() {
|
|
document.getElementById('term-modal').classList.remove('open');
|
|
document.getElementById('btn-proxy-login').disabled = false;
|
|
document.getElementById('btn-core-term').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) {
|
|
// Gateway
|
|
const gw = state.gateway || {};
|
|
document.getElementById('gw-dot').className = `dot ${gw.status || 'disconnected'}`;
|
|
document.getElementById('gw-status').textContent =
|
|
(STATUS_LABELS[gw.status] || gw.status) + (gw.handshakeOk ? ' (Handshake OK)' : '');
|
|
document.getElementById('gw-error').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}`;
|
|
|
|
// Pipeline-Eintraege nur in Pipeline-Tab (nicht in Alle)
|
|
if (source === 'pipeline') {
|
|
const pipeLevel = level === 'error' ? 'pipeline-err' : level === 'info' && message.includes('>>>') ? 'pipeline-ok' : 'pipeline-step';
|
|
appendToLog('pipeline', pipeLevel, `${time} ${message}`);
|
|
logCounts.pipeline++;
|
|
document.getElementById('count-pipeline').textContent = logCounts.pipeline;
|
|
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;
|
|
}
|
|
|
|
// 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) {
|
|
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>`;
|
|
}
|
|
}
|
|
const html = `${linked}${ttsBlock}<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;
|
|
}
|
|
}
|
|
|
|
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',
|
|
};
|
|
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]));
|
|
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="deleteXttsVoice('${esc(v.name).replace(/'/g, "\\'")}')" style="padding:2px 8px;font-size:10px;color:#FF6B6B;" title="Stimme loeschen">X</button>`
|
|
+ `</div>`;
|
|
}
|
|
html += '</div>';
|
|
box.innerHTML = html;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// ── 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;
|
|
send({ action: 'send_voice_config', ttsEnabled, xttsVoice, whisperModel });
|
|
}
|
|
|
|
// ── 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>`;
|
|
}
|
|
}
|
|
|
|
// ── Highlight-Trigger ────────────────────────
|
|
function loadHighlightTriggers() {
|
|
send({ action: 'get_triggers' });
|
|
}
|
|
function saveHighlightTriggers() {
|
|
const text = document.getElementById('highlight-triggers').value;
|
|
const triggers = text.split('\n').map(t => t.trim()).filter(t => t.length > 0);
|
|
send({ action: 'save_triggers', triggers });
|
|
document.getElementById('trigger-status').textContent = 'Gespeichert (' + triggers.length + ' Trigger)';
|
|
document.getElementById('trigger-status').style.color = '#34C759';
|
|
}
|
|
// Beim Tab-Wechsel zu Einstellungen: Trigger laden
|
|
const origSwitchMainTab = typeof switchMainTab === 'function' ? switchMainTab : null;
|
|
|
|
// ── 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 });
|
|
}
|
|
|
|
// ── TTS Diagnose ─────────────────────────────
|
|
function ttsLog(msg) {
|
|
const el = document.getElementById('tts-log');
|
|
const time = new Date().toLocaleTimeString('de-DE');
|
|
el.innerHTML += `<div>[${time}] ${escapeHtml(msg)}</div>`;
|
|
el.scrollTop = el.scrollHeight;
|
|
}
|
|
|
|
function testTTS(voice) {
|
|
const text = document.getElementById('tts-test-text').value.trim();
|
|
if (!text) return;
|
|
ttsLog(`Teste ${voice}: "${text}"...`);
|
|
send({ action: 'test_tts', voice, text });
|
|
}
|
|
|
|
function checkTTSStatus() {
|
|
ttsLog('Pruefe TTS-Status...');
|
|
send({ action: 'check_tts' });
|
|
}
|
|
|
|
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,'>');
|
|
}
|
|
|
|
// Enter-Taste sendet via Gateway
|
|
document.getElementById('chat-input').addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') testRVS();
|
|
});
|
|
|
|
// 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';
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Session Viewer ────────────────────────────────────────
|
|
|
|
function loadSessions() {
|
|
document.getElementById('sessions-list').innerHTML = '<div style="color:#8888AA;padding:8px;">Lade...</div>';
|
|
send({ action: 'list_sessions' });
|
|
}
|
|
|
|
let currentActiveSession = '';
|
|
|
|
function renderSessions(data) {
|
|
const container = document.getElementById('sessions-list');
|
|
if (data.error) {
|
|
container.innerHTML = `<div style="color:#FF6B6B;padding:8px;">Fehler: ${escapeHtml(data.error)}</div>`;
|
|
return;
|
|
}
|
|
if (!data.sessions || data.sessions.length === 0) {
|
|
container.innerHTML = data.raw
|
|
? `<pre style="color:#555570;font-size:10px;white-space:pre-wrap;padding:8px;">${escapeHtml(data.raw)}</pre>`
|
|
: '<div style="color:#555570;padding:8px;text-align:center;">Keine Sessions gefunden</div>';
|
|
return;
|
|
}
|
|
|
|
const active = data.sessions.filter(s => !s.archived);
|
|
const archives = data.sessions.filter(s => s.archived);
|
|
|
|
const headerRow = '<tr style="color:#8888AA;font-size:10px;text-align:left;border-bottom:1px solid #1E1E2E;">'
|
|
+ '<th style="padding:4px 6px;">Session</th>'
|
|
+ '<th style="padding:4px 6px;">Msgs</th>'
|
|
+ '<th style="padding:4px 6px;">Zuletzt</th>'
|
|
+ '<th style="padding:4px 6px;"></th></tr>';
|
|
|
|
const rowFor = (s, opts) => {
|
|
const date = s.modified ? new Date(s.modified * 1000).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) : '?';
|
|
const key = escapeHtml(s.sessionKey || s.path.split('/').pop());
|
|
const orphanBadge = s.orphan ? ' <span style="background:#FF3B30;color:#fff;font-size:9px;padding:1px 4px;border-radius:3px;">verwaist</span>' : '';
|
|
const archivedBadge = s.archived ? ' <span style="background:#555570;color:#fff;font-size:9px;padding:1px 4px;border-radius:3px;">archiv</span>' : '';
|
|
const modelBadge = s.model ? `<div style="font-size:9px;color:#555570;">${escapeHtml(s.model)}</div>` : '';
|
|
const isActive = (s.sessionKey === currentActiveSession) && !s.archived;
|
|
const keyColor = isActive ? '#34C759' : (s.archived || s.orphan ? '#8888AA' : '#E0E0F0');
|
|
const activeBadge = isActive ? ' <span style="background:#34C759;color:#000;font-size:9px;padding:1px 4px;border-radius:3px;">aktiv</span>' : '';
|
|
const rowBg = isActive ? 'background:rgba(52,199,89,0.08);' : (s.archived ? 'background:rgba(136,136,170,0.04);' : '');
|
|
|
|
let actions = '';
|
|
if (s.archived) {
|
|
// Archive: nur Export + Loeschen (kein Aktivieren — wuerde aktive Session ueberschreiben)
|
|
actions = `<button class="btn secondary" onclick="event.stopPropagation();deleteSession('${escapeHtml(s.path)}')" style="padding:2px 6px;font-size:10px;color:#FF6B6B;margin-right:2px;" title="Archiv endgueltig loeschen">X</button>`
|
|
+ `<button class="btn secondary" onclick="event.stopPropagation();exportSession('${escapeHtml(s.path)}','${escapeHtml(s.sessionKey)}')" style="padding:2px 6px;font-size:10px;color:#8888AA;" title="Als Markdown exportieren">⬇</button>`;
|
|
} else {
|
|
actions = (isActive ? '' : `<button class="btn secondary" onclick="event.stopPropagation();activateSession('${escapeHtml(s.sessionKey)}')" style="padding:2px 6px;font-size:10px;color:#34C759;margin-right:2px;" title="Aktivieren">▶</button>`)
|
|
+ `<button class="btn secondary" onclick="event.stopPropagation();deleteSession('${escapeHtml(s.path)}')" style="padding:2px 6px;font-size:10px;color:#FF6B6B;margin-right:2px;" title="Loeschen">X</button>`
|
|
+ `<button class="btn secondary" onclick="event.stopPropagation();exportSession('${escapeHtml(s.path)}','${escapeHtml(s.sessionKey)}')" style="padding:2px 6px;font-size:10px;color:#8888AA;" title="Als Markdown exportieren">⬇</button>`;
|
|
}
|
|
|
|
return `<tr style="border-bottom:1px solid #0D0D1A;cursor:pointer;${rowBg}" onmouseover="this.style.background='#1E1E2E'" onmouseout="this.style.background='${isActive ? 'rgba(52,199,89,0.08)' : (s.archived ? 'rgba(136,136,170,0.04)' : '')}'">`
|
|
+ `<td style="padding:4px 6px;" onclick="viewSession('${escapeHtml(s.path)}')">`
|
|
+ `<div style="color:${keyColor};">${key}${activeBadge}${orphanBadge}${archivedBadge}</div>${modelBadge}</td>`
|
|
+ `<td style="padding:4px 6px;color:#8888AA;">${s.lines}</td>`
|
|
+ `<td style="padding:4px 6px;color:#8888AA;font-size:10px;">${date}</td>`
|
|
+ `<td style="padding:4px 6px;white-space:nowrap;">${actions}</td></tr>`;
|
|
};
|
|
|
|
let html = '<table style="width:100%;border-collapse:collapse;">' + headerRow;
|
|
for (const s of active) html += rowFor(s);
|
|
html += '</table>';
|
|
|
|
if (archives.length > 0) {
|
|
html += `<details style="margin-top:12px;" ${archives.length <= 5 ? 'open' : ''}>`
|
|
+ `<summary style="color:#8888AA;font-size:11px;cursor:pointer;padding:4px 0;">`
|
|
+ `Archivierte Versionen (${archives.length}) — von OpenClaw beim Session-Reset gesichert`
|
|
+ `</summary>`
|
|
+ `<table style="width:100%;border-collapse:collapse;margin-top:6px;">` + headerRow;
|
|
for (const s of archives) html += rowFor(s);
|
|
html += '</table></details>';
|
|
}
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function viewSession(path) {
|
|
const detail = document.getElementById('session-detail');
|
|
const title = document.getElementById('session-detail-title');
|
|
const content = document.getElementById('session-detail-content');
|
|
detail.style.display = 'block';
|
|
title.textContent = path.split('/').pop();
|
|
content.innerHTML = '<div style="color:#8888AA;">Lade...</div>';
|
|
send({ action: 'read_session', sessionPath: path });
|
|
}
|
|
|
|
function renderSessionDetail(data) {
|
|
const content = document.getElementById('session-detail-content');
|
|
if (data.error) {
|
|
content.innerHTML = `<div style="color:#FF6B6B;">${escapeHtml(data.error)}</div>`;
|
|
return;
|
|
}
|
|
if (data.raw) {
|
|
content.innerHTML = `<pre style="color:#555570;font-size:10px;white-space:pre-wrap;">${escapeHtml(data.raw)}</pre>`;
|
|
return;
|
|
}
|
|
if (!data.messages || data.messages.length === 0) {
|
|
content.innerHTML = '<div style="color:#555570;">Keine Nachrichten</div>';
|
|
return;
|
|
}
|
|
let html = '';
|
|
for (const msg of data.messages) {
|
|
const role = msg.role || msg.type || '?';
|
|
const text = extractMessageText(msg);
|
|
const roleColor = role === 'user' ? '#0096FF' : role === 'assistant' ? '#34C759' : '#8888AA';
|
|
html += `<div style="margin-bottom:4px;padding:4px 6px;border-left:2px solid ${roleColor};background:#0D0D1A;border-radius:0 4px 4px 0;">`
|
|
+ `<span style="color:${roleColor};font-size:10px;font-weight:bold;">${escapeHtml(role)}</span> `
|
|
+ `<span style="color:#E0E0F0;">${escapeHtml(text.slice(0, 500))}${text.length > 500 ? '...' : ''}</span>`
|
|
+ '</div>';
|
|
}
|
|
content.innerHTML = html;
|
|
}
|
|
|
|
function extractMessageText(msg) {
|
|
if (typeof msg.content === 'string') return msg.content;
|
|
if (Array.isArray(msg.content)) {
|
|
return msg.content.filter(c => c.type === 'text').map(c => c.text || '').join('');
|
|
}
|
|
if (msg.text) return msg.text;
|
|
if (msg.message) return typeof msg.message === 'string' ? msg.message : JSON.stringify(msg.message);
|
|
return JSON.stringify(msg).slice(0, 200);
|
|
}
|
|
|
|
function closeSessionDetail() {
|
|
document.getElementById('session-detail').style.display = 'none';
|
|
}
|
|
|
|
function deleteSession(path) {
|
|
const name = path.split('/').pop();
|
|
if (!confirm(`Session "${name}" wirklich loeschen?`)) return;
|
|
send({ action: 'delete_session', sessionPath: path });
|
|
}
|
|
|
|
function exportSession(path, sessionKey) {
|
|
send({ action: 'export_session', sessionPath: path, sessionKey });
|
|
}
|
|
|
|
function activateSession(sessionKey) {
|
|
send({ action: 'set_active_session', sessionKey });
|
|
}
|
|
|
|
function createSession() {
|
|
const name = prompt('Name fuer neue Session (a-z, 0-9, -, _):');
|
|
if (!name) return;
|
|
send({ action: 'create_session', sessionName: name.trim() });
|
|
}
|
|
|
|
function updateActiveSessionBar(sessionKey) {
|
|
currentActiveSession = sessionKey || '';
|
|
const bar = document.getElementById('active-session-bar');
|
|
const nameEl = document.getElementById('active-session-name');
|
|
if (currentActiveSession) {
|
|
bar.style.display = 'block';
|
|
nameEl.textContent = currentActiveSession;
|
|
} else {
|
|
bar.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// ── Brain Viewer ────────────────────────────────────────
|
|
|
|
function loadBrain() {
|
|
document.getElementById('brain-list').innerHTML = '<div style="color:#8888AA;padding:8px;">Lade...</div>';
|
|
document.getElementById('brain-empty').style.display = 'none';
|
|
send({ action: 'list_brain' });
|
|
}
|
|
|
|
function renderBrainList(data) {
|
|
const container = document.getElementById('brain-list');
|
|
const emptyEl = document.getElementById('brain-empty');
|
|
|
|
if (data.error) {
|
|
container.innerHTML = `<div style="color:#FF6B6B;padding:8px;">Fehler: ${escapeHtml(data.error)}</div>`;
|
|
emptyEl.style.display = 'none';
|
|
return;
|
|
}
|
|
if (!data.files || data.files.length === 0) {
|
|
container.innerHTML = '';
|
|
emptyEl.style.display = 'block';
|
|
return;
|
|
}
|
|
emptyEl.style.display = 'none';
|
|
|
|
const TYPE_COLORS = { user: '#0096FF', feedback: '#FFD60A', project: '#34C759', reference: '#FF9500' };
|
|
let html = '';
|
|
for (const f of data.files) {
|
|
if (f.name === '.gitkeep') continue;
|
|
const color = TYPE_COLORS[f.memType] || '#8888AA';
|
|
const date = f.modified ? new Date(f.modified * 1000).toLocaleString('de-DE') : '?';
|
|
html += `<div style="padding:6px 8px;border-bottom:1px solid #0D0D1A;cursor:pointer;display:flex;align-items:center;gap:8px;" onmouseover="this.style.background='#1E1E2E'" onmouseout="this.style.background=''" onclick="viewBrainFile('${escapeHtml(f.name)}')">`
|
|
+ `<span style="background:${color};width:8px;height:8px;border-radius:50%;flex-shrink:0;" title="${escapeHtml(f.memType || 'unbekannt')}"></span>`
|
|
+ `<div style="flex:1;min-width:0;">`
|
|
+ `<div style="color:#E0E0F0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${escapeHtml(f.name)}</div>`
|
|
+ (f.description ? `<div style="color:#8888AA;font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${escapeHtml(f.description)}</div>` : '')
|
|
+ `</div>`
|
|
+ `<div style="color:#555570;font-size:10px;white-space:nowrap;">${escapeHtml(f.size)}</div>`
|
|
+ '</div>';
|
|
}
|
|
container.innerHTML = html || '<div style="color:#555570;padding:8px;text-align:center;">Nur .gitkeep gefunden</div>';
|
|
}
|
|
|
|
function viewBrainFile(name) {
|
|
const panel = document.getElementById('brain-content');
|
|
const title = document.getElementById('brain-content-title');
|
|
const text = document.getElementById('brain-content-text');
|
|
panel.style.display = 'block';
|
|
title.textContent = name;
|
|
text.textContent = 'Lade...';
|
|
send({ action: 'read_brain_file', filename: name });
|
|
}
|
|
|
|
function renderBrainContent(data) {
|
|
const text = document.getElementById('brain-content-text');
|
|
if (data.error) {
|
|
text.textContent = `Fehler: ${data.error}`;
|
|
text.style.color = '#FF6B6B';
|
|
return;
|
|
}
|
|
text.style.color = '#E0E0F0';
|
|
text.textContent = data.content || '(leer)';
|
|
}
|
|
|
|
function closeBrainContent() {
|
|
document.getElementById('brain-content').style.display = 'none';
|
|
}
|
|
|
|
// ── 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
|
|
document.querySelectorAll('.main-nav-btn').forEach(b => {
|
|
if (b.textContent.trim().toLowerCase().includes(tab === 'main' ? 'main' : 'einstellung')) b.classList.add('active');
|
|
});
|
|
// Einstellungen: Config + Trigger + QR laden
|
|
if (tab === 'settings') {
|
|
loadHighlightTriggers();
|
|
send({ action: 'get_voice_config' });
|
|
loadRuntimeConfig();
|
|
loadOnboardingQR();
|
|
}
|
|
}
|
|
|
|
// ── 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 ──────────────────────
|
|
|
|
function loadOpenClawConfig() {
|
|
document.getElementById('openclaw-config').textContent = 'Lade...';
|
|
send({ action: 'get_openclaw_config' });
|
|
}
|
|
|
|
// Toggle-Checkbox initial korrekt setzen
|
|
const ttsToggleEl = document.getElementById('tts-debug-toggle');
|
|
if (ttsToggleEl) ttsToggleEl.checked = showTtsDebug;
|
|
|
|
connectWS();
|
|
</script>
|
|
</body>
|
|
</html>
|