@intranefr/superbackend 1.6.6 → 1.7.7
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/.env.example +4 -0
- package/README.md +18 -0
- package/package.json +8 -2
- package/public/js/admin-superdemos.js +396 -0
- package/public/sdk/superdemos.iife.js +614 -0
- package/public/superdemos-qa.html +324 -0
- package/sdk/superdemos/browser/src/index.js +719 -0
- package/src/cli/agent-chat.js +369 -0
- package/src/cli/agent-list.js +42 -0
- package/src/controllers/adminAgentsChat.controller.js +172 -0
- package/src/controllers/adminSuperDemos.controller.js +382 -0
- package/src/controllers/superDemosPublic.controller.js +126 -0
- package/src/middleware.js +102 -19
- package/src/models/BlogAutomationLock.js +4 -4
- package/src/models/BlogPost.js +16 -16
- package/src/models/CacheEntry.js +17 -6
- package/src/models/JsonConfig.js +2 -4
- package/src/models/RateLimitMetricBucket.js +10 -5
- package/src/models/SuperDemo.js +38 -0
- package/src/models/SuperDemoProject.js +32 -0
- package/src/models/SuperDemoStep.js +27 -0
- package/src/routes/adminAgents.routes.js +10 -0
- package/src/routes/adminMarkdowns.routes.js +3 -0
- package/src/routes/adminSuperDemos.routes.js +31 -0
- package/src/routes/superDemos.routes.js +9 -0
- package/src/services/auditLogger.js +75 -37
- package/src/services/email.service.js +18 -3
- package/src/services/superDemosAuthoringSessions.service.js +132 -0
- package/src/services/superDemosWs.service.js +164 -0
- package/src/services/terminalsWs.service.js +35 -3
- package/src/utils/rbac/rightsRegistry.js +2 -0
- package/views/admin-agents.ejs +261 -11
- package/views/admin-dashboard.ejs +78 -8
- package/views/admin-superdemos.ejs +335 -0
- package/views/admin-terminals.ejs +462 -34
- package/views/partials/admin/agents-chat.ejs +80 -0
- package/views/partials/dashboard/nav-items.ejs +1 -0
- package/views/partials/dashboard/tab-bar.ejs +6 -0
- package/cookies.txt +0 -6
- package/cookies1.txt +0 -6
- package/cookies2.txt +0 -6
- package/cookies3.txt +0 -6
- package/cookies4.txt +0 -5
- package/cookies_old.txt +0 -5
- package/cookies_old_test.txt +0 -6
- package/cookies_super.txt +0 -5
- package/cookies_super_test.txt +0 -6
- package/cookies_test.txt +0 -6
- package/test-access.js +0 -63
- package/test-iframe-fix.html +0 -63
- package/test-iframe.html +0 -14
package/views/admin-agents.ejs
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
8
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
9
9
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
|
10
|
+
<style>
|
|
11
|
+
[v-cloak] { display: none; }
|
|
12
|
+
</style>
|
|
10
13
|
</head>
|
|
11
14
|
<body class="bg-gray-50 min-h-screen">
|
|
12
15
|
<div id="app" class="p-6 max-w-6xl mx-auto" v-cloak>
|
|
@@ -15,13 +18,19 @@
|
|
|
15
18
|
<h1 class="text-2xl font-bold text-gray-800">AI Agents</h1>
|
|
16
19
|
<p class="text-gray-500">Configure your intelligent agent gateway</p>
|
|
17
20
|
</div>
|
|
18
|
-
<button @click="openCreateModal" class="bg-indigo-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-indigo-700 transition">
|
|
21
|
+
<button v-if="activeTab === 'agents'" @click="openCreateModal" class="bg-indigo-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-indigo-700 transition">
|
|
19
22
|
<i class="ti ti-plus"></i>
|
|
20
23
|
Create Agent
|
|
21
24
|
</button>
|
|
22
25
|
</div>
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
<div class="mb-6 bg-white border border-gray-200 rounded-lg p-1 inline-flex gap-1">
|
|
28
|
+
<button @click="activeTab = 'agents'" class="px-4 py-2 rounded-md text-sm font-medium" :class="activeTab === 'agents' ? 'bg-indigo-600 text-white' : 'text-gray-600 hover:bg-gray-100'">Agents</button>
|
|
29
|
+
<button @click="activeTab = 'chat'" class="px-4 py-2 rounded-md text-sm font-medium" :class="activeTab === 'chat' ? 'bg-indigo-600 text-white' : 'text-gray-600 hover:bg-gray-100'">Chat UI</button>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div v-if="activeTab === 'agents'">
|
|
33
|
+
|
|
25
34
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
26
35
|
<div v-for="agent in agents" :key="agent._id" class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition">
|
|
27
36
|
<div class="p-6">
|
|
@@ -59,7 +68,6 @@
|
|
|
59
68
|
</div>
|
|
60
69
|
</div>
|
|
61
70
|
|
|
62
|
-
<!-- Empty State -->
|
|
63
71
|
<div v-if="agents.length === 0" class="bg-white rounded-xl border-2 border-dashed border-gray-200 p-12 text-center">
|
|
64
72
|
<i class="ti ti-robot text-6xl text-gray-200 mb-4 inline-block"></i>
|
|
65
73
|
<h3 class="text-lg font-medium text-gray-800">No agents found</h3>
|
|
@@ -68,8 +76,12 @@
|
|
|
68
76
|
Create Agent
|
|
69
77
|
</button>
|
|
70
78
|
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div v-if="activeTab === 'chat'">
|
|
82
|
+
<%- include('partials/admin/agents-chat') %>
|
|
83
|
+
</div>
|
|
71
84
|
|
|
72
|
-
<!-- Modal -->
|
|
73
85
|
<div v-if="showModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
74
86
|
<div class="bg-white rounded-xl shadow-xl w-full max-w-2xl overflow-hidden max-h-[90vh] flex flex-col">
|
|
75
87
|
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center shrink-0">
|
|
@@ -140,7 +152,7 @@
|
|
|
140
152
|
</div>
|
|
141
153
|
|
|
142
154
|
<script>
|
|
143
|
-
const { createApp, ref, onMounted } = Vue;
|
|
155
|
+
const { createApp, ref, reactive, onMounted, nextTick } = Vue;
|
|
144
156
|
|
|
145
157
|
createApp({
|
|
146
158
|
setup() {
|
|
@@ -149,6 +161,7 @@
|
|
|
149
161
|
const showModal = ref(false);
|
|
150
162
|
const editingAgent = ref(null);
|
|
151
163
|
const saving = ref(false);
|
|
164
|
+
const activeTab = ref('agents');
|
|
152
165
|
|
|
153
166
|
const availableTools = ['query_database', 'get_system_stats', 'raw_db_query', 'exec'];
|
|
154
167
|
|
|
@@ -164,24 +177,248 @@
|
|
|
164
177
|
|
|
165
178
|
const baseUrl = '<%= baseUrl %>';
|
|
166
179
|
|
|
180
|
+
const chatScrollEl = ref(null);
|
|
181
|
+
const chat = reactive({
|
|
182
|
+
selectedAgentId: '',
|
|
183
|
+
chatId: '',
|
|
184
|
+
input: '',
|
|
185
|
+
messages: [],
|
|
186
|
+
sessions: [],
|
|
187
|
+
showSessions: false,
|
|
188
|
+
sending: false,
|
|
189
|
+
status: 'idle',
|
|
190
|
+
usage: null,
|
|
191
|
+
isFullscreen: false,
|
|
192
|
+
dbName: ''
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
let activeSseController = null;
|
|
196
|
+
|
|
197
|
+
const selectedAgent = () => agents.value.find(a => a._id === chat.selectedAgentId);
|
|
198
|
+
|
|
199
|
+
const scrollChatToBottom = async () => {
|
|
200
|
+
await nextTick();
|
|
201
|
+
if (chatScrollEl.value) chatScrollEl.value.scrollTop = chatScrollEl.value.scrollHeight;
|
|
202
|
+
};
|
|
203
|
+
|
|
167
204
|
const fetchAgents = async () => {
|
|
168
205
|
const res = await fetch(`${baseUrl}/api/admin/agents`);
|
|
169
206
|
const data = await res.json();
|
|
170
207
|
agents.value = data.items;
|
|
208
|
+
if (!chat.selectedAgentId && agents.value.length > 0) {
|
|
209
|
+
chat.selectedAgentId = agents.value[0]._id;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const fetchChatHealth = async () => {
|
|
214
|
+
try {
|
|
215
|
+
const res = await fetch(`${baseUrl}/api/admin/agents/chat/health`);
|
|
216
|
+
if (!res.ok) return;
|
|
217
|
+
const data = await res.json();
|
|
218
|
+
chat.dbName = data.dbName || '';
|
|
219
|
+
} catch (_) {
|
|
220
|
+
// no-op
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const newSession = async () => {
|
|
225
|
+
if (!chat.selectedAgentId) return;
|
|
226
|
+
const res = await fetch(`${baseUrl}/api/admin/agents/chat/session/new`, {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: { 'Content-Type': 'application/json' },
|
|
229
|
+
body: JSON.stringify({ agentId: chat.selectedAgentId })
|
|
230
|
+
});
|
|
231
|
+
const data = await res.json();
|
|
232
|
+
if (!res.ok) return alert(data.error || 'Failed to create session');
|
|
233
|
+
chat.chatId = data.chatId;
|
|
234
|
+
chat.messages = [];
|
|
235
|
+
chat.status = 'idle';
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const loadSessions = async () => {
|
|
239
|
+
if (!chat.selectedAgentId) return;
|
|
240
|
+
const res = await fetch(`${baseUrl}/api/admin/agents/chat/sessions?agentId=${encodeURIComponent(chat.selectedAgentId)}`);
|
|
241
|
+
const data = await res.json();
|
|
242
|
+
if (!res.ok) return alert(data.error || 'Failed to load sessions');
|
|
243
|
+
chat.sessions = data.items || [];
|
|
244
|
+
chat.showSessions = true;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const switchSession = async (sessionId) => {
|
|
248
|
+
console.log('[switchSession] Switching to session:', sessionId);
|
|
249
|
+
chat.chatId = sessionId;
|
|
250
|
+
chat.messages = [];
|
|
251
|
+
chat.status = 'loading';
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const url = `${baseUrl}/api/admin/agents/chat/session/${encodeURIComponent(sessionId)}/messages`;
|
|
255
|
+
console.log('[switchSession] Fetching from:', url);
|
|
256
|
+
const res = await fetch(url);
|
|
257
|
+
const data = await res.json();
|
|
258
|
+
|
|
259
|
+
console.log('[switchSession] Response status:', res.status, 'data:', data);
|
|
260
|
+
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
console.error('Failed to load session:', data.error);
|
|
263
|
+
chat.status = 'error';
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
chat.messages = data.messages || [];
|
|
268
|
+
chat.status = 'idle';
|
|
269
|
+
console.log('[switchSession] Loaded', chat.messages.length, 'messages');
|
|
270
|
+
await scrollChatToBottom();
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.error('Error loading session:', err);
|
|
273
|
+
chat.status = 'error';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
chat.showSessions = false;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const promptRenameSession = async () => {
|
|
280
|
+
if (!chat.chatId) return;
|
|
281
|
+
const label = prompt('Session label');
|
|
282
|
+
if (!label) return;
|
|
283
|
+
const res = await fetch(`${baseUrl}/api/admin/agents/chat/session/rename`, {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
headers: { 'Content-Type': 'application/json' },
|
|
286
|
+
body: JSON.stringify({ chatId: chat.chatId, label })
|
|
287
|
+
});
|
|
288
|
+
const data = await res.json();
|
|
289
|
+
if (!res.ok) return alert(data.error || 'Failed to rename session');
|
|
290
|
+
await loadSessions();
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const compactSession = async () => {
|
|
294
|
+
if (!chat.selectedAgentId || !chat.chatId) return;
|
|
295
|
+
const res = await fetch(`${baseUrl}/api/admin/agents/chat/session/compact`, {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
headers: { 'Content-Type': 'application/json' },
|
|
298
|
+
body: JSON.stringify({ agentId: chat.selectedAgentId, chatId: chat.chatId })
|
|
299
|
+
});
|
|
300
|
+
const data = await res.json();
|
|
301
|
+
if (!res.ok) return alert(data.error || 'Failed to compact session');
|
|
302
|
+
alert(`Snapshot created: ${data.snapshotId}`);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const stopGeneration = () => {
|
|
306
|
+
if (activeSseController) activeSseController.abort();
|
|
307
|
+
chat.sending = false;
|
|
308
|
+
chat.status = 'stopped';
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const sendChatMessage = async () => {
|
|
312
|
+
if (!chat.selectedAgentId || !chat.input.trim() || chat.sending) return;
|
|
313
|
+
if (!chat.chatId) await newSession();
|
|
314
|
+
if (!chat.chatId) return;
|
|
315
|
+
|
|
316
|
+
const promptText = chat.input;
|
|
317
|
+
chat.input = '';
|
|
318
|
+
chat.messages.push({ role: 'user', text: promptText });
|
|
319
|
+
|
|
320
|
+
const currentAgent = selectedAgent();
|
|
321
|
+
const assistantMsg = { role: 'assistant', text: '', agentName: currentAgent?.name || 'Agent' };
|
|
322
|
+
chat.messages.push(assistantMsg);
|
|
323
|
+
chat.sending = true;
|
|
324
|
+
chat.status = 'starting';
|
|
325
|
+
await scrollChatToBottom();
|
|
326
|
+
|
|
327
|
+
const controller = new AbortController();
|
|
328
|
+
activeSseController = controller;
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const res = await fetch(`${baseUrl}/api/admin/agents/chat/stream`, {
|
|
332
|
+
method: 'POST',
|
|
333
|
+
headers: { 'Content-Type': 'application/json' },
|
|
334
|
+
body: JSON.stringify({
|
|
335
|
+
agentId: chat.selectedAgentId,
|
|
336
|
+
chatId: chat.chatId,
|
|
337
|
+
content: promptText
|
|
338
|
+
}),
|
|
339
|
+
signal: controller.signal,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
if (!res.ok || !res.body) {
|
|
343
|
+
const data = await res.json().catch(() => ({}));
|
|
344
|
+
throw new Error(data.error || 'Failed to stream response');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const reader = res.body.getReader();
|
|
348
|
+
const decoder = new TextDecoder();
|
|
349
|
+
let buffer = '';
|
|
350
|
+
|
|
351
|
+
while (true) {
|
|
352
|
+
const { done, value } = await reader.read();
|
|
353
|
+
if (done) break;
|
|
354
|
+
buffer += decoder.decode(value, { stream: true });
|
|
355
|
+
const chunks = buffer.split('\n\n');
|
|
356
|
+
buffer = chunks.pop() || '';
|
|
357
|
+
|
|
358
|
+
for (const chunk of chunks) {
|
|
359
|
+
const lines = chunk.split('\n');
|
|
360
|
+
let event = 'message';
|
|
361
|
+
let data = '';
|
|
362
|
+
for (const line of lines) {
|
|
363
|
+
if (line.startsWith('event: ')) event = line.slice(7).trim();
|
|
364
|
+
if (line.startsWith('data: ')) data += line.slice(6);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let parsed = {};
|
|
368
|
+
try { parsed = JSON.parse(data || '{}'); } catch (_) { parsed = {}; }
|
|
369
|
+
|
|
370
|
+
if (event === 'progress') {
|
|
371
|
+
chat.status = parsed.status || chat.status;
|
|
372
|
+
if (parsed.status === 'streaming_content' && typeof parsed.token === 'string') {
|
|
373
|
+
assistantMsg.text += parsed.token;
|
|
374
|
+
await scrollChatToBottom();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (event === 'done') {
|
|
379
|
+
chat.chatId = parsed.chatId || chat.chatId;
|
|
380
|
+
if (!assistantMsg.text) assistantMsg.text = parsed.text || '';
|
|
381
|
+
chat.usage = parsed.usage
|
|
382
|
+
? { total: parsed.usage.total_tokens || ((parsed.usage.prompt_tokens || 0) + (parsed.usage.completion_tokens || 0)) }
|
|
383
|
+
: null;
|
|
384
|
+
chat.status = 'done';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (event === 'error') {
|
|
388
|
+
throw new Error(parsed.error || 'Streaming failed');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (err.name !== 'AbortError') {
|
|
394
|
+
assistantMsg.text = assistantMsg.text || `Error: ${err.message}`;
|
|
395
|
+
chat.status = 'error';
|
|
396
|
+
}
|
|
397
|
+
} finally {
|
|
398
|
+
if (activeSseController === controller) activeSseController = null;
|
|
399
|
+
chat.sending = false;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const onChatInputKeydown = (e) => {
|
|
404
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
405
|
+
e.preventDefault();
|
|
406
|
+
sendChatMessage();
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const toggleChatFullscreen = () => {
|
|
411
|
+
chat.isFullscreen = !chat.isFullscreen;
|
|
171
412
|
};
|
|
172
413
|
|
|
173
414
|
const fetchLLMConfig = async () => {
|
|
174
|
-
// We need an endpoint to get LLM providers.
|
|
175
|
-
// For now, let's try to get it from the LLM admin API if it exists
|
|
176
415
|
try {
|
|
177
416
|
const res = await fetch(`${baseUrl}/api/admin/llm/providers`);
|
|
178
417
|
if (res.ok) {
|
|
179
418
|
const data = await res.json();
|
|
180
419
|
providers.value = data.providers || {};
|
|
181
420
|
}
|
|
182
|
-
} catch (e) {
|
|
183
|
-
console.warn('Failed to fetch LLM providers');
|
|
184
|
-
}
|
|
421
|
+
} catch (e) { console.warn('Failed to fetch LLM providers'); }
|
|
185
422
|
};
|
|
186
423
|
|
|
187
424
|
const openCreateModal = () => {
|
|
@@ -251,6 +488,7 @@
|
|
|
251
488
|
onMounted(() => {
|
|
252
489
|
fetchAgents();
|
|
253
490
|
fetchLLMConfig();
|
|
491
|
+
fetchChatHealth();
|
|
254
492
|
});
|
|
255
493
|
|
|
256
494
|
return {
|
|
@@ -260,11 +498,23 @@
|
|
|
260
498
|
editingAgent,
|
|
261
499
|
formData,
|
|
262
500
|
saving,
|
|
501
|
+
activeTab,
|
|
263
502
|
availableTools,
|
|
503
|
+
chat,
|
|
504
|
+
chatScrollEl,
|
|
264
505
|
openCreateModal,
|
|
265
506
|
editAgent,
|
|
266
507
|
confirmDelete,
|
|
267
|
-
saveAgent
|
|
508
|
+
saveAgent,
|
|
509
|
+
newSession,
|
|
510
|
+
loadSessions,
|
|
511
|
+
switchSession,
|
|
512
|
+
promptRenameSession,
|
|
513
|
+
compactSession,
|
|
514
|
+
sendChatMessage,
|
|
515
|
+
onChatInputKeydown,
|
|
516
|
+
stopGeneration,
|
|
517
|
+
toggleChatFullscreen
|
|
268
518
|
};
|
|
269
519
|
}
|
|
270
520
|
}).mount('#app');
|
|
@@ -68,6 +68,14 @@
|
|
|
68
68
|
</div>
|
|
69
69
|
</header>
|
|
70
70
|
|
|
71
|
+
<!-- Tab Limit Notification -->
|
|
72
|
+
<div v-if="showTabLimitNotification" class="bg-blue-50 border-l-4 border-blue-400 p-4 mx-6 mt-4 rounded-r-lg shadow-sm">
|
|
73
|
+
<div class="flex items-center">
|
|
74
|
+
<i class="ti ti-info-circle-filled text-blue-600 mr-3"></i>
|
|
75
|
+
<p class="text-sm text-blue-800">{{ tabLimitMessage }}</p>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
71
79
|
<div class="flex-1 flex overflow-hidden">
|
|
72
80
|
<template v-if="!globalZenMode">
|
|
73
81
|
<%- include('partials/dashboard/sidebar') %>
|
|
@@ -109,6 +117,7 @@
|
|
|
109
117
|
// Initialize globals from EJS locals first (must run before nav-items)
|
|
110
118
|
window.BASE_URL = '<%= baseUrl %>';
|
|
111
119
|
window.ADMIN_PATH = '<%= adminPath %>';
|
|
120
|
+
window.MAX_TABS = <%- typeof maxTabs !== 'undefined' ? maxTabs : 5 %>;
|
|
112
121
|
</script>
|
|
113
122
|
|
|
114
123
|
<%- include('partials/dashboard/nav-items') %>
|
|
@@ -120,6 +129,7 @@
|
|
|
120
129
|
setup() {
|
|
121
130
|
const baseUrl = window.BASE_URL;
|
|
122
131
|
const adminBase = window.ADMIN_PATH || '/admin';
|
|
132
|
+
const maxTabs = window.MAX_TABS || 5;
|
|
123
133
|
const navSections = window.NAV_SECTIONS || [];
|
|
124
134
|
|
|
125
135
|
// Tabs state
|
|
@@ -130,6 +140,10 @@
|
|
|
130
140
|
// User authentication state
|
|
131
141
|
const currentUser = ref({});
|
|
132
142
|
const showUserMenu = ref(false);
|
|
143
|
+
|
|
144
|
+
// Tab limit notification state
|
|
145
|
+
const showTabLimitNotification = ref(false);
|
|
146
|
+
const tabLimitMessage = ref('');
|
|
133
147
|
|
|
134
148
|
// ESC ESC to exit Zen Mode
|
|
135
149
|
let escCount = 0;
|
|
@@ -173,12 +187,24 @@
|
|
|
173
187
|
availableModuleIds.includes(tab.id)
|
|
174
188
|
);
|
|
175
189
|
|
|
176
|
-
|
|
190
|
+
// Enforce tab limit - keep only the first maxTabs tabs
|
|
191
|
+
const limitedTabs = existingTabs.slice(0, maxTabs);
|
|
192
|
+
|
|
193
|
+
if (limitedTabs.length > 0) {
|
|
194
|
+
// Show notification if tabs were trimmed due to limit
|
|
195
|
+
if (existingTabs.length > maxTabs) {
|
|
196
|
+
tabLimitMessage.value = `Loaded ${limitedTabs.length} of ${existingTabs.length} saved tabs (limit: ${maxTabs}).`;
|
|
197
|
+
showTabLimitNotification.value = true;
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
showTabLimitNotification.value = false;
|
|
200
|
+
}, 4000);
|
|
201
|
+
}
|
|
202
|
+
|
|
177
203
|
return {
|
|
178
|
-
tabs:
|
|
179
|
-
activeTabId:
|
|
204
|
+
tabs: limitedTabs,
|
|
205
|
+
activeTabId: limitedTabs.some(t => t.id === data.activeTabId)
|
|
180
206
|
? data.activeTabId
|
|
181
|
-
: (
|
|
207
|
+
: (limitedTabs.length > 0 ? limitedTabs[0].id : null)
|
|
182
208
|
};
|
|
183
209
|
}
|
|
184
210
|
}
|
|
@@ -250,11 +276,23 @@
|
|
|
250
276
|
|
|
251
277
|
if (tabs.length === 0) return null;
|
|
252
278
|
|
|
279
|
+
// Enforce tab limit - keep only the first maxTabs tabs
|
|
280
|
+
const limitedTabs = tabs.slice(0, maxTabs);
|
|
281
|
+
|
|
282
|
+
// Show notification if tabs were trimmed due to limit
|
|
283
|
+
if (tabs.length > maxTabs) {
|
|
284
|
+
tabLimitMessage.value = `Loaded ${limitedTabs.length} of ${tabs.length} URL tabs (limit: ${maxTabs}).`;
|
|
285
|
+
showTabLimitNotification.value = true;
|
|
286
|
+
setTimeout(() => {
|
|
287
|
+
showTabLimitNotification.value = false;
|
|
288
|
+
}, 4000);
|
|
289
|
+
}
|
|
290
|
+
|
|
253
291
|
return {
|
|
254
|
-
tabs,
|
|
255
|
-
activeTabId:
|
|
292
|
+
tabs: limitedTabs,
|
|
293
|
+
activeTabId: limitedTabs.some(t => t.id === activeTabParam)
|
|
256
294
|
? activeTabParam
|
|
257
|
-
:
|
|
295
|
+
: limitedTabs[0].id
|
|
258
296
|
};
|
|
259
297
|
} catch (error) {
|
|
260
298
|
console.warn('Failed to load tabs from URL:', error);
|
|
@@ -300,11 +338,37 @@
|
|
|
300
338
|
);
|
|
301
339
|
});
|
|
302
340
|
|
|
341
|
+
// Tab count and limit computed properties
|
|
342
|
+
const tabCount = computed(() => tabs.value.length);
|
|
343
|
+
const isTabLimitReached = computed(() => tabs.value.length >= maxTabs);
|
|
344
|
+
const tabCountDisplay = computed(() => `${tabs.value.length}/${maxTabs} tabs`);
|
|
345
|
+
|
|
303
346
|
// Tab methods
|
|
304
347
|
const openTab = (item) => {
|
|
305
348
|
if (!item || !item.id) return;
|
|
306
349
|
const existing = tabs.value.find(t => t.id === item.id);
|
|
307
350
|
if (!existing) {
|
|
351
|
+
// Auto-close oldest tab if limit would be exceeded
|
|
352
|
+
if (tabs.value.length >= maxTabs) {
|
|
353
|
+
const oldestTab = tabs.value[0]; // Leftmost tab
|
|
354
|
+
const wasOldestActive = activeTabId.value === oldestTab.id;
|
|
355
|
+
|
|
356
|
+
// Show notification about auto-closed tab
|
|
357
|
+
tabLimitMessage.value = `Closed "${oldestTab.label}" to make room for "${item.label}".`;
|
|
358
|
+
showTabLimitNotification.value = true;
|
|
359
|
+
setTimeout(() => {
|
|
360
|
+
showTabLimitNotification.value = false;
|
|
361
|
+
}, 3000);
|
|
362
|
+
|
|
363
|
+
// Close the oldest tab
|
|
364
|
+
tabs.value.shift(); // Remove first element
|
|
365
|
+
|
|
366
|
+
// If we closed the active tab and there are still tabs, activate the new first tab
|
|
367
|
+
if (wasOldestActive && tabs.value.length > 0) {
|
|
368
|
+
activeTabId.value = tabs.value[0].id;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
308
372
|
tabs.value.push({
|
|
309
373
|
id: item.id,
|
|
310
374
|
label: item.label,
|
|
@@ -527,7 +591,13 @@
|
|
|
527
591
|
selectPaletteItem,
|
|
528
592
|
selectModule,
|
|
529
593
|
handleLogout,
|
|
530
|
-
getIframeSrc
|
|
594
|
+
getIframeSrc,
|
|
595
|
+
// Tab limit properties
|
|
596
|
+
tabCount,
|
|
597
|
+
isTabLimitReached,
|
|
598
|
+
tabCountDisplay,
|
|
599
|
+
showTabLimitNotification,
|
|
600
|
+
tabLimitMessage
|
|
531
601
|
};
|
|
532
602
|
}
|
|
533
603
|
}).mount('#app');
|