@luckydraw/cumulus 0.15.2 → 0.16.1
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.
|
@@ -345,10 +345,10 @@
|
|
|
345
345
|
' max-width: 78ch; align-self: center; width: 100%;',
|
|
346
346
|
'}',
|
|
347
347
|
'.cumulus-attach-btn {',
|
|
348
|
-
' width:
|
|
348
|
+
' width: 2em; height: 2em; flex-shrink: 0;',
|
|
349
349
|
' background: #3d3d3d; border: 1px solid #4a4a4a;',
|
|
350
|
-
' border-radius: 0.
|
|
351
|
-
' font-size:
|
|
350
|
+
' border-radius: 0.45em; color: #aaa;',
|
|
351
|
+
' font-size: 1em; line-height: 1; cursor: pointer;',
|
|
352
352
|
' display: flex; align-items: center; justify-content: center;',
|
|
353
353
|
' padding: 0;',
|
|
354
354
|
'}',
|
|
@@ -422,6 +422,158 @@
|
|
|
422
422
|
' cursor: pointer; font-size: 11px; padding: 2px 6px; font-family: inherit;',
|
|
423
423
|
'}',
|
|
424
424
|
'.cumulus-auth-logout:hover { color: #ef4444; }',
|
|
425
|
+
|
|
426
|
+
/* ── Standalone layout ── */
|
|
427
|
+
'.cumulus-standalone-root {',
|
|
428
|
+
' display: flex; flex-direction: row;',
|
|
429
|
+
' width: 100%; height: 100vh;',
|
|
430
|
+
' background: #1e1e1e; overflow: hidden;',
|
|
431
|
+
'}',
|
|
432
|
+
|
|
433
|
+
/* ── Sidebar ── */
|
|
434
|
+
'.cumulus-sidebar {',
|
|
435
|
+
' width: 220px; flex-shrink: 0;',
|
|
436
|
+
' background: #252525;',
|
|
437
|
+
' border-right: 1px solid #333;',
|
|
438
|
+
' display: flex; flex-direction: column;',
|
|
439
|
+
' overflow: hidden;',
|
|
440
|
+
'}',
|
|
441
|
+
'.cumulus-sidebar-header {',
|
|
442
|
+
' padding: 10px 12px 6px;',
|
|
443
|
+
' display: flex; align-items: center; justify-content: space-between;',
|
|
444
|
+
' border-bottom: 1px solid #333;',
|
|
445
|
+
' flex-shrink: 0;',
|
|
446
|
+
'}',
|
|
447
|
+
'.cumulus-sidebar-title {',
|
|
448
|
+
' font-size: 11px; font-weight: 600; color: #888;',
|
|
449
|
+
' text-transform: uppercase; letter-spacing: 0.05em;',
|
|
450
|
+
'}',
|
|
451
|
+
'.cumulus-sidebar-status {',
|
|
452
|
+
' display: flex; align-items: center; gap: 5px;',
|
|
453
|
+
'}',
|
|
454
|
+
'.cumulus-sidebar-scroll {',
|
|
455
|
+
' flex: 1; overflow-y: auto; padding: 6px 0;',
|
|
456
|
+
'}',
|
|
457
|
+
'.cumulus-sidebar-scroll::-webkit-scrollbar { width: 4px; }',
|
|
458
|
+
'.cumulus-sidebar-scroll::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; }',
|
|
459
|
+
'.cumulus-sidebar-section-label {',
|
|
460
|
+
' font-size: 10px; font-weight: 600; color: #666;',
|
|
461
|
+
' text-transform: uppercase; letter-spacing: 0.06em;',
|
|
462
|
+
' padding: 8px 12px 4px;',
|
|
463
|
+
'}',
|
|
464
|
+
'.cumulus-sidebar-divider {',
|
|
465
|
+
' border: none; border-top: 1px solid #333;',
|
|
466
|
+
' margin: 6px 0;',
|
|
467
|
+
'}',
|
|
468
|
+
'.cumulus-thread-item {',
|
|
469
|
+
' display: flex; align-items: center; gap: 8px;',
|
|
470
|
+
' padding: 7px 12px;',
|
|
471
|
+
' cursor: pointer;',
|
|
472
|
+
' font-size: 13px; color: #ccc;',
|
|
473
|
+
' border-left: 2px solid transparent;',
|
|
474
|
+
' user-select: none;',
|
|
475
|
+
' transition: background 0.1s ease;',
|
|
476
|
+
'}',
|
|
477
|
+
'.cumulus-thread-item:hover { background: #2a2a2a; }',
|
|
478
|
+
'.cumulus-thread-item.selected {',
|
|
479
|
+
' border-left-color: #0066cc;',
|
|
480
|
+
' background: #1e2a3a;',
|
|
481
|
+
' color: #e0e0e0;',
|
|
482
|
+
'}',
|
|
483
|
+
'.cumulus-thread-dot {',
|
|
484
|
+
' width: 7px; height: 7px; border-radius: 50%;',
|
|
485
|
+
' flex-shrink: 0;',
|
|
486
|
+
'}',
|
|
487
|
+
'.cumulus-thread-dot.active { background: #22c55e; }',
|
|
488
|
+
'.cumulus-thread-dot.inactive {',
|
|
489
|
+
' background: transparent;',
|
|
490
|
+
' border: 1px solid #555;',
|
|
491
|
+
'}',
|
|
492
|
+
'.cumulus-thread-name {',
|
|
493
|
+
' flex: 1; overflow: hidden;',
|
|
494
|
+
' text-overflow: ellipsis; white-space: nowrap;',
|
|
495
|
+
'}',
|
|
496
|
+
'.cumulus-thread-count {',
|
|
497
|
+
' font-size: 11px; color: #666;',
|
|
498
|
+
' flex-shrink: 0;',
|
|
499
|
+
'}',
|
|
500
|
+
'.cumulus-sidebar-footer {',
|
|
501
|
+
' padding: 8px 10px;',
|
|
502
|
+
' border-top: 1px solid #333;',
|
|
503
|
+
' flex-shrink: 0;',
|
|
504
|
+
'}',
|
|
505
|
+
'.cumulus-new-thread-btn {',
|
|
506
|
+
' width: 100%;',
|
|
507
|
+
' background: #2a2a2a; border: 1px solid #3a3a3a;',
|
|
508
|
+
' border-radius: 0.45em; color: #aaa;',
|
|
509
|
+
' padding: 7px 10px; font-size: 13px;',
|
|
510
|
+
' cursor: pointer; font-family: inherit;',
|
|
511
|
+
' text-align: left;',
|
|
512
|
+
'}',
|
|
513
|
+
'.cumulus-new-thread-btn:hover { background: #333; color: #ddd; border-color: #0066cc; }',
|
|
514
|
+
'.cumulus-new-thread-input {',
|
|
515
|
+
' width: 100%;',
|
|
516
|
+
' background: #3d3d3d; border: 1px solid #0066cc;',
|
|
517
|
+
' border-radius: 0.45em; color: #e0e0e0;',
|
|
518
|
+
' padding: 7px 10px; font-size: 13px;',
|
|
519
|
+
' font-family: inherit; outline: none;',
|
|
520
|
+
'}',
|
|
521
|
+
|
|
522
|
+
/* ── Content area (holds panels) ── */
|
|
523
|
+
'.cumulus-content-area {',
|
|
524
|
+
' flex: 1; display: flex; flex-direction: row;',
|
|
525
|
+
' overflow: hidden;',
|
|
526
|
+
'}',
|
|
527
|
+
|
|
528
|
+
/* ── Thread panel (inside content area) ── */
|
|
529
|
+
'.cumulus-thread-panel {',
|
|
530
|
+
' flex: 1; display: flex; flex-direction: column;',
|
|
531
|
+
' overflow: hidden; background: #1e1e1e;',
|
|
532
|
+
' border-left: 1px solid #333;',
|
|
533
|
+
' min-width: 0;',
|
|
534
|
+
'}',
|
|
535
|
+
'.cumulus-thread-panel:first-child { border-left: none; }',
|
|
536
|
+
'.cumulus-thread-panel-header {',
|
|
537
|
+
' display: flex; align-items: center; justify-content: space-between;',
|
|
538
|
+
' padding: 0 0.85em;',
|
|
539
|
+
' height: 40px;',
|
|
540
|
+
' background: #2d2d2d;',
|
|
541
|
+
' border-bottom: 1px solid #3a3a3a;',
|
|
542
|
+
' flex-shrink: 0;',
|
|
543
|
+
'}',
|
|
544
|
+
'.cumulus-thread-panel-title {',
|
|
545
|
+
' font-weight: 600; font-size: 14px; color: #e0e0e0;',
|
|
546
|
+
' overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
|
547
|
+
'}',
|
|
548
|
+
'.cumulus-thread-panel-close {',
|
|
549
|
+
' background: none; border: none; color: #666;',
|
|
550
|
+
' cursor: pointer; font-size: 18px; padding: 2px 4px;',
|
|
551
|
+
' line-height: 1; flex-shrink: 0;',
|
|
552
|
+
'}',
|
|
553
|
+
'.cumulus-thread-panel-close:hover { color: #e0e0e0; }',
|
|
554
|
+
|
|
555
|
+
/* ── Standalone empty state (no threads selected) ── */
|
|
556
|
+
'.cumulus-standalone-empty {',
|
|
557
|
+
' flex: 1; display: flex; flex-direction: column;',
|
|
558
|
+
' align-items: center; justify-content: center;',
|
|
559
|
+
' color: #555; font-size: 15px; text-align: center; padding: 20px;',
|
|
560
|
+
' gap: 10px;',
|
|
561
|
+
'}',
|
|
562
|
+
'.cumulus-standalone-empty-hint {',
|
|
563
|
+
' font-size: 12px; color: #444;',
|
|
564
|
+
'}',
|
|
565
|
+
|
|
566
|
+
/* ── Top bar for standalone (connection status + logout) ── */
|
|
567
|
+
'.cumulus-topbar {',
|
|
568
|
+
' display: flex; align-items: center; justify-content: space-between;',
|
|
569
|
+
' padding: 0 14px;',
|
|
570
|
+
' height: 40px;',
|
|
571
|
+
' background: #2d2d2d;',
|
|
572
|
+
' border-bottom: 1px solid #3a3a3a;',
|
|
573
|
+
' flex-shrink: 0;',
|
|
574
|
+
'}',
|
|
575
|
+
'.cumulus-topbar-title { font-weight: 600; font-size: 15px; color: #e0e0e0; }',
|
|
576
|
+
'.cumulus-topbar-right { display: flex; align-items: center; gap: 10px; }',
|
|
425
577
|
].join('\n');
|
|
426
578
|
|
|
427
579
|
// ─── HTML Escaping ───────────────────────────────────────────────────────────
|
|
@@ -694,6 +846,7 @@
|
|
|
694
846
|
var sessionId = opts.sessionId;
|
|
695
847
|
var onMessage = opts.onMessage;
|
|
696
848
|
var onStatus = opts.onStatus;
|
|
849
|
+
var skipHistory = opts.skipHistory || false;
|
|
697
850
|
|
|
698
851
|
var ws = null;
|
|
699
852
|
var reconnectTimer = null;
|
|
@@ -710,7 +863,9 @@
|
|
|
710
863
|
onStatus('connected');
|
|
711
864
|
reconnectDelay = 1000;
|
|
712
865
|
ws.send(JSON.stringify({ type: 'auth', apiKey: apiKey }));
|
|
713
|
-
|
|
866
|
+
if (!skipHistory) {
|
|
867
|
+
ws.send(JSON.stringify({ type: 'history', threadName: sessionId, limit: 50 }));
|
|
868
|
+
}
|
|
714
869
|
};
|
|
715
870
|
|
|
716
871
|
ws.onmessage = function (event) {
|
|
@@ -777,41 +932,552 @@
|
|
|
777
932
|
styleEl.textContent = STYLES;
|
|
778
933
|
document.head.appendChild(styleEl);
|
|
779
934
|
|
|
780
|
-
// ── State ──
|
|
781
|
-
var messages = []; // { role, content, attachments? }
|
|
782
|
-
var streaming = false;
|
|
783
|
-
var stopRequested = false;
|
|
784
|
-
var streamBuffer = '';
|
|
785
|
-
var connection = null;
|
|
786
|
-
var connectionStatus = 'disconnected';
|
|
787
|
-
var authenticated = false;
|
|
788
|
-
var pendingAttachments = []; // { base64, mimeType, name, dataUrl, isImage }
|
|
789
|
-
|
|
790
935
|
// ── Root container ──
|
|
791
936
|
var container = document.createElement('div');
|
|
792
937
|
container.className = 'cumulus-widget';
|
|
793
938
|
|
|
794
|
-
//
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
939
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
940
|
+
// EMBEDDED MODE — unchanged from original behavior
|
|
941
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
942
|
+
if (!standalone) {
|
|
943
|
+
// ── State ──
|
|
944
|
+
var messages = [];
|
|
945
|
+
var streaming = false;
|
|
946
|
+
var stopRequested = false;
|
|
947
|
+
var streamBuffer = '';
|
|
948
|
+
var connection = null;
|
|
949
|
+
var connectionStatus = 'disconnected';
|
|
950
|
+
var authenticated = false;
|
|
951
|
+
var pendingAttachments = [];
|
|
952
|
+
|
|
953
|
+
// ── Panel ──
|
|
954
|
+
var panel = document.createElement('div');
|
|
955
|
+
panel.className = 'cumulus-panel floating';
|
|
956
|
+
panel.style.display = 'none';
|
|
957
|
+
panel.setAttribute('data-testid', 'webchat-panel');
|
|
958
|
+
|
|
959
|
+
// ── Header ──
|
|
960
|
+
var header = document.createElement('div');
|
|
961
|
+
header.className = 'cumulus-header';
|
|
962
|
+
header.innerHTML =
|
|
963
|
+
'<span class="cumulus-header-title">Cumulus</span>' +
|
|
964
|
+
'<span class="cumulus-header-status">' +
|
|
965
|
+
'<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
|
|
966
|
+
'<span data-testid="webchat-status-text">Disconnected</span>' +
|
|
967
|
+
'</span>' +
|
|
968
|
+
'<button class="cumulus-close-btn" data-testid="webchat-close">×</button>';
|
|
969
|
+
panel.appendChild(header);
|
|
970
|
+
|
|
971
|
+
// ── Auth panel ──
|
|
972
|
+
var authPanel = document.createElement('div');
|
|
973
|
+
authPanel.className = 'cumulus-auth';
|
|
974
|
+
authPanel.setAttribute('data-testid', 'webchat-auth');
|
|
975
|
+
authPanel.innerHTML =
|
|
976
|
+
'<div class="cumulus-auth-title">Cumulus Chat</div>' +
|
|
977
|
+
'<div class="cumulus-auth-subtitle">Enter your API key to connect. The key will be saved in your browser.</div>' +
|
|
978
|
+
'<input class="cumulus-auth-input" data-testid="webchat-auth-input" type="password" placeholder="sk-cumulus-..." autocomplete="off" />' +
|
|
979
|
+
'<button class="cumulus-auth-btn" data-testid="webchat-auth-submit">Connect</button>' +
|
|
980
|
+
'<div class="cumulus-auth-error" data-testid="webchat-auth-error"></div>';
|
|
981
|
+
panel.appendChild(authPanel);
|
|
982
|
+
|
|
983
|
+
// ── Messages area ──
|
|
984
|
+
var messagesEl = document.createElement('div');
|
|
985
|
+
messagesEl.className = 'cumulus-messages';
|
|
986
|
+
messagesEl.setAttribute('data-testid', 'webchat-messages');
|
|
987
|
+
messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
|
|
988
|
+
messagesEl.style.display = 'none';
|
|
989
|
+
panel.appendChild(messagesEl);
|
|
990
|
+
|
|
991
|
+
// ── Input area ──
|
|
992
|
+
var inputArea = document.createElement('div');
|
|
993
|
+
inputArea.className = 'cumulus-input-area';
|
|
994
|
+
inputArea.style.display = 'none';
|
|
995
|
+
|
|
996
|
+
var attachStrip = document.createElement('div');
|
|
997
|
+
attachStrip.className = 'cumulus-attach-strip';
|
|
998
|
+
attachStrip.setAttribute('data-testid', 'webchat-attach-strip');
|
|
999
|
+
attachStrip.style.display = 'none';
|
|
1000
|
+
inputArea.appendChild(attachStrip);
|
|
1001
|
+
|
|
1002
|
+
var inputRow = document.createElement('div');
|
|
1003
|
+
inputRow.className = 'cumulus-input-row';
|
|
1004
|
+
|
|
1005
|
+
var fileInput = document.createElement('input');
|
|
1006
|
+
fileInput.type = 'file';
|
|
1007
|
+
fileInput.multiple = true;
|
|
1008
|
+
fileInput.accept = 'image/*,.pdf,.txt,.md,.js,.ts,.py,.json,.csv';
|
|
1009
|
+
fileInput.style.display = 'none';
|
|
1010
|
+
fileInput.setAttribute('data-testid', 'webchat-file-input');
|
|
1011
|
+
|
|
1012
|
+
var attachBtn = document.createElement('button');
|
|
1013
|
+
attachBtn.className = 'cumulus-attach-btn';
|
|
1014
|
+
attachBtn.setAttribute('data-testid', 'webchat-attach-btn');
|
|
1015
|
+
attachBtn.setAttribute('title', 'Attach files');
|
|
1016
|
+
attachBtn.textContent = '+';
|
|
1017
|
+
|
|
1018
|
+
var input = document.createElement('textarea');
|
|
1019
|
+
input.className = 'cumulus-input';
|
|
1020
|
+
input.setAttribute('data-testid', 'webchat-input');
|
|
1021
|
+
input.placeholder = 'Type a message\u2026';
|
|
1022
|
+
input.rows = 1;
|
|
1023
|
+
|
|
1024
|
+
var sendBtn = document.createElement('button');
|
|
1025
|
+
sendBtn.className = 'cumulus-send-btn';
|
|
1026
|
+
sendBtn.setAttribute('data-testid', 'webchat-send');
|
|
1027
|
+
sendBtn.textContent = 'Send';
|
|
1028
|
+
|
|
1029
|
+
inputRow.appendChild(fileInput);
|
|
1030
|
+
inputRow.appendChild(attachBtn);
|
|
1031
|
+
inputRow.appendChild(input);
|
|
1032
|
+
inputRow.appendChild(sendBtn);
|
|
1033
|
+
inputArea.appendChild(inputRow);
|
|
1034
|
+
panel.appendChild(inputArea);
|
|
1035
|
+
container.appendChild(panel);
|
|
1036
|
+
|
|
1037
|
+
// ── Floating bubble ──
|
|
1038
|
+
var bubble = document.createElement('button');
|
|
1039
|
+
bubble.className = 'cumulus-bubble';
|
|
1040
|
+
bubble.setAttribute('data-testid', 'webchat-bubble');
|
|
1041
|
+
bubble.setAttribute('title', 'Open chat');
|
|
1042
|
+
bubble.innerHTML =
|
|
1043
|
+
'<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>';
|
|
1044
|
+
container.appendChild(bubble);
|
|
1045
|
+
|
|
1046
|
+
// ── View switching ──
|
|
1047
|
+
function showAuthView() {
|
|
1048
|
+
authenticated = false;
|
|
1049
|
+
authPanel.style.display = 'flex';
|
|
1050
|
+
messagesEl.style.display = 'none';
|
|
1051
|
+
inputArea.style.display = 'none';
|
|
1052
|
+
var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
|
|
1053
|
+
if (errEl) errEl.textContent = '';
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function showChatView() {
|
|
1057
|
+
authenticated = true;
|
|
1058
|
+
authPanel.style.display = 'none';
|
|
1059
|
+
messagesEl.style.display = 'flex';
|
|
1060
|
+
inputArea.style.display = 'flex';
|
|
1061
|
+
input.focus();
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// ── Rendering ──
|
|
1065
|
+
function scrollToBottom() {
|
|
1066
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function isWideUserMessage(content) {
|
|
1070
|
+
if (/```/.test(content)) return true;
|
|
1071
|
+
var lineCount = (content.match(/\n/g) || []).length + 1;
|
|
1072
|
+
return lineCount > 3;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function buildUserMsgEl(msg) {
|
|
1076
|
+
var el = document.createElement('div');
|
|
1077
|
+
el.className = 'cumulus-msg user' + (isWideUserMessage(msg.content) ? ' wide' : '');
|
|
1078
|
+
el.textContent = msg.content;
|
|
1079
|
+
if (msg.attachments && msg.attachments.length > 0) {
|
|
1080
|
+
var attRow = document.createElement('div');
|
|
1081
|
+
attRow.className = 'cumulus-msg-attachments';
|
|
1082
|
+
msg.attachments.forEach(function (att) {
|
|
1083
|
+
if (att.isImage) {
|
|
1084
|
+
var img = document.createElement('img');
|
|
1085
|
+
img.className = 'cumulus-msg-img';
|
|
1086
|
+
img.src = att.dataUrl;
|
|
1087
|
+
img.alt = att.name;
|
|
1088
|
+
img.setAttribute('data-testid', 'webchat-msg-img');
|
|
1089
|
+
attRow.appendChild(img);
|
|
1090
|
+
} else {
|
|
1091
|
+
var badge = document.createElement('span');
|
|
1092
|
+
badge.className = 'cumulus-msg-file-badge';
|
|
1093
|
+
badge.setAttribute('data-testid', 'webchat-msg-file');
|
|
1094
|
+
badge.textContent = '\uD83D\uDCCE ' + att.name;
|
|
1095
|
+
attRow.appendChild(badge);
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
el.appendChild(attRow);
|
|
1099
|
+
}
|
|
1100
|
+
return el;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function buildAssistantMsgEl(content, isStreaming) {
|
|
1104
|
+
var el = document.createElement('div');
|
|
1105
|
+
el.className = 'cumulus-msg assistant';
|
|
1106
|
+
if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
|
|
1107
|
+
if (content) {
|
|
1108
|
+
el.innerHTML = renderMarkdown(content);
|
|
1109
|
+
if (isStreaming) {
|
|
1110
|
+
el.innerHTML += '<span class="cumulus-cursor"></span>';
|
|
1111
|
+
}
|
|
1112
|
+
el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
|
|
1113
|
+
btn.addEventListener('click', function () {
|
|
1114
|
+
var targetId = btn.getAttribute('data-copy-target');
|
|
1115
|
+
var codeEl = el.querySelector('[data-code-id="' + targetId + '"]');
|
|
1116
|
+
if (codeEl && navigator.clipboard) {
|
|
1117
|
+
navigator.clipboard.writeText(codeEl.textContent || '').then(function () {
|
|
1118
|
+
btn.textContent = 'Copied!';
|
|
1119
|
+
setTimeout(function () { btn.textContent = 'Copy'; }, 2000);
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
} else if (isStreaming) {
|
|
1125
|
+
el.innerHTML =
|
|
1126
|
+
'<span style="color:#666;font-style:italic">Thinking\u2026</span>' +
|
|
1127
|
+
'<span class="cumulus-cursor"></span>';
|
|
1128
|
+
}
|
|
1129
|
+
return el;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function renderMessages() {
|
|
1133
|
+
messagesEl.innerHTML = '';
|
|
1134
|
+
if (messages.length === 0 && !streaming) {
|
|
1135
|
+
messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
for (var i = 0; i < messages.length; i++) {
|
|
1139
|
+
var msg = messages[i];
|
|
1140
|
+
var row = document.createElement('div');
|
|
1141
|
+
row.className = 'cumulus-msg-row';
|
|
1142
|
+
if (msg.role === 'user') {
|
|
1143
|
+
row.appendChild(buildUserMsgEl(msg));
|
|
1144
|
+
} else {
|
|
1145
|
+
row.appendChild(buildAssistantMsgEl(msg.content, false));
|
|
1146
|
+
}
|
|
1147
|
+
messagesEl.appendChild(row);
|
|
1148
|
+
}
|
|
1149
|
+
if (streaming) {
|
|
1150
|
+
var row = document.createElement('div');
|
|
1151
|
+
row.className = 'cumulus-msg-row';
|
|
1152
|
+
row.appendChild(buildAssistantMsgEl(streamBuffer, true));
|
|
1153
|
+
messagesEl.appendChild(row);
|
|
1154
|
+
}
|
|
1155
|
+
scrollToBottom();
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function updateSendBtn() {
|
|
1159
|
+
if (streaming) {
|
|
1160
|
+
sendBtn.textContent = 'Stop';
|
|
1161
|
+
sendBtn.classList.add('stop');
|
|
1162
|
+
sendBtn.disabled = false;
|
|
1163
|
+
} else {
|
|
1164
|
+
sendBtn.textContent = 'Send';
|
|
1165
|
+
sendBtn.classList.remove('stop');
|
|
1166
|
+
sendBtn.disabled = false;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function updateStatus(status) {
|
|
1171
|
+
connectionStatus = status;
|
|
1172
|
+
var dot = panel.querySelector('.cumulus-status-dot');
|
|
1173
|
+
var text = panel.querySelector('[data-testid="webchat-status-text"]');
|
|
1174
|
+
if (dot) dot.className = 'cumulus-status-dot ' + status;
|
|
1175
|
+
if (text) text.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function renderAttachStrip() {
|
|
1179
|
+
attachStrip.innerHTML = '';
|
|
1180
|
+
if (pendingAttachments.length === 0) {
|
|
1181
|
+
attachStrip.style.display = 'none';
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
attachStrip.style.display = 'flex';
|
|
1185
|
+
pendingAttachments.forEach(function (att, idx) {
|
|
1186
|
+
var chip = document.createElement('div');
|
|
1187
|
+
chip.className = 'cumulus-attach-chip';
|
|
1188
|
+
chip.setAttribute('data-testid', 'webchat-attach-chip');
|
|
1189
|
+
if (att.isImage) {
|
|
1190
|
+
var thumb = document.createElement('img');
|
|
1191
|
+
thumb.className = 'cumulus-attach-chip-thumb';
|
|
1192
|
+
thumb.src = att.dataUrl;
|
|
1193
|
+
thumb.alt = att.name;
|
|
1194
|
+
chip.appendChild(thumb);
|
|
1195
|
+
} else {
|
|
1196
|
+
var icon = document.createElement('div');
|
|
1197
|
+
icon.className = 'cumulus-attach-chip-icon';
|
|
1198
|
+
icon.textContent = fileExtension(att.name);
|
|
1199
|
+
chip.appendChild(icon);
|
|
1200
|
+
}
|
|
1201
|
+
var nameEl = document.createElement('div');
|
|
1202
|
+
nameEl.className = 'cumulus-attach-chip-name';
|
|
1203
|
+
nameEl.textContent = att.name;
|
|
1204
|
+
chip.appendChild(nameEl);
|
|
1205
|
+
var removeBtn = document.createElement('button');
|
|
1206
|
+
removeBtn.className = 'cumulus-attach-chip-remove';
|
|
1207
|
+
removeBtn.setAttribute('data-testid', 'webchat-attach-remove');
|
|
1208
|
+
removeBtn.textContent = '\xD7';
|
|
1209
|
+
removeBtn.setAttribute('title', 'Remove attachment');
|
|
1210
|
+
(function (index) {
|
|
1211
|
+
removeBtn.addEventListener('click', function () {
|
|
1212
|
+
pendingAttachments.splice(index, 1);
|
|
1213
|
+
renderAttachStrip();
|
|
1214
|
+
});
|
|
1215
|
+
})(idx);
|
|
1216
|
+
chip.appendChild(removeBtn);
|
|
1217
|
+
attachStrip.appendChild(chip);
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
async function addFilesToPending(files) {
|
|
1222
|
+
for (var i = 0; i < files.length; i++) {
|
|
1223
|
+
var file = files[i];
|
|
1224
|
+
try {
|
|
1225
|
+
var info = await readFileAsBase64(file);
|
|
1226
|
+
var dataUrl = await readFileAsDataUrl(file);
|
|
1227
|
+
pendingAttachments.push({
|
|
1228
|
+
base64: info.base64,
|
|
1229
|
+
mimeType: info.mimeType,
|
|
1230
|
+
name: file.name,
|
|
1231
|
+
dataUrl: dataUrl,
|
|
1232
|
+
isImage: file.type.startsWith('image/'),
|
|
1233
|
+
});
|
|
1234
|
+
} catch (e) {
|
|
1235
|
+
console.error('[Cumulus] Failed to read file:', file.name, e);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
renderAttachStrip();
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function handleServerMessage(data) {
|
|
1242
|
+
switch (data.type) {
|
|
1243
|
+
case 'auth_ok':
|
|
1244
|
+
showChatView();
|
|
1245
|
+
break;
|
|
1246
|
+
case 'auth_error':
|
|
1247
|
+
console.error('[Cumulus] Auth failed:', data.error);
|
|
1248
|
+
break;
|
|
1249
|
+
case 'history':
|
|
1250
|
+
if (data.messages && data.messages.length > 0) {
|
|
1251
|
+
messages = data.messages.map(function (m) {
|
|
1252
|
+
return { role: m.role, content: m.content };
|
|
1253
|
+
});
|
|
1254
|
+
renderMessages();
|
|
1255
|
+
}
|
|
1256
|
+
break;
|
|
1257
|
+
case 'token':
|
|
1258
|
+
if (stopRequested) break;
|
|
1259
|
+
if (!streaming) {
|
|
1260
|
+
streaming = true;
|
|
1261
|
+
streamBuffer = '';
|
|
1262
|
+
updateSendBtn();
|
|
1263
|
+
}
|
|
1264
|
+
streamBuffer += data.text;
|
|
1265
|
+
renderMessages();
|
|
1266
|
+
break;
|
|
1267
|
+
case 'segment':
|
|
1268
|
+
break;
|
|
1269
|
+
case 'done':
|
|
1270
|
+
if (stopRequested) {
|
|
1271
|
+
stopRequested = false;
|
|
1272
|
+
streaming = false;
|
|
1273
|
+
if (streamBuffer) {
|
|
1274
|
+
messages.push({ role: 'assistant', content: streamBuffer });
|
|
1275
|
+
}
|
|
1276
|
+
streamBuffer = '';
|
|
1277
|
+
updateSendBtn();
|
|
1278
|
+
renderMessages();
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
streaming = false;
|
|
1282
|
+
messages.push({ role: 'assistant', content: data.response || streamBuffer });
|
|
1283
|
+
streamBuffer = '';
|
|
1284
|
+
updateSendBtn();
|
|
1285
|
+
renderMessages();
|
|
1286
|
+
break;
|
|
1287
|
+
case 'error':
|
|
1288
|
+
stopRequested = false;
|
|
1289
|
+
streaming = false;
|
|
1290
|
+
if (streamBuffer) {
|
|
1291
|
+
messages.push({ role: 'assistant', content: streamBuffer + '\n\n[Error: ' + (data.error || 'Unknown error') + ']' });
|
|
1292
|
+
} else {
|
|
1293
|
+
messages.push({ role: 'assistant', content: '[Error: ' + (data.error || 'Unknown error') + ']' });
|
|
1294
|
+
}
|
|
1295
|
+
streamBuffer = '';
|
|
1296
|
+
updateSendBtn();
|
|
1297
|
+
renderMessages();
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function sendMessage() {
|
|
1303
|
+
if (streaming) {
|
|
1304
|
+
stopRequested = true;
|
|
1305
|
+
streaming = false;
|
|
1306
|
+
updateSendBtn();
|
|
1307
|
+
if (streamBuffer) {
|
|
1308
|
+
messages.push({ role: 'assistant', content: streamBuffer + ' [stopped]' });
|
|
1309
|
+
streamBuffer = '';
|
|
1310
|
+
}
|
|
1311
|
+
renderMessages();
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
var text = input.value.trim();
|
|
1315
|
+
if (!text && pendingAttachments.length === 0) return;
|
|
1316
|
+
if (!connection) return;
|
|
1317
|
+
var attachSnapshot = pendingAttachments.slice();
|
|
1318
|
+
var displayText = text || '(attachment)';
|
|
1319
|
+
messages.push({ role: 'user', content: displayText, attachments: attachSnapshot });
|
|
1320
|
+
input.value = '';
|
|
1321
|
+
input.style.height = 'auto';
|
|
1322
|
+
pendingAttachments = [];
|
|
1323
|
+
renderAttachStrip();
|
|
1324
|
+
streaming = true;
|
|
1325
|
+
stopRequested = false;
|
|
1326
|
+
streamBuffer = '';
|
|
1327
|
+
updateSendBtn();
|
|
1328
|
+
renderMessages();
|
|
1329
|
+
var imagePayload = attachSnapshot
|
|
1330
|
+
.filter(function (a) { return a.isImage; })
|
|
1331
|
+
.map(function (a) { return { mimeType: a.mimeType, base64: a.base64 }; });
|
|
1332
|
+
var payload = {
|
|
1333
|
+
type: 'message',
|
|
1334
|
+
threadName: sessionId,
|
|
1335
|
+
message: text || ' ',
|
|
1336
|
+
};
|
|
1337
|
+
if (imagePayload.length > 0) payload.images = imagePayload;
|
|
1338
|
+
connection.send(payload);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// ── Event listeners ──
|
|
1342
|
+
sendBtn.addEventListener('click', sendMessage);
|
|
1343
|
+
input.addEventListener('keydown', function (e) {
|
|
1344
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1345
|
+
e.preventDefault();
|
|
1346
|
+
sendMessage();
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
input.addEventListener('input', function () {
|
|
1350
|
+
input.style.height = 'auto';
|
|
1351
|
+
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
|
|
1352
|
+
});
|
|
1353
|
+
input.addEventListener('paste', function (e) {
|
|
1354
|
+
var items = e.clipboardData && e.clipboardData.items;
|
|
1355
|
+
if (!items) return;
|
|
1356
|
+
var filesToAdd = [];
|
|
1357
|
+
for (var i = 0; i < items.length; i++) {
|
|
1358
|
+
var item = items[i];
|
|
1359
|
+
if (item.kind === 'file') {
|
|
1360
|
+
var file = item.getAsFile();
|
|
1361
|
+
if (file) filesToAdd.push(file);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (filesToAdd.length > 0) {
|
|
1365
|
+
e.preventDefault();
|
|
1366
|
+
addFilesToPending(filesToAdd);
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
fileInput.addEventListener('change', function () {
|
|
1370
|
+
if (fileInput.files && fileInput.files.length > 0) {
|
|
1371
|
+
addFilesToPending(Array.from(fileInput.files));
|
|
1372
|
+
fileInput.value = '';
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
attachBtn.addEventListener('click', function () {
|
|
1376
|
+
fileInput.click();
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
bubble.addEventListener('click', function () {
|
|
1380
|
+
var showing = panel.style.display !== 'none';
|
|
1381
|
+
panel.style.display = showing ? 'none' : 'flex';
|
|
1382
|
+
if (!showing && !connection) {
|
|
1383
|
+
startConnection();
|
|
1384
|
+
}
|
|
1385
|
+
if (!showing) {
|
|
1386
|
+
input.focus();
|
|
1387
|
+
scrollToBottom();
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
var closeBtn = panel.querySelector('.cumulus-close-btn');
|
|
1392
|
+
if (closeBtn) {
|
|
1393
|
+
closeBtn.addEventListener('click', function () {
|
|
1394
|
+
panel.style.display = 'none';
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Embedded mode: no auth panel handling needed (API key from attribute)
|
|
1399
|
+
function startConnection() {
|
|
1400
|
+
if (connection) { connection.close(); connection = null; }
|
|
1401
|
+
connection = createConnection({
|
|
1402
|
+
wsUrl: wsUrl,
|
|
1403
|
+
apiKey: activeApiKey,
|
|
1404
|
+
sessionId: sessionId,
|
|
1405
|
+
onMessage: handleServerMessage,
|
|
1406
|
+
onStatus: updateStatus,
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function mount(target) {
|
|
1411
|
+
(target || document.body).appendChild(container);
|
|
1412
|
+
// Embedded: connect on first open (bubble click), not eagerly
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function destroy() {
|
|
1416
|
+
if (connection) connection.close();
|
|
1417
|
+
container.remove();
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
return { mount: mount, destroy: destroy, send: sendMessage };
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1424
|
+
// STANDALONE MODE — sidebar + multi-panel layout
|
|
1425
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
799
1426
|
|
|
800
|
-
// ──
|
|
801
|
-
var
|
|
802
|
-
|
|
803
|
-
|
|
1427
|
+
// ── Standalone state ──
|
|
1428
|
+
var connection = null;
|
|
1429
|
+
var connectionStatus = 'disconnected';
|
|
1430
|
+
var authenticated = false;
|
|
1431
|
+
|
|
1432
|
+
// Thread registry: threadName -> { messages, streaming, streamBuffer, stopRequested, pendingAttachments }
|
|
1433
|
+
var threadStates = {};
|
|
1434
|
+
|
|
1435
|
+
// Thread list from server: [{ name, messageCount, lastActivity }]
|
|
1436
|
+
var allThreads = [];
|
|
1437
|
+
|
|
1438
|
+
// Currently visible panel names (ordered, max 3)
|
|
1439
|
+
var visibleThreads = [];
|
|
1440
|
+
|
|
1441
|
+
// Periodic refresh timer
|
|
1442
|
+
var refreshTimer = null;
|
|
1443
|
+
|
|
1444
|
+
// ── Helper: get or create per-thread state ──
|
|
1445
|
+
function getThreadState(threadName) {
|
|
1446
|
+
if (!threadStates[threadName]) {
|
|
1447
|
+
threadStates[threadName] = {
|
|
1448
|
+
messages: [],
|
|
1449
|
+
streaming: false,
|
|
1450
|
+
streamBuffer: '',
|
|
1451
|
+
stopRequested: false,
|
|
1452
|
+
pendingAttachments: [],
|
|
1453
|
+
historyLoaded: false,
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
return threadStates[threadName];
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// ── Root layout ──
|
|
1460
|
+
var standaloneRoot = document.createElement('div');
|
|
1461
|
+
standaloneRoot.className = 'cumulus-standalone-root';
|
|
1462
|
+
|
|
1463
|
+
// ── Outer wrapper (auth view or app view) ──
|
|
1464
|
+
// Auth panel — shown before connection
|
|
1465
|
+
var authWrapper = document.createElement('div');
|
|
1466
|
+
authWrapper.className = 'cumulus-panel standalone';
|
|
1467
|
+
authWrapper.setAttribute('data-testid', 'webchat-panel');
|
|
1468
|
+
authWrapper.style.display = 'flex';
|
|
1469
|
+
|
|
1470
|
+
// Auth header
|
|
1471
|
+
var authHeader = document.createElement('div');
|
|
1472
|
+
authHeader.className = 'cumulus-header';
|
|
1473
|
+
authHeader.innerHTML =
|
|
804
1474
|
'<span class="cumulus-header-title">Cumulus</span>' +
|
|
805
1475
|
'<span class="cumulus-header-status">' +
|
|
806
1476
|
'<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
|
|
807
1477
|
'<span data-testid="webchat-status-text">Disconnected</span>' +
|
|
808
|
-
'</span>'
|
|
809
|
-
|
|
810
|
-
? '<button class="cumulus-auth-logout" data-testid="webchat-logout" style="display:none">Logout</button>'
|
|
811
|
-
: '<button class="cumulus-close-btn" data-testid="webchat-close">×</button>');
|
|
812
|
-
panel.appendChild(header);
|
|
1478
|
+
'</span>';
|
|
1479
|
+
authWrapper.appendChild(authHeader);
|
|
813
1480
|
|
|
814
|
-
// ── Auth panel ──
|
|
815
1481
|
var authPanel = document.createElement('div');
|
|
816
1482
|
authPanel.className = 'cumulus-auth';
|
|
817
1483
|
authPanel.setAttribute('data-testid', 'webchat-auth');
|
|
@@ -821,492 +1487,834 @@
|
|
|
821
1487
|
'<input class="cumulus-auth-input" data-testid="webchat-auth-input" type="password" placeholder="sk-cumulus-..." autocomplete="off" />' +
|
|
822
1488
|
'<button class="cumulus-auth-btn" data-testid="webchat-auth-submit">Connect</button>' +
|
|
823
1489
|
'<div class="cumulus-auth-error" data-testid="webchat-auth-error"></div>';
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
//
|
|
827
|
-
var
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
var
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1490
|
+
authWrapper.appendChild(authPanel);
|
|
1491
|
+
|
|
1492
|
+
// App layout — shown after auth
|
|
1493
|
+
var appLayout = document.createElement('div');
|
|
1494
|
+
appLayout.style.cssText = 'display:none; flex-direction:column; width:100%; height:100vh; overflow:hidden;';
|
|
1495
|
+
|
|
1496
|
+
// Top bar
|
|
1497
|
+
var topbar = document.createElement('div');
|
|
1498
|
+
topbar.className = 'cumulus-topbar';
|
|
1499
|
+
topbar.innerHTML =
|
|
1500
|
+
'<span class="cumulus-topbar-title">Cumulus</span>' +
|
|
1501
|
+
'<div class="cumulus-topbar-right">' +
|
|
1502
|
+
'<span class="cumulus-header-status">' +
|
|
1503
|
+
'<span class="cumulus-status-dot" data-testid="webchat-status-dot-app"></span>' +
|
|
1504
|
+
'<span data-testid="webchat-status-text-app">Disconnected</span>' +
|
|
1505
|
+
'</span>' +
|
|
1506
|
+
'<button class="cumulus-auth-logout" data-testid="webchat-logout">Logout</button>' +
|
|
1507
|
+
'</div>';
|
|
1508
|
+
appLayout.appendChild(topbar);
|
|
1509
|
+
|
|
1510
|
+
// Sidebar + content row
|
|
1511
|
+
var appRow = document.createElement('div');
|
|
1512
|
+
appRow.style.cssText = 'display:flex; flex-direction:row; flex:1; overflow:hidden;';
|
|
1513
|
+
|
|
1514
|
+
// ── Sidebar ──
|
|
1515
|
+
var sidebar = document.createElement('div');
|
|
1516
|
+
sidebar.className = 'cumulus-sidebar';
|
|
1517
|
+
sidebar.setAttribute('data-testid', 'webchat-sidebar');
|
|
1518
|
+
|
|
1519
|
+
// Sidebar header
|
|
1520
|
+
var sidebarHeader = document.createElement('div');
|
|
1521
|
+
sidebarHeader.className = 'cumulus-sidebar-header';
|
|
1522
|
+
sidebarHeader.innerHTML = '<span class="cumulus-sidebar-title">Threads</span>';
|
|
1523
|
+
sidebar.appendChild(sidebarHeader);
|
|
1524
|
+
|
|
1525
|
+
// Sidebar scroll area
|
|
1526
|
+
var sidebarScroll = document.createElement('div');
|
|
1527
|
+
sidebarScroll.className = 'cumulus-sidebar-scroll';
|
|
1528
|
+
sidebar.appendChild(sidebarScroll);
|
|
1529
|
+
|
|
1530
|
+
// Sidebar footer with + New button
|
|
1531
|
+
var sidebarFooter = document.createElement('div');
|
|
1532
|
+
sidebarFooter.className = 'cumulus-sidebar-footer';
|
|
1533
|
+
|
|
1534
|
+
var newThreadBtn = document.createElement('button');
|
|
1535
|
+
newThreadBtn.className = 'cumulus-new-thread-btn';
|
|
1536
|
+
newThreadBtn.setAttribute('data-testid', 'webchat-thread-new');
|
|
1537
|
+
newThreadBtn.textContent = '+ New thread';
|
|
1538
|
+
|
|
1539
|
+
var newThreadInput = document.createElement('input');
|
|
1540
|
+
newThreadInput.className = 'cumulus-new-thread-input';
|
|
1541
|
+
newThreadInput.setAttribute('data-testid', 'webchat-thread-new-input');
|
|
1542
|
+
newThreadInput.placeholder = 'Thread name\u2026';
|
|
1543
|
+
newThreadInput.style.display = 'none';
|
|
1544
|
+
|
|
1545
|
+
sidebarFooter.appendChild(newThreadBtn);
|
|
1546
|
+
sidebarFooter.appendChild(newThreadInput);
|
|
1547
|
+
sidebar.appendChild(sidebarFooter);
|
|
1548
|
+
|
|
1549
|
+
// ── Content area ──
|
|
1550
|
+
var contentArea = document.createElement('div');
|
|
1551
|
+
contentArea.className = 'cumulus-content-area';
|
|
1552
|
+
|
|
1553
|
+
appRow.appendChild(sidebar);
|
|
1554
|
+
appRow.appendChild(contentArea);
|
|
1555
|
+
appLayout.appendChild(appRow);
|
|
1556
|
+
|
|
1557
|
+
standaloneRoot.appendChild(authWrapper);
|
|
1558
|
+
standaloneRoot.appendChild(appLayout);
|
|
1559
|
+
container.appendChild(standaloneRoot);
|
|
1560
|
+
|
|
1561
|
+
// ── Status update (updates both auth-phase and app-phase dots) ──
|
|
1562
|
+
function updateStatus(status) {
|
|
1563
|
+
connectionStatus = status;
|
|
1564
|
+
// Auth phase dot
|
|
1565
|
+
var dot = authWrapper.querySelector('[data-testid="webchat-status-dot"]');
|
|
1566
|
+
var text = authWrapper.querySelector('[data-testid="webchat-status-text"]');
|
|
1567
|
+
if (dot) dot.className = 'cumulus-status-dot ' + status;
|
|
1568
|
+
if (text) text.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
|
1569
|
+
// App phase dot
|
|
1570
|
+
var dotApp = appLayout.querySelector('[data-testid="webchat-status-dot-app"]');
|
|
1571
|
+
var textApp = appLayout.querySelector('[data-testid="webchat-status-text-app"]');
|
|
1572
|
+
if (dotApp) dotApp.className = 'cumulus-status-dot ' + status;
|
|
1573
|
+
if (textApp) textApp.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
|
897
1574
|
}
|
|
898
1575
|
|
|
899
1576
|
// ── View switching ──
|
|
900
1577
|
function showAuthView() {
|
|
901
1578
|
authenticated = false;
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1579
|
+
authWrapper.style.display = 'flex';
|
|
1580
|
+
appLayout.style.display = 'none';
|
|
1581
|
+
visibleThreads = [];
|
|
1582
|
+
threadStates = {};
|
|
1583
|
+
allThreads = [];
|
|
1584
|
+
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
|
|
907
1585
|
var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
|
|
908
1586
|
if (errEl) errEl.textContent = '';
|
|
909
1587
|
}
|
|
910
1588
|
|
|
911
|
-
function
|
|
1589
|
+
function showAppView() {
|
|
912
1590
|
authenticated = true;
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1591
|
+
authWrapper.style.display = 'none';
|
|
1592
|
+
appLayout.style.display = 'flex';
|
|
1593
|
+
// Request thread list from server
|
|
1594
|
+
requestThreadList();
|
|
1595
|
+
// Periodic refresh every 30s
|
|
1596
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
1597
|
+
refreshTimer = setInterval(requestThreadList, 30000);
|
|
919
1598
|
}
|
|
920
1599
|
|
|
921
|
-
// ──
|
|
922
|
-
function
|
|
923
|
-
|
|
1600
|
+
// ── Thread list ──
|
|
1601
|
+
function requestThreadList() {
|
|
1602
|
+
if (connection) {
|
|
1603
|
+
connection.send({ type: 'threads' });
|
|
1604
|
+
}
|
|
924
1605
|
}
|
|
925
1606
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
var lineCount = (content.match(/\n/g) || []).length + 1;
|
|
930
|
-
return lineCount > 3;
|
|
931
|
-
}
|
|
1607
|
+
// ── Sidebar rendering ──
|
|
1608
|
+
function renderSidebar() {
|
|
1609
|
+
sidebarScroll.innerHTML = '';
|
|
932
1610
|
|
|
933
|
-
|
|
934
|
-
var
|
|
935
|
-
|
|
936
|
-
|
|
1611
|
+
var visibleSet = {};
|
|
1612
|
+
for (var i = 0; i < visibleThreads.length; i++) {
|
|
1613
|
+
visibleSet[visibleThreads[i]] = true;
|
|
1614
|
+
}
|
|
937
1615
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
var badge = document.createElement('span');
|
|
951
|
-
badge.className = 'cumulus-msg-file-badge';
|
|
952
|
-
badge.setAttribute('data-testid', 'webchat-msg-file');
|
|
953
|
-
badge.textContent = '\uD83D\uDCCE ' + att.name; // 📎
|
|
954
|
-
attRow.appendChild(badge);
|
|
955
|
-
}
|
|
1616
|
+
// Active section: threads currently visible as panels
|
|
1617
|
+
var activeThreads = visibleThreads.slice(); // preserve order
|
|
1618
|
+
if (activeThreads.length > 0) {
|
|
1619
|
+
var activeLabel = document.createElement('div');
|
|
1620
|
+
activeLabel.className = 'cumulus-sidebar-section-label';
|
|
1621
|
+
activeLabel.textContent = 'Active';
|
|
1622
|
+
sidebarScroll.appendChild(activeLabel);
|
|
1623
|
+
|
|
1624
|
+
var activeSection = document.createElement('div');
|
|
1625
|
+
activeSection.setAttribute('data-testid', 'webchat-sidebar-active');
|
|
1626
|
+
activeThreads.forEach(function (name) {
|
|
1627
|
+
activeSection.appendChild(buildThreadItem(name, true, true));
|
|
956
1628
|
});
|
|
957
|
-
|
|
1629
|
+
sidebarScroll.appendChild(activeSection);
|
|
1630
|
+
|
|
1631
|
+
var divider = document.createElement('hr');
|
|
1632
|
+
divider.className = 'cumulus-sidebar-divider';
|
|
1633
|
+
sidebarScroll.appendChild(divider);
|
|
958
1634
|
}
|
|
959
|
-
return el;
|
|
960
|
-
}
|
|
961
1635
|
|
|
962
|
-
|
|
963
|
-
var
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1636
|
+
// All section: all threads from server, sorted by lastActivity desc
|
|
1637
|
+
var allLabel = document.createElement('div');
|
|
1638
|
+
allLabel.className = 'cumulus-sidebar-section-label';
|
|
1639
|
+
allLabel.textContent = 'All';
|
|
1640
|
+
sidebarScroll.appendChild(allLabel);
|
|
1641
|
+
|
|
1642
|
+
var allSection = document.createElement('div');
|
|
1643
|
+
allSection.setAttribute('data-testid', 'webchat-sidebar-all');
|
|
1644
|
+
|
|
1645
|
+
if (allThreads.length === 0) {
|
|
1646
|
+
var emptyNote = document.createElement('div');
|
|
1647
|
+
emptyNote.style.cssText = 'padding: 8px 12px; font-size: 12px; color: #555; font-style: italic;';
|
|
1648
|
+
emptyNote.textContent = 'No threads yet';
|
|
1649
|
+
allSection.appendChild(emptyNote);
|
|
1650
|
+
} else {
|
|
1651
|
+
// Sort by lastActivity descending
|
|
1652
|
+
var sorted = allThreads.slice().sort(function (a, b) {
|
|
1653
|
+
return (b.lastActivity || 0) - (a.lastActivity || 0);
|
|
1654
|
+
});
|
|
1655
|
+
sorted.forEach(function (t) {
|
|
1656
|
+
allSection.appendChild(buildThreadItem(t.name, visibleSet[t.name] || false, false, t.messageCount));
|
|
983
1657
|
});
|
|
984
|
-
} else if (isStreaming) {
|
|
985
|
-
el.innerHTML =
|
|
986
|
-
'<span style="color:#666;font-style:italic">Thinking\u2026</span>' +
|
|
987
|
-
'<span class="cumulus-cursor"></span>';
|
|
988
1658
|
}
|
|
989
|
-
|
|
1659
|
+
sidebarScroll.appendChild(allSection);
|
|
990
1660
|
}
|
|
991
1661
|
|
|
992
|
-
function
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1662
|
+
function buildThreadItem(name, isSelected, isActive, messageCount) {
|
|
1663
|
+
var item = document.createElement('div');
|
|
1664
|
+
item.className = 'cumulus-thread-item' + (isSelected ? ' selected' : '');
|
|
1665
|
+
item.setAttribute('data-testid', 'webchat-thread-item');
|
|
1666
|
+
item.setAttribute('data-thread-name', name);
|
|
1667
|
+
|
|
1668
|
+
var dot = document.createElement('span');
|
|
1669
|
+
dot.className = 'cumulus-thread-dot ' + (isActive ? 'active' : 'inactive');
|
|
1670
|
+
item.appendChild(dot);
|
|
1671
|
+
|
|
1672
|
+
var nameEl = document.createElement('span');
|
|
1673
|
+
nameEl.className = 'cumulus-thread-name';
|
|
1674
|
+
nameEl.textContent = name;
|
|
1675
|
+
nameEl.setAttribute('title', name);
|
|
1676
|
+
item.appendChild(nameEl);
|
|
1677
|
+
|
|
1678
|
+
if (messageCount !== undefined && messageCount !== null) {
|
|
1679
|
+
var countEl = document.createElement('span');
|
|
1680
|
+
countEl.className = 'cumulus-thread-count';
|
|
1681
|
+
countEl.textContent = messageCount;
|
|
1682
|
+
item.appendChild(countEl);
|
|
998
1683
|
}
|
|
999
1684
|
|
|
1000
|
-
|
|
1001
|
-
var
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
if (msg.role === 'user') {
|
|
1006
|
-
row.appendChild(buildUserMsgEl(msg));
|
|
1685
|
+
item.addEventListener('click', function (e) {
|
|
1686
|
+
var multiSelect = e.metaKey || e.ctrlKey;
|
|
1687
|
+
if (multiSelect) {
|
|
1688
|
+
togglePanelThread(name);
|
|
1007
1689
|
} else {
|
|
1008
|
-
|
|
1690
|
+
soloThread(name);
|
|
1009
1691
|
}
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1692
|
+
});
|
|
1012
1693
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
row.className = 'cumulus-msg-row';
|
|
1016
|
-
row.appendChild(buildAssistantMsgEl(streamBuffer, true));
|
|
1017
|
-
messagesEl.appendChild(row);
|
|
1018
|
-
}
|
|
1694
|
+
return item;
|
|
1695
|
+
}
|
|
1019
1696
|
|
|
1020
|
-
|
|
1697
|
+
// ── Panel management ──
|
|
1698
|
+
function soloThread(name) {
|
|
1699
|
+
visibleThreads = [name];
|
|
1700
|
+
renderSidebar();
|
|
1701
|
+
renderContentArea();
|
|
1702
|
+
loadThreadHistoryIfNeeded(name);
|
|
1021
1703
|
}
|
|
1022
1704
|
|
|
1023
|
-
function
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1705
|
+
function togglePanelThread(name) {
|
|
1706
|
+
var idx = visibleThreads.indexOf(name);
|
|
1707
|
+
if (idx !== -1) {
|
|
1708
|
+
// Remove from visible
|
|
1709
|
+
visibleThreads.splice(idx, 1);
|
|
1028
1710
|
} else {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1711
|
+
// Add if under max
|
|
1712
|
+
if (visibleThreads.length < 3) {
|
|
1713
|
+
visibleThreads.push(name);
|
|
1714
|
+
}
|
|
1715
|
+
// If already at max 3, replace the last one
|
|
1716
|
+
else {
|
|
1717
|
+
visibleThreads[visibleThreads.length - 1] = name;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
renderSidebar();
|
|
1721
|
+
renderContentArea();
|
|
1722
|
+
if (idx === -1) {
|
|
1723
|
+
loadThreadHistoryIfNeeded(name);
|
|
1032
1724
|
}
|
|
1033
1725
|
}
|
|
1034
1726
|
|
|
1035
|
-
function
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1727
|
+
function removeThreadFromView(name) {
|
|
1728
|
+
var idx = visibleThreads.indexOf(name);
|
|
1729
|
+
if (idx !== -1) {
|
|
1730
|
+
visibleThreads.splice(idx, 1);
|
|
1731
|
+
// Clear stale DOM render hooks so background messages don't update detached nodes
|
|
1732
|
+
var state = threadStates[name];
|
|
1733
|
+
if (state) { state._renderMessages = null; state._updateSendBtn = null; }
|
|
1734
|
+
renderSidebar();
|
|
1735
|
+
renderContentArea();
|
|
1736
|
+
}
|
|
1041
1737
|
}
|
|
1042
1738
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1739
|
+
function loadThreadHistoryIfNeeded(name) {
|
|
1740
|
+
var state = getThreadState(name);
|
|
1741
|
+
if (!state.historyLoaded && connection) {
|
|
1742
|
+
state.historyLoaded = true;
|
|
1743
|
+
connection.send({ type: 'history', threadName: name, limit: 50 });
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// ── Content area rendering ──
|
|
1748
|
+
function renderContentArea() {
|
|
1749
|
+
// Clear stale render hooks before wiping the DOM
|
|
1750
|
+
for (var _ci = 0; _ci < visibleThreads.length; _ci++) {
|
|
1751
|
+
var _cs = threadStates[visibleThreads[_ci]];
|
|
1752
|
+
if (_cs) { _cs._renderMessages = null; _cs._updateSendBtn = null; }
|
|
1753
|
+
}
|
|
1754
|
+
contentArea.innerHTML = '';
|
|
1755
|
+
|
|
1756
|
+
if (visibleThreads.length === 0) {
|
|
1757
|
+
var emptyEl = document.createElement('div');
|
|
1758
|
+
emptyEl.className = 'cumulus-standalone-empty';
|
|
1759
|
+
emptyEl.innerHTML =
|
|
1760
|
+
'<div>Select a thread from the sidebar or create a new one</div>' +
|
|
1761
|
+
'<div class="cumulus-standalone-empty-hint">Cmd/Ctrl+Click to open multiple threads side by side</div>';
|
|
1762
|
+
contentArea.appendChild(emptyEl);
|
|
1048
1763
|
return;
|
|
1049
1764
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1765
|
+
|
|
1766
|
+
for (var i = 0; i < visibleThreads.length; i++) {
|
|
1767
|
+
contentArea.appendChild(buildThreadPanel(visibleThreads[i]));
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// ── Thread panel builder ──
|
|
1772
|
+
function buildThreadPanel(threadName) {
|
|
1773
|
+
var state = getThreadState(threadName);
|
|
1774
|
+
|
|
1775
|
+
var panel = document.createElement('div');
|
|
1776
|
+
panel.className = 'cumulus-thread-panel';
|
|
1777
|
+
panel.setAttribute('data-testid', 'webchat-panel-' + threadName);
|
|
1778
|
+
panel.setAttribute('data-thread-panel', threadName);
|
|
1779
|
+
|
|
1780
|
+
// Panel header
|
|
1781
|
+
var panelHeader = document.createElement('div');
|
|
1782
|
+
panelHeader.className = 'cumulus-thread-panel-header';
|
|
1783
|
+
|
|
1784
|
+
var titleLeft = document.createElement('div');
|
|
1785
|
+
titleLeft.style.cssText = 'display:flex; align-items:center; gap:7px; overflow:hidden;';
|
|
1786
|
+
|
|
1787
|
+
var statusDot = document.createElement('span');
|
|
1788
|
+
statusDot.className = 'cumulus-status-dot ' + connectionStatus;
|
|
1789
|
+
statusDot.setAttribute('data-thread-status-dot', threadName);
|
|
1790
|
+
titleLeft.appendChild(statusDot);
|
|
1791
|
+
|
|
1792
|
+
var titleEl = document.createElement('span');
|
|
1793
|
+
titleEl.className = 'cumulus-thread-panel-title';
|
|
1794
|
+
titleEl.textContent = threadName;
|
|
1795
|
+
titleEl.setAttribute('title', threadName);
|
|
1796
|
+
titleLeft.appendChild(titleEl);
|
|
1797
|
+
|
|
1798
|
+
panelHeader.appendChild(titleLeft);
|
|
1799
|
+
|
|
1800
|
+
var closeBtn = document.createElement('button');
|
|
1801
|
+
closeBtn.className = 'cumulus-thread-panel-close';
|
|
1802
|
+
closeBtn.setAttribute('data-testid', 'webchat-panel-close');
|
|
1803
|
+
closeBtn.setAttribute('title', 'Close panel');
|
|
1804
|
+
closeBtn.textContent = '\xD7';
|
|
1805
|
+
closeBtn.addEventListener('click', function () {
|
|
1806
|
+
removeThreadFromView(threadName);
|
|
1807
|
+
});
|
|
1808
|
+
panelHeader.appendChild(closeBtn);
|
|
1809
|
+
panel.appendChild(panelHeader);
|
|
1810
|
+
|
|
1811
|
+
// Messages area
|
|
1812
|
+
var messagesEl = document.createElement('div');
|
|
1813
|
+
messagesEl.className = 'cumulus-messages';
|
|
1814
|
+
messagesEl.setAttribute('data-testid', 'webchat-messages');
|
|
1815
|
+
messagesEl.setAttribute('data-thread-messages', threadName);
|
|
1816
|
+
panel.appendChild(messagesEl);
|
|
1817
|
+
|
|
1818
|
+
// Input area
|
|
1819
|
+
var inputArea = document.createElement('div');
|
|
1820
|
+
inputArea.className = 'cumulus-input-area';
|
|
1821
|
+
|
|
1822
|
+
// Attachment strip
|
|
1823
|
+
var attachStrip = document.createElement('div');
|
|
1824
|
+
attachStrip.className = 'cumulus-attach-strip';
|
|
1825
|
+
attachStrip.setAttribute('data-testid', 'webchat-attach-strip');
|
|
1826
|
+
attachStrip.style.display = 'none';
|
|
1827
|
+
inputArea.appendChild(attachStrip);
|
|
1828
|
+
|
|
1829
|
+
// Input row
|
|
1830
|
+
var inputRow = document.createElement('div');
|
|
1831
|
+
inputRow.className = 'cumulus-input-row';
|
|
1832
|
+
|
|
1833
|
+
var fileInput = document.createElement('input');
|
|
1834
|
+
fileInput.type = 'file';
|
|
1835
|
+
fileInput.multiple = true;
|
|
1836
|
+
fileInput.accept = 'image/*,.pdf,.txt,.md,.js,.ts,.py,.json,.csv';
|
|
1837
|
+
fileInput.style.display = 'none';
|
|
1838
|
+
fileInput.setAttribute('data-testid', 'webchat-file-input');
|
|
1839
|
+
|
|
1840
|
+
var attachBtn = document.createElement('button');
|
|
1841
|
+
attachBtn.className = 'cumulus-attach-btn';
|
|
1842
|
+
attachBtn.setAttribute('data-testid', 'webchat-attach-btn');
|
|
1843
|
+
attachBtn.setAttribute('title', 'Attach files');
|
|
1844
|
+
attachBtn.textContent = '+';
|
|
1845
|
+
|
|
1846
|
+
var inputEl = document.createElement('textarea');
|
|
1847
|
+
inputEl.className = 'cumulus-input';
|
|
1848
|
+
inputEl.setAttribute('data-testid', 'webchat-input');
|
|
1849
|
+
inputEl.placeholder = 'Message ' + threadName + '\u2026';
|
|
1850
|
+
inputEl.rows = 1;
|
|
1851
|
+
|
|
1852
|
+
var sendBtn = document.createElement('button');
|
|
1853
|
+
sendBtn.className = 'cumulus-send-btn';
|
|
1854
|
+
sendBtn.setAttribute('data-testid', 'webchat-send');
|
|
1855
|
+
sendBtn.textContent = 'Send';
|
|
1856
|
+
|
|
1857
|
+
inputRow.appendChild(fileInput);
|
|
1858
|
+
inputRow.appendChild(attachBtn);
|
|
1859
|
+
inputRow.appendChild(inputEl);
|
|
1860
|
+
inputRow.appendChild(sendBtn);
|
|
1861
|
+
inputArea.appendChild(inputRow);
|
|
1862
|
+
panel.appendChild(inputArea);
|
|
1863
|
+
|
|
1864
|
+
// ── Panel-local render functions ──
|
|
1865
|
+
function scrollToBottom() {
|
|
1866
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
function isWideUserMessage(content) {
|
|
1870
|
+
if (/```/.test(content)) return true;
|
|
1871
|
+
var lineCount = (content.match(/\n/g) || []).length + 1;
|
|
1872
|
+
return lineCount > 3;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
function buildUserMsgEl(msg) {
|
|
1876
|
+
var el = document.createElement('div');
|
|
1877
|
+
el.className = 'cumulus-msg user' + (isWideUserMessage(msg.content) ? ' wide' : '');
|
|
1878
|
+
el.textContent = msg.content;
|
|
1879
|
+
if (msg.attachments && msg.attachments.length > 0) {
|
|
1880
|
+
var attRow = document.createElement('div');
|
|
1881
|
+
attRow.className = 'cumulus-msg-attachments';
|
|
1882
|
+
msg.attachments.forEach(function (att) {
|
|
1883
|
+
if (att.isImage) {
|
|
1884
|
+
var img = document.createElement('img');
|
|
1885
|
+
img.className = 'cumulus-msg-img';
|
|
1886
|
+
img.src = att.dataUrl;
|
|
1887
|
+
img.alt = att.name;
|
|
1888
|
+
img.setAttribute('data-testid', 'webchat-msg-img');
|
|
1889
|
+
attRow.appendChild(img);
|
|
1890
|
+
} else {
|
|
1891
|
+
var badge = document.createElement('span');
|
|
1892
|
+
badge.className = 'cumulus-msg-file-badge';
|
|
1893
|
+
badge.setAttribute('data-testid', 'webchat-msg-file');
|
|
1894
|
+
badge.textContent = '\uD83D\uDCCE ' + att.name;
|
|
1895
|
+
attRow.appendChild(badge);
|
|
1896
|
+
}
|
|
1897
|
+
});
|
|
1898
|
+
el.appendChild(attRow);
|
|
1067
1899
|
}
|
|
1900
|
+
return el;
|
|
1901
|
+
}
|
|
1068
1902
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1903
|
+
function buildAssistantMsgEl(content, isStreaming) {
|
|
1904
|
+
var el = document.createElement('div');
|
|
1905
|
+
el.className = 'cumulus-msg assistant';
|
|
1906
|
+
if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
|
|
1907
|
+
if (content) {
|
|
1908
|
+
el.innerHTML = renderMarkdown(content);
|
|
1909
|
+
if (isStreaming) {
|
|
1910
|
+
el.innerHTML += '<span class="cumulus-cursor"></span>';
|
|
1911
|
+
}
|
|
1912
|
+
el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
|
|
1913
|
+
btn.addEventListener('click', function () {
|
|
1914
|
+
var targetId = btn.getAttribute('data-copy-target');
|
|
1915
|
+
var codeEl = el.querySelector('[data-code-id="' + targetId + '"]');
|
|
1916
|
+
if (codeEl && navigator.clipboard) {
|
|
1917
|
+
navigator.clipboard.writeText(codeEl.textContent || '').then(function () {
|
|
1918
|
+
btn.textContent = 'Copied!';
|
|
1919
|
+
setTimeout(function () { btn.textContent = 'Copy'; }, 2000);
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1083
1923
|
});
|
|
1084
|
-
}
|
|
1924
|
+
} else if (isStreaming) {
|
|
1925
|
+
el.innerHTML =
|
|
1926
|
+
'<span style="color:#666;font-style:italic">Thinking\u2026</span>' +
|
|
1927
|
+
'<span class="cumulus-cursor"></span>';
|
|
1928
|
+
}
|
|
1929
|
+
return el;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function renderPanelMessages() {
|
|
1933
|
+
messagesEl.innerHTML = '';
|
|
1934
|
+
if (state.messages.length === 0 && !state.streaming) {
|
|
1935
|
+
messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
for (var i = 0; i < state.messages.length; i++) {
|
|
1939
|
+
var msg = state.messages[i];
|
|
1940
|
+
var row = document.createElement('div');
|
|
1941
|
+
row.className = 'cumulus-msg-row';
|
|
1942
|
+
if (msg.role === 'user') {
|
|
1943
|
+
row.appendChild(buildUserMsgEl(msg));
|
|
1944
|
+
} else {
|
|
1945
|
+
row.appendChild(buildAssistantMsgEl(msg.content, false));
|
|
1946
|
+
}
|
|
1947
|
+
messagesEl.appendChild(row);
|
|
1948
|
+
}
|
|
1949
|
+
if (state.streaming) {
|
|
1950
|
+
var row = document.createElement('div');
|
|
1951
|
+
row.className = 'cumulus-msg-row';
|
|
1952
|
+
row.appendChild(buildAssistantMsgEl(state.streamBuffer, true));
|
|
1953
|
+
messagesEl.appendChild(row);
|
|
1954
|
+
}
|
|
1955
|
+
scrollToBottom();
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function updatePanelSendBtn() {
|
|
1959
|
+
if (state.streaming) {
|
|
1960
|
+
sendBtn.textContent = 'Stop';
|
|
1961
|
+
sendBtn.classList.add('stop');
|
|
1962
|
+
sendBtn.disabled = false;
|
|
1963
|
+
} else {
|
|
1964
|
+
sendBtn.textContent = 'Send';
|
|
1965
|
+
sendBtn.classList.remove('stop');
|
|
1966
|
+
sendBtn.disabled = false;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function renderPanelAttachStrip() {
|
|
1971
|
+
attachStrip.innerHTML = '';
|
|
1972
|
+
if (state.pendingAttachments.length === 0) {
|
|
1973
|
+
attachStrip.style.display = 'none';
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
attachStrip.style.display = 'flex';
|
|
1977
|
+
state.pendingAttachments.forEach(function (att, idx) {
|
|
1978
|
+
var chip = document.createElement('div');
|
|
1979
|
+
chip.className = 'cumulus-attach-chip';
|
|
1980
|
+
chip.setAttribute('data-testid', 'webchat-attach-chip');
|
|
1981
|
+
if (att.isImage) {
|
|
1982
|
+
var thumb = document.createElement('img');
|
|
1983
|
+
thumb.className = 'cumulus-attach-chip-thumb';
|
|
1984
|
+
thumb.src = att.dataUrl;
|
|
1985
|
+
thumb.alt = att.name;
|
|
1986
|
+
chip.appendChild(thumb);
|
|
1987
|
+
} else {
|
|
1988
|
+
var icon = document.createElement('div');
|
|
1989
|
+
icon.className = 'cumulus-attach-chip-icon';
|
|
1990
|
+
icon.textContent = fileExtension(att.name);
|
|
1991
|
+
chip.appendChild(icon);
|
|
1992
|
+
}
|
|
1993
|
+
var nameEl = document.createElement('div');
|
|
1994
|
+
nameEl.className = 'cumulus-attach-chip-name';
|
|
1995
|
+
nameEl.textContent = att.name;
|
|
1996
|
+
chip.appendChild(nameEl);
|
|
1997
|
+
var removeBtn = document.createElement('button');
|
|
1998
|
+
removeBtn.className = 'cumulus-attach-chip-remove';
|
|
1999
|
+
removeBtn.setAttribute('data-testid', 'webchat-attach-remove');
|
|
2000
|
+
removeBtn.textContent = '\xD7';
|
|
2001
|
+
removeBtn.setAttribute('title', 'Remove attachment');
|
|
2002
|
+
(function (index) {
|
|
2003
|
+
removeBtn.addEventListener('click', function () {
|
|
2004
|
+
state.pendingAttachments.splice(index, 1);
|
|
2005
|
+
renderPanelAttachStrip();
|
|
2006
|
+
});
|
|
2007
|
+
})(idx);
|
|
2008
|
+
chip.appendChild(removeBtn);
|
|
2009
|
+
attachStrip.appendChild(chip);
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
async function addFilesToPending(files) {
|
|
2014
|
+
for (var i = 0; i < files.length; i++) {
|
|
2015
|
+
var file = files[i];
|
|
2016
|
+
try {
|
|
2017
|
+
var info = await readFileAsBase64(file);
|
|
2018
|
+
var dataUrl = await readFileAsDataUrl(file);
|
|
2019
|
+
state.pendingAttachments.push({
|
|
2020
|
+
base64: info.base64,
|
|
2021
|
+
mimeType: info.mimeType,
|
|
2022
|
+
name: file.name,
|
|
2023
|
+
dataUrl: dataUrl,
|
|
2024
|
+
isImage: file.type.startsWith('image/'),
|
|
2025
|
+
});
|
|
2026
|
+
} catch (e) {
|
|
2027
|
+
console.error('[Cumulus] Failed to read file:', file.name, e);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
renderPanelAttachStrip();
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// Store references on the state so the router can call re-render
|
|
2034
|
+
state._renderMessages = renderPanelMessages;
|
|
2035
|
+
state._updateSendBtn = updatePanelSendBtn;
|
|
2036
|
+
|
|
2037
|
+
// ── Send message for this panel ──
|
|
2038
|
+
function sendPanelMessage() {
|
|
2039
|
+
if (state.streaming) {
|
|
2040
|
+
state.stopRequested = true;
|
|
2041
|
+
state.streaming = false;
|
|
2042
|
+
updatePanelSendBtn();
|
|
2043
|
+
if (state.streamBuffer) {
|
|
2044
|
+
state.messages.push({ role: 'assistant', content: state.streamBuffer + ' [stopped]' });
|
|
2045
|
+
state.streamBuffer = '';
|
|
2046
|
+
}
|
|
2047
|
+
renderPanelMessages();
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
var text = inputEl.value.trim();
|
|
2051
|
+
if (!text && state.pendingAttachments.length === 0) return;
|
|
2052
|
+
if (!connection) return;
|
|
2053
|
+
|
|
2054
|
+
var attachSnapshot = state.pendingAttachments.slice();
|
|
2055
|
+
var displayText = text || '(attachment)';
|
|
2056
|
+
state.messages.push({ role: 'user', content: displayText, attachments: attachSnapshot });
|
|
2057
|
+
inputEl.value = '';
|
|
2058
|
+
inputEl.style.height = 'auto';
|
|
2059
|
+
state.pendingAttachments = [];
|
|
2060
|
+
renderPanelAttachStrip();
|
|
2061
|
+
state.streaming = true;
|
|
2062
|
+
state.stopRequested = false;
|
|
2063
|
+
state.streamBuffer = '';
|
|
2064
|
+
updatePanelSendBtn();
|
|
2065
|
+
renderPanelMessages();
|
|
2066
|
+
|
|
2067
|
+
var imagePayload = attachSnapshot
|
|
2068
|
+
.filter(function (a) { return a.isImage; })
|
|
2069
|
+
.map(function (a) { return { mimeType: a.mimeType, base64: a.base64 }; });
|
|
2070
|
+
|
|
2071
|
+
var payload = {
|
|
2072
|
+
type: 'message',
|
|
2073
|
+
threadName: threadName,
|
|
2074
|
+
message: text || ' ',
|
|
2075
|
+
};
|
|
2076
|
+
if (imagePayload.length > 0) payload.images = imagePayload;
|
|
2077
|
+
connection.send(payload);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// ── Panel event listeners ──
|
|
2081
|
+
sendBtn.addEventListener('click', sendPanelMessage);
|
|
1085
2082
|
|
|
1086
|
-
|
|
1087
|
-
|
|
2083
|
+
inputEl.addEventListener('keydown', function (e) {
|
|
2084
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
2085
|
+
e.preventDefault();
|
|
2086
|
+
sendPanelMessage();
|
|
2087
|
+
}
|
|
1088
2088
|
});
|
|
1089
|
-
}
|
|
1090
2089
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
2090
|
+
inputEl.addEventListener('input', function () {
|
|
2091
|
+
inputEl.style.height = 'auto';
|
|
2092
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
inputEl.addEventListener('paste', function (e) {
|
|
2096
|
+
var items = e.clipboardData && e.clipboardData.items;
|
|
2097
|
+
if (!items) return;
|
|
2098
|
+
var filesToAdd = [];
|
|
2099
|
+
for (var i = 0; i < items.length; i++) {
|
|
2100
|
+
var item = items[i];
|
|
2101
|
+
if (item.kind === 'file') {
|
|
2102
|
+
var file = item.getAsFile();
|
|
2103
|
+
if (file) filesToAdd.push(file);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
if (filesToAdd.length > 0) {
|
|
2107
|
+
e.preventDefault();
|
|
2108
|
+
addFilesToPending(filesToAdd);
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
fileInput.addEventListener('change', function () {
|
|
2113
|
+
if (fileInput.files && fileInput.files.length > 0) {
|
|
2114
|
+
addFilesToPending(Array.from(fileInput.files));
|
|
2115
|
+
fileInput.value = '';
|
|
1106
2116
|
}
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
attachBtn.addEventListener('click', function () {
|
|
2120
|
+
fileInput.click();
|
|
2121
|
+
});
|
|
2122
|
+
|
|
2123
|
+
// Initial render
|
|
2124
|
+
renderPanelMessages();
|
|
2125
|
+
renderPanelAttachStrip();
|
|
2126
|
+
|
|
2127
|
+
return panel;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// ── Re-render a visible thread panel's messages in-place ──
|
|
2131
|
+
// Called by the message router after state mutation
|
|
2132
|
+
function refreshThreadPanel(threadName) {
|
|
2133
|
+
var state = threadStates[threadName];
|
|
2134
|
+
if (!state) return;
|
|
2135
|
+
|
|
2136
|
+
// Re-render messages in-place if panel is visible
|
|
2137
|
+
if (state._renderMessages) {
|
|
2138
|
+
state._renderMessages();
|
|
2139
|
+
}
|
|
2140
|
+
if (state._updateSendBtn) {
|
|
2141
|
+
state._updateSendBtn();
|
|
1107
2142
|
}
|
|
1108
|
-
renderAttachStrip();
|
|
1109
2143
|
}
|
|
1110
2144
|
|
|
1111
|
-
// ── Message
|
|
2145
|
+
// ── Message router ──
|
|
1112
2146
|
function handleServerMessage(data) {
|
|
1113
2147
|
switch (data.type) {
|
|
1114
2148
|
case 'auth_ok':
|
|
1115
|
-
|
|
2149
|
+
showAppView();
|
|
1116
2150
|
break;
|
|
1117
2151
|
|
|
1118
2152
|
case 'auth_error':
|
|
1119
2153
|
console.error('[Cumulus] Auth failed:', data.error);
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
2154
|
+
clearStoredApiKey();
|
|
2155
|
+
activeApiKey = '';
|
|
2156
|
+
if (connection) { connection.close(); connection = null; }
|
|
2157
|
+
showAuthView();
|
|
2158
|
+
var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
|
|
2159
|
+
if (errEl) errEl.textContent = 'Authentication failed. Check your API key.';
|
|
2160
|
+
break;
|
|
2161
|
+
|
|
2162
|
+
case 'threads':
|
|
2163
|
+
// Server response to { type: 'threads' } request
|
|
2164
|
+
if (data.threads) {
|
|
2165
|
+
allThreads = data.threads;
|
|
2166
|
+
renderSidebar();
|
|
2167
|
+
}
|
|
2168
|
+
break;
|
|
2169
|
+
|
|
2170
|
+
case 'thread_created':
|
|
2171
|
+
// Server confirms new thread; add to list and select it
|
|
2172
|
+
if (data.threadName) {
|
|
2173
|
+
// Add to allThreads if not already present
|
|
2174
|
+
var exists = false;
|
|
2175
|
+
for (var i = 0; i < allThreads.length; i++) {
|
|
2176
|
+
if (allThreads[i].name === data.threadName) { exists = true; break; }
|
|
2177
|
+
}
|
|
2178
|
+
if (!exists) {
|
|
2179
|
+
allThreads.push({ name: data.threadName, messageCount: 0, lastActivity: Date.now() });
|
|
2180
|
+
}
|
|
2181
|
+
soloThread(data.threadName);
|
|
1127
2182
|
}
|
|
1128
2183
|
break;
|
|
1129
2184
|
|
|
1130
2185
|
case 'history':
|
|
1131
|
-
if (data.
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
2186
|
+
if (data.threadName) {
|
|
2187
|
+
var state = getThreadState(data.threadName);
|
|
2188
|
+
if (data.messages && data.messages.length > 0) {
|
|
2189
|
+
state.messages = data.messages.map(function (m) {
|
|
2190
|
+
return { role: m.role, content: m.content };
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
refreshThreadPanel(data.threadName);
|
|
1136
2194
|
}
|
|
1137
2195
|
break;
|
|
1138
2196
|
|
|
1139
2197
|
case 'token':
|
|
1140
|
-
if (
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
2198
|
+
if (data.threadName) {
|
|
2199
|
+
var state = getThreadState(data.threadName);
|
|
2200
|
+
if (state.stopRequested) break;
|
|
2201
|
+
if (!state.streaming) {
|
|
2202
|
+
state.streaming = true;
|
|
2203
|
+
state.streamBuffer = '';
|
|
2204
|
+
}
|
|
2205
|
+
state.streamBuffer += data.text;
|
|
2206
|
+
// Update lastActivity for this thread in allThreads
|
|
2207
|
+
updateThreadActivity(data.threadName);
|
|
2208
|
+
refreshThreadPanel(data.threadName);
|
|
1145
2209
|
}
|
|
1146
|
-
streamBuffer += data.text;
|
|
1147
|
-
renderMessages();
|
|
1148
2210
|
break;
|
|
1149
2211
|
|
|
1150
2212
|
case 'segment':
|
|
1151
|
-
//
|
|
2213
|
+
// Reserved for future verbose display
|
|
1152
2214
|
break;
|
|
1153
2215
|
|
|
1154
2216
|
case 'done':
|
|
1155
|
-
if (
|
|
1156
|
-
|
|
1157
|
-
stopRequested
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
2217
|
+
if (data.threadName) {
|
|
2218
|
+
var state = getThreadState(data.threadName);
|
|
2219
|
+
if (state.stopRequested) {
|
|
2220
|
+
state.stopRequested = false;
|
|
2221
|
+
state.streaming = false;
|
|
2222
|
+
if (state.streamBuffer) {
|
|
2223
|
+
state.messages.push({ role: 'assistant', content: state.streamBuffer });
|
|
2224
|
+
}
|
|
2225
|
+
state.streamBuffer = '';
|
|
2226
|
+
updateThreadActivity(data.threadName);
|
|
2227
|
+
refreshThreadPanel(data.threadName);
|
|
2228
|
+
break;
|
|
1161
2229
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
2230
|
+
state.streaming = false;
|
|
2231
|
+
state.messages.push({ role: 'assistant', content: data.response || state.streamBuffer });
|
|
2232
|
+
state.streamBuffer = '';
|
|
2233
|
+
updateThreadActivity(data.threadName);
|
|
2234
|
+
refreshThreadPanel(data.threadName);
|
|
1166
2235
|
}
|
|
1167
|
-
streaming = false;
|
|
1168
|
-
messages.push({ role: 'assistant', content: data.response || streamBuffer });
|
|
1169
|
-
streamBuffer = '';
|
|
1170
|
-
updateSendBtn();
|
|
1171
|
-
renderMessages();
|
|
1172
2236
|
break;
|
|
1173
2237
|
|
|
1174
2238
|
case 'error':
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
2239
|
+
if (data.threadName) {
|
|
2240
|
+
var state = getThreadState(data.threadName);
|
|
2241
|
+
state.stopRequested = false;
|
|
2242
|
+
state.streaming = false;
|
|
2243
|
+
if (state.streamBuffer) {
|
|
2244
|
+
state.messages.push({ role: 'assistant', content: state.streamBuffer + '\n\n[Error: ' + (data.error || 'Unknown error') + ']' });
|
|
2245
|
+
} else {
|
|
2246
|
+
state.messages.push({ role: 'assistant', content: '[Error: ' + (data.error || 'Unknown error') + ']' });
|
|
2247
|
+
}
|
|
2248
|
+
state.streamBuffer = '';
|
|
2249
|
+
refreshThreadPanel(data.threadName);
|
|
1181
2250
|
}
|
|
1182
|
-
streamBuffer = '';
|
|
1183
|
-
updateSendBtn();
|
|
1184
|
-
renderMessages();
|
|
1185
2251
|
break;
|
|
1186
2252
|
}
|
|
1187
2253
|
}
|
|
1188
2254
|
|
|
1189
|
-
function
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
updateSendBtn();
|
|
1195
|
-
// Finalize current buffer
|
|
1196
|
-
if (streamBuffer) {
|
|
1197
|
-
messages.push({ role: 'assistant', content: streamBuffer + ' [stopped]' });
|
|
1198
|
-
streamBuffer = '';
|
|
2255
|
+
function updateThreadActivity(threadName) {
|
|
2256
|
+
for (var i = 0; i < allThreads.length; i++) {
|
|
2257
|
+
if (allThreads[i].name === threadName) {
|
|
2258
|
+
allThreads[i].lastActivity = Date.now();
|
|
2259
|
+
break;
|
|
1199
2260
|
}
|
|
1200
|
-
renderMessages();
|
|
1201
|
-
return;
|
|
1202
2261
|
}
|
|
2262
|
+
// Re-render sidebar to reflect new sort order / move thread up
|
|
2263
|
+
renderSidebar();
|
|
2264
|
+
}
|
|
1203
2265
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
if (!connection) return;
|
|
1207
|
-
|
|
1208
|
-
var attachSnapshot = pendingAttachments.slice();
|
|
1209
|
-
var displayText = text || '(attachment)';
|
|
1210
|
-
|
|
1211
|
-
messages.push({ role: 'user', content: displayText, attachments: attachSnapshot });
|
|
1212
|
-
input.value = '';
|
|
1213
|
-
input.style.height = 'auto';
|
|
1214
|
-
pendingAttachments = [];
|
|
1215
|
-
renderAttachStrip();
|
|
1216
|
-
streaming = true;
|
|
1217
|
-
stopRequested = false;
|
|
1218
|
-
streamBuffer = '';
|
|
1219
|
-
updateSendBtn();
|
|
1220
|
-
renderMessages();
|
|
1221
|
-
|
|
1222
|
-
var imagePayload = attachSnapshot
|
|
1223
|
-
.filter(function (a) { return a.isImage; })
|
|
1224
|
-
.map(function (a) { return { mimeType: a.mimeType, base64: a.base64 }; });
|
|
1225
|
-
|
|
1226
|
-
var payload = {
|
|
1227
|
-
type: 'message',
|
|
1228
|
-
threadName: sessionId,
|
|
1229
|
-
message: text || ' ',
|
|
1230
|
-
};
|
|
1231
|
-
if (imagePayload.length > 0) payload.images = imagePayload;
|
|
2266
|
+
// ── New thread creation ──
|
|
2267
|
+
var newThreadCreating = false;
|
|
1232
2268
|
|
|
1233
|
-
|
|
2269
|
+
function showNewThreadInput() {
|
|
2270
|
+
if (newThreadCreating) return;
|
|
2271
|
+
newThreadCreating = true;
|
|
2272
|
+
newThreadBtn.style.display = 'none';
|
|
2273
|
+
newThreadInput.style.display = 'block';
|
|
2274
|
+
newThreadInput.value = '';
|
|
2275
|
+
newThreadInput.focus();
|
|
1234
2276
|
}
|
|
1235
2277
|
|
|
1236
|
-
|
|
1237
|
-
|
|
2278
|
+
function hideNewThreadInput() {
|
|
2279
|
+
newThreadCreating = false;
|
|
2280
|
+
newThreadBtn.style.display = 'block';
|
|
2281
|
+
newThreadInput.style.display = 'none';
|
|
2282
|
+
newThreadInput.value = '';
|
|
2283
|
+
}
|
|
1238
2284
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
2285
|
+
function submitNewThread() {
|
|
2286
|
+
var name = newThreadInput.value.trim();
|
|
2287
|
+
if (!name) { hideNewThreadInput(); return; }
|
|
2288
|
+
if (connection) {
|
|
2289
|
+
connection.send({ type: 'create_thread', threadName: name });
|
|
1243
2290
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
});
|
|
1250
|
-
|
|
1251
|
-
// Paste handler
|
|
1252
|
-
input.addEventListener('paste', function (e) {
|
|
1253
|
-
var items = e.clipboardData && e.clipboardData.items;
|
|
1254
|
-
if (!items) return;
|
|
1255
|
-
|
|
1256
|
-
var hasNonText = false;
|
|
1257
|
-
var filesToAdd = [];
|
|
1258
|
-
for (var i = 0; i < items.length; i++) {
|
|
1259
|
-
var item = items[i];
|
|
1260
|
-
if (item.kind === 'file') {
|
|
1261
|
-
hasNonText = true;
|
|
1262
|
-
var file = item.getAsFile();
|
|
1263
|
-
if (file) filesToAdd.push(file);
|
|
1264
|
-
}
|
|
2291
|
+
hideNewThreadInput();
|
|
2292
|
+
// Optimistically add and show (server will confirm with thread_created)
|
|
2293
|
+
var exists = false;
|
|
2294
|
+
for (var i = 0; i < allThreads.length; i++) {
|
|
2295
|
+
if (allThreads[i].name === name) { exists = true; break; }
|
|
1265
2296
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
e.preventDefault(); // prevent default paste of binary data
|
|
1269
|
-
addFilesToPending(filesToAdd);
|
|
2297
|
+
if (!exists) {
|
|
2298
|
+
allThreads.push({ name: name, messageCount: 0, lastActivity: Date.now() });
|
|
1270
2299
|
}
|
|
1271
|
-
|
|
1272
|
-
}
|
|
2300
|
+
soloThread(name);
|
|
2301
|
+
}
|
|
1273
2302
|
|
|
1274
|
-
|
|
1275
|
-
fileInput.addEventListener('change', function () {
|
|
1276
|
-
if (fileInput.files && fileInput.files.length > 0) {
|
|
1277
|
-
addFilesToPending(Array.from(fileInput.files));
|
|
1278
|
-
fileInput.value = ''; // reset so same file can be re-added
|
|
1279
|
-
}
|
|
1280
|
-
});
|
|
2303
|
+
newThreadBtn.addEventListener('click', showNewThreadInput);
|
|
1281
2304
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
2305
|
+
newThreadInput.addEventListener('keydown', function (e) {
|
|
2306
|
+
if (e.key === 'Enter') { e.preventDefault(); submitNewThread(); }
|
|
2307
|
+
if (e.key === 'Escape') { hideNewThreadInput(); }
|
|
1285
2308
|
});
|
|
1286
2309
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
startConnection();
|
|
1294
|
-
}
|
|
1295
|
-
if (!showing) {
|
|
1296
|
-
input.focus();
|
|
1297
|
-
scrollToBottom();
|
|
1298
|
-
}
|
|
1299
|
-
});
|
|
1300
|
-
|
|
1301
|
-
var closeBtn = panel.querySelector('.cumulus-close-btn');
|
|
1302
|
-
if (closeBtn) {
|
|
1303
|
-
closeBtn.addEventListener('click', function () {
|
|
1304
|
-
panel.style.display = 'none';
|
|
1305
|
-
});
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
2310
|
+
newThreadInput.addEventListener('blur', function () {
|
|
2311
|
+
// Small delay so click on submit doesn't race with blur
|
|
2312
|
+
setTimeout(function () {
|
|
2313
|
+
if (newThreadCreating) hideNewThreadInput();
|
|
2314
|
+
}, 150);
|
|
2315
|
+
});
|
|
1308
2316
|
|
|
1309
|
-
// ── Auth
|
|
2317
|
+
// ── Auth events ──
|
|
1310
2318
|
var authInput = authPanel.querySelector('[data-testid="webchat-auth-input"]');
|
|
1311
2319
|
var authSubmitBtn = authPanel.querySelector('[data-testid="webchat-auth-submit"]');
|
|
1312
2320
|
|
|
@@ -1325,20 +2333,13 @@
|
|
|
1325
2333
|
if (e.key === 'Enter') { e.preventDefault(); submitAuth(); }
|
|
1326
2334
|
});
|
|
1327
2335
|
|
|
1328
|
-
// Logout
|
|
1329
|
-
var logoutBtn =
|
|
2336
|
+
// ── Logout ──
|
|
2337
|
+
var logoutBtn = appLayout.querySelector('[data-testid="webchat-logout"]');
|
|
1330
2338
|
if (logoutBtn) {
|
|
1331
2339
|
logoutBtn.addEventListener('click', function () {
|
|
1332
2340
|
clearStoredApiKey();
|
|
1333
2341
|
activeApiKey = '';
|
|
1334
2342
|
if (connection) { connection.close(); connection = null; }
|
|
1335
|
-
messages = [];
|
|
1336
|
-
streaming = false;
|
|
1337
|
-
stopRequested = false;
|
|
1338
|
-
streamBuffer = '';
|
|
1339
|
-
pendingAttachments = [];
|
|
1340
|
-
renderAttachStrip();
|
|
1341
|
-
updateSendBtn();
|
|
1342
2343
|
showAuthView();
|
|
1343
2344
|
authInput.value = '';
|
|
1344
2345
|
});
|
|
@@ -1353,28 +2354,28 @@
|
|
|
1353
2354
|
sessionId: sessionId,
|
|
1354
2355
|
onMessage: handleServerMessage,
|
|
1355
2356
|
onStatus: updateStatus,
|
|
2357
|
+
skipHistory: true, // standalone manages history per-thread
|
|
1356
2358
|
});
|
|
1357
2359
|
}
|
|
1358
2360
|
|
|
1359
2361
|
// ── Mount / Destroy ──
|
|
1360
2362
|
function mount(target) {
|
|
1361
2363
|
(target || document.body).appendChild(container);
|
|
1362
|
-
if (
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
authInput.focus();
|
|
1368
|
-
}
|
|
2364
|
+
if (activeApiKey) {
|
|
2365
|
+
startConnection();
|
|
2366
|
+
} else {
|
|
2367
|
+
showAuthView();
|
|
2368
|
+
authInput.focus();
|
|
1369
2369
|
}
|
|
1370
2370
|
}
|
|
1371
2371
|
|
|
1372
2372
|
function destroy() {
|
|
1373
2373
|
if (connection) connection.close();
|
|
2374
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
1374
2375
|
container.remove();
|
|
1375
2376
|
}
|
|
1376
2377
|
|
|
1377
|
-
return { mount: mount, destroy: destroy
|
|
2378
|
+
return { mount: mount, destroy: destroy };
|
|
1378
2379
|
}
|
|
1379
2380
|
|
|
1380
2381
|
// ─── Auto-mount ──────────────────────────────────────────────────────────────
|