hyperclaw 5.2.3 → 5.2.4
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.
- package/dist/chat-ChYGHacA.js +324 -0
- package/dist/daemon-DY73xDpS.js +7 -0
- package/dist/daemon-jRZ0qvfS.js +404 -0
- package/dist/engine-CJtV0X2-.js +7 -0
- package/dist/engine-COYE-fVt.js +323 -0
- package/dist/hyperclawbot-sfzAMwh-.js +508 -0
- package/dist/mcp-loader-D_NWYUqh.js +93 -0
- package/dist/onboard-DMTgAKzs.js +3865 -0
- package/dist/onboard-I3AirQv9.js +13 -0
- package/dist/orchestrator-6ljWiZWu.js +189 -0
- package/dist/orchestrator-mdElwt2i.js +6 -0
- package/dist/run-main.js +25 -25
- package/dist/server-DG804qOP.js +4 -0
- package/dist/server-jaMeUOfM.js +1294 -0
- package/dist/skill-runtime-CZ8uTBra.js +102 -0
- package/dist/skill-runtime-DwNX3pnp.js +5 -0
- package/dist/src-D0vSZWOd.js +458 -0
- package/dist/src-ij58SST0.js +63 -0
- package/dist/sub-agent-tools-DYKPR3aI.js +39 -0
- package/package.json +1 -1
- package/static/chat.html +314 -187
- package/static/dashboard.html +133 -20
package/static/chat.html
CHANGED
|
@@ -1,213 +1,340 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en"
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
</
|
|
36
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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', '
|
|
148
|
-
isStreaming = false;
|
|
149
|
-
sendBtn.disabled = false;
|
|
206
|
+
currentBubble = addMsg('assistant', '', true);
|
|
150
207
|
}
|
|
151
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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>
|