ape-claw 0.1.6 → 0.1.8
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/README.md +157 -8
- package/docs/operator/01-quickstart.md +81 -72
- package/docs/operator/03-cli-reference.md +46 -5
- package/package.json +1 -1
- package/src/cli.mjs +551 -15
- package/src/lib/openclaw-paths.mjs +65 -0
- package/src/server/index.mjs +8 -2
- package/src/server/middleware/auth.mjs +10 -0
- package/src/server/routes/forge-agent.mjs +438 -58
- package/src/server/routes/openclaw-env.mjs +206 -0
- package/src/server/routes/skills.mjs +80 -3
- package/ui/forge/css/forge.css +153 -2
- package/ui/forge/index.html +40 -6
- package/ui/forge/js/forge-attachments.js +616 -377
- package/ui/forge/js/forge-chat.js +201 -90
- package/ui/forge/js/forge-data.js +305 -11
- package/ui/forge/js/forge-scene.js +178 -18
- package/ui/index.html +3 -3
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* forge-chat.js — Agent chat panel for ClawBot Forge.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 3. Local without key: falls back to /api/chat (basic message relay)
|
|
4
|
+
* Gateway mode:
|
|
5
|
+
* - Forge always uses /api/forge/chat (OpenClaw gateway takeover path)
|
|
6
|
+
* - No fallback to legacy /api/chat relay
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
/* ══════════════════════════════════════════════════════════
|
|
@@ -15,12 +14,16 @@ const input = () => document.getElementById("forgeChatInput");
|
|
|
15
14
|
const sendBtn = () => document.getElementById("forgeChatSendBtn");
|
|
16
15
|
const counter = () => document.getElementById("forgeChatCounter");
|
|
17
16
|
const badge = () => document.getElementById("forgeChatBadge");
|
|
17
|
+
const gatewayChip = () => document.getElementById("forgeGatewayChip");
|
|
18
|
+
const gatewayRefreshBtn = () => document.getElementById("forgeGatewayRefresh");
|
|
19
|
+
const gatewayRestartBtn = () => document.getElementById("forgeGatewayRestart");
|
|
18
20
|
|
|
19
21
|
let unread = 0;
|
|
20
22
|
let streaming = false;
|
|
21
23
|
let forgeAgentAvailable = null; // null = unknown, true/false after probe
|
|
22
24
|
let localAgentName = "Agent";
|
|
23
|
-
let
|
|
25
|
+
let gatewayPollTimer = null;
|
|
26
|
+
let lastMotionIntentAt = 0;
|
|
24
27
|
|
|
25
28
|
const conversationHistory = [];
|
|
26
29
|
|
|
@@ -35,6 +38,8 @@ function escapeHtml(s) {
|
|
|
35
38
|
|
|
36
39
|
function renderChatText(raw) {
|
|
37
40
|
let text = String(raw || "");
|
|
41
|
+
// Optional motion directives for Forge scene control (hidden from chat UI).
|
|
42
|
+
text = text.replace(/\[\[MOTION:[^\]]+\]\]/gi, "");
|
|
38
43
|
// Strip common citation markers from provider answers (e.g. [1], [2]).
|
|
39
44
|
text = text.replace(/\[(\d+)\]/g, "");
|
|
40
45
|
|
|
@@ -57,6 +62,50 @@ function renderChatText(raw) {
|
|
|
57
62
|
return html;
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
function userAskedForMotion(prompt) {
|
|
66
|
+
const s = String(prompt || "").toLowerCase();
|
|
67
|
+
if (!s) return false;
|
|
68
|
+
return /\b(move|walk|patrol|wander|goto|go to|navigate|step|turn|come here|go there)\b/.test(s);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function applyMotionIntentsFromText(raw, opts = {}) {
|
|
72
|
+
const allowActiveMotion = Boolean(opts.allowActiveMotion);
|
|
73
|
+
const s = String(raw || "");
|
|
74
|
+
const intents = [];
|
|
75
|
+
const matches = s.match(/\[\[MOTION:[^\]]+\]\]/gi) || [];
|
|
76
|
+
for (const m of matches) {
|
|
77
|
+
const payload = m.replace(/^\[\[MOTION:/i, "").replace(/\]\]$/, "").trim();
|
|
78
|
+
const upper = payload.toUpperCase();
|
|
79
|
+
if (upper === "HALT") intents.push({ type: "halt" });
|
|
80
|
+
else if (upper === "PATROL") intents.push({ type: "patrol", durationMs: 20000 });
|
|
81
|
+
else if (upper === "WANDER") intents.push({ type: "wander", durationMs: 10000 });
|
|
82
|
+
else if (upper.startsWith("GOTO")) {
|
|
83
|
+
const nums = payload.match(/-?\d+(\.\d+)?/g) || [];
|
|
84
|
+
if (nums.length >= 2) {
|
|
85
|
+
intents.push({ type: "goto", x: Number(nums[0]), z: Number(nums[1]), durationMs: 15000 });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!intents.length) return;
|
|
90
|
+
const setMotion = window.__forgeSetMotionIntent;
|
|
91
|
+
if (typeof setMotion !== "function") return;
|
|
92
|
+
// Stability rule: apply only the final directive in the response.
|
|
93
|
+
// Some model outputs include multiple tags while "thinking", which causes jitter.
|
|
94
|
+
const intent = intents[intents.length - 1];
|
|
95
|
+
|
|
96
|
+
// Unless the user explicitly asked for movement, keep posture steady.
|
|
97
|
+
if (!allowActiveMotion && intent.type !== "halt") {
|
|
98
|
+
setMotion({ type: "halt" });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Throttle active motion changes to avoid rapid target resets.
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
if (intent.type !== "halt" && now - lastMotionIntentAt < 5000) return;
|
|
105
|
+
if (intent.type !== "halt") lastMotionIntentAt = now;
|
|
106
|
+
setMotion(intent);
|
|
107
|
+
}
|
|
108
|
+
|
|
60
109
|
/* ══════════════════════════════════════════════════════════
|
|
61
110
|
Environment detection
|
|
62
111
|
══════════════════════════════════════════════════════════ */
|
|
@@ -67,17 +116,74 @@ function isWebsiteMode() {
|
|
|
67
116
|
|
|
68
117
|
async function probeForgeAgent() {
|
|
69
118
|
try {
|
|
70
|
-
const res = await fetch("/api/forge/status", { signal: AbortSignal.timeout(
|
|
119
|
+
const res = await fetch("/api/forge/status", { signal: AbortSignal.timeout(10000) });
|
|
71
120
|
if (!res.ok) return false;
|
|
72
121
|
const data = await res.json();
|
|
73
122
|
if (data.agentName) localAgentName = data.agentName;
|
|
74
|
-
if (data.provider) localProvider = data.provider;
|
|
75
123
|
return data.configured === true;
|
|
76
124
|
} catch {
|
|
77
125
|
return false;
|
|
78
126
|
}
|
|
79
127
|
}
|
|
80
128
|
|
|
129
|
+
async function fetchGatewayStatus() {
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetch("/api/forge/status", { signal: AbortSignal.timeout(9000) });
|
|
132
|
+
if (!res.ok) return null;
|
|
133
|
+
return await res.json();
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function setGatewayChip(state, text) {
|
|
140
|
+
const chip = gatewayChip();
|
|
141
|
+
if (!chip) return;
|
|
142
|
+
chip.dataset.state = state;
|
|
143
|
+
chip.textContent = text;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function refreshGatewayChip() {
|
|
147
|
+
const status = await fetchGatewayStatus();
|
|
148
|
+
if (!status) {
|
|
149
|
+
setGatewayChip("err", "Gateway: unreachable");
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
if (status.gatewayReady || status.configured) {
|
|
153
|
+
setGatewayChip("ok", "Gateway: online");
|
|
154
|
+
} else if (status.gatewayCli) {
|
|
155
|
+
setGatewayChip("warn", "Gateway: starting/idle");
|
|
156
|
+
} else {
|
|
157
|
+
setGatewayChip("err", "Gateway: OpenClaw CLI missing");
|
|
158
|
+
}
|
|
159
|
+
return status;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function runGatewayAction(action) {
|
|
163
|
+
const restart = gatewayRestartBtn();
|
|
164
|
+
const refresh = gatewayRefreshBtn();
|
|
165
|
+
if (restart) restart.disabled = true;
|
|
166
|
+
if (refresh) refresh.disabled = true;
|
|
167
|
+
setGatewayChip("warn", action === "restart" ? "Gateway: restarting..." : "Gateway: updating...");
|
|
168
|
+
try {
|
|
169
|
+
const res = await fetch("/api/forge/gateway/control", {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: { "content-type": "application/json" },
|
|
172
|
+
body: JSON.stringify({ action }),
|
|
173
|
+
});
|
|
174
|
+
const data = await res.json().catch(() => null);
|
|
175
|
+
if (!res.ok || !data?.ok) {
|
|
176
|
+
throw new Error(data?.error || `HTTP ${res.status}`);
|
|
177
|
+
}
|
|
178
|
+
await refreshGatewayChip();
|
|
179
|
+
} catch (err) {
|
|
180
|
+
setGatewayChip("err", `Gateway: ${err.message}`);
|
|
181
|
+
} finally {
|
|
182
|
+
if (restart) restart.disabled = false;
|
|
183
|
+
if (refresh) refresh.disabled = false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
81
187
|
/* ══════════════════════════════════════════════════════════
|
|
82
188
|
Message rendering
|
|
83
189
|
══════════════════════════════════════════════════════════ */
|
|
@@ -149,11 +255,7 @@ async function sendMessage() {
|
|
|
149
255
|
const btn = sendBtn();
|
|
150
256
|
if (btn) btn.disabled = true;
|
|
151
257
|
|
|
152
|
-
|
|
153
|
-
await sendToForgeAgent(text);
|
|
154
|
-
} else {
|
|
155
|
-
await sendToLocalChat(text);
|
|
156
|
-
}
|
|
258
|
+
await sendToForgeAgent(text);
|
|
157
259
|
}
|
|
158
260
|
|
|
159
261
|
/* ══════════════════════════════════════════════════════════
|
|
@@ -163,6 +265,40 @@ async function sendToForgeAgent(text) {
|
|
|
163
265
|
const btn = sendBtn();
|
|
164
266
|
const bodyEl = appendMsg("agent", "");
|
|
165
267
|
let buffer = "";
|
|
268
|
+
let firstChunkSeen = false;
|
|
269
|
+
let pendingTimer = null;
|
|
270
|
+
const pendingStartedAt = Date.now();
|
|
271
|
+
|
|
272
|
+
function renderPending() {
|
|
273
|
+
if (!bodyEl || firstChunkSeen) return;
|
|
274
|
+
const sec = Math.max(0, Math.floor((Date.now() - pendingStartedAt) / 1000));
|
|
275
|
+
const stage = sec < 5
|
|
276
|
+
? "Analyzing request..."
|
|
277
|
+
: sec < 12
|
|
278
|
+
? "Checking OpenClaw context..."
|
|
279
|
+
: "Waiting for gateway response...";
|
|
280
|
+
bodyEl.classList.add("chat-pending");
|
|
281
|
+
bodyEl.innerHTML = `
|
|
282
|
+
<span class="chat-pending-wrap">
|
|
283
|
+
<span class="chat-pending-label">OpenClaw agent is responding</span>
|
|
284
|
+
<span class="chat-pending-dots"><i></i><i></i><i></i></span>
|
|
285
|
+
<span class="chat-pending-stage">${stage}</span>
|
|
286
|
+
<span class="chat-pending-time">${sec}s</span>
|
|
287
|
+
</span>
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function stopPending() {
|
|
292
|
+
firstChunkSeen = true;
|
|
293
|
+
if (pendingTimer) {
|
|
294
|
+
clearInterval(pendingTimer);
|
|
295
|
+
pendingTimer = null;
|
|
296
|
+
}
|
|
297
|
+
if (bodyEl) bodyEl.classList.remove("chat-pending");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
renderPending();
|
|
301
|
+
pendingTimer = setInterval(renderPending, 700);
|
|
166
302
|
|
|
167
303
|
try {
|
|
168
304
|
async function requestOnce() {
|
|
@@ -192,6 +328,7 @@ async function sendToForgeAgent(text) {
|
|
|
192
328
|
if (errData.retryAfter) {
|
|
193
329
|
errMsg += `, retry in ${errData.retryAfter}s`;
|
|
194
330
|
}
|
|
331
|
+
stopPending();
|
|
195
332
|
if (bodyEl) bodyEl.textContent = `Error: ${errMsg}`;
|
|
196
333
|
finishStreaming();
|
|
197
334
|
return;
|
|
@@ -200,9 +337,12 @@ async function sendToForgeAgent(text) {
|
|
|
200
337
|
const contentType = res.headers.get("content-type") || "";
|
|
201
338
|
if (!contentType.includes("text/event-stream")) {
|
|
202
339
|
const data = await res.json().catch(() => ({}));
|
|
340
|
+
stopPending();
|
|
203
341
|
if (bodyEl) bodyEl.innerHTML = renderChatText(data.reply || data.message || "No response");
|
|
204
342
|
if (data.reply || data.message) {
|
|
205
|
-
|
|
343
|
+
const replyText = data.reply || data.message;
|
|
344
|
+
applyMotionIntentsFromText(replyText, { allowActiveMotion: userAskedForMotion(text) });
|
|
345
|
+
conversationHistory.push({ role: "assistant", content: replyText });
|
|
206
346
|
}
|
|
207
347
|
finishStreaming();
|
|
208
348
|
return;
|
|
@@ -228,6 +368,7 @@ async function sendToForgeAgent(text) {
|
|
|
228
368
|
const chunk = JSON.parse(data);
|
|
229
369
|
const t = chunk.text || chunk.content || "";
|
|
230
370
|
if (t) {
|
|
371
|
+
if (!firstChunkSeen) stopPending();
|
|
231
372
|
buffer += t;
|
|
232
373
|
if (bodyEl) bodyEl.innerHTML = renderChatText(buffer);
|
|
233
374
|
const box = msgBox();
|
|
@@ -238,52 +379,21 @@ async function sendToForgeAgent(text) {
|
|
|
238
379
|
}
|
|
239
380
|
|
|
240
381
|
if (buffer) {
|
|
382
|
+
applyMotionIntentsFromText(buffer, { allowActiveMotion: userAskedForMotion(text) });
|
|
241
383
|
conversationHistory.push({ role: "assistant", content: buffer });
|
|
242
384
|
} else if (bodyEl && !bodyEl.textContent) {
|
|
385
|
+
stopPending();
|
|
243
386
|
bodyEl.textContent = "No response received";
|
|
244
387
|
}
|
|
245
388
|
} catch (err) {
|
|
389
|
+
stopPending();
|
|
246
390
|
if (bodyEl) {
|
|
247
391
|
const msg = buffer || `Connection error: ${err.message}`;
|
|
248
392
|
bodyEl.innerHTML = renderChatText(msg);
|
|
249
393
|
}
|
|
250
394
|
}
|
|
251
395
|
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/* ══════════════════════════════════════════════════════════
|
|
256
|
-
Local fallback: POST /api/chat (basic message relay)
|
|
257
|
-
══════════════════════════════════════════════════════════ */
|
|
258
|
-
async function sendToLocalChat(text) {
|
|
259
|
-
const btn = sendBtn();
|
|
260
|
-
|
|
261
|
-
try {
|
|
262
|
-
const res = await fetch("/api/chat", {
|
|
263
|
-
method: "POST",
|
|
264
|
-
headers: { "content-type": "application/json" },
|
|
265
|
-
body: JSON.stringify({ text, room: "forge" }),
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
if (!res.ok) {
|
|
269
|
-
appendMsg("agent", `Error: ${res.status} ${res.statusText}`);
|
|
270
|
-
finishStreaming();
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const data = await res.json();
|
|
275
|
-
if (data.reply) {
|
|
276
|
-
appendMsg("agent", data.reply);
|
|
277
|
-
} else if (data.streamId) {
|
|
278
|
-
streamResponse(data.streamId);
|
|
279
|
-
return;
|
|
280
|
-
} else {
|
|
281
|
-
appendMsg("agent", data.message?.text || "Message sent");
|
|
282
|
-
}
|
|
283
|
-
} catch (err) {
|
|
284
|
-
appendMsg("agent", `Connection error: ${err.message}`);
|
|
285
|
-
}
|
|
286
|
-
|
|
396
|
+
stopPending();
|
|
287
397
|
finishStreaming();
|
|
288
398
|
}
|
|
289
399
|
|
|
@@ -295,36 +405,6 @@ function finishStreaming() {
|
|
|
295
405
|
input()?.focus();
|
|
296
406
|
}
|
|
297
407
|
|
|
298
|
-
/* ══════════════════════════════════════════════════════════
|
|
299
|
-
SSE streaming for local mode long responses
|
|
300
|
-
══════════════════════════════════════════════════════════ */
|
|
301
|
-
function streamResponse(streamId) {
|
|
302
|
-
const bodyEl = appendMsg("agent", "");
|
|
303
|
-
const evtSrc = new EventSource(`/api/chat/stream?id=${encodeURIComponent(streamId)}`);
|
|
304
|
-
let buffer = "";
|
|
305
|
-
|
|
306
|
-
evtSrc.onmessage = (e) => {
|
|
307
|
-
if (e.data === "[DONE]") {
|
|
308
|
-
evtSrc.close();
|
|
309
|
-
finishStreaming();
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
try {
|
|
313
|
-
const chunk = JSON.parse(e.data);
|
|
314
|
-
buffer += chunk.text || chunk.content || "";
|
|
315
|
-
if (bodyEl) bodyEl.innerHTML = renderChatText(buffer);
|
|
316
|
-
const box = msgBox();
|
|
317
|
-
if (box) box.scrollTop = box.scrollHeight;
|
|
318
|
-
} catch { /* ignore parse errors */ }
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
evtSrc.onerror = () => {
|
|
322
|
-
evtSrc.close();
|
|
323
|
-
if (!buffer && bodyEl) bodyEl.textContent = "Stream interrupted";
|
|
324
|
-
finishStreaming();
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
|
|
328
408
|
/* ══════════════════════════════════════════════════════════
|
|
329
409
|
Character counter
|
|
330
410
|
══════════════════════════════════════════════════════════ */
|
|
@@ -341,43 +421,62 @@ async function init() {
|
|
|
341
421
|
const inp = input();
|
|
342
422
|
const btn = sendBtn();
|
|
343
423
|
const indicator = document.getElementById("forgeChatAgentIndicator");
|
|
424
|
+
const refreshBtn = gatewayRefreshBtn();
|
|
425
|
+
const restartBtn = gatewayRestartBtn();
|
|
426
|
+
|
|
427
|
+
refreshBtn?.addEventListener("click", async () => { await refreshGatewayChip(); });
|
|
428
|
+
restartBtn?.addEventListener("click", async () => { await runGatewayAction("restart"); });
|
|
344
429
|
|
|
345
430
|
if (isWebsiteMode()) {
|
|
431
|
+
refreshBtn?.setAttribute("style", "display:none");
|
|
432
|
+
restartBtn?.setAttribute("style", "display:none");
|
|
433
|
+
setGatewayChip("ok", "Gateway: main session");
|
|
346
434
|
forgeAgentAvailable = true;
|
|
347
435
|
if (inp) inp.disabled = false;
|
|
348
436
|
if (btn) btn.disabled = false;
|
|
349
437
|
if (inp) inp.placeholder = "Ask The Clawllector anything...";
|
|
350
438
|
if (indicator) {
|
|
351
|
-
indicator.textContent = "
|
|
439
|
+
indicator.textContent = "Connected via OpenClaw Gateway (main session)";
|
|
352
440
|
indicator.style.display = "block";
|
|
353
441
|
}
|
|
354
442
|
} else {
|
|
355
|
-
|
|
443
|
+
const status = await fetchGatewayStatus();
|
|
444
|
+
let warmedStatus = status;
|
|
445
|
+
if (!status?.configured) {
|
|
446
|
+
// First-run recovery: auto-attempt gateway restart once before warning.
|
|
447
|
+
await runGatewayAction("restart");
|
|
448
|
+
warmedStatus = await fetchGatewayStatus();
|
|
449
|
+
}
|
|
450
|
+
forgeAgentAvailable = Boolean(warmedStatus?.configured || (await probeForgeAgent()));
|
|
451
|
+
if (warmedStatus?.agentName) localAgentName = warmedStatus.agentName;
|
|
356
452
|
|
|
357
453
|
if (forgeAgentAvailable) {
|
|
358
454
|
if (inp) { inp.disabled = false; inp.placeholder = `Ask ${localAgentName} anything...`; }
|
|
359
455
|
if (btn) btn.disabled = false;
|
|
360
456
|
if (indicator) {
|
|
361
|
-
|
|
362
|
-
indicator.textContent = `\u{1F99E} Connected to ${localAgentName}${providerTag}`;
|
|
457
|
+
indicator.textContent = "Connected via OpenClaw Gateway (main session)";
|
|
363
458
|
indicator.style.display = "block";
|
|
364
459
|
}
|
|
365
460
|
} else {
|
|
366
|
-
if (inp) inp.disabled =
|
|
367
|
-
if (btn) btn.disabled =
|
|
461
|
+
if (inp) inp.disabled = false;
|
|
462
|
+
if (btn) btn.disabled = false;
|
|
368
463
|
if (indicator) {
|
|
369
464
|
indicator.innerHTML =
|
|
370
|
-
'\u{
|
|
371
|
-
'
|
|
372
|
-
'<code style="color:var(--neon-cyan,#63d7ff)">ANTHROPIC_API_KEY</code>, ' +
|
|
373
|
-
'<code style="color:var(--neon-cyan,#63d7ff)">PERPLEXITY_API_KEY</code>, ' +
|
|
374
|
-
'or <code style="color:var(--neon-cyan,#63d7ff)">OLLAMA_HOST</code>) to enable. ' +
|
|
375
|
-
'<a href="/docs" style="color:var(--accent,#cfff04)">Setup guide</a>';
|
|
465
|
+
'\u{26A0}\uFE0F OpenClaw Gateway not ready. Start/restart gateway, then retry. ' +
|
|
466
|
+
'<code style="color:var(--neon-cyan,#63d7ff)">openclaw gateway start</code>';
|
|
376
467
|
indicator.style.display = "block";
|
|
377
468
|
}
|
|
378
469
|
}
|
|
379
470
|
}
|
|
380
471
|
|
|
472
|
+
if (!isWebsiteMode()) {
|
|
473
|
+
await refreshGatewayChip();
|
|
474
|
+
if (gatewayPollTimer) clearInterval(gatewayPollTimer);
|
|
475
|
+
gatewayPollTimer = setInterval(() => {
|
|
476
|
+
refreshGatewayChip().catch(() => {});
|
|
477
|
+
}, 20_000);
|
|
478
|
+
}
|
|
479
|
+
|
|
381
480
|
if (inp) {
|
|
382
481
|
inp.addEventListener("input", updateCounter);
|
|
383
482
|
inp.addEventListener("keydown", (e) => {
|
|
@@ -392,6 +491,18 @@ async function init() {
|
|
|
392
491
|
btn.addEventListener("click", sendMessage);
|
|
393
492
|
}
|
|
394
493
|
|
|
494
|
+
document.querySelectorAll(".forge-chat-quick-btn").forEach((el) => {
|
|
495
|
+
el.addEventListener("click", () => {
|
|
496
|
+
const prompt = String(el.getAttribute("data-prompt") || "").trim();
|
|
497
|
+
if (!prompt) return;
|
|
498
|
+
const inp = input();
|
|
499
|
+
if (!inp) return;
|
|
500
|
+
inp.value = prompt;
|
|
501
|
+
updateCounter();
|
|
502
|
+
sendMessage();
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
395
506
|
const box = msgBox();
|
|
396
507
|
if (box) {
|
|
397
508
|
box.addEventListener("click", () => {
|