hyperclaw 5.2.3 → 5.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +17 -111
  2. package/dist/chat-CbntPqAD.js +324 -0
  3. package/dist/chat-ChYGHacA.js +324 -0
  4. package/dist/daemon-BVz136O4.js +7 -0
  5. package/dist/daemon-DY73xDpS.js +7 -0
  6. package/dist/daemon-jRZ0qvfS.js +404 -0
  7. package/dist/daemon-rXG1-sEx.js +404 -0
  8. package/dist/engine-8auuZb7x.js +7 -0
  9. package/dist/engine-CJtV0X2-.js +7 -0
  10. package/dist/engine-COYE-fVt.js +323 -0
  11. package/dist/engine-fLaefti6.js +323 -0
  12. package/dist/hyperclawbot-CFz_TKkS.js +508 -0
  13. package/dist/hyperclawbot-sfzAMwh-.js +508 -0
  14. package/dist/mcp-loader-CMtuM3oJ.js +93 -0
  15. package/dist/mcp-loader-D_NWYUqh.js +93 -0
  16. package/dist/onboard-CO0j5jJ1.js +13 -0
  17. package/dist/onboard-DMTgAKzs.js +3865 -0
  18. package/dist/onboard-I3AirQv9.js +13 -0
  19. package/dist/onboard-cY-Emhr7.js +3865 -0
  20. package/dist/orchestrator-6ljWiZWu.js +189 -0
  21. package/dist/orchestrator-BJ08fjSq.js +6 -0
  22. package/dist/orchestrator-DrFHEhaP.js +189 -0
  23. package/dist/orchestrator-mdElwt2i.js +6 -0
  24. package/dist/run-main.js +25 -25
  25. package/dist/server-CRqwlLX9.js +1294 -0
  26. package/dist/server-DG804qOP.js +4 -0
  27. package/dist/server-DqXe03-t.js +4 -0
  28. package/dist/server-jaMeUOfM.js +1294 -0
  29. package/dist/skill-runtime-C-3EKfkr.js +102 -0
  30. package/dist/skill-runtime-CZ8uTBra.js +102 -0
  31. package/dist/skill-runtime-CpPJV2Ga.js +5 -0
  32. package/dist/skill-runtime-DwNX3pnp.js +5 -0
  33. package/dist/src-B3eBN96E.js +458 -0
  34. package/dist/src-D0vSZWOd.js +458 -0
  35. package/dist/src-ij58SST0.js +63 -0
  36. package/dist/src-sWcDdkdR.js +63 -0
  37. package/dist/sub-agent-tools-BI8UISC4.js +39 -0
  38. package/dist/sub-agent-tools-DYKPR3aI.js +39 -0
  39. package/package.json +1 -1
  40. package/static/chat.html +314 -187
  41. package/static/dashboard.html +133 -20
package/static/chat.html CHANGED
@@ -1,213 +1,340 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en" class="dark">
2
+ <html lang="en" data-theme="dark">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>HyperClaw Chat</title>
7
7
  <script src="https://cdn.tailwindcss.com"></script>
8
8
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
9
10
  <style>
