leedab 0.2.0 → 0.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/dashboard/routes.js
CHANGED
|
@@ -65,7 +65,8 @@ export function createRoutes(config) {
|
|
|
65
65
|
result.text ??
|
|
66
66
|
result.content ??
|
|
67
67
|
stdout.trim();
|
|
68
|
-
|
|
68
|
+
const thoughts = await readLatestThoughts(stateDir, session ?? "console");
|
|
69
|
+
json(res, { reply, thoughts, session: session ?? "console" });
|
|
69
70
|
}
|
|
70
71
|
catch {
|
|
71
72
|
json(res, {
|
|
@@ -88,6 +89,7 @@ export function createRoutes(config) {
|
|
|
88
89
|
const jsonlPath = resolve(sessionsDir, `${session}.jsonl`);
|
|
89
90
|
const raw = await readFile(jsonlPath, "utf-8");
|
|
90
91
|
const messages = [];
|
|
92
|
+
let pendingThoughts = [];
|
|
91
93
|
for (const line of raw.split("\n")) {
|
|
92
94
|
if (!line.trim())
|
|
93
95
|
continue;
|
|
@@ -99,11 +101,16 @@ export function createRoutes(config) {
|
|
|
99
101
|
if (!msg || (msg.role !== "user" && msg.role !== "assistant"))
|
|
100
102
|
continue;
|
|
101
103
|
let text = "";
|
|
104
|
+
const theseThoughts = [];
|
|
102
105
|
if (Array.isArray(msg.content)) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
for (const b of msg.content) {
|
|
107
|
+
if (b?.type === "text" && typeof b.text === "string") {
|
|
108
|
+
text += (text ? "\n" : "") + b.text;
|
|
109
|
+
}
|
|
110
|
+
else if (b?.type === "thinking" && typeof b.thinking === "string") {
|
|
111
|
+
theseThoughts.push(b.thinking);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
107
114
|
}
|
|
108
115
|
else if (typeof msg.content === "string") {
|
|
109
116
|
text = msg.content;
|
|
@@ -111,12 +118,22 @@ export function createRoutes(config) {
|
|
|
111
118
|
// Strip all OpenClaw "(untrusted metadata)" blocks and timestamp prefix
|
|
112
119
|
text = text.replace(/^(\w[\w\s]*\(untrusted metadata\):\n```json\n[\s\S]*?\n```\n\n)+/, "");
|
|
113
120
|
text = text.replace(/^\[[\w]{3} \d{4}-\d{2}-\d{2} \d{2}:\d{2} \w+\] /, "");
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
if (msg.role === "assistant") {
|
|
122
|
+
// Accumulate thoughts across tool-use turns until we see text
|
|
123
|
+
pendingThoughts.push(...theseThoughts);
|
|
124
|
+
if (text) {
|
|
125
|
+
messages.push({
|
|
126
|
+
role: msg.role,
|
|
127
|
+
text,
|
|
128
|
+
thoughts: pendingThoughts.length ? pendingThoughts : undefined,
|
|
129
|
+
timestamp: entry.timestamp,
|
|
130
|
+
});
|
|
131
|
+
pendingThoughts = [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (text) {
|
|
135
|
+
messages.push({ role: msg.role, text, timestamp: entry.timestamp });
|
|
136
|
+
pendingThoughts = [];
|
|
120
137
|
}
|
|
121
138
|
}
|
|
122
139
|
catch { }
|
|
@@ -128,7 +145,7 @@ export function createRoutes(config) {
|
|
|
128
145
|
}
|
|
129
146
|
},
|
|
130
147
|
/**
|
|
131
|
-
* GET /api/whoami — current OS user's first name
|
|
148
|
+
* GET /api/whoami — current OS user's first name + configured agent name
|
|
132
149
|
*/
|
|
133
150
|
"GET /api/whoami": async (_req, res) => {
|
|
134
151
|
let firstName = userInfo().username;
|
|
@@ -139,7 +156,7 @@ export function createRoutes(config) {
|
|
|
139
156
|
firstName = fullName.split(/\s+/)[0];
|
|
140
157
|
}
|
|
141
158
|
catch { }
|
|
142
|
-
json(res, { name: firstName });
|
|
159
|
+
json(res, { name: firstName, agent: config.agent?.name ?? "LeedAB" });
|
|
143
160
|
},
|
|
144
161
|
/**
|
|
145
162
|
* GET /api/status — channel health status
|
|
@@ -703,6 +720,49 @@ function parseSessionFirstMessage(text) {
|
|
|
703
720
|
userText = userText.replace(/\nUntrusted context \(metadata[\s\S]*$/, "");
|
|
704
721
|
return { senderName, cleanText: userText.trim() };
|
|
705
722
|
}
|
|
723
|
+
/**
|
|
724
|
+
* Walk the session JSONL backwards and collect `thinking` block content from
|
|
725
|
+
* the most recent assistant turn(s) since the last user message. Returns an
|
|
726
|
+
* ordered list of thought strings (oldest → newest).
|
|
727
|
+
*/
|
|
728
|
+
async function readLatestThoughts(stateDir, session) {
|
|
729
|
+
try {
|
|
730
|
+
const jsonlPath = resolve(stateDir, "agents", "main", "sessions", `${session}.jsonl`);
|
|
731
|
+
const raw = await readFile(jsonlPath, "utf-8");
|
|
732
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
733
|
+
const thoughts = [];
|
|
734
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
735
|
+
let entry;
|
|
736
|
+
try {
|
|
737
|
+
entry = JSON.parse(lines[i]);
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (entry.type !== "message")
|
|
743
|
+
continue;
|
|
744
|
+
const msg = entry.message;
|
|
745
|
+
if (!msg)
|
|
746
|
+
continue;
|
|
747
|
+
// Stop once we hit the user message that triggered this turn.
|
|
748
|
+
if (msg.role === "user")
|
|
749
|
+
break;
|
|
750
|
+
if (msg.role !== "assistant")
|
|
751
|
+
continue;
|
|
752
|
+
if (!Array.isArray(msg.content))
|
|
753
|
+
continue;
|
|
754
|
+
for (const block of msg.content) {
|
|
755
|
+
if (block?.type === "thinking" && typeof block.thinking === "string") {
|
|
756
|
+
thoughts.unshift(block.thinking);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return thoughts;
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
return [];
|
|
764
|
+
}
|
|
765
|
+
}
|
|
706
766
|
function json(res, data, status = 200) {
|
|
707
767
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
708
768
|
res.end(JSON.stringify(data));
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
<style>
|
|
12
12
|
.page-header { display:flex; align-items:center; justify-content:space-between; padding:0 20px; height:52px; border-bottom:1px solid var(--border); background:var(--bg); }
|
|
13
13
|
.page-header-left { display:flex; align-items:center; gap:10px; }
|
|
14
|
-
.page-header-title {
|
|
14
|
+
.page-header-title { display:flex; align-items:center; }
|
|
15
|
+
.page-header-title .logo-img { height:22px; width:auto; display:block; }
|
|
15
16
|
.page-nav { display:flex; align-items:center; gap:2px; }
|
|
16
17
|
.page-nav a, .page-nav button { color:var(--text-dim); text-decoration:none; display:flex; align-items:center; gap:5px; padding:6px 12px; border-radius:8px; font-size:13px; font-weight:450; transition:all 0.15s; background:none; border:none; cursor:pointer; font-family:inherit; }
|
|
17
18
|
.page-nav a:hover, .page-nav button:hover { color:var(--text-secondary); background:var(--surface-raised); }
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
|
|
57
58
|
<div class="page-header">
|
|
58
59
|
<div class="page-header-left">
|
|
59
|
-
<a href="/" class="page-header-title"
|
|
60
|
+
<a href="/" class="page-header-title" aria-label="LeedAB"><img class="logo-img" alt="LeedAB"></a>
|
|
60
61
|
</div>
|
|
61
62
|
<div class="page-nav">
|
|
62
63
|
<button class="theme-btn" onclick="toggleTheme()" title="Toggle theme">
|
|
@@ -272,6 +273,7 @@
|
|
|
272
273
|
const saved = localStorage.getItem("leedab-theme") || "dark";
|
|
273
274
|
document.documentElement.setAttribute("data-theme", saved);
|
|
274
275
|
updateThemeIcon(saved);
|
|
276
|
+
updateLogo(saved);
|
|
275
277
|
}
|
|
276
278
|
function toggleTheme() {
|
|
277
279
|
const current = document.documentElement.getAttribute("data-theme") || "dark";
|
|
@@ -279,6 +281,11 @@
|
|
|
279
281
|
document.documentElement.setAttribute("data-theme", next);
|
|
280
282
|
localStorage.setItem("leedab-theme", next);
|
|
281
283
|
updateThemeIcon(next);
|
|
284
|
+
updateLogo(next);
|
|
285
|
+
}
|
|
286
|
+
function updateLogo(theme) {
|
|
287
|
+
const src = theme === "dark" ? "/logo-dark.png" : "/logo-light.png";
|
|
288
|
+
document.querySelectorAll(".logo-img").forEach(img => img.src = src);
|
|
282
289
|
}
|
|
283
290
|
function updateThemeIcon(theme) {
|
|
284
291
|
const sun = document.getElementById("theme-icon-sun");
|
|
@@ -75,9 +75,14 @@
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
.header-title {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.header-logo {
|
|
83
|
+
height: 22px;
|
|
84
|
+
width: auto;
|
|
85
|
+
display: block;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
|
|
@@ -105,6 +110,40 @@
|
|
|
105
110
|
background: var(--surface-raised);
|
|
106
111
|
}
|
|
107
112
|
|
|
113
|
+
/* ── Main column below header ── */
|
|
114
|
+
.main-col {
|
|
115
|
+
flex: 1;
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
min-height: 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Empty state: center the chat content + input bar together */
|
|
122
|
+
body.empty-mode .main-col {
|
|
123
|
+
justify-content: center;
|
|
124
|
+
padding-top: 5vh;
|
|
125
|
+
}
|
|
126
|
+
body.empty-mode .chat-wrap {
|
|
127
|
+
flex: 0 0 auto;
|
|
128
|
+
overflow: visible;
|
|
129
|
+
}
|
|
130
|
+
body.empty-mode .chat-wrap::before,
|
|
131
|
+
body.empty-mode .chat-wrap::after {
|
|
132
|
+
display: none;
|
|
133
|
+
}
|
|
134
|
+
body.empty-mode .chat-area {
|
|
135
|
+
height: auto;
|
|
136
|
+
overflow: visible;
|
|
137
|
+
padding: 0 24px;
|
|
138
|
+
}
|
|
139
|
+
body.empty-mode .empty-state {
|
|
140
|
+
flex: 0 0 auto;
|
|
141
|
+
padding: 0;
|
|
142
|
+
}
|
|
143
|
+
body.empty-mode .input-bar {
|
|
144
|
+
padding-top: 36px;
|
|
145
|
+
}
|
|
146
|
+
|
|
108
147
|
/* ── Chat area ── */
|
|
109
148
|
.chat-wrap {
|
|
110
149
|
flex: 1;
|
|
@@ -302,6 +341,39 @@
|
|
|
302
341
|
font-size: 13px;
|
|
303
342
|
}
|
|
304
343
|
|
|
344
|
+
/* Thoughts (collapsible, shown before final answer) */
|
|
345
|
+
.thoughts {
|
|
346
|
+
margin-bottom: 8px;
|
|
347
|
+
border: 1px solid var(--border);
|
|
348
|
+
border-radius: var(--radius);
|
|
349
|
+
background: var(--surface);
|
|
350
|
+
font-size: 13px;
|
|
351
|
+
}
|
|
352
|
+
.thoughts > summary {
|
|
353
|
+
cursor: pointer;
|
|
354
|
+
list-style: none;
|
|
355
|
+
padding: 8px 12px;
|
|
356
|
+
color: var(--text-dim);
|
|
357
|
+
display: flex;
|
|
358
|
+
align-items: center;
|
|
359
|
+
gap: 6px;
|
|
360
|
+
user-select: none;
|
|
361
|
+
}
|
|
362
|
+
.thoughts > summary::-webkit-details-marker { display: none; }
|
|
363
|
+
.thoughts > summary::before {
|
|
364
|
+
content: "▸";
|
|
365
|
+
font-size: 10px;
|
|
366
|
+
transition: transform 0.15s;
|
|
367
|
+
}
|
|
368
|
+
.thoughts[open] > summary::before { transform: rotate(90deg); }
|
|
369
|
+
.thoughts .thought {
|
|
370
|
+
padding: 8px 12px;
|
|
371
|
+
border-top: 1px solid var(--border);
|
|
372
|
+
color: var(--text-secondary);
|
|
373
|
+
white-space: pre-wrap;
|
|
374
|
+
line-height: 1.5;
|
|
375
|
+
}
|
|
376
|
+
|
|
305
377
|
.thinking-spinner {
|
|
306
378
|
width: 14px;
|
|
307
379
|
height: 14px;
|
|
@@ -322,64 +394,44 @@
|
|
|
322
394
|
align-items: center;
|
|
323
395
|
justify-content: center;
|
|
324
396
|
text-align: center;
|
|
397
|
+
opacity: 0;
|
|
398
|
+
transition: opacity 0.3s ease;
|
|
399
|
+
}
|
|
400
|
+
.empty-state.ready {
|
|
401
|
+
opacity: 1;
|
|
325
402
|
}
|
|
326
403
|
|
|
327
404
|
.empty-inner {
|
|
328
405
|
max-width: 480px;
|
|
329
406
|
}
|
|
330
407
|
|
|
331
|
-
.empty-logo {
|
|
332
|
-
width: 48px;
|
|
333
|
-
height: 48px;
|
|
334
|
-
border-radius: 14px;
|
|
335
|
-
margin: 0 auto 20px;
|
|
336
|
-
position: relative;
|
|
337
|
-
overflow: hidden;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
.empty-logo img {
|
|
341
|
-
width: 100%;
|
|
342
|
-
height: 100%;
|
|
343
|
-
object-fit: cover;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
.empty-logo::after {
|
|
347
|
-
content: '';
|
|
348
|
-
position: absolute;
|
|
349
|
-
inset: -8px;
|
|
350
|
-
border-radius: 20px;
|
|
351
|
-
background: var(--accent);
|
|
352
|
-
opacity: 0.06;
|
|
353
|
-
filter: blur(16px);
|
|
354
|
-
z-index: -1;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
408
|
.empty-state h3 {
|
|
358
|
-
font-size:
|
|
409
|
+
font-size: 26px;
|
|
359
410
|
font-weight: 600;
|
|
360
|
-
margin-bottom:
|
|
411
|
+
margin-bottom: 4px;
|
|
361
412
|
color: var(--text);
|
|
362
|
-
letter-spacing: -0.
|
|
413
|
+
letter-spacing: -0.03em;
|
|
363
414
|
}
|
|
364
415
|
|
|
365
|
-
.empty-
|
|
366
|
-
font-size:
|
|
416
|
+
.empty-subtitle {
|
|
417
|
+
font-size: 15px;
|
|
367
418
|
color: var(--text-dim);
|
|
368
419
|
line-height: 1.5;
|
|
420
|
+
margin: 0;
|
|
369
421
|
}
|
|
370
422
|
|
|
371
423
|
.suggestions {
|
|
372
424
|
display: grid;
|
|
373
425
|
grid-template-columns: 1fr 1fr;
|
|
374
|
-
gap:
|
|
375
|
-
margin-top:
|
|
426
|
+
gap: 12px;
|
|
427
|
+
margin-top: 44px;
|
|
376
428
|
}
|
|
377
429
|
|
|
378
430
|
.suggestion {
|
|
379
431
|
display: flex;
|
|
380
432
|
align-items: flex-start;
|
|
381
|
-
gap:
|
|
382
|
-
padding:
|
|
433
|
+
gap: 14px;
|
|
434
|
+
padding: 18px;
|
|
383
435
|
background: var(--surface);
|
|
384
436
|
border: 1px solid var(--border);
|
|
385
437
|
border-radius: var(--radius-md);
|
|
@@ -391,6 +443,18 @@
|
|
|
391
443
|
text-align: left;
|
|
392
444
|
}
|
|
393
445
|
|
|
446
|
+
.suggestion-icon {
|
|
447
|
+
flex-shrink: 0;
|
|
448
|
+
width: 36px;
|
|
449
|
+
height: 36px;
|
|
450
|
+
border-radius: 10px;
|
|
451
|
+
background: var(--accent-soft);
|
|
452
|
+
display: flex;
|
|
453
|
+
align-items: center;
|
|
454
|
+
justify-content: center;
|
|
455
|
+
color: var(--accent);
|
|
456
|
+
}
|
|
457
|
+
|
|
394
458
|
.suggestion:hover {
|
|
395
459
|
border-color: var(--accent);
|
|
396
460
|
background: var(--accent-soft);
|
|
@@ -398,6 +462,11 @@
|
|
|
398
462
|
transform: translateY(-1px);
|
|
399
463
|
}
|
|
400
464
|
|
|
465
|
+
.suggestion:hover .suggestion-icon {
|
|
466
|
+
background: var(--accent);
|
|
467
|
+
color: white;
|
|
468
|
+
}
|
|
469
|
+
|
|
401
470
|
.suggestion:active {
|
|
402
471
|
transform: translateY(0);
|
|
403
472
|
}
|
|
@@ -454,11 +523,13 @@
|
|
|
454
523
|
border: 1px solid var(--border);
|
|
455
524
|
border-radius: 18px;
|
|
456
525
|
padding: 4px 4px 4px 16px;
|
|
457
|
-
transition: border-color 0.2s;
|
|
526
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
527
|
+
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
|
458
528
|
}
|
|
459
529
|
|
|
460
530
|
.input-wrap:focus-within {
|
|
461
531
|
border-color: var(--border-hover);
|
|
532
|
+
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
462
533
|
}
|
|
463
534
|
|
|
464
535
|
.input-wrap textarea {
|
|
@@ -509,13 +580,14 @@
|
|
|
509
580
|
font-size: 11px;
|
|
510
581
|
color: var(--text-faint);
|
|
511
582
|
margin-top: 8px;
|
|
583
|
+
opacity: 0.6;
|
|
512
584
|
}
|
|
513
585
|
</style>
|
|
514
586
|
</head>
|
|
515
587
|
<body>
|
|
516
588
|
<div class="header">
|
|
517
589
|
<div class="header-left">
|
|
518
|
-
<a href="/" class="header-title"
|
|
590
|
+
<a href="/" class="header-title" aria-label="LeedAB"><img class="logo-img header-logo" alt="LeedAB"></a>
|
|
519
591
|
</div>
|
|
520
592
|
<div class="header-nav">
|
|
521
593
|
<a href="/admin">
|
|
@@ -533,13 +605,13 @@
|
|
|
533
605
|
</div>
|
|
534
606
|
</div>
|
|
535
607
|
|
|
608
|
+
<div class="main-col">
|
|
536
609
|
<div class="chat-wrap">
|
|
537
610
|
<div class="chat-area" id="chat-area">
|
|
538
611
|
<div class="empty-state" id="empty-state">
|
|
539
612
|
<div class="empty-inner">
|
|
540
|
-
<
|
|
541
|
-
<
|
|
542
|
-
<p></p>
|
|
613
|
+
<h3 id="empty-greeting"></h3>
|
|
614
|
+
<p class="empty-subtitle" id="empty-subtitle">What can I help with?</p>
|
|
543
615
|
<div class="suggestions">
|
|
544
616
|
<div class="suggestion" onclick="sendSuggestion('Which deliveries are delayed today and what\'s the estimated impact on SLAs?')">
|
|
545
617
|
<div class="suggestion-icon">
|
|
@@ -594,6 +666,7 @@
|
|
|
594
666
|
</div>
|
|
595
667
|
<div class="disclaimer">LeedAB is AI. Verify important information.</div>
|
|
596
668
|
</div>
|
|
669
|
+
</div>
|
|
597
670
|
|
|
598
671
|
<script>
|
|
599
672
|
// Theme
|
|
@@ -613,18 +686,46 @@
|
|
|
613
686
|
const input = document.getElementById("msg-input");
|
|
614
687
|
const sendBtn = document.getElementById("send-btn");
|
|
615
688
|
|
|
689
|
+
// Empty-mode centers the welcome + input together; drop it as soon as
|
|
690
|
+
// the conversation starts so the input pins back to the bottom.
|
|
691
|
+
if (emptyState) document.body.classList.add("empty-mode");
|
|
692
|
+
function exitEmptyMode() {
|
|
693
|
+
document.body.classList.remove("empty-mode");
|
|
694
|
+
exitEmptyMode();
|
|
695
|
+
}
|
|
696
|
+
|
|
616
697
|
// Session: "console" default, or from URL for viewing history
|
|
617
698
|
const params = new URLSearchParams(window.location.search);
|
|
618
699
|
let sessionId = params.get("session") || "console";
|
|
619
700
|
|
|
620
701
|
let userName = "You";
|
|
621
702
|
let userInitial = "Y";
|
|
703
|
+
let agentName = "LeedAB";
|
|
622
704
|
fetch("/api/whoami").then(r => r.json()).then(d => {
|
|
623
705
|
if (d.name) {
|
|
624
706
|
userName = d.name;
|
|
625
707
|
userInitial = d.name.charAt(0).toUpperCase();
|
|
626
708
|
}
|
|
627
|
-
|
|
709
|
+
if (d.agent) {
|
|
710
|
+
agentName = d.agent;
|
|
711
|
+
input.placeholder = `Message ${agentName}...`;
|
|
712
|
+
const disc = document.querySelector(".disclaimer");
|
|
713
|
+
if (disc) disc.textContent = `${agentName} is AI. Verify important information.`;
|
|
714
|
+
document.title = agentName;
|
|
715
|
+
}
|
|
716
|
+
// Time-aware greeting
|
|
717
|
+
const greet = document.getElementById("empty-greeting");
|
|
718
|
+
if (greet) {
|
|
719
|
+
const h = new Date().getHours();
|
|
720
|
+
const tod = h < 12 ? "Good morning" : h < 17 ? "Good afternoon" : "Good evening";
|
|
721
|
+
greet.textContent = d.name ? `${tod}, ${d.name}.` : tod + ".";
|
|
722
|
+
}
|
|
723
|
+
if (emptyState) emptyState.classList.add("ready");
|
|
724
|
+
}).catch(() => {
|
|
725
|
+
const greet = document.getElementById("empty-greeting");
|
|
726
|
+
if (greet) greet.textContent = "How can I help?";
|
|
727
|
+
if (emptyState) emptyState.classList.add("ready");
|
|
728
|
+
});
|
|
628
729
|
|
|
629
730
|
input.addEventListener("input", () => {
|
|
630
731
|
input.style.height = "auto";
|
|
@@ -654,7 +755,7 @@
|
|
|
654
755
|
const text = input.value.trim();
|
|
655
756
|
if (!text) return;
|
|
656
757
|
|
|
657
|
-
|
|
758
|
+
exitEmptyMode();
|
|
658
759
|
|
|
659
760
|
appendMessage(text, "user");
|
|
660
761
|
input.value = "";
|
|
@@ -676,7 +777,7 @@
|
|
|
676
777
|
if (data.error) {
|
|
677
778
|
appendMessage("Something went wrong. Is the gateway running?", "agent");
|
|
678
779
|
} else {
|
|
679
|
-
appendMessage(data.reply, "agent");
|
|
780
|
+
appendMessage(data.reply, "agent", data.thoughts);
|
|
680
781
|
}
|
|
681
782
|
} catch (err) {
|
|
682
783
|
thinkingEl.remove();
|
|
@@ -692,10 +793,10 @@
|
|
|
692
793
|
const res = await fetch(`/api/chat/history?session=${encodeURIComponent(sid)}`);
|
|
693
794
|
const messages = await res.json();
|
|
694
795
|
if (messages.length) {
|
|
695
|
-
|
|
796
|
+
exitEmptyMode();
|
|
696
797
|
for (const m of messages) {
|
|
697
798
|
const type = m.role === "user" ? "user" : "agent";
|
|
698
|
-
appendMessage(m.text || m.content || "", type);
|
|
799
|
+
appendMessage(m.text || m.content || "", type, m.thoughts);
|
|
699
800
|
}
|
|
700
801
|
}
|
|
701
802
|
} catch {}
|
|
@@ -705,7 +806,7 @@
|
|
|
705
806
|
return new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
|
|
706
807
|
}
|
|
707
808
|
|
|
708
|
-
function appendMessage(text, type) {
|
|
809
|
+
function appendMessage(text, type, thoughts) {
|
|
709
810
|
const row = document.createElement("div");
|
|
710
811
|
row.className = `msg-row ${type}`;
|
|
711
812
|
|
|
@@ -726,7 +827,24 @@
|
|
|
726
827
|
|
|
727
828
|
const meta = document.createElement("div");
|
|
728
829
|
meta.className = "msg-meta";
|
|
729
|
-
meta.innerHTML = `<span class="msg-name">${type === "agent" ?
|
|
830
|
+
meta.innerHTML = `<span class="msg-name">${type === "agent" ? agentName : userName}</span><span class="msg-time">${now()}</span>`;
|
|
831
|
+
|
|
832
|
+
content.appendChild(meta);
|
|
833
|
+
|
|
834
|
+
if (type === "agent" && Array.isArray(thoughts) && thoughts.length) {
|
|
835
|
+
const details = document.createElement("details");
|
|
836
|
+
details.className = "thoughts";
|
|
837
|
+
const summary = document.createElement("summary");
|
|
838
|
+
summary.textContent = `Thought for a moment · ${thoughts.length} step${thoughts.length === 1 ? "" : "s"}`;
|
|
839
|
+
details.appendChild(summary);
|
|
840
|
+
for (const t of thoughts) {
|
|
841
|
+
const div = document.createElement("div");
|
|
842
|
+
div.className = "thought";
|
|
843
|
+
div.textContent = t;
|
|
844
|
+
details.appendChild(div);
|
|
845
|
+
}
|
|
846
|
+
content.appendChild(details);
|
|
847
|
+
}
|
|
730
848
|
|
|
731
849
|
const bubble = document.createElement("div");
|
|
732
850
|
bubble.className = `msg-bubble ${type}`;
|
|
@@ -736,7 +854,6 @@
|
|
|
736
854
|
bubble.textContent = text;
|
|
737
855
|
}
|
|
738
856
|
|
|
739
|
-
content.appendChild(meta);
|
|
740
857
|
content.appendChild(bubble);
|
|
741
858
|
row.appendChild(avatar);
|
|
742
859
|
row.appendChild(content);
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
<style>
|
|
12
12
|
.page-header { display:flex; align-items:center; justify-content:space-between; padding:0 20px; height:52px; border-bottom:1px solid var(--border); background:var(--bg); }
|
|
13
13
|
.page-header-left { display:flex; align-items:center; gap:10px; }
|
|
14
|
-
.page-header-title {
|
|
14
|
+
.page-header-title { display:flex; align-items:center; }
|
|
15
|
+
.page-header-title .logo-img { height:22px; width:auto; display:block; }
|
|
15
16
|
.page-nav { display:flex; align-items:center; gap:2px; }
|
|
16
17
|
.page-nav a, .page-nav button { color:var(--text-dim); text-decoration:none; display:flex; align-items:center; gap:5px; padding:6px 12px; border-radius:8px; font-size:13px; font-weight:450; transition:all 0.15s; background:none; border:none; cursor:pointer; font-family:inherit; }
|
|
17
18
|
.page-nav a:hover, .page-nav button:hover { color:var(--text-secondary); background:var(--surface-raised); }
|
|
@@ -20,13 +21,9 @@
|
|
|
20
21
|
|
|
21
22
|
<div class="page-header">
|
|
22
23
|
<div class="page-header-left">
|
|
23
|
-
<a href="/" class="page-header-title"
|
|
24
|
+
<a href="/" class="page-header-title" aria-label="LeedAB"><img class="logo-img" alt="LeedAB"></a>
|
|
24
25
|
</div>
|
|
25
26
|
<div class="page-nav">
|
|
26
|
-
<button class="theme-btn" onclick="toggleTheme()" title="Toggle theme">
|
|
27
|
-
<svg id="theme-icon-sun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
|
28
|
-
<svg id="theme-icon-moon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
29
|
-
</button>
|
|
30
27
|
</div>
|
|
31
28
|
</div>
|
|
32
29
|
|
|
@@ -106,22 +103,8 @@
|
|
|
106
103
|
function initTheme() {
|
|
107
104
|
const saved = localStorage.getItem("leedab-theme") || "dark";
|
|
108
105
|
document.documentElement.setAttribute("data-theme", saved);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
function toggleTheme() {
|
|
112
|
-
const current = document.documentElement.getAttribute("data-theme") || "dark";
|
|
113
|
-
const next = current === "dark" ? "light" : "dark";
|
|
114
|
-
document.documentElement.setAttribute("data-theme", next);
|
|
115
|
-
localStorage.setItem("leedab-theme", next);
|
|
116
|
-
updateThemeIcon(next);
|
|
117
|
-
}
|
|
118
|
-
function updateThemeIcon(theme) {
|
|
119
|
-
const sun = document.getElementById("theme-icon-sun");
|
|
120
|
-
const moon = document.getElementById("theme-icon-moon");
|
|
121
|
-
if (sun && moon) {
|
|
122
|
-
sun.style.display = theme === "dark" ? "block" : "none";
|
|
123
|
-
moon.style.display = theme === "light" ? "block" : "none";
|
|
124
|
-
}
|
|
106
|
+
const src = saved === "dark" ? "/logo-dark.png" : "/logo-light.png";
|
|
107
|
+
document.querySelectorAll(".logo-img").forEach(img => img.src = src);
|
|
125
108
|
}
|
|
126
109
|
initTheme();
|
|
127
110
|
|