vectra-js 0.9.3 → 0.9.5

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.
Binary file
package/src/ui/script.js CHANGED
@@ -1,4 +1,50 @@
1
1
  document.addEventListener('DOMContentLoaded', () => {
2
+ // --- Dark Mode Logic ---
3
+ const themeToggleBtn = document.getElementById('theme-toggle');
4
+ const darkIcon = document.getElementById('theme-toggle-dark-icon');
5
+ const lightIcon = document.getElementById('theme-toggle-light-icon');
6
+
7
+ // Initial State
8
+ if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
9
+ document.documentElement.classList.add('dark');
10
+ if (lightIcon) lightIcon.classList.remove('hidden');
11
+ } else {
12
+ document.documentElement.classList.remove('dark');
13
+ if (darkIcon) darkIcon.classList.remove('hidden');
14
+ }
15
+
16
+ // Toggle Event
17
+ if (themeToggleBtn) {
18
+ themeToggleBtn.addEventListener('click', () => {
19
+ // Toggle icons
20
+ if (darkIcon) darkIcon.classList.toggle('hidden');
21
+ if (lightIcon) lightIcon.classList.toggle('hidden');
22
+
23
+ // If is set in local storage
24
+ if (localStorage.getItem('color-theme')) {
25
+ if (localStorage.getItem('color-theme') === 'light') {
26
+ document.documentElement.classList.add('dark');
27
+ localStorage.setItem('color-theme', 'dark');
28
+ } else {
29
+ document.documentElement.classList.remove('dark');
30
+ localStorage.setItem('color-theme', 'light');
31
+ }
32
+ } else {
33
+ // If not in local storage
34
+ if (document.documentElement.classList.contains('dark')) {
35
+ document.documentElement.classList.remove('dark');
36
+ localStorage.setItem('color-theme', 'light');
37
+ } else {
38
+ document.documentElement.classList.add('dark');
39
+ localStorage.setItem('color-theme', 'dark');
40
+ }
41
+ }
42
+
43
+ // Trigger preview update
44
+ if (typeof updatePreview === 'function') updatePreview();
45
+ });
46
+ }
47
+
2
48
  // --- Navigation Logic (Smooth Scrolling & Scroll Spy) ---
3
49
  const links = document.querySelectorAll('.nav-item');
4
50
  const sections = document.querySelectorAll('section');
@@ -60,13 +106,13 @@ document.addEventListener('DOMContentLoaded', () => {
60
106
 
61
107
  function updateSidebarState(currentId) {
62
108
  links.forEach(link => {
63
- link.classList.remove('bg-indigo-50', 'text-indigo-600', 'active', 'border-l-4', 'border-indigo-600');
109
+ link.classList.remove('bg-brand-50', 'text-brand-600', 'active', 'border-l-4', 'border-brand-600');
64
110
  link.classList.add('text-gray-600', 'hover:bg-gray-50');
65
111
 
66
112
  // We use a slight visual indicator for active state
67
113
  if (link.getAttribute('data-target') === currentId) {
68
114
  link.classList.remove('text-gray-600', 'hover:bg-gray-50');
69
- link.classList.add('bg-indigo-50', 'text-indigo-600', 'active');
115
+ link.classList.add('bg-brand-50', 'text-brand-600', 'active');
70
116
  }
71
117
  });
72
118
  }
@@ -138,8 +184,8 @@ document.addEventListener('DOMContentLoaded', () => {
138
184
  const row = document.createElement('div');
139
185
  row.className = 'flex items-center space-x-2 header-row';
140
186
  row.innerHTML = `
141
- <input type="text" placeholder="Key" value="${key}" class="header-key block w-1/2 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm py-2 px-3 border">
142
- <input type="text" placeholder="Value" value="${value}" class="header-value block w-1/2 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm py-2 px-3 border">
187
+ <input type="text" placeholder="Key" value="${key}" class="header-key block w-1/2 rounded-md border-gray-300 dark:border-white/10 bg-white dark:bg-dark-950 text-slate-900 dark:text-white shadow-sm focus:border-brand-500 focus:ring-brand-500 sm:text-sm py-2 px-3 border">
188
+ <input type="text" placeholder="Value" value="${value}" class="header-value block w-1/2 rounded-md border-gray-300 dark:border-white/10 bg-white dark:bg-dark-950 text-slate-900 dark:text-white shadow-sm focus:border-brand-500 focus:ring-brand-500 sm:text-sm py-2 px-3 border">
143
189
  <button type="button" class="remove-header p-2 text-gray-400 hover:text-red-500">
144
190
  <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
145
191
  </button>
@@ -308,12 +354,15 @@ function setBackend(type) {
308
354
  const btnNode = document.getElementById('backend-node');
309
355
  const btnPython = document.getElementById('backend-python');
310
356
 
357
+ const activeClass = 'px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-200 text-brand-600 dark:text-brand-400 bg-white dark:bg-brand-900/20 shadow-sm';
358
+ const inactiveClass = 'px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-200 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white';
359
+
311
360
  if (isPythonBackend) {
312
- btnPython.className = 'px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-200 text-indigo-600 bg-indigo-50 shadow-sm';
313
- btnNode.className = 'px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-200 text-gray-500 hover:text-gray-900';
361
+ btnPython.className = activeClass;
362
+ btnNode.className = inactiveClass;
314
363
  } else {
315
- btnNode.className = 'px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-200 text-indigo-600 bg-indigo-50 shadow-sm';
316
- btnPython.className = 'px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-200 text-gray-500 hover:text-gray-900';
364
+ btnNode.className = activeClass;
365
+ btnPython.className = inactiveClass;
317
366
  }
318
367
 
319
368
  updatePreview();
@@ -447,8 +496,8 @@ function addHeaderRow(key = '', value = '') {
447
496
  const row = document.createElement('div');
448
497
  row.className = 'flex items-center space-x-2 header-row mb-2';
449
498
  row.innerHTML = `
450
- <input type="text" placeholder="Key" value="${key}" class="header-key block w-1/2 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm py-2 px-3 border">
451
- <input type="text" placeholder="Value" value="${value}" class="header-value block w-1/2 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm py-2 px-3 border">
499
+ <input type="text" placeholder="Key" value="${key}" class="header-key block w-1/2 rounded-md border-white/10 bg-dark-950 text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm py-2 px-3 border">
500
+ <input type="text" placeholder="Value" value="${value}" class="header-value block w-1/2 rounded-md border-white/10 bg-dark-950 text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm py-2 px-3 border">
452
501
  <button type="button" class="remove-header p-2 text-gray-400 hover:text-red-500 transition-colors">
453
502
  <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
454
503
  </button>
package/src/ui/style.css CHANGED
@@ -79,7 +79,7 @@ body {
79
79
  }
80
80
 
81
81
  .nav-item.active {
82
- background-color: #eef2ff;
82
+ background-color: #f5f3ff;
83
83
  color: var(--primary);
84
84
  }
85
85
 
@@ -176,7 +176,7 @@ label {
176
176
  .form-control:focus {
177
177
  outline: none;
178
178
  border-color: var(--primary);
179
- box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
179
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
180
180
  background-color: #fff;
181
181
  }
182
182
 
@@ -2,6 +2,37 @@ const http = require('http');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { RAGConfigSchema, ProviderType, ChunkingStrategy, RetrievalStrategy } = require('./config');
5
+ const sqlite3 = require('sqlite3').verbose();
6
+
7
+
8
+ // Helper to get DB connection from config
9
+ const getDb = (configPath) => {
10
+ try {
11
+ if (!fs.existsSync(configPath)) return null;
12
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
13
+ if (!cfg.observability || !cfg.observability.enabled) return null;
14
+
15
+ let dbPath = cfg.observability.sqlitePath || 'vectra-observability.db';
16
+ if (!path.isAbsolute(dbPath)) {
17
+ dbPath = path.resolve(path.dirname(configPath), dbPath);
18
+ }
19
+
20
+ return new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY);
21
+ } catch (e) {
22
+ console.error('Failed to open observability DB:', e);
23
+ return null;
24
+ }
25
+ };
26
+
27
+ // Promisify sqlite3 methods
28
+ const dbQuery = (db, method, sql, params = []) => {
29
+ return new Promise((resolve, reject) => {
30
+ db[method](sql, params, (err, result) => {
31
+ if (err) reject(err);
32
+ else resolve(result);
33
+ });
34
+ });
35
+ };
5
36
 
6
37
  // Simple browser opener using child_process since 'open' package might not be installed
7
38
  const { exec } = require('child_process');
@@ -57,7 +88,7 @@ function serveStatic(res, filePath, contentType) {
57
88
  }
58
89
  }
59
90
 
60
- function start(configPath, port = 8766, openInBrowser = true) {
91
+ function start(configPath, mode = 'webconfig', port = 8766, openInBrowser = true) {
61
92
  const absConfigPath = path.resolve(configPath);
62
93
 
63
94
  const createServer = (currentPort) => {
@@ -67,6 +98,132 @@ function start(configPath, port = 8766, openInBrowser = true) {
67
98
  res.end(JSON.stringify(obj));
68
99
  };
69
100
 
101
+ // --- Dashboard Routes ---
102
+
103
+ // Redirect /dashboard to /dashboard/ to handle relative assets correctly
104
+ if (req.method === 'GET' && req.url === '/dashboard') {
105
+ res.writeHead(301, { 'Location': '/dashboard/' });
106
+ res.end();
107
+ return;
108
+ }
109
+
110
+ // Serve Dashboard HTML
111
+ if (req.method === 'GET' && req.url === '/dashboard/') {
112
+ const filePath = path.join(__dirname, 'dashboard', 'index.html');
113
+ serveStatic(res, filePath, 'text/html; charset=utf-8');
114
+ return;
115
+ }
116
+
117
+ // Serve Dashboard Assets
118
+ if (req.method === 'GET' && req.url.startsWith('/dashboard/')) {
119
+ const assetName = req.url.split('?')[0].replace('/dashboard/', '');
120
+ const filePath = path.join(__dirname, 'dashboard', assetName);
121
+ if (fs.existsSync(filePath)) {
122
+ const ext = path.extname(filePath);
123
+ let type = 'text/plain';
124
+ if (ext === '.css') type = 'text/css';
125
+ if (ext === '.js') type = 'application/javascript';
126
+ if (ext === '.html') type = 'text/html';
127
+
128
+ serveStatic(res, filePath, type);
129
+ return;
130
+ }
131
+ }
132
+
133
+ // --- Observability API ---
134
+
135
+ if (req.method === 'GET' && req.url.startsWith('/api/observability/')) {
136
+ const db = getDb(absConfigPath);
137
+ if (!db) {
138
+ sendJson(400, { error: 'Observability not enabled or DB not found' });
139
+ return;
140
+ }
141
+
142
+ const handleRequest = async () => {
143
+ const url = new URL(req.url, `http://${req.headers.host}`);
144
+ const projectId = url.searchParams.get('projectId');
145
+ // Sanitize projectId to avoid injection if possible, though strict typing helps.
146
+ // Using parameterized queries is safer.
147
+
148
+ let projectFilter = '';
149
+ const params = [];
150
+
151
+ if (projectId && projectId !== 'all') {
152
+ projectFilter = 'WHERE project_id = ?';
153
+ params.push(projectId);
154
+ }
155
+
156
+ try {
157
+ if (url.pathname.endsWith('/stats')) {
158
+ // Aggregate stats
159
+ // Fix: projectFilter starts with WHERE, but here we append to existing WHERE if name='queryRAG'.
160
+ // So: WHERE name='queryRAG' AND project_id = ?
161
+ const whereClause = projectFilter ? `AND project_id = ?` : '';
162
+
163
+ const totalReq = await dbQuery(db, 'get', `SELECT COUNT(*) as count FROM traces WHERE name = 'queryRAG' ${whereClause}`, params);
164
+
165
+ const avgLat = await dbQuery(db, 'get', `SELECT AVG(value) as val FROM metrics WHERE name = 'query_latency' ${whereClause}`, params);
166
+ const tokensP = await dbQuery(db, 'get', `SELECT SUM(value) as val FROM metrics WHERE name = 'prompt_chars' ${whereClause}`, params);
167
+ const tokensC = await dbQuery(db, 'get', `SELECT SUM(value) as val FROM metrics WHERE name = 'completion_chars' ${whereClause}`, params);
168
+
169
+ // History for charts (last 50 query latencies)
170
+ const history = await dbQuery(db, 'all', `
171
+ SELECT m.timestamp, m.value as latency,
172
+ (SELECT value FROM metrics m2 WHERE m2.timestamp = m.timestamp AND m2.name = 'prompt_chars') +
173
+ (SELECT value FROM metrics m3 WHERE m3.timestamp = m.timestamp AND m3.name = 'completion_chars') as tokens
174
+ FROM metrics m
175
+ WHERE m.name = 'query_latency' ${whereClause}
176
+ ORDER BY m.timestamp DESC LIMIT 50
177
+ `, params);
178
+
179
+ sendJson(200, {
180
+ totalRequests: totalReq ? totalReq.count : 0,
181
+ avgLatency: avgLat ? avgLat.val : 0,
182
+ totalPromptChars: tokensP ? tokensP.val : 0,
183
+ totalCompletionChars: tokensC ? tokensC.val : 0,
184
+ history: history ? history.reverse() : []
185
+ });
186
+ }
187
+ else if (url.pathname.endsWith('/traces')) {
188
+ const whereClause = projectFilter ? `AND project_id = ?` : '';
189
+ const traces = await dbQuery(db, 'all', `SELECT * FROM traces WHERE name = 'queryRAG' ${whereClause} ORDER BY start_time DESC LIMIT 50`, params);
190
+ sendJson(200, traces);
191
+ }
192
+ else if (url.pathname.match(/\/traces\/([a-zA-Z0-9-]+)$/)) {
193
+ const traceId = url.pathname.split('/').pop();
194
+ const spans = await dbQuery(db, 'all', `SELECT * FROM traces WHERE trace_id = ?`, [traceId]);
195
+ sendJson(200, spans);
196
+ }
197
+ else if (url.pathname.endsWith('/sessions')) {
198
+ // sessions has project_id column
199
+ const sessions = await dbQuery(db, 'all', `SELECT * FROM sessions ${projectFilter} ORDER BY last_activity_time DESC LIMIT 50`, params);
200
+ // Parse metadata JSON
201
+ if (sessions) {
202
+ sessions.forEach(s => { if(s.metadata) s.metadata = JSON.parse(s.metadata); });
203
+ }
204
+ sendJson(200, sessions);
205
+ }
206
+ else if (url.pathname.endsWith('/projects')) {
207
+ const projects = await dbQuery(db, 'all', `SELECT DISTINCT project_id FROM traces`);
208
+ sendJson(200, projects.map(p => p.project_id).filter(Boolean));
209
+ }
210
+ else {
211
+ sendJson(404, { error: 'Unknown endpoint' });
212
+ }
213
+ } catch (e) {
214
+ console.error(e);
215
+ sendJson(500, { error: e.message });
216
+ } finally {
217
+ db.close();
218
+ }
219
+ };
220
+
221
+ handleRequest();
222
+ return;
223
+ }
224
+
225
+ // --- Config UI Routes (Legacy) ---
226
+
70
227
  // Serve index.html
71
228
  if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
72
229
  const filePath = path.join(__dirname, 'ui', 'index.html');
@@ -161,7 +318,10 @@ function start(configPath, port = 8766, openInBrowser = true) {
161
318
  });
162
319
 
163
320
  server.listen(currentPort, () => {
164
- const url = `http://localhost:${currentPort}/`;
321
+ let url = `http://localhost:${currentPort}/`;
322
+ if (mode === 'dashboard') {
323
+ url = `http://localhost:${currentPort}/dashboard`;
324
+ }
165
325
  console.log(`Vectra WebConfig running at ${url}`);
166
326
  if (openInBrowser) {
167
327
  openBrowser(url);