vectra-js 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/npm-publish.yml +39 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/README.md +30 -0
- package/bin/vectra.js +9 -3
- package/package.json +24 -22
- package/src/backends/chroma_store.js +13 -2
- package/src/config.js +9 -0
- package/src/core.js +203 -1
- package/src/dashboard/dashboard-script.js +260 -0
- package/src/dashboard/index.html +362 -0
- package/src/dashboard/logo.png +0 -0
- package/src/dashboard/trace-script.js +184 -0
- package/src/dashboard/trace.html +239 -0
- package/src/observability.js +226 -0
- package/src/processor.js +1 -1
- package/src/ui/index.html +278 -236
- package/src/ui/logo.png +0 -0
- package/src/ui/script.js +59 -10
- package/src/ui/style.css +2 -2
- package/src/webconfig_server.js +162 -2
package/src/core.js
CHANGED
|
@@ -16,12 +16,20 @@ const { LLMReranker } = require('./reranker');
|
|
|
16
16
|
const { InMemoryHistory, RedisHistory, PostgresHistory } = require('./memory');
|
|
17
17
|
const { OllamaBackend } = require('./backends/ollama');
|
|
18
18
|
const { v5: uuidv5 } = require('uuid');
|
|
19
|
+
const { v4: uuidv4 } = require('uuid');
|
|
20
|
+
const SQLiteLogger = require('./observability');
|
|
19
21
|
|
|
20
22
|
class VectraClient {
|
|
21
23
|
constructor(config) {
|
|
22
24
|
const parsed = RAGConfigSchema.parse(config);
|
|
23
25
|
this.config = parsed;
|
|
24
26
|
this.callbacks = config.callbacks || [];
|
|
27
|
+
|
|
28
|
+
// Initialize observability
|
|
29
|
+
this.logger = (this.config.observability && this.config.observability.enabled)
|
|
30
|
+
? new SQLiteLogger(this.config.observability)
|
|
31
|
+
: null;
|
|
32
|
+
|
|
25
33
|
// Initialize processor
|
|
26
34
|
const agenticLlm = (this.config.chunking && this.config.chunking.agenticLlm)
|
|
27
35
|
? this.createLLM(this.config.chunking.agenticLlm)
|
|
@@ -128,6 +136,12 @@ class VectraClient {
|
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
async ingestDocuments(filePath) {
|
|
139
|
+
const traceId = uuidv4();
|
|
140
|
+
const rootSpanId = uuidv4();
|
|
141
|
+
const tStart = Date.now();
|
|
142
|
+
const provider = this.config.embedding.provider;
|
|
143
|
+
const modelName = this.config.embedding.modelName;
|
|
144
|
+
|
|
131
145
|
try {
|
|
132
146
|
const stats = await fs.promises.stat(filePath);
|
|
133
147
|
|
|
@@ -292,8 +306,35 @@ class VectraClient {
|
|
|
292
306
|
}
|
|
293
307
|
const durationMs = Date.now() - t0;
|
|
294
308
|
this.trigger('onIngestEnd', filePath, chunks.length, durationMs);
|
|
309
|
+
|
|
310
|
+
this.logger.logTrace({
|
|
311
|
+
traceId,
|
|
312
|
+
spanId: rootSpanId,
|
|
313
|
+
name: 'ingestDocuments',
|
|
314
|
+
startTime: tStart,
|
|
315
|
+
endTime: Date.now(),
|
|
316
|
+
input: { filePath },
|
|
317
|
+
output: { chunks: chunks.length, durationMs },
|
|
318
|
+
attributes: { fileSize: size },
|
|
319
|
+
provider,
|
|
320
|
+
modelName
|
|
321
|
+
});
|
|
322
|
+
this.logger.logMetric({ name: 'ingest_latency', value: durationMs, tags: { type: 'single_file' } });
|
|
323
|
+
|
|
295
324
|
} catch (e) {
|
|
296
325
|
this.trigger('onError', e);
|
|
326
|
+
this.logger.logTrace({
|
|
327
|
+
traceId,
|
|
328
|
+
spanId: rootSpanId,
|
|
329
|
+
name: 'ingestDocuments',
|
|
330
|
+
startTime: tStart,
|
|
331
|
+
endTime: Date.now(),
|
|
332
|
+
input: { filePath },
|
|
333
|
+
error: { message: e.message },
|
|
334
|
+
status: 'error',
|
|
335
|
+
provider,
|
|
336
|
+
modelName
|
|
337
|
+
});
|
|
297
338
|
throw e;
|
|
298
339
|
}
|
|
299
340
|
}
|
|
@@ -459,6 +500,19 @@ class VectraClient {
|
|
|
459
500
|
}
|
|
460
501
|
|
|
461
502
|
async queryRAG(query, filter = null, stream = false, sessionId = null) {
|
|
503
|
+
const traceId = uuidv4();
|
|
504
|
+
const rootSpanId = uuidv4();
|
|
505
|
+
const tStart = Date.now();
|
|
506
|
+
|
|
507
|
+
if (sessionId) {
|
|
508
|
+
this.logger.updateSession(sessionId, null, { lastQuery: query });
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const provider = this.config.llm.provider;
|
|
512
|
+
const modelName = this.config.llm.modelName;
|
|
513
|
+
const embeddingProvider = this.config.embedding.provider;
|
|
514
|
+
const embeddingModelName = this.config.embedding.modelName;
|
|
515
|
+
|
|
462
516
|
try {
|
|
463
517
|
const tRetrieval = Date.now();
|
|
464
518
|
this.trigger('onRetrievalStart', query);
|
|
@@ -505,6 +559,20 @@ class VectraClient {
|
|
|
505
559
|
|
|
506
560
|
const retrievalMs = Date.now() - tRetrieval;
|
|
507
561
|
this.trigger('onRetrievalEnd', docs.length, retrievalMs);
|
|
562
|
+
|
|
563
|
+
this.logger.logTrace({
|
|
564
|
+
traceId,
|
|
565
|
+
spanId: uuidv4(),
|
|
566
|
+
parentSpanId: rootSpanId,
|
|
567
|
+
name: 'retrieval',
|
|
568
|
+
startTime: tRetrieval,
|
|
569
|
+
endTime: Date.now(),
|
|
570
|
+
input: { query, filter, strategy },
|
|
571
|
+
output: { documentsFound: docs.length },
|
|
572
|
+
provider: embeddingProvider,
|
|
573
|
+
modelName: embeddingModelName
|
|
574
|
+
});
|
|
575
|
+
|
|
508
576
|
const terms = query.toLowerCase().split(/\W+/).filter(t=>t.length>2);
|
|
509
577
|
docs = docs.map(d => {
|
|
510
578
|
const kws = Array.isArray(d.metadata?.keywords) ? d.metadata.keywords.map(k=>String(k).toLowerCase()) : [];
|
|
@@ -547,7 +615,91 @@ class VectraClient {
|
|
|
547
615
|
if (stream) {
|
|
548
616
|
// Streaming return
|
|
549
617
|
if (!this.llm.generateStream) throw new Error("Streaming not implemented for this provider");
|
|
550
|
-
|
|
618
|
+
|
|
619
|
+
this.logger.logTrace({
|
|
620
|
+
traceId,
|
|
621
|
+
spanId: uuidv4(),
|
|
622
|
+
parentSpanId: rootSpanId,
|
|
623
|
+
name: 'generation_stream_start',
|
|
624
|
+
startTime: tGen,
|
|
625
|
+
endTime: Date.now(),
|
|
626
|
+
input: { prompt },
|
|
627
|
+
output: { stream: true },
|
|
628
|
+
provider,
|
|
629
|
+
modelName
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const originalStream = await this.llm.generateStream(prompt, systemInst);
|
|
633
|
+
const self = this;
|
|
634
|
+
|
|
635
|
+
async function* wrappedStream() {
|
|
636
|
+
let fullAnswer = '';
|
|
637
|
+
try {
|
|
638
|
+
for await (const chunk of originalStream) {
|
|
639
|
+
const delta = (chunk && chunk.delta) ? chunk.delta : (typeof chunk === 'string' ? chunk : '');
|
|
640
|
+
fullAnswer += delta;
|
|
641
|
+
yield chunk;
|
|
642
|
+
}
|
|
643
|
+
} catch (e) {
|
|
644
|
+
self.trigger('onError', e);
|
|
645
|
+
self.logger.logTrace({
|
|
646
|
+
traceId,
|
|
647
|
+
spanId: rootSpanId,
|
|
648
|
+
name: 'queryRAG',
|
|
649
|
+
startTime: tStart,
|
|
650
|
+
endTime: Date.now(),
|
|
651
|
+
input: { query, sessionId },
|
|
652
|
+
error: { message: e.message, stack: e.stack },
|
|
653
|
+
status: 'error',
|
|
654
|
+
provider,
|
|
655
|
+
modelName
|
|
656
|
+
});
|
|
657
|
+
throw e;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Stream finished successfully
|
|
661
|
+
const genMs = Date.now() - tGen;
|
|
662
|
+
self.trigger('onGenerationEnd', fullAnswer, genMs);
|
|
663
|
+
|
|
664
|
+
const promptChars = prompt.length;
|
|
665
|
+
const answerChars = fullAnswer.length;
|
|
666
|
+
|
|
667
|
+
self.logger.logTrace({
|
|
668
|
+
traceId,
|
|
669
|
+
spanId: uuidv4(),
|
|
670
|
+
parentSpanId: rootSpanId,
|
|
671
|
+
name: 'generation',
|
|
672
|
+
startTime: tGen,
|
|
673
|
+
endTime: Date.now(),
|
|
674
|
+
input: { prompt },
|
|
675
|
+
output: { answer: fullAnswer.substring(0, 1000) },
|
|
676
|
+
attributes: { prompt_chars: promptChars, completion_chars: answerChars },
|
|
677
|
+
provider,
|
|
678
|
+
modelName
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
self.logger.logMetric({ name: 'prompt_chars', value: promptChars });
|
|
682
|
+
self.logger.logMetric({ name: 'completion_chars', value: answerChars });
|
|
683
|
+
|
|
684
|
+
self.logger.logTrace({
|
|
685
|
+
traceId,
|
|
686
|
+
spanId: rootSpanId,
|
|
687
|
+
name: 'queryRAG',
|
|
688
|
+
startTime: tStart,
|
|
689
|
+
endTime: Date.now(),
|
|
690
|
+
input: { query, sessionId },
|
|
691
|
+
output: { success: true },
|
|
692
|
+
attributes: { retrievalMs, genMs, docCount: docs.length },
|
|
693
|
+
provider,
|
|
694
|
+
modelName
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
self.logger.logMetric({ name: 'query_latency', value: Date.now() - tStart, tags: { type: 'total' } });
|
|
698
|
+
self.logger.logMetric({ name: 'retrieval_latency', value: retrievalMs, tags: { type: 'retrieval' } });
|
|
699
|
+
self.logger.logMetric({ name: 'generation_latency', value: genMs, tags: { type: 'generation' } });
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return wrappedStream();
|
|
551
703
|
} else {
|
|
552
704
|
const answer = await this.llm.generate(prompt, systemInst);
|
|
553
705
|
if (this.history && sessionId) {
|
|
@@ -561,6 +713,44 @@ class VectraClient {
|
|
|
561
713
|
}
|
|
562
714
|
const genMs = Date.now() - tGen;
|
|
563
715
|
this.trigger('onGenerationEnd', answer, genMs);
|
|
716
|
+
|
|
717
|
+
const promptChars = prompt.length;
|
|
718
|
+
const answerChars = answer ? String(answer).length : 0;
|
|
719
|
+
|
|
720
|
+
this.logger.logTrace({
|
|
721
|
+
traceId,
|
|
722
|
+
spanId: uuidv4(),
|
|
723
|
+
parentSpanId: rootSpanId,
|
|
724
|
+
name: 'generation',
|
|
725
|
+
startTime: tGen,
|
|
726
|
+
endTime: Date.now(),
|
|
727
|
+
input: { prompt },
|
|
728
|
+
output: { answer: String(answer).substring(0, 1000) }, // Truncate for log
|
|
729
|
+
attributes: { prompt_chars: promptChars, completion_chars: answerChars },
|
|
730
|
+
provider,
|
|
731
|
+
modelName
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
this.logger.logMetric({ name: 'prompt_chars', value: promptChars });
|
|
735
|
+
this.logger.logMetric({ name: 'completion_chars', value: answerChars });
|
|
736
|
+
|
|
737
|
+
this.logger.logTrace({
|
|
738
|
+
traceId,
|
|
739
|
+
spanId: rootSpanId,
|
|
740
|
+
name: 'queryRAG',
|
|
741
|
+
startTime: tStart,
|
|
742
|
+
endTime: Date.now(),
|
|
743
|
+
input: { query, sessionId },
|
|
744
|
+
output: { success: true },
|
|
745
|
+
attributes: { retrievalMs, genMs, docCount: docs.length },
|
|
746
|
+
provider,
|
|
747
|
+
modelName
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
this.logger.logMetric({ name: 'query_latency', value: Date.now() - tStart, tags: { type: 'total' } });
|
|
751
|
+
this.logger.logMetric({ name: 'retrieval_latency', value: retrievalMs, tags: { type: 'retrieval' } });
|
|
752
|
+
this.logger.logMetric({ name: 'generation_latency', value: genMs, tags: { type: 'generation' } });
|
|
753
|
+
|
|
564
754
|
if (this.config.generation && this.config.generation.outputFormat === 'json') {
|
|
565
755
|
try { const parsed = JSON.parse(String(answer)); return { answer: parsed, sources: docs.map(d => d.metadata) }; } catch { return { answer, sources: docs.map(d => d.metadata) }; }
|
|
566
756
|
}
|
|
@@ -568,6 +758,18 @@ class VectraClient {
|
|
|
568
758
|
}
|
|
569
759
|
} catch (e) {
|
|
570
760
|
this.trigger('onError', e);
|
|
761
|
+
this.logger.logTrace({
|
|
762
|
+
traceId,
|
|
763
|
+
spanId: rootSpanId,
|
|
764
|
+
name: 'queryRAG',
|
|
765
|
+
startTime: tStart,
|
|
766
|
+
endTime: Date.now(),
|
|
767
|
+
input: { query, sessionId },
|
|
768
|
+
error: { message: e.message, stack: e.stack },
|
|
769
|
+
status: 'error',
|
|
770
|
+
provider,
|
|
771
|
+
modelName
|
|
772
|
+
});
|
|
571
773
|
throw e;
|
|
572
774
|
}
|
|
573
775
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// State
|
|
2
|
+
let currentView = 'overview';
|
|
3
|
+
let currentProject = 'all';
|
|
4
|
+
let lastStats = null;
|
|
5
|
+
|
|
6
|
+
// Init
|
|
7
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
8
|
+
loadProjects();
|
|
9
|
+
loadDashboardData();
|
|
10
|
+
|
|
11
|
+
// Auto-refresh every 30s
|
|
12
|
+
setInterval(loadDashboardData, 30000);
|
|
13
|
+
|
|
14
|
+
// Initialize Icons
|
|
15
|
+
if (window.lucide) {
|
|
16
|
+
window.lucide.createIcons();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Project filter
|
|
20
|
+
const projectSelect = document.getElementById('projectSelect');
|
|
21
|
+
if (projectSelect) {
|
|
22
|
+
projectSelect.addEventListener('change', (e) => {
|
|
23
|
+
currentProject = e.target.value;
|
|
24
|
+
loadDashboardData();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function switchView(view) {
|
|
30
|
+
currentView = view;
|
|
31
|
+
|
|
32
|
+
// Update content visibility
|
|
33
|
+
['overview', 'traces', 'sessions'].forEach(v => {
|
|
34
|
+
const div = document.getElementById(`view-${v}`);
|
|
35
|
+
if (v === view) {
|
|
36
|
+
div.classList.remove('hidden');
|
|
37
|
+
} else {
|
|
38
|
+
div.classList.add('hidden');
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Update Sidebar Links
|
|
43
|
+
document.querySelectorAll('.sidebar-link').forEach(el => {
|
|
44
|
+
el.classList.remove('active', 'bg-gray-100', 'text-slate-900');
|
|
45
|
+
el.classList.add('text-gray-600');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const btn = document.getElementById(`btn-${view}`);
|
|
49
|
+
if (btn) {
|
|
50
|
+
btn.classList.add('active', 'bg-gray-100', 'text-slate-900');
|
|
51
|
+
btn.classList.remove('text-gray-600');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (view === 'traces') loadTraces();
|
|
55
|
+
if (view === 'sessions') loadSessions();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function fetchAPI(endpoint) {
|
|
59
|
+
try {
|
|
60
|
+
const url = `/api/observability/${endpoint}${currentProject !== 'all' ? `?projectId=${currentProject}` : ''}`;
|
|
61
|
+
const res = await fetch(url);
|
|
62
|
+
if (!res.ok) throw new Error('API Error');
|
|
63
|
+
return await res.json();
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error('Fetch error:', e);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function loadProjects() {
|
|
71
|
+
const projects = await fetchAPI('projects');
|
|
72
|
+
const select = document.getElementById('projectSelect');
|
|
73
|
+
if (projects && projects.length > 0) {
|
|
74
|
+
// Keep 'all' option
|
|
75
|
+
// Add projects
|
|
76
|
+
projects.forEach(p => {
|
|
77
|
+
if (p === 'all') return; // skip if somehow in db
|
|
78
|
+
const opt = document.createElement('option');
|
|
79
|
+
opt.value = p;
|
|
80
|
+
opt.textContent = p;
|
|
81
|
+
select.appendChild(opt);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function loadDashboardData() {
|
|
87
|
+
const stats = await fetchAPI('stats');
|
|
88
|
+
if (!stats) return;
|
|
89
|
+
|
|
90
|
+
lastStats = stats;
|
|
91
|
+
|
|
92
|
+
// Update Stats Cards
|
|
93
|
+
document.getElementById('stat-total-req').textContent = stats.totalRequests || 0;
|
|
94
|
+
document.getElementById('stat-avg-latency').textContent = Math.round(stats.avgLatency || 0);
|
|
95
|
+
document.getElementById('stat-tokens').textContent = ((stats.totalPromptChars || 0) + (stats.totalCompletionChars || 0)).toLocaleString();
|
|
96
|
+
document.getElementById('stat-errors').textContent = '0%'; // Placeholder for now
|
|
97
|
+
|
|
98
|
+
// Update Charts
|
|
99
|
+
updateCharts(stats.history);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let latencyChartInst = null;
|
|
103
|
+
let tokenChartInst = null;
|
|
104
|
+
|
|
105
|
+
function updateCharts(history = []) {
|
|
106
|
+
// History is expected to be array of { timestamp, latency, tokens }
|
|
107
|
+
// If not provided by API yet, mock or skip
|
|
108
|
+
if (!history.length) return;
|
|
109
|
+
|
|
110
|
+
const labels = history.map(h => new Date(h.timestamp).toLocaleTimeString());
|
|
111
|
+
const latencies = history.map(h => h.latency);
|
|
112
|
+
const tokens = history.map(h => h.tokens);
|
|
113
|
+
|
|
114
|
+
// Latency Chart
|
|
115
|
+
const ctxL = document.getElementById('latencyChart').getContext('2d');
|
|
116
|
+
const isDark = document.documentElement.classList.contains('dark');
|
|
117
|
+
const gridColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
|
118
|
+
const textColor = isDark ? '#9ca3af' : '#64748b';
|
|
119
|
+
|
|
120
|
+
if (latencyChartInst) latencyChartInst.destroy();
|
|
121
|
+
latencyChartInst = new Chart(ctxL, {
|
|
122
|
+
type: 'line',
|
|
123
|
+
data: {
|
|
124
|
+
labels,
|
|
125
|
+
datasets: [{
|
|
126
|
+
label: 'Latency (ms)',
|
|
127
|
+
data: latencies,
|
|
128
|
+
borderColor: '#8b5cf6',
|
|
129
|
+
tension: 0.4,
|
|
130
|
+
fill: true,
|
|
131
|
+
backgroundColor: isDark ? 'rgba(139, 92, 246, 0.1)' : 'rgba(139, 92, 246, 0.05)'
|
|
132
|
+
}]
|
|
133
|
+
},
|
|
134
|
+
options: {
|
|
135
|
+
responsive: true,
|
|
136
|
+
maintainAspectRatio: false,
|
|
137
|
+
plugins: { legend: { display: false } },
|
|
138
|
+
scales: {
|
|
139
|
+
y: {
|
|
140
|
+
beginAtZero: true,
|
|
141
|
+
grid: { color: gridColor, borderDash: [2, 4] },
|
|
142
|
+
ticks: { color: textColor }
|
|
143
|
+
},
|
|
144
|
+
x: {
|
|
145
|
+
grid: { display: false },
|
|
146
|
+
ticks: { color: textColor }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Token Chart
|
|
153
|
+
const ctxT = document.getElementById('tokenChart').getContext('2d');
|
|
154
|
+
if (tokenChartInst) tokenChartInst.destroy();
|
|
155
|
+
tokenChartInst = new Chart(ctxT, {
|
|
156
|
+
type: 'bar',
|
|
157
|
+
data: {
|
|
158
|
+
labels,
|
|
159
|
+
datasets: [{
|
|
160
|
+
label: 'Tokens',
|
|
161
|
+
data: tokens,
|
|
162
|
+
backgroundColor: '#0ea5e9',
|
|
163
|
+
borderRadius: 4
|
|
164
|
+
}]
|
|
165
|
+
},
|
|
166
|
+
options: {
|
|
167
|
+
responsive: true,
|
|
168
|
+
maintainAspectRatio: false,
|
|
169
|
+
plugins: { legend: { display: false } },
|
|
170
|
+
scales: {
|
|
171
|
+
y: {
|
|
172
|
+
beginAtZero: true,
|
|
173
|
+
grid: { color: gridColor, borderDash: [2, 4] },
|
|
174
|
+
ticks: { color: textColor }
|
|
175
|
+
},
|
|
176
|
+
x: {
|
|
177
|
+
grid: { display: false },
|
|
178
|
+
ticks: { color: textColor }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function loadTraces() {
|
|
186
|
+
const traces = await fetchAPI('traces'); // Expects list of recent traces
|
|
187
|
+
const tbody = document.getElementById('traces-table-body');
|
|
188
|
+
tbody.innerHTML = '';
|
|
189
|
+
|
|
190
|
+
if (!traces || traces.length === 0) {
|
|
191
|
+
tbody.innerHTML = '<tr><td colspan="6" class="px-6 py-4 text-center text-sm text-slate-500 dark:text-gray-400">No traces found</td></tr>';
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
traces.forEach(t => {
|
|
196
|
+
const row = document.createElement('tr');
|
|
197
|
+
row.className = 'hover:bg-slate-50 dark:hover:bg-white/5 transition-colors cursor-pointer';
|
|
198
|
+
row.onclick = () => window.location.href = `/dashboard/trace.html?id=${t.trace_id}`;
|
|
199
|
+
|
|
200
|
+
const statusColor = t.error && t.error !== '{}' ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' : 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400';
|
|
201
|
+
const statusText = t.error && t.error !== '{}' ? 'Error' : 'Success';
|
|
202
|
+
|
|
203
|
+
row.innerHTML = `
|
|
204
|
+
<td class="px-6 py-4 whitespace-nowrap text-xs font-mono text-slate-500 dark:text-gray-400">${t.trace_id.slice(0, 8)}...</td>
|
|
205
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-slate-900 dark:text-white">${t.name}</td>
|
|
206
|
+
<td class="px-6 py-4 whitespace-nowrap"><span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${statusColor}">${statusText}</span></td>
|
|
207
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-gray-400">${t.end_time - t.start_time}ms</td>
|
|
208
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-gray-400">${new Date(t.start_time).toLocaleString()}</td>
|
|
209
|
+
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium text-brand-600 dark:text-brand-400 hover:text-brand-900 dark:hover:text-brand-300">View</td>
|
|
210
|
+
`;
|
|
211
|
+
tbody.appendChild(row);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function loadSessions() {
|
|
216
|
+
const sessions = await fetchAPI('sessions');
|
|
217
|
+
const tbody = document.getElementById('sessions-table-body');
|
|
218
|
+
tbody.innerHTML = '';
|
|
219
|
+
|
|
220
|
+
if (!sessions || sessions.length === 0) {
|
|
221
|
+
tbody.innerHTML = '<tr><td colspan="4" class="px-6 py-4 text-center text-sm text-slate-500">No active sessions</td></tr>';
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
sessions.forEach(s => {
|
|
226
|
+
const row = document.createElement('tr');
|
|
227
|
+
row.className = 'hover:bg-slate-50 dark:hover:bg-white/5 transition-colors';
|
|
228
|
+
|
|
229
|
+
row.innerHTML = `
|
|
230
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-slate-900 dark:text-white">${s.session_id}</td>
|
|
231
|
+
<td class="px-6 py-4 text-sm text-slate-500 dark:text-gray-400 truncate max-w-xs">${s.metadata?.last_query || '-'}</td>
|
|
232
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-gray-400">${new Date(s.last_activity_time).toLocaleString()}</td>
|
|
233
|
+
<td class="px-6 py-4 text-sm text-slate-500 dark:text-gray-400 font-mono text-xs">${JSON.stringify(s.metadata || {}).slice(0, 30)}...</td>
|
|
234
|
+
`;
|
|
235
|
+
tbody.appendChild(row);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* Modal functions removed as trace details are now on a separate page */
|
|
240
|
+
function showTraceDetails(traceId) {
|
|
241
|
+
window.location.href = `/dashboard/trace.html?id=${traceId}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function closeModal() {
|
|
245
|
+
// Deprecated
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function toggleTheme() {
|
|
249
|
+
if (document.documentElement.classList.contains('dark')) {
|
|
250
|
+
document.documentElement.classList.remove('dark');
|
|
251
|
+
localStorage.setItem('color-theme', 'light');
|
|
252
|
+
} else {
|
|
253
|
+
document.documentElement.classList.add('dark');
|
|
254
|
+
localStorage.setItem('color-theme', 'dark');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (lastStats && lastStats.history) {
|
|
258
|
+
updateCharts(lastStats.history);
|
|
259
|
+
}
|
|
260
|
+
}
|