agentgui 1.0.211 → 1.0.212
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/database.js +18 -0
- package/package.json +1 -1
- package/server.js +22 -4
- package/static/index.html +79 -0
- package/static/js/client.js +299 -44
- package/static/js/websocket-manager.js +220 -216
package/static/js/client.js
CHANGED
|
@@ -45,7 +45,6 @@ class AgentGUIClient {
|
|
|
45
45
|
agentSelector: null
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
// Chunk polling state (must be in constructor so it exists before any event handlers fire)
|
|
49
48
|
this.chunkPollState = {
|
|
50
49
|
isPolling: false,
|
|
51
50
|
lastFetchTimestamp: 0,
|
|
@@ -55,6 +54,14 @@ class AgentGUIClient {
|
|
|
55
54
|
abortController: null
|
|
56
55
|
};
|
|
57
56
|
|
|
57
|
+
this._pollIntervalByTier = {
|
|
58
|
+
excellent: 100, good: 200, fair: 400, poor: 800, bad: 1500, unknown: 200
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
this._renderedSeqs = new Map();
|
|
62
|
+
this._inflightRequests = new Map();
|
|
63
|
+
this._previousConvAbort = null;
|
|
64
|
+
|
|
58
65
|
// Router state
|
|
59
66
|
this.routerState = {
|
|
60
67
|
currentConversationId: null,
|
|
@@ -113,6 +120,7 @@ class AgentGUIClient {
|
|
|
113
120
|
this.wsManager.on('connected', () => {
|
|
114
121
|
console.log('WebSocket connected');
|
|
115
122
|
this.updateConnectionStatus('connected');
|
|
123
|
+
this._recoverMissedChunks();
|
|
116
124
|
this.emit('ws:connected');
|
|
117
125
|
});
|
|
118
126
|
|
|
@@ -136,10 +144,8 @@ class AgentGUIClient {
|
|
|
136
144
|
this.showError('Connection error: ' + (data.error?.message || 'unknown'));
|
|
137
145
|
});
|
|
138
146
|
|
|
139
|
-
this.wsManager.on('
|
|
140
|
-
|
|
141
|
-
this.updateConnectionStatus('error');
|
|
142
|
-
this.showError('Failed to reconnect to server after ' + data.attempts + ' attempts');
|
|
147
|
+
this.wsManager.on('latency_update', (data) => {
|
|
148
|
+
this._updateConnectionIndicator(data.quality);
|
|
143
149
|
});
|
|
144
150
|
}
|
|
145
151
|
|
|
@@ -584,10 +590,41 @@ class AgentGUIClient {
|
|
|
584
590
|
requestAnimationFrame(() => {
|
|
585
591
|
this._scrollRafPending = false;
|
|
586
592
|
const scrollContainer = document.getElementById('output-scroll');
|
|
587
|
-
if (scrollContainer)
|
|
593
|
+
if (!scrollContainer) return;
|
|
594
|
+
const distFromBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight;
|
|
595
|
+
if (distFromBottom < 150) {
|
|
596
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
597
|
+
this._removeNewContentPill();
|
|
598
|
+
} else {
|
|
599
|
+
this._unseenCount = (this._unseenCount || 0) + 1;
|
|
600
|
+
this._showNewContentPill();
|
|
601
|
+
}
|
|
588
602
|
});
|
|
589
603
|
}
|
|
590
604
|
|
|
605
|
+
_showNewContentPill() {
|
|
606
|
+
let pill = document.getElementById('new-content-pill');
|
|
607
|
+
const scrollContainer = document.getElementById('output-scroll');
|
|
608
|
+
if (!scrollContainer) return;
|
|
609
|
+
if (!pill) {
|
|
610
|
+
pill = document.createElement('button');
|
|
611
|
+
pill.id = 'new-content-pill';
|
|
612
|
+
pill.className = 'new-content-pill';
|
|
613
|
+
pill.addEventListener('click', () => {
|
|
614
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
615
|
+
this._removeNewContentPill();
|
|
616
|
+
});
|
|
617
|
+
scrollContainer.appendChild(pill);
|
|
618
|
+
}
|
|
619
|
+
pill.textContent = (this._unseenCount || 1) + ' new';
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
_removeNewContentPill() {
|
|
623
|
+
this._unseenCount = 0;
|
|
624
|
+
const pill = document.getElementById('new-content-pill');
|
|
625
|
+
if (pill) pill.remove();
|
|
626
|
+
}
|
|
627
|
+
|
|
591
628
|
handleStreamingError(data) {
|
|
592
629
|
console.error('Streaming error:', data);
|
|
593
630
|
|
|
@@ -688,6 +725,22 @@ class AgentGUIClient {
|
|
|
688
725
|
return;
|
|
689
726
|
}
|
|
690
727
|
|
|
728
|
+
if (data.message.role === 'user') {
|
|
729
|
+
const pending = outputEl.querySelector('.message-sending');
|
|
730
|
+
if (pending) {
|
|
731
|
+
pending.id = '';
|
|
732
|
+
pending.setAttribute('data-msg-id', data.message.id);
|
|
733
|
+
pending.classList.remove('message-sending');
|
|
734
|
+
const ts = pending.querySelector('.message-timestamp');
|
|
735
|
+
if (ts) {
|
|
736
|
+
ts.style.opacity = '1';
|
|
737
|
+
ts.textContent = new Date(data.message.created_at).toLocaleString();
|
|
738
|
+
}
|
|
739
|
+
this.emit('message:created', data);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
691
744
|
const messageHtml = `
|
|
692
745
|
<div class="message message-${data.message.role}" data-msg-id="${data.message.id}">
|
|
693
746
|
<div class="message-role">${data.message.role.charAt(0).toUpperCase() + data.message.role.slice(1)}</div>
|
|
@@ -983,20 +1036,25 @@ class AgentGUIClient {
|
|
|
983
1036
|
return;
|
|
984
1037
|
}
|
|
985
1038
|
|
|
1039
|
+
const savedPrompt = prompt;
|
|
986
1040
|
if (this.ui.messageInput) {
|
|
987
1041
|
this.ui.messageInput.value = '';
|
|
988
1042
|
this.ui.messageInput.style.height = 'auto';
|
|
989
1043
|
}
|
|
990
1044
|
|
|
1045
|
+
const pendingId = 'pending-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6);
|
|
1046
|
+
this._showOptimisticMessage(pendingId, savedPrompt);
|
|
1047
|
+
this.disableControls();
|
|
1048
|
+
|
|
991
1049
|
try {
|
|
992
1050
|
if (this.state.currentConversation?.id) {
|
|
993
|
-
await this.streamToConversation(this.state.currentConversation.id,
|
|
1051
|
+
await this.streamToConversation(this.state.currentConversation.id, savedPrompt, agentId);
|
|
1052
|
+
this._confirmOptimisticMessage(pendingId);
|
|
994
1053
|
} else {
|
|
995
|
-
this.disableControls();
|
|
996
1054
|
const response = await fetch(window.__BASE_URL + '/api/conversations', {
|
|
997
1055
|
method: 'POST',
|
|
998
1056
|
headers: { 'Content-Type': 'application/json' },
|
|
999
|
-
body: JSON.stringify({ agentId, title:
|
|
1057
|
+
body: JSON.stringify({ agentId, title: savedPrompt.substring(0, 50) })
|
|
1000
1058
|
});
|
|
1001
1059
|
const { conversation } = await response.json();
|
|
1002
1060
|
this.state.currentConversation = conversation;
|
|
@@ -1006,15 +1064,124 @@ class AgentGUIClient {
|
|
|
1006
1064
|
window.conversationManager.select(conversation.id);
|
|
1007
1065
|
}
|
|
1008
1066
|
|
|
1009
|
-
await this.streamToConversation(conversation.id,
|
|
1067
|
+
await this.streamToConversation(conversation.id, savedPrompt, agentId);
|
|
1068
|
+
this._confirmOptimisticMessage(pendingId);
|
|
1010
1069
|
}
|
|
1011
1070
|
} catch (error) {
|
|
1012
1071
|
console.error('Execution error:', error);
|
|
1013
|
-
this.
|
|
1072
|
+
this._failOptimisticMessage(pendingId, savedPrompt, error.message);
|
|
1014
1073
|
this.enableControls();
|
|
1015
1074
|
}
|
|
1016
1075
|
}
|
|
1017
1076
|
|
|
1077
|
+
_showOptimisticMessage(pendingId, content) {
|
|
1078
|
+
const messagesEl = document.querySelector('.conversation-messages');
|
|
1079
|
+
if (!messagesEl) return;
|
|
1080
|
+
const div = document.createElement('div');
|
|
1081
|
+
div.className = 'message message-user message-sending';
|
|
1082
|
+
div.id = pendingId;
|
|
1083
|
+
div.innerHTML = `<div class="message-role">User</div><div class="message-text">${this.escapeHtml(content)}</div><div class="message-timestamp" style="opacity:0.5">Sending...</div>`;
|
|
1084
|
+
messagesEl.appendChild(div);
|
|
1085
|
+
this.scrollToBottom();
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
_confirmOptimisticMessage(pendingId) {
|
|
1089
|
+
const el = document.getElementById(pendingId);
|
|
1090
|
+
if (!el) return;
|
|
1091
|
+
el.classList.remove('message-sending');
|
|
1092
|
+
const ts = el.querySelector('.message-timestamp');
|
|
1093
|
+
if (ts) {
|
|
1094
|
+
ts.style.opacity = '1';
|
|
1095
|
+
ts.textContent = new Date().toLocaleString();
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
_failOptimisticMessage(pendingId, content, errorMsg) {
|
|
1100
|
+
const el = document.getElementById(pendingId);
|
|
1101
|
+
if (!el) return;
|
|
1102
|
+
el.classList.remove('message-sending');
|
|
1103
|
+
el.classList.add('message-send-failed');
|
|
1104
|
+
const ts = el.querySelector('.message-timestamp');
|
|
1105
|
+
if (ts) {
|
|
1106
|
+
ts.style.opacity = '1';
|
|
1107
|
+
ts.innerHTML = `<span style="color:var(--color-error)">Failed: ${this.escapeHtml(errorMsg)}</span>`;
|
|
1108
|
+
}
|
|
1109
|
+
if (this.ui.messageInput) {
|
|
1110
|
+
this.ui.messageInput.value = content;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
async _recoverMissedChunks() {
|
|
1115
|
+
if (!this.state.currentSession?.id) return;
|
|
1116
|
+
if (!this.state.streamingConversations.has(this.state.currentConversation?.id)) return;
|
|
1117
|
+
|
|
1118
|
+
const sessionId = this.state.currentSession.id;
|
|
1119
|
+
const lastSeq = this.wsManager.getLastSeq(sessionId);
|
|
1120
|
+
if (lastSeq < 0) return;
|
|
1121
|
+
|
|
1122
|
+
try {
|
|
1123
|
+
const url = `${window.__BASE_URL}/api/sessions/${sessionId}/chunks?sinceSeq=${lastSeq}`;
|
|
1124
|
+
const resp = await fetch(url);
|
|
1125
|
+
if (!resp.ok) return;
|
|
1126
|
+
const { chunks: rawChunks } = await resp.json();
|
|
1127
|
+
if (!rawChunks || rawChunks.length === 0) return;
|
|
1128
|
+
|
|
1129
|
+
const chunks = rawChunks.map(c => ({
|
|
1130
|
+
...c,
|
|
1131
|
+
block: typeof c.data === 'string' ? JSON.parse(c.data) : c.data
|
|
1132
|
+
})).filter(c => c.block && c.block.type);
|
|
1133
|
+
|
|
1134
|
+
const dedupedChunks = chunks.filter(c => {
|
|
1135
|
+
const seqSet = this._renderedSeqs.get(sessionId);
|
|
1136
|
+
return !seqSet || !seqSet.has(c.sequence);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
if (dedupedChunks.length > 0) {
|
|
1140
|
+
this.renderChunkBatch(dedupedChunks);
|
|
1141
|
+
}
|
|
1142
|
+
} catch (e) {
|
|
1143
|
+
console.warn('Chunk recovery failed:', e.message);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
_dedupedFetch(key, fetchFn) {
|
|
1148
|
+
if (this._inflightRequests.has(key)) {
|
|
1149
|
+
return this._inflightRequests.get(key);
|
|
1150
|
+
}
|
|
1151
|
+
const promise = fetchFn().finally(() => {
|
|
1152
|
+
this._inflightRequests.delete(key);
|
|
1153
|
+
});
|
|
1154
|
+
this._inflightRequests.set(key, promise);
|
|
1155
|
+
return promise;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
_getAdaptivePollInterval() {
|
|
1159
|
+
const quality = this.wsManager?.latency?.quality || 'unknown';
|
|
1160
|
+
return this._pollIntervalByTier[quality] || 200;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
_showSkeletonLoading(conversationId) {
|
|
1164
|
+
const outputEl = document.getElementById('output');
|
|
1165
|
+
if (!outputEl) return;
|
|
1166
|
+
const conv = this.state.conversations.find(c => c.id === conversationId);
|
|
1167
|
+
const title = conv?.title || 'Conversation';
|
|
1168
|
+
const wdInfo = conv?.workingDirectory ? ` - ${this.escapeHtml(conv.workingDirectory)}` : '';
|
|
1169
|
+
outputEl.innerHTML = `
|
|
1170
|
+
<div class="conversation-header">
|
|
1171
|
+
<h2>${this.escapeHtml(title)}</h2>
|
|
1172
|
+
<p class="text-secondary">${conv?.agentType || 'unknown'} - ${conv ? new Date(conv.created_at).toLocaleDateString() : ''}${wdInfo}</p>
|
|
1173
|
+
</div>
|
|
1174
|
+
<div class="conversation-messages">
|
|
1175
|
+
<div class="skeleton-loading">
|
|
1176
|
+
<div class="skeleton-block skeleton-pulse" style="height:3rem;margin-bottom:0.75rem;border-radius:0.5rem;background:var(--color-bg-secondary);"></div>
|
|
1177
|
+
<div class="skeleton-block skeleton-pulse" style="height:6rem;margin-bottom:0.75rem;border-radius:0.5rem;background:var(--color-bg-secondary);"></div>
|
|
1178
|
+
<div class="skeleton-block skeleton-pulse" style="height:2rem;margin-bottom:0.75rem;border-radius:0.5rem;background:var(--color-bg-secondary);"></div>
|
|
1179
|
+
<div class="skeleton-block skeleton-pulse" style="height:5rem;margin-bottom:0.75rem;border-radius:0.5rem;background:var(--color-bg-secondary);"></div>
|
|
1180
|
+
</div>
|
|
1181
|
+
</div>
|
|
1182
|
+
`;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1018
1185
|
async streamToConversation(conversationId, prompt, agentId) {
|
|
1019
1186
|
try {
|
|
1020
1187
|
if (this.wsManager.isConnected) {
|
|
@@ -1078,6 +1245,12 @@ class AgentGUIClient {
|
|
|
1078
1245
|
async fetchChunks(conversationId, since = 0) {
|
|
1079
1246
|
if (!conversationId) return [];
|
|
1080
1247
|
|
|
1248
|
+
if (this.chunkPollState.abortController) {
|
|
1249
|
+
this.chunkPollState.abortController.abort();
|
|
1250
|
+
}
|
|
1251
|
+
this.chunkPollState.abortController = new AbortController();
|
|
1252
|
+
const signal = this.chunkPollState.abortController.signal;
|
|
1253
|
+
|
|
1081
1254
|
try {
|
|
1082
1255
|
const params = new URLSearchParams();
|
|
1083
1256
|
if (since > 0) {
|
|
@@ -1085,7 +1258,7 @@ class AgentGUIClient {
|
|
|
1085
1258
|
}
|
|
1086
1259
|
|
|
1087
1260
|
const url = `${window.__BASE_URL}/api/conversations/${conversationId}/chunks?${params.toString()}`;
|
|
1088
|
-
const response = await fetch(url);
|
|
1261
|
+
const response = await fetch(url, { signal });
|
|
1089
1262
|
|
|
1090
1263
|
if (!response.ok) {
|
|
1091
1264
|
throw new Error(`HTTP ${response.status}`);
|
|
@@ -1096,7 +1269,6 @@ class AgentGUIClient {
|
|
|
1096
1269
|
throw new Error('Invalid chunks response');
|
|
1097
1270
|
}
|
|
1098
1271
|
|
|
1099
|
-
// Parse JSON data field for each chunk
|
|
1100
1272
|
const chunks = data.chunks.map(chunk => ({
|
|
1101
1273
|
...chunk,
|
|
1102
1274
|
block: typeof chunk.data === 'string' ? JSON.parse(chunk.data) : chunk.data
|
|
@@ -1104,6 +1276,7 @@ class AgentGUIClient {
|
|
|
1104
1276
|
|
|
1105
1277
|
return chunks;
|
|
1106
1278
|
} catch (error) {
|
|
1279
|
+
if (error.name === 'AbortError') return [];
|
|
1107
1280
|
console.error('Error fetching chunks:', error);
|
|
1108
1281
|
throw error;
|
|
1109
1282
|
}
|
|
@@ -1122,7 +1295,7 @@ class AgentGUIClient {
|
|
|
1122
1295
|
|
|
1123
1296
|
pollState.isPolling = true;
|
|
1124
1297
|
pollState.lastFetchTimestamp = Date.now();
|
|
1125
|
-
pollState.backoffDelay =
|
|
1298
|
+
pollState.backoffDelay = this._getAdaptivePollInterval();
|
|
1126
1299
|
pollState.sessionCheckCounter = 0;
|
|
1127
1300
|
pollState.emptyPollCount = 0;
|
|
1128
1301
|
|
|
@@ -1157,7 +1330,7 @@ class AgentGUIClient {
|
|
|
1157
1330
|
const chunks = await this.fetchChunks(conversationId, pollState.lastFetchTimestamp);
|
|
1158
1331
|
|
|
1159
1332
|
if (chunks.length > 0) {
|
|
1160
|
-
pollState.backoffDelay =
|
|
1333
|
+
pollState.backoffDelay = this._getAdaptivePollInterval();
|
|
1161
1334
|
pollState.emptyPollCount = 0;
|
|
1162
1335
|
const lastChunk = chunks[chunks.length - 1];
|
|
1163
1336
|
pollState.lastFetchTimestamp = lastChunk.created_at;
|
|
@@ -1227,6 +1400,10 @@ class AgentGUIClient {
|
|
|
1227
1400
|
const groups = {};
|
|
1228
1401
|
for (const chunk of chunks) {
|
|
1229
1402
|
const sid = chunk.sessionId;
|
|
1403
|
+
if (!this._renderedSeqs.has(sid)) this._renderedSeqs.set(sid, new Set());
|
|
1404
|
+
const seqSet = this._renderedSeqs.get(sid);
|
|
1405
|
+
if (chunk.sequence !== undefined && seqSet.has(chunk.sequence)) continue;
|
|
1406
|
+
if (chunk.sequence !== undefined) seqSet.add(chunk.sequence);
|
|
1230
1407
|
if (!groups[sid]) groups[sid] = [];
|
|
1231
1408
|
groups[sid].push(chunk);
|
|
1232
1409
|
}
|
|
@@ -1260,40 +1437,42 @@ class AgentGUIClient {
|
|
|
1260
1437
|
* Load agents
|
|
1261
1438
|
*/
|
|
1262
1439
|
async loadAgents() {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
window.dispatchEvent(new CustomEvent('agents-loaded', { detail: { agents } }));
|
|
1440
|
+
return this._dedupedFetch('loadAgents', async () => {
|
|
1441
|
+
try {
|
|
1442
|
+
const response = await fetch(window.__BASE_URL + '/api/agents');
|
|
1443
|
+
const { agents } = await response.json();
|
|
1444
|
+
this.state.agents = agents;
|
|
1445
|
+
|
|
1446
|
+
if (this.ui.agentSelector) {
|
|
1447
|
+
this.ui.agentSelector.innerHTML = agents
|
|
1448
|
+
.map(agent => `<option value="${agent.id}">${agent.name}</option>`)
|
|
1449
|
+
.join('');
|
|
1450
|
+
}
|
|
1276
1451
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1452
|
+
window.dispatchEvent(new CustomEvent('agents-loaded', { detail: { agents } }));
|
|
1453
|
+
return agents;
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
console.error('Failed to load agents:', error);
|
|
1456
|
+
return [];
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1282
1459
|
}
|
|
1283
1460
|
|
|
1284
1461
|
/**
|
|
1285
1462
|
* Load conversations
|
|
1286
1463
|
*/
|
|
1287
1464
|
async loadConversations() {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1465
|
+
return this._dedupedFetch('loadConversations', async () => {
|
|
1466
|
+
try {
|
|
1467
|
+
const response = await fetch(window.__BASE_URL + '/api/conversations');
|
|
1468
|
+
const { conversations } = await response.json();
|
|
1469
|
+
this.state.conversations = conversations;
|
|
1470
|
+
return conversations;
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
console.error('Failed to load conversations:', error);
|
|
1473
|
+
return [];
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1297
1476
|
}
|
|
1298
1477
|
|
|
1299
1478
|
/**
|
|
@@ -1304,6 +1483,73 @@ class AgentGUIClient {
|
|
|
1304
1483
|
this.ui.statusIndicator.dataset.status = status;
|
|
1305
1484
|
this.ui.statusIndicator.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
|
1306
1485
|
}
|
|
1486
|
+
if (status === 'disconnected' || status === 'reconnecting') {
|
|
1487
|
+
this._updateConnectionIndicator(status);
|
|
1488
|
+
} else if (status === 'connected') {
|
|
1489
|
+
this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
_updateConnectionIndicator(quality) {
|
|
1494
|
+
if (this._indicatorDebounce) return;
|
|
1495
|
+
this._indicatorDebounce = true;
|
|
1496
|
+
setTimeout(() => { this._indicatorDebounce = false; }, 1000);
|
|
1497
|
+
|
|
1498
|
+
let indicator = document.getElementById('connection-indicator');
|
|
1499
|
+
if (!indicator) {
|
|
1500
|
+
indicator = document.createElement('div');
|
|
1501
|
+
indicator.id = 'connection-indicator';
|
|
1502
|
+
indicator.className = 'connection-indicator';
|
|
1503
|
+
indicator.innerHTML = '<span class="connection-dot"></span><span class="connection-label"></span>';
|
|
1504
|
+
indicator.addEventListener('click', () => this._toggleConnectionTooltip());
|
|
1505
|
+
const header = document.querySelector('.header-right') || document.querySelector('.app-header');
|
|
1506
|
+
if (header) {
|
|
1507
|
+
header.style.position = 'relative';
|
|
1508
|
+
header.appendChild(indicator);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const dot = indicator.querySelector('.connection-dot');
|
|
1513
|
+
const label = indicator.querySelector('.connection-label');
|
|
1514
|
+
if (!dot || !label) return;
|
|
1515
|
+
|
|
1516
|
+
dot.className = 'connection-dot';
|
|
1517
|
+
if (quality === 'disconnected' || quality === 'reconnecting') {
|
|
1518
|
+
dot.classList.add(quality);
|
|
1519
|
+
label.textContent = quality === 'reconnecting' ? 'Reconnecting...' : 'Disconnected';
|
|
1520
|
+
} else {
|
|
1521
|
+
dot.classList.add(quality);
|
|
1522
|
+
const latency = this.wsManager?.latency;
|
|
1523
|
+
label.textContent = latency?.avg > 0 ? Math.round(latency.avg) + 'ms' : '';
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
_toggleConnectionTooltip() {
|
|
1528
|
+
let tooltip = document.getElementById('connection-tooltip');
|
|
1529
|
+
if (tooltip) { tooltip.remove(); return; }
|
|
1530
|
+
|
|
1531
|
+
const indicator = document.getElementById('connection-indicator');
|
|
1532
|
+
if (!indicator) return;
|
|
1533
|
+
|
|
1534
|
+
tooltip = document.createElement('div');
|
|
1535
|
+
tooltip.id = 'connection-tooltip';
|
|
1536
|
+
tooltip.className = 'connection-tooltip';
|
|
1537
|
+
|
|
1538
|
+
const latency = this.wsManager?.latency || {};
|
|
1539
|
+
const stats = this.wsManager?.stats || {};
|
|
1540
|
+
const state = this.wsManager?.connectionState || 'unknown';
|
|
1541
|
+
|
|
1542
|
+
tooltip.innerHTML = [
|
|
1543
|
+
`<div>State: ${state}</div>`,
|
|
1544
|
+
`<div>Latency: ${Math.round(latency.avg || 0)}ms</div>`,
|
|
1545
|
+
`<div>Jitter: ${Math.round(latency.jitter || 0)}ms</div>`,
|
|
1546
|
+
`<div>Quality: ${latency.quality || 'unknown'}</div>`,
|
|
1547
|
+
`<div>Reconnects: ${stats.totalReconnects || 0}</div>`,
|
|
1548
|
+
`<div>Uptime: ${stats.lastConnectedTime ? Math.round((Date.now() - stats.lastConnectedTime) / 1000) + 's' : 'N/A'}</div>`
|
|
1549
|
+
].join('');
|
|
1550
|
+
|
|
1551
|
+
indicator.appendChild(tooltip);
|
|
1552
|
+
setTimeout(() => { if (tooltip.parentNode) tooltip.remove(); }, 5000);
|
|
1307
1553
|
}
|
|
1308
1554
|
|
|
1309
1555
|
/**
|
|
@@ -1404,6 +1650,12 @@ class AgentGUIClient {
|
|
|
1404
1650
|
|
|
1405
1651
|
async loadConversationMessages(conversationId) {
|
|
1406
1652
|
try {
|
|
1653
|
+
if (this._previousConvAbort) {
|
|
1654
|
+
this._previousConvAbort.abort();
|
|
1655
|
+
}
|
|
1656
|
+
this._previousConvAbort = new AbortController();
|
|
1657
|
+
const convSignal = this._previousConvAbort.signal;
|
|
1658
|
+
|
|
1407
1659
|
this.cacheCurrentConversation();
|
|
1408
1660
|
this.stopChunkPolling();
|
|
1409
1661
|
var prevId = this.state.currentConversation?.id;
|
|
@@ -1420,7 +1672,7 @@ class AgentGUIClient {
|
|
|
1420
1672
|
}
|
|
1421
1673
|
|
|
1422
1674
|
const cached = this.conversationCache.get(conversationId);
|
|
1423
|
-
if (cached && (Date.now() - cached.timestamp) <
|
|
1675
|
+
if (cached && (Date.now() - cached.timestamp) < 300000) {
|
|
1424
1676
|
const outputEl = document.getElementById('output');
|
|
1425
1677
|
if (outputEl) {
|
|
1426
1678
|
outputEl.innerHTML = '';
|
|
@@ -1437,7 +1689,9 @@ class AgentGUIClient {
|
|
|
1437
1689
|
|
|
1438
1690
|
this.conversationCache.delete(conversationId);
|
|
1439
1691
|
|
|
1440
|
-
|
|
1692
|
+
this._showSkeletonLoading(conversationId);
|
|
1693
|
+
|
|
1694
|
+
const resp = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/full`, { signal: convSignal });
|
|
1441
1695
|
if (resp.status === 404) {
|
|
1442
1696
|
console.warn('Conversation no longer exists:', conversationId);
|
|
1443
1697
|
this.state.currentConversation = null;
|
|
@@ -1624,6 +1878,7 @@ class AgentGUIClient {
|
|
|
1624
1878
|
this.restoreScrollPosition(conversationId);
|
|
1625
1879
|
}
|
|
1626
1880
|
} catch (error) {
|
|
1881
|
+
if (error.name === 'AbortError') return;
|
|
1627
1882
|
console.error('Failed to load conversation messages:', error);
|
|
1628
1883
|
this.showError('Failed to load conversation: ' + error.message);
|
|
1629
1884
|
}
|