10
- body{background:#0f172a;color:#e2e8f0;font-family:system-ui}
11
- .msg-user{background:linear-gradient(135deg,#0891b2 0%,#06b6d4 100%)}
12
- .msg-asst{background:#1e293b;border:1px solid #334155}
13
- .typing span{animation:dot 1.4s infinite}
14
- .typing span:nth-child(2){animation-delay:.2s}
15
- .typing span:nth-child(3){animation-delay:.4s}
16
- @keyframes dot{0%,80%,100%{opacity:.3}40%{opacity:1}}
17
- .prose pre{background:#0f172a;padding:.75rem;border-radius:6px;overflow-x:auto}
18
- .prose code{background:#1e293b;padding:.1em .3em;border-radius:4px;font-size:.9em}
11
+ :root {
12
+ --bg: #090e1a;
13
+ --bg2: #0d1526;
14
+ --bg3: #111d33;
15
+ --border: #1e2d4a;
16
+ --accent: #00c2ff;
17
+ --accent2: #0077b6;
18
+ --text: #e2eaf8;
19
+ --text2: #8ba0c0;
20
+ --user-bg: linear-gradient(135deg, #0077b6, #00c2ff);
21
+ --asst-bg: #111d33;
22
+ --header-bg: rgba(9,14,26,0.95);
23
+ --shadow: 0 4px 24px rgba(0,194,255,0.08);
24
+ }
25
+ [data-theme="light"] {
26
+ --bg: #eef4ff;
27
+ --bg2: #dde8ff;
28
+ --bg3: #ffffff;
29
+ --border: #b8cdf0;
30
+ --accent: #0077b6;
31
+ --accent2: #005f99;
32
+ --text: #0d1d3a;
33
+ --text2: #3a5580;
34
+ --user-bg: linear-gradient(135deg, #0077b6, #00aee0);
35
+ --asst-bg: #ffffff;
36
+ --header-bg: rgba(238,244,255,0.97);
37
+ --shadow: 0 4px 24px rgba(0,119,182,0.10);
38
+ }
39
+ * { box-sizing: border-box; margin: 0; padding: 0; }
40
+ body { background: var(--bg); color: var(--text); font-family: 'Inter', system-ui, sans-serif; height: 100vh; display: flex; flex-direction: column; transition: background .3s, color .3s; }
41
+
42
+ /* ── BANNER ─── */
43
+ .banner-wrap { background: var(--bg2); border-bottom: 1px solid var(--border); padding: 14px 20px; display: flex; align-items: center; gap: 16px; box-shadow: var(--shadow); position: sticky; top: 0; z-index: 100; backdrop-filter: blur(8px); background: var(--header-bg); }
44
+ .banner-logo { font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: clamp(18px, 4vw, 28px); letter-spacing: 2px; background: linear-gradient(90deg, var(--accent), #7dd4fc, var(--accent)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; text-shadow: none; line-height: 1; user-select: none; }
45
+ .banner-eagle { font-size: 28px; filter: drop-shadow(0 0 8px var(--accent)); }
46
+ .banner-info { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
47
+ .banner-sub { font-size: 11px; color: var(--text2); letter-spacing: 0.5px; font-family: 'JetBrains Mono', monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
48
+ .banner-status { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 500; }
49
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: #ef4444; flex-shrink: 0; }
50
+ .dot.connected { background: #22c55e; box-shadow: 0 0 6px #22c55e; animation: pulse-green 2s infinite; }
51
+ @keyframes pulse-green { 0%,100%{box-shadow:0 0 4px #22c55e} 50%{box-shadow:0 0 12px #22c55e} }
52
+ .banner-actions { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
53
+ .btn-icon { background: var(--bg3); border: 1px solid var(--border); color: var(--text2); width: 34px; height: 34px; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; transition: all .2s; }
54
+ .btn-icon:hover { border-color: var(--accent); color: var(--accent); }
55
+
56
+ /* ── MESSAGES ─── */
57
+ #messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 16px; scroll-behavior: smooth; }
58
+ #messages::-webkit-scrollbar { width: 4px; }
59
+ #messages::-webkit-scrollbar-track { background: transparent; }
60
+ #messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
61
+ .msg-row { display: flex; gap: 10px; max-width: 800px; animation: fadeIn .25s ease; }
62
+ @keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:none} }
63
+ .msg-row.user { flex-direction: row-reverse; align-self: flex-end; }
64
+ .msg-row.assistant { align-self: flex-start; }
65
+ .msg-avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; border: 1px solid var(--border); background: var(--bg3); }
66
+ .msg-bubble { max-width: min(70vw, 560px); padding: 12px 16px; border-radius: 16px; font-size: 14px; line-height: 1.6; }
67
+ .msg-row.user .msg-bubble { background: var(--user-bg); color: #fff; border-bottom-right-radius: 4px; }
68
+ .msg-row.assistant .msg-bubble { background: var(--asst-bg); border: 1px solid var(--border); border-bottom-left-radius: 4px; color: var(--text); }
69
+ .msg-bubble .prose h1,.msg-bubble .prose h2,.msg-bubble .prose h3 { color: var(--accent); margin: 8px 0 4px; font-size: 1em; font-weight: 600; }
70
+ .msg-bubble .prose p { margin: 4px 0; }
71
+ .msg-bubble .prose pre { background: var(--bg); border: 1px solid var(--border); padding: 10px 12px; border-radius: 8px; overflow-x: auto; margin: 8px 0; font-size: 12px; }
72
+ .msg-bubble .prose code { background: var(--bg); padding: 1px 5px; border-radius: 4px; font-size: 12px; font-family: 'JetBrains Mono', monospace; color: var(--accent); }
73
+ .msg-bubble .prose pre code { background: none; padding: 0; color: var(--text); }
74
+ .msg-bubble .prose ul,.msg-bubble .prose ol { padding-left: 18px; margin: 4px 0; }
75
+ .msg-bubble .prose a { color: var(--accent); text-decoration: underline; }
76
+ .msg-time { font-size: 10px; color: var(--text2); margin-top: 4px; text-align: right; }
77
+ .msg-row.assistant .msg-time { text-align: left; }
78
+
79
+ /* ── TYPING ─── */
80
+ .typing-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); animation: blink 1.3s infinite; }
81
+ .typing-dot:nth-child(2){animation-delay:.2s}.typing-dot:nth-child(3){animation-delay:.4s}
82
+ @keyframes blink{0%,80%,100%{opacity:.2}40%{opacity:1}}
83
+
84
+ /* ── INPUT ─── */
85
+ .input-wrap { padding: 12px 20px 16px; border-top: 1px solid var(--border); background: var(--header-bg); backdrop-filter: blur(8px); }
86
+ .input-row { display: flex; gap: 10px; align-items: flex-end; max-width: 800px; margin: 0 auto; }
87
+ #input { flex: 1; background: var(--bg3); border: 1px solid var(--border); color: var(--text); border-radius: 14px; padding: 12px 16px; font-size: 14px; font-family: 'Inter', sans-serif; resize: none; min-height: 46px; max-height: 120px; outline: none; transition: border .2s, box-shadow .2s; line-height: 1.5; }
88
+ #input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 15%, transparent); }
89
+ #input::placeholder { color: var(--text2); }
90
+ #send-btn { width: 46px; height: 46px; border-radius: 14px; background: var(--accent); border: none; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; transition: all .2s; flex-shrink: 0; }
91
+ #send-btn:hover:not(:disabled) { background: var(--accent2); transform: scale(1.05); }
92
+ #send-btn:disabled { opacity: 0.4; cursor: default; transform: none; }
93
+ .input-hint { font-size: 11px; color: var(--text2); text-align: center; margin-top: 6px; }
94
+
95
+ /* ── WELCOME ─── */
96
+ .welcome { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; gap: 12px; text-align: center; padding: 32px; }
97
+ .welcome-logo { font-family: 'JetBrains Mono', monospace; font-size: clamp(32px, 8vw, 64px); font-weight: 700; letter-spacing: 4px; background: linear-gradient(135deg, #7dd4fc, var(--accent), #0077b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; line-height: 1; }
98
+ .welcome-sub { font-size: 14px; color: var(--text2); max-width: 380px; line-height: 1.6; }
99
+ .welcome-chips { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 4px; }
100
+ .chip { background: var(--bg3); border: 1px solid var(--border); color: var(--text2); padding: 7px 14px; border-radius: 20px; font-size: 12px; cursor: pointer; transition: all .2s; }
101
+ .chip:hover { border-color: var(--accent); color: var(--accent); }
19
102
  </style>
20
103
  </head>
21
- <body class="min-h-screen flex flex-col">
22
- <header class="p-4 border-b border-slate-700 flex items-center justify-between flex-wrap gap-2">
23
- <div class="flex items-center gap-3">
24
- <span class="text-2xl">🦅</span>
25
- <div>
26
- <h1 class="text-xl font-bold text-cyan-400">HyperClaw Chat</h1>
27
- <p id="status" class="text-sm text-slate-400">Connecting...</p>
28
- </div>
29
- </div>
30
- <div class="flex items-center gap-2 text-sm">
31
- <span id="ws-dot" class="w-2 h-2 rounded-full bg-slate-500"></span>
32
- <span id="ws-status">WebSocket</span>
33
- <span id="agent-info" class="text-slate-500"></span>
104
+ <body>
105
+
106
+ <!-- HEADER BANNER -->
107
+ <header class="banner-wrap">
108
+ <span class="banner-eagle">🦅</span>
109
+ <div style="display:flex;flex-direction:column;gap:1px">
110
+ <div class="banner-logo">HYPERCLAW</div>
111
+ <div class="banner-sub" id="agent-sub">AI Gateway Platform · Connecting…</div>
112
+ </div>
113
+ <div style="flex:1"></div>
114
+ <div class="banner-status">
115
+ <span class="dot" id="ws-dot"></span>
116
+ <span id="ws-label" style="color:var(--text2);font-size:13px">Offline</span>
117
+ </div>
118
+ <div class="banner-actions">
119
+ <button class="btn-icon" id="theme-btn" title="Toggle theme">🌙</button>
120
+ <button class="btn-icon" id="clear-btn" title="Clear chat">🗑️</button>
121
+ </div>
122
+ </header>
123
+
124
+ <!-- MESSAGES -->
125
+ <main id="messages">
126
+ <div class="welcome" id="welcome">
127
+ <div class="welcome-logo">HYPERCLAW</div>
128
+ <p class="welcome-sub">Your personal AI assistant — running on your hardware. Ask anything or pick a suggestion:</p>
129
+ <div class="welcome-chips">
130
+ <span class="chip" onclick="fillInput(this)">What can you do?</span>
131
+ <span class="chip" onclick="fillInput(this)">What's the weather today?</span>
132
+ <span class="chip" onclick="fillInput(this)">Search the web for AI news</span>
133
+ <span class="chip" onclick="fillInput(this)">Help me write a script</span>
134
+ <span class="chip" onclick="fillInput(this)">Show system status</span>
34
135
  </div>
35
- </header>
36
- <main class="flex-1 overflow-y-auto p-4 space-y-4" id="messages"></main>
37
- <footer class="p-4 border-t border-slate-700">
38
- <form id="form" class="flex gap-2 items-end">
39
- <div class="flex-1">
40
- <textarea id="input" rows="1" placeholder="Message HyperClaw..."
41
- class="w-full bg-slate-800 border border-slate-600 rounded-xl px-4 py-3 text-white resize-none focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
42
- style="min-height:48px;max-height:120px" autocomplete="off"></textarea>
43
- </div>
44
- <button type="submit" id="send-btn" class="bg-cyan-600 hover:bg-cyan-700 disabled:opacity-50 px-5 py-3 rounded-xl font-medium h-12">
45
-
46
- </button>
47
- </form>
48
- </footer>
49
- <script>
50
- const port = location.port || 18789;
51
- const wsUrl = `ws://${location.hostname}:${port}`;
52
- const apiUrl = `http://${location.hostname}:${port}`;
53
- const messagesEl = document.getElementById('messages');
54
- const statusEl = document.getElementById('status');
55
- const wsDot = document.getElementById('ws-dot');
56
- const wsStatus = document.getElementById('ws-status');
57
- const agentInfo = document.getElementById('agent-info');
58
- const form = document.getElementById('form');
59
- const input = document.getElementById('input');
60
- const sendBtn = document.getElementById('send-btn');
61
-
62
- let ws = null;
63
- let isStreaming = false;
64
-
65
- function setWsConnected(ok) {
66
- wsDot.className = 'w-2 h-2 rounded-full ' + (ok ? 'bg-cyan-400 animate-pulse' : 'bg-red-500');
67
- wsStatus.textContent = ok ? 'Connected' : 'Disconnected';
68
- }
136
+ </div>
137
+ </main>
69
138
 
70
- function addMsg(role, content, streaming = false) {
71
- const div = document.createElement('div');
72
- div.className = 'flex gap-3 ' + (role === 'user' ? 'flex-row-reverse' : '');
73
- const bubble = document.createElement('div');
74
- bubble.className = 'max-w-[85%] rounded-2xl px-4 py-3 ' + (role === 'user' ? 'msg-user text-slate-900' : 'msg-asst');
75
- if (role === 'assistant' && !streaming) {
76
- bubble.innerHTML = '<div class="prose prose-invert prose-sm max-w-none">' + marked.parse(content || '') + '</div>';
77
- } else {
78
- bubble.textContent = content || '';
79
- }
80
- if (role === 'assistant') {
81
- const icon = document.createElement('div');
82
- icon.className = 'w-8 h-8 rounded-full bg-cyan-900/30 flex items-center justify-center flex-shrink-0';
83
- icon.textContent = '🦅';
84
- div.appendChild(icon);
85
- }
86
- div.appendChild(bubble);
87
- div.dataset.streaming = streaming ? '1' : '0';
88
- messagesEl.appendChild(div);
89
- messagesEl.scrollTop = messagesEl.scrollHeight;
90
- return bubble;
91
- }
139
+ <!-- INPUT -->
140
+ <div class="input-wrap">
141
+ <form class="input-row" id="form">
142
+ <textarea id="input" rows="1" placeholder="Message HyperClaw… (Enter to send, Shift+Enter for new line)" autocomplete="off"></textarea>
143
+ <button type="submit" id="send-btn">↑</button>
144
+ </form>
145
+ <div class="input-hint">🦅 Hyper · <span id="model-hint">—</span></div>
146
+ </div>
92
147
 
93
- function addTyping() {
94
- const div = document.createElement('div');
95
- div.className = 'flex gap-3';
96
- div.id = 'typing-indicator';
97
- div.innerHTML = '<div class="w-8 h-8 rounded-full bg-cyan-900/30 flex items-center justify-center">🦅</div><div class="msg-asst rounded-2xl px-4 py-3"><span class="typing"><span>.</span><span>.</span><span>.</span></span></div>';
98
- messagesEl.appendChild(div);
99
- messagesEl.scrollTop = messagesEl.scrollHeight;
100
- }
148
+ <script>
149
+ const port = location.port || 18789;
150
+ const wsUrl = `ws://${location.hostname}:${port}`;
151
+ const apiUrl = `http://${location.hostname}:${port}`;
101
152
 
102
- function removeTyping() {
103
- document.getElementById('typing-indicator')?.remove();
104
- }
153
+ let ws = null, isStreaming = false, currentBubble = null;
154
+ const messagesEl = document.getElementById('messages');
155
+ const welcomeEl = document.getElementById('welcome');
156
+ const wsDot = document.getElementById('ws-dot');
157
+ const wsLabel = document.getElementById('ws-label');
158
+ const agentSub = document.getElementById('agent-sub');
159
+ const modelHint = document.getElementById('model-hint');
160
+ const input = document.getElementById('input');
161
+ const sendBtn = document.getElementById('send-btn');
162
+ const form = document.getElementById('form');
105
163
 
106
- function connectWs() {
107
- ws = new WebSocket(wsUrl);
108
- ws.onopen = () => {
109
- setWsConnected(true);
110
- statusEl.textContent = 'Connected';
111
- };
112
- ws.onclose = () => {
113
- setWsConnected(false);
114
- statusEl.textContent = 'Disconnected — reconnecting…';
115
- setTimeout(connectWs, 3000);
116
- };
117
- ws.onerror = () => setWsConnected(false);
118
- ws.onmessage = (e) => {
119
- try {
120
- const msg = JSON.parse(e.data);
121
- if (msg.type === 'connect.ok' || msg.type === 'auth.ok') {
122
- statusEl.textContent = 'Ready';
123
- } else if (msg.type === 'chat:chunk') {
124
- const last = messagesEl.querySelector('[data-streaming="1"]');
125
- if (last) {
126
- last.textContent = (last.textContent || '') + (msg.content || '');
127
- } else {
128
- removeTyping();
129
- const bubble = addMsg('assistant', msg.content || '', true);
130
- bubble.dataset.streaming = '1';
131
- }
132
- messagesEl.scrollTop = messagesEl.scrollHeight;
133
- } else if (msg.type === 'chat:response') {
134
- removeTyping();
135
- const last = messagesEl.querySelector('[data-streaming="1"]');
136
- if (last) {
137
- last.dataset.streaming = '0';
138
- last.innerHTML = '<div class="prose prose-invert prose-sm max-w-none">' + marked.parse(last.textContent || msg.content || '') + '</div>';
139
- } else {
140
- addMsg('assistant', msg.content || '');
141
- }
142
- isStreaming = false;
143
- sendBtn.disabled = false;
144
- messagesEl.scrollTop = messagesEl.scrollHeight;
145
- } else if (msg.type === 'error') {
164
+ // ── Theme ────────────────────────────────────────────
165
+ const themeBtn = document.getElementById('theme-btn');
166
+ let dark = true;
167
+ themeBtn.onclick = () => {
168
+ dark = !dark;
169
+ document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
170
+ themeBtn.textContent = dark ? '🌙' : '☀️';
171
+ };
172
+
173
+ // ── Clear ────────────────────────────────────────────
174
+ document.getElementById('clear-btn').onclick = () => {
175
+ messagesEl.querySelectorAll('.msg-row').forEach(e => e.remove());
176
+ welcomeEl.style.display = 'flex';
177
+ };
178
+
179
+ // ── Chips ────────────────────────────────────────────
180
+ function fillInput(el) {
181
+ input.value = el.textContent;
182
+ input.focus();
183
+ autoResize();
184
+ }
185
+
186
+ // ── WebSocket ────────────────────────────────────────
187
+ function setConnected(ok) {
188
+ wsDot.className = 'dot' + (ok ? ' connected' : '');
189
+ wsLabel.textContent = ok ? 'Connected' : 'Reconnecting…';
190
+ wsLabel.style.color = ok ? '#22c55e' : 'var(--text2)';
191
+ }
192
+
193
+ function connectWs() {
194
+ ws = new WebSocket(wsUrl);
195
+ ws.onopen = () => { setConnected(true); };
196
+ ws.onclose = () => { setConnected(false); setTimeout(connectWs, 3000); };
197
+ ws.onerror = () => setConnected(false);
198
+ ws.onmessage = (e) => {
199
+ try {
200
+ const msg = JSON.parse(e.data);
201
+ if (msg.type === 'connect.ok' || msg.type === 'auth.ok') {
202
+ // connected
203
+ } else if (msg.type === 'chat:chunk') {
204
+ if (!currentBubble) {
146
205
  removeTyping();
147
- addMsg('assistant', 'Error: ' + (msg.message || 'Unknown'));
148
- isStreaming = false;
149
- sendBtn.disabled = false;
206
+ currentBubble = addMsg('assistant', '', true);
150
207
  }
151
- } catch (_) {}
152
- };
153
- }
208
+ currentBubble._raw = (currentBubble._raw || '') + (msg.content || '');
209
+ currentBubble.textContent = currentBubble._raw;
210
+ messagesEl.scrollTop = messagesEl.scrollHeight;
211
+ } else if (msg.type === 'chat:response') {
212
+ removeTyping();
213
+ if (currentBubble) {
214
+ currentBubble.innerHTML = '<div class="prose">' + marked.parse(currentBubble._raw || msg.content || '') + '</div>';
215
+ currentBubble = null;
216
+ } else {
217
+ addMsg('assistant', msg.content || '');
218
+ }
219
+ addTimestamp();
220
+ finishStreaming();
221
+ } else if (msg.type === 'error') {
222
+ removeTyping();
223
+ addMsg('assistant', '⚠️ ' + (msg.message || 'Unknown error'));
224
+ currentBubble = null;
225
+ finishStreaming();
226
+ }
227
+ } catch (_) {}
228
+ };
229
+ }
230
+
231
+ function finishStreaming() {
232
+ isStreaming = false;
233
+ sendBtn.disabled = false;
234
+ currentBubble = null;
235
+ messagesEl.scrollTop = messagesEl.scrollHeight;
236
+ }
154
237
 
155
- function sendViaWs(text) {
156
- if (!ws || ws.readyState !== WebSocket.OPEN) return false;
157
- ws.send(JSON.stringify({ type: 'chat:message', content: text, source: 'webchat' }));
158
- return true;
238
+ // ── Message helpers ───────────────────────────────────
239
+ function now() {
240
+ return new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
241
+ }
242
+ let lastTimestampEl = null;
243
+
244
+ function addMsg(role, content, streaming = false) {
245
+ welcomeEl.style.display = 'none';
246
+ const row = document.createElement('div');
247
+ row.className = 'msg-row ' + role;
248
+ const avatar = document.createElement('div');
249
+ avatar.className = 'msg-avatar';
250
+ avatar.textContent = role === 'user' ? '👤' : '🦅';
251
+ const bubble = document.createElement('div');
252
+ bubble.className = 'msg-bubble';
253
+ if (!streaming && content) {
254
+ bubble.innerHTML = '<div class="prose">' + (role === 'assistant' ? marked.parse(content) : escHtml(content)) + '</div>';
255
+ } else {
256
+ bubble.textContent = content || '';
159
257
  }
258
+ row.appendChild(avatar);
259
+ row.appendChild(bubble);
260
+ messagesEl.appendChild(row);
261
+ messagesEl.scrollTop = messagesEl.scrollHeight;
262
+ return bubble;
263
+ }
264
+
265
+ function addTimestamp() {
266
+ const t = document.createElement('div');
267
+ t.className = 'msg-time';
268
+ t.textContent = now();
269
+ const last = messagesEl.querySelector('.msg-row.assistant:last-child');
270
+ if (last) last.appendChild(t);
271
+ }
160
272
 
161
- form.onsubmit = async (e) => {
162
- e.preventDefault();
163
- const msg = input.value.trim();
164
- if (!msg || isStreaming) return;
165
- addMsg('user', msg);
166
- input.value = '';
167
- input.style.height = 'auto';
168
- sendBtn.disabled = true;
169
- isStreaming = true;
170
- addTyping();
171
-
172
- if (sendViaWs(msg)) {
173
- // WebSocket handles response
174
- } else {
175
- // Fallback to REST
273
+ function addTyping() {
274
+ welcomeEl.style.display = 'none';
275
+ const row = document.createElement('div');
276
+ row.className = 'msg-row assistant';
277
+ row.id = 'typing-row';
278
+ row.innerHTML = '<div class="msg-avatar">🦅</div><div class="msg-bubble" style="padding:14px 18px"><span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span></div>';
279
+ messagesEl.appendChild(row);
280
+ messagesEl.scrollTop = messagesEl.scrollHeight;
281
+ }
282
+
283
+ function removeTyping() {
284
+ document.getElementById('typing-row')?.remove();
285
+ }
286
+
287
+ function escHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
288
+
289
+ // ── Send ─────────────────────────────────────────────
290
+ form.onsubmit = async (e) => {
291
+ e.preventDefault();
292
+ const msg = input.value.trim();
293
+ if (!msg || isStreaming) return;
294
+ addMsg('user', msg);
295
+ input.value = '';
296
+ input.style.height = 'auto';
297
+ sendBtn.disabled = true;
298
+ isStreaming = true;
299
+ addTyping();
300
+
301
+ if (ws && ws.readyState === WebSocket.OPEN) {
302
+ ws.send(JSON.stringify({ type: 'chat:message', content: msg, source: 'webchat' }));
303
+ } else {
304
+ // REST fallback
305
+ try {
306
+ const res = await fetch(apiUrl + '/api/chat', {
307
+ method: 'POST', headers: {'Content-Type':'application/json'},
308
+ body: JSON.stringify({message: msg})
309
+ });
310
+ const data = await res.json();
176
311
  removeTyping();
177
- statusEl.textContent = 'Thinking...';
178
- try {
179
- const res = await fetch(apiUrl + '/api/chat', {
180
- method: 'POST',
181
- headers: { 'Content-Type': 'application/json' },
182
- body: JSON.stringify({ message: msg })
183
- });
184
- const data = await res.json();
185
- addMsg('assistant', data.response || data.error || '(no response)');
186
- statusEl.textContent = 'Ready';
187
- } catch (err) {
188
- addMsg('assistant', 'Error: ' + err.message);
189
- statusEl.textContent = 'Error';
190
- }
191
- isStreaming = false;
192
- sendBtn.disabled = false;
312
+ addMsg('assistant', data.response || data.error || '(no response)');
313
+ addTimestamp();
314
+ } catch (err) {
315
+ removeTyping();
316
+ addMsg('assistant', '⚠️ Could not reach gateway: ' + err.message);
193
317
  }
194
- };
318
+ finishStreaming();
319
+ }
320
+ };
195
321
 
196
- input.addEventListener('input', () => {
197
- input.style.height = 'auto';
198
- input.style.height = Math.min(input.scrollHeight, 120) + 'px';
199
- });
200
- input.addEventListener('keydown', (e) => {
201
- if (e.key === 'Enter' && !e.shiftKey) {
202
- e.preventDefault();
203
- form.dispatchEvent(new Event('submit'));
204
- }
205
- });
322
+ // ── Textarea auto-resize ──────────────────────────────
323
+ function autoResize() {
324
+ input.style.height = 'auto';
325
+ input.style.height = Math.min(input.scrollHeight, 120) + 'px';
326
+ }
327
+ input.addEventListener('input', autoResize);
328
+ input.addEventListener('keydown', (e) => {
329
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); form.dispatchEvent(new Event('submit')); }
330
+ });
206
331
 
207
- connectWs();
208
- fetch(apiUrl + '/api/status').then(r => r.json()).then(d => {
209
- agentInfo.textContent = d.agentName ? `${d.agentName} ${d.model || ''}` : '';
210
- }).catch(() => {});
211
- </script>
332
+ // ── Init ─────────────────────────────────────────────
333
+ connectWs();
334
+ fetch(apiUrl + '/api/status').then(r => r.json()).then(d => {
335
+ if (d.agentName) agentSub.textContent = `${d.agentName} · ${d.model || '—'} · port ${d.port || port}`;
336
+ if (d.model) modelHint.textContent = d.model;
337
+ }).catch(() => {});
338
+ </script>
212
339
  </body>
213
340
  </html>