query_lens 0.1.3 → 0.2.0
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.
- checksums.yaml +4 -4
- data/README.md +3 -1
- data/app/views/query_lens/layouts/application.html.erb +42 -0
- data/app/views/query_lens/queries/show.html.erb +253 -12
- data/lib/query_lens/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 301dbff9b6c8506d45959771354536fea51393e70f56562f4f4a9890a167eda9
|
|
4
|
+
data.tar.gz: ac7ffd65456c5481f3d42caba3c1af9ab9ee1b8217e8d9cb220c6b7be9a55f86
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 79c4a4ce7fd9fc2838698b92a1b34bc25f6d97b65845c22cbef2519c0342a5f899700171145a04ae3a35a14de316b2c13fdb906e1b581ea11f8a730773c32abf
|
|
7
|
+
data.tar.gz: 1fb00ec3b9f5acf320dfd1a5e973e0f6e2c23f99b6e64f4e65312b54e860db2a43deaf5f2dcb70d83fdf962a0794d4281e13745c931064fb94e86ef52dfb8fa5
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# QueryLens
|
|
2
2
|
|
|
3
|
+
**[querylens.ai](https://querylens.ai)**
|
|
4
|
+
|
|
3
5
|
A mountable Rails engine that lets users write natural language questions and get SQL queries generated by AI, executed against their database, with results displayed — all in one interface. Think "Blazer meets ChatGPT."
|
|
4
6
|
|
|
5
7
|
Powered by [RubyLLM](https://rubyllm.com), QueryLens works with any major AI provider: OpenAI, Anthropic (Claude), Google Gemini, DeepSeek, Mistral, Ollama (local models), and more.
|
|
@@ -21,7 +23,7 @@ Powered by [RubyLLM](https://rubyllm.com), QueryLens works with any major AI pro
|
|
|
21
23
|
- Interactive conversation with context (follow-up questions refine queries)
|
|
22
24
|
- Read-only query execution (safety enforced at transaction level)
|
|
23
25
|
- Editable SQL editor with syntax highlighting
|
|
24
|
-
- Results displayed as sortable tables
|
|
26
|
+
- Results displayed as sortable tables with **Chart.js charting** (bar, line, pie, scatter) and auto-detected chart types
|
|
25
27
|
- Configurable authentication, timeouts, and row limits
|
|
26
28
|
- Zero frontend dependencies (self-contained CSS, vanilla JS)
|
|
27
29
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>QueryLens</title>
|
|
7
7
|
<%= csrf_meta_tags %>
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
|
8
9
|
<style>
|
|
9
10
|
/* ── Reset & Base ── */
|
|
10
11
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -511,6 +512,47 @@
|
|
|
511
512
|
color: #ca8a04; font-size: 11px; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
512
513
|
}
|
|
513
514
|
|
|
515
|
+
/* ── Chart Toggle & Controls ── */
|
|
516
|
+
.ql-chart-toggle {
|
|
517
|
+
display: inline-flex; border-radius: 8px; overflow: hidden;
|
|
518
|
+
background: #1c1c21; border: 1px solid #27272a; margin-left: 12px;
|
|
519
|
+
}
|
|
520
|
+
.ql-chart-toggle-btn {
|
|
521
|
+
padding: 4px 14px; font-size: 12px; font-weight: 600; border: none;
|
|
522
|
+
background: transparent; color: #71717a; cursor: pointer;
|
|
523
|
+
transition: all 0.15s; letter-spacing: 0.01em;
|
|
524
|
+
}
|
|
525
|
+
.ql-chart-toggle-btn:hover { color: #a1a1aa; }
|
|
526
|
+
.ql-chart-toggle-btn.active {
|
|
527
|
+
background: #6366f1; color: #fff;
|
|
528
|
+
}
|
|
529
|
+
.ql-chart-controls {
|
|
530
|
+
display: flex; align-items: center; gap: 16px; padding: 10px 20px;
|
|
531
|
+
background: #141417; border-bottom: 1px solid #27272a; flex-shrink: 0;
|
|
532
|
+
}
|
|
533
|
+
.ql-chart-control-group {
|
|
534
|
+
display: flex; align-items: center; gap: 6px;
|
|
535
|
+
}
|
|
536
|
+
.ql-chart-label {
|
|
537
|
+
font-size: 11px; font-weight: 600; color: #71717a;
|
|
538
|
+
text-transform: uppercase; letter-spacing: 0.04em;
|
|
539
|
+
}
|
|
540
|
+
.ql-chart-select {
|
|
541
|
+
padding: 5px 28px 5px 10px; font-size: 12px; font-family: inherit;
|
|
542
|
+
background: #0f0f12; color: #d4d4d8; border: 1px solid #3f3f46;
|
|
543
|
+
border-radius: 6px; outline: none; cursor: pointer;
|
|
544
|
+
appearance: none; -webkit-appearance: none;
|
|
545
|
+
background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
546
|
+
background-repeat: no-repeat; background-position: right 8px center;
|
|
547
|
+
transition: border-color 0.15s;
|
|
548
|
+
}
|
|
549
|
+
.ql-chart-select:focus { border-color: #6366f1; }
|
|
550
|
+
.ql-chart-container {
|
|
551
|
+
display: flex; align-items: center; justify-content: center;
|
|
552
|
+
height: 360px; padding: 16px 20px;
|
|
553
|
+
}
|
|
554
|
+
.ql-chart-container canvas { max-height: 100%; }
|
|
555
|
+
|
|
514
556
|
/* ── Animations ── */
|
|
515
557
|
@keyframes ql-spin { to { transform: rotate(360deg); } }
|
|
516
558
|
@keyframes ql-fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
|
@@ -178,6 +178,8 @@
|
|
|
178
178
|
let conversation = [];
|
|
179
179
|
let currentConversationId = null;
|
|
180
180
|
let conversationsList = [];
|
|
181
|
+
let chartInstance = null;
|
|
182
|
+
let lastResultData = null;
|
|
181
183
|
|
|
182
184
|
// ── Load restricted tables ──
|
|
183
185
|
(async function loadInfo() {
|
|
@@ -286,6 +288,10 @@
|
|
|
286
288
|
if (!sql) return;
|
|
287
289
|
|
|
288
290
|
setRunning(true);
|
|
291
|
+
destroyChart();
|
|
292
|
+
lastResultData = null;
|
|
293
|
+
const oldToggle = document.querySelector('.ql-chart-toggle');
|
|
294
|
+
if (oldToggle) oldToggle.remove();
|
|
289
295
|
el.results.innerHTML = '<div class="ql-loading-row"><div class="ql-spinner"></div>Running query...</div>';
|
|
290
296
|
el.rowCount.textContent = '';
|
|
291
297
|
|
|
@@ -324,33 +330,260 @@
|
|
|
324
330
|
}
|
|
325
331
|
}
|
|
326
332
|
|
|
333
|
+
// ── Chart Constants ──
|
|
334
|
+
const CHART_COLORS = ['#818CF8','#F472B6','#34D399','#FBBF24','#60A5FA','#A78BFA','#FB923C','#2DD4BF','#E879F9','#4ADE80'];
|
|
335
|
+
|
|
336
|
+
// ── Column Classification ──
|
|
337
|
+
function classifyColumns(data) {
|
|
338
|
+
const sampleRows = data.rows.slice(0, 20);
|
|
339
|
+
return data.columns.map((col, i) => {
|
|
340
|
+
const values = sampleRows.map(r => r[i]).filter(v => v !== null && v !== undefined && String(v).trim() !== '');
|
|
341
|
+
if (values.length === 0) return { name: col, index: i, type: 'categorical' };
|
|
342
|
+
const dateCount = values.filter(v => /^\d{4}-\d{2}-\d{2}/.test(String(v))).length;
|
|
343
|
+
if (dateCount / values.length > 0.7) return { name: col, index: i, type: 'date' };
|
|
344
|
+
const numCount = values.filter(v => !isNaN(Number(v))).length;
|
|
345
|
+
if (numCount / values.length > 0.7) return { name: col, index: i, type: 'numeric' };
|
|
346
|
+
return { name: col, index: i, type: 'categorical' };
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function isChartable(classified) {
|
|
351
|
+
if (classified.length < 2) return false;
|
|
352
|
+
return classified.some(c => c.type === 'numeric');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function autoDetectChart(classified, data) {
|
|
356
|
+
const dates = classified.filter(c => c.type === 'date');
|
|
357
|
+
const nums = classified.filter(c => c.type === 'numeric');
|
|
358
|
+
const cats = classified.filter(c => c.type === 'categorical');
|
|
359
|
+
|
|
360
|
+
let type = 'bar', xCol = classified[0], yCol = nums[0];
|
|
361
|
+
|
|
362
|
+
if (dates.length > 0 && nums.length > 0) {
|
|
363
|
+
type = 'line'; xCol = dates[0]; yCol = nums[0];
|
|
364
|
+
} else if (cats.length > 0 && nums.length > 0) {
|
|
365
|
+
const uniqueLabels = new Set(data.rows.map(r => r[cats[0].index]));
|
|
366
|
+
if (uniqueLabels.size <= 10) { type = 'pie'; xCol = cats[0]; yCol = nums[0]; }
|
|
367
|
+
else { type = 'bar'; xCol = cats[0]; yCol = nums[0]; }
|
|
368
|
+
} else if (nums.length >= 2 && cats.length === 0 && dates.length === 0) {
|
|
369
|
+
type = 'scatter'; xCol = nums[0]; yCol = nums[1];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { type, xIndex: xCol.index, yIndex: yCol.index };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── Chart Rendering ──
|
|
376
|
+
function destroyChart() {
|
|
377
|
+
if (chartInstance) { chartInstance.destroy(); chartInstance = null; }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function renderChart(data, chartType, xIndex, yIndex) {
|
|
381
|
+
destroyChart();
|
|
382
|
+
const container = document.getElementById('ql-chart-container');
|
|
383
|
+
if (!container) return;
|
|
384
|
+
container.innerHTML = '<canvas id="ql-chart-canvas"></canvas>';
|
|
385
|
+
const canvas = document.getElementById('ql-chart-canvas');
|
|
386
|
+
const ctx = canvas.getContext('2d');
|
|
387
|
+
|
|
388
|
+
const labels = data.rows.map(r => r[xIndex] === null ? '' : String(r[xIndex]));
|
|
389
|
+
const values = data.rows.map(r => r[yIndex] === null ? null : Number(r[yIndex]));
|
|
390
|
+
const xLabel = data.columns[xIndex];
|
|
391
|
+
const yLabel = data.columns[yIndex];
|
|
392
|
+
|
|
393
|
+
const baseOpts = {
|
|
394
|
+
responsive: true, maintainAspectRatio: false,
|
|
395
|
+
plugins: {
|
|
396
|
+
legend: { labels: { color: '#a1a1aa', font: { size: 12 } } },
|
|
397
|
+
tooltip: { backgroundColor: '#27272a', titleColor: '#e4e4e7', bodyColor: '#d4d4d8', borderColor: '#3f3f46', borderWidth: 1 }
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
let config;
|
|
402
|
+
|
|
403
|
+
if (chartType === 'pie') {
|
|
404
|
+
config = {
|
|
405
|
+
type: 'pie',
|
|
406
|
+
data: {
|
|
407
|
+
labels: labels,
|
|
408
|
+
datasets: [{ data: values, backgroundColor: CHART_COLORS.slice(0, labels.length), borderColor: '#0f0f12', borderWidth: 2 }]
|
|
409
|
+
},
|
|
410
|
+
options: { ...baseOpts, plugins: { ...baseOpts.plugins, legend: { position: 'right', labels: { color: '#a1a1aa', font: { size: 11 }, padding: 12 } } } }
|
|
411
|
+
};
|
|
412
|
+
} else if (chartType === 'scatter') {
|
|
413
|
+
const points = data.rows.map(r => ({
|
|
414
|
+
x: r[xIndex] === null ? null : Number(r[xIndex]),
|
|
415
|
+
y: r[yIndex] === null ? null : Number(r[yIndex])
|
|
416
|
+
})).filter(p => p.x !== null && p.y !== null);
|
|
417
|
+
config = {
|
|
418
|
+
type: 'scatter',
|
|
419
|
+
data: { datasets: [{ data: points, backgroundColor: CHART_COLORS[0], borderColor: CHART_COLORS[0], pointRadius: 4 }] },
|
|
420
|
+
options: {
|
|
421
|
+
...baseOpts,
|
|
422
|
+
plugins: { ...baseOpts.plugins, legend: { display: false } },
|
|
423
|
+
scales: {
|
|
424
|
+
x: { title: { display: true, text: xLabel, color: '#a1a1aa' }, ticks: { color: '#71717a' }, grid: { color: '#1c1c21' } },
|
|
425
|
+
y: { title: { display: true, text: yLabel, color: '#a1a1aa' }, ticks: { color: '#71717a' }, grid: { color: '#1c1c21' } }
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
} else {
|
|
430
|
+
// bar or line
|
|
431
|
+
config = {
|
|
432
|
+
type: chartType,
|
|
433
|
+
data: {
|
|
434
|
+
labels: labels,
|
|
435
|
+
datasets: [{
|
|
436
|
+
label: yLabel,
|
|
437
|
+
data: values,
|
|
438
|
+
backgroundColor: chartType === 'line' ? 'rgba(129,140,248,0.15)' : CHART_COLORS[0],
|
|
439
|
+
borderColor: CHART_COLORS[0],
|
|
440
|
+
borderWidth: chartType === 'line' ? 2 : 0,
|
|
441
|
+
fill: chartType === 'line',
|
|
442
|
+
tension: 0.3,
|
|
443
|
+
pointBackgroundColor: CHART_COLORS[0],
|
|
444
|
+
pointRadius: chartType === 'line' ? 3 : 0,
|
|
445
|
+
borderRadius: chartType === 'bar' ? 4 : 0
|
|
446
|
+
}]
|
|
447
|
+
},
|
|
448
|
+
options: {
|
|
449
|
+
...baseOpts,
|
|
450
|
+
plugins: { ...baseOpts.plugins, legend: { display: false } },
|
|
451
|
+
scales: {
|
|
452
|
+
x: { ticks: { color: '#71717a', maxRotation: 45, font: { size: 11 } }, grid: { color: '#1c1c21' } },
|
|
453
|
+
y: { ticks: { color: '#71717a', font: { size: 11 } }, grid: { color: '#1c1c21' }, beginAtZero: true }
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
chartInstance = new Chart(ctx, config);
|
|
460
|
+
}
|
|
461
|
+
|
|
327
462
|
// ── Render Results ──
|
|
328
463
|
function renderResults(data) {
|
|
464
|
+
destroyChart();
|
|
465
|
+
lastResultData = null;
|
|
466
|
+
|
|
329
467
|
if (!data.columns || !data.columns.length) {
|
|
330
468
|
el.results.innerHTML = '<div class="ql-results-empty"><span>Query returned no results</span></div>';
|
|
331
469
|
el.rowCount.textContent = '';
|
|
470
|
+
// Remove chart toggle if present
|
|
471
|
+
const oldToggle = document.querySelector('.ql-chart-toggle');
|
|
472
|
+
if (oldToggle) oldToggle.remove();
|
|
332
473
|
return;
|
|
333
474
|
}
|
|
334
475
|
|
|
335
|
-
|
|
336
|
-
|
|
476
|
+
// Build table HTML
|
|
477
|
+
let tableHtml = '';
|
|
478
|
+
if (data.truncated) tableHtml += '<div class="ql-truncated">Results truncated to ' + data.row_count + ' rows</div>';
|
|
337
479
|
|
|
338
|
-
|
|
339
|
-
data.columns.forEach(c => {
|
|
340
|
-
|
|
480
|
+
tableHtml += '<table class="ql-table"><thead><tr>';
|
|
481
|
+
data.columns.forEach(c => { tableHtml += '<th>' + esc(c) + '</th>'; });
|
|
482
|
+
tableHtml += '</tr></thead><tbody>';
|
|
341
483
|
|
|
342
484
|
data.rows.forEach(row => {
|
|
343
|
-
|
|
485
|
+
tableHtml += '<tr>';
|
|
344
486
|
row.forEach(cell => {
|
|
345
|
-
if (cell === null)
|
|
346
|
-
else
|
|
487
|
+
if (cell === null) tableHtml += '<td><span class="ql-null">NULL</span></td>';
|
|
488
|
+
else tableHtml += '<td>' + esc(String(cell)) + '</td>';
|
|
347
489
|
});
|
|
348
|
-
|
|
490
|
+
tableHtml += '</tr>';
|
|
349
491
|
});
|
|
492
|
+
tableHtml += '</tbody></table>';
|
|
493
|
+
|
|
494
|
+
// Check if chartable
|
|
495
|
+
const classified = classifyColumns(data);
|
|
496
|
+
const chartable = isChartable(classified);
|
|
497
|
+
|
|
498
|
+
// Build results area: table wrapper + chart container (hidden)
|
|
499
|
+
let resultsHtml = '<div id="ql-table-wrap">' + tableHtml + '</div>';
|
|
500
|
+
if (chartable) {
|
|
501
|
+
resultsHtml += '<div id="ql-chart-controls" class="ql-chart-controls" style="display:none;">';
|
|
502
|
+
resultsHtml += '<div class="ql-chart-control-group"><span class="ql-chart-label">Type</span>';
|
|
503
|
+
resultsHtml += '<select class="ql-chart-select" id="ql-chart-type-select"><option value="bar">Bar</option><option value="line">Line</option><option value="pie">Pie</option><option value="scatter">Scatter</option></select></div>';
|
|
504
|
+
resultsHtml += '<div class="ql-chart-control-group"><span class="ql-chart-label">X Axis</span>';
|
|
505
|
+
resultsHtml += '<select class="ql-chart-select" id="ql-chart-x-select">';
|
|
506
|
+
data.columns.forEach((c, i) => { resultsHtml += '<option value="' + i + '">' + esc(c) + '</option>'; });
|
|
507
|
+
resultsHtml += '</select></div>';
|
|
508
|
+
resultsHtml += '<div class="ql-chart-control-group"><span class="ql-chart-label">Y Axis</span>';
|
|
509
|
+
resultsHtml += '<select class="ql-chart-select" id="ql-chart-y-select">';
|
|
510
|
+
data.columns.forEach((c, i) => { resultsHtml += '<option value="' + i + '">' + esc(c) + '</option>'; });
|
|
511
|
+
resultsHtml += '</select></div>';
|
|
512
|
+
resultsHtml += '</div>';
|
|
513
|
+
resultsHtml += '<div id="ql-chart-container" class="ql-chart-container" style="display:none;"></div>';
|
|
514
|
+
}
|
|
350
515
|
|
|
351
|
-
|
|
352
|
-
el.results.innerHTML = html;
|
|
516
|
+
el.results.innerHTML = resultsHtml;
|
|
353
517
|
el.rowCount.textContent = data.row_count + ' row' + (data.row_count === 1 ? '' : 's') + (data.truncated ? ' (truncated)' : '');
|
|
518
|
+
|
|
519
|
+
// Add or remove toggle in toolbar
|
|
520
|
+
const oldToggle = document.querySelector('.ql-chart-toggle');
|
|
521
|
+
if (oldToggle) oldToggle.remove();
|
|
522
|
+
|
|
523
|
+
if (chartable) {
|
|
524
|
+
lastResultData = data;
|
|
525
|
+
const auto = autoDetectChart(classified, data);
|
|
526
|
+
|
|
527
|
+
// Create toggle in results toolbar
|
|
528
|
+
const toolbar = document.querySelector('.ql-results-toolbar');
|
|
529
|
+
const toggle = document.createElement('div');
|
|
530
|
+
toggle.className = 'ql-chart-toggle';
|
|
531
|
+
toggle.innerHTML = '<button class="ql-chart-toggle-btn active" data-view="table">Table</button>' +
|
|
532
|
+
'<button class="ql-chart-toggle-btn" data-view="chart">Chart</button>';
|
|
533
|
+
toolbar.querySelector('.ql-results-title').after(toggle);
|
|
534
|
+
|
|
535
|
+
// Set auto-detected defaults on dropdowns
|
|
536
|
+
document.getElementById('ql-chart-type-select').value = auto.type;
|
|
537
|
+
document.getElementById('ql-chart-x-select').value = auto.xIndex;
|
|
538
|
+
document.getElementById('ql-chart-y-select').value = auto.yIndex;
|
|
539
|
+
|
|
540
|
+
let chartRendered = false;
|
|
541
|
+
|
|
542
|
+
// Toggle behavior
|
|
543
|
+
toggle.addEventListener('click', (e) => {
|
|
544
|
+
const btn = e.target.closest('.ql-chart-toggle-btn');
|
|
545
|
+
if (!btn) return;
|
|
546
|
+
const view = btn.dataset.view;
|
|
547
|
+
toggle.querySelectorAll('.ql-chart-toggle-btn').forEach(b => b.classList.remove('active'));
|
|
548
|
+
btn.classList.add('active');
|
|
549
|
+
|
|
550
|
+
const tableWrap = document.getElementById('ql-table-wrap');
|
|
551
|
+
const chartControls = document.getElementById('ql-chart-controls');
|
|
552
|
+
const chartContainer = document.getElementById('ql-chart-container');
|
|
553
|
+
|
|
554
|
+
if (view === 'table') {
|
|
555
|
+
if (tableWrap) tableWrap.style.display = '';
|
|
556
|
+
if (chartControls) chartControls.style.display = 'none';
|
|
557
|
+
if (chartContainer) chartContainer.style.display = 'none';
|
|
558
|
+
} else {
|
|
559
|
+
if (tableWrap) tableWrap.style.display = 'none';
|
|
560
|
+
if (chartControls) chartControls.style.display = 'flex';
|
|
561
|
+
if (chartContainer) chartContainer.style.display = 'flex';
|
|
562
|
+
if (!chartRendered) {
|
|
563
|
+
renderChart(data,
|
|
564
|
+
document.getElementById('ql-chart-type-select').value,
|
|
565
|
+
parseInt(document.getElementById('ql-chart-x-select').value, 10),
|
|
566
|
+
parseInt(document.getElementById('ql-chart-y-select').value, 10)
|
|
567
|
+
);
|
|
568
|
+
chartRendered = true;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Re-render chart on control changes
|
|
574
|
+
const rerender = () => {
|
|
575
|
+
if (!lastResultData) return;
|
|
576
|
+
chartRendered = true;
|
|
577
|
+
renderChart(lastResultData,
|
|
578
|
+
document.getElementById('ql-chart-type-select').value,
|
|
579
|
+
parseInt(document.getElementById('ql-chart-x-select').value, 10),
|
|
580
|
+
parseInt(document.getElementById('ql-chart-y-select').value, 10)
|
|
581
|
+
);
|
|
582
|
+
};
|
|
583
|
+
document.getElementById('ql-chart-type-select').addEventListener('change', rerender);
|
|
584
|
+
document.getElementById('ql-chart-x-select').addEventListener('change', rerender);
|
|
585
|
+
document.getElementById('ql-chart-y-select').addEventListener('change', rerender);
|
|
586
|
+
}
|
|
354
587
|
}
|
|
355
588
|
|
|
356
589
|
// ── Messages ──
|
|
@@ -533,7 +766,11 @@
|
|
|
533
766
|
// Restore SQL editor
|
|
534
767
|
el.sql.value = data.last_sql || '';
|
|
535
768
|
|
|
536
|
-
// Clear results
|
|
769
|
+
// Clear results and chart
|
|
770
|
+
destroyChart();
|
|
771
|
+
lastResultData = null;
|
|
772
|
+
const oldToggle = document.querySelector('.ql-chart-toggle');
|
|
773
|
+
if (oldToggle) oldToggle.remove();
|
|
537
774
|
el.results.innerHTML = '<div class="ql-results-empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 0 1-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0 1 12 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125m19.5 0v1.5c0 .621-.504 1.125-1.125 1.125M2.25 5.625v1.5c0 .621.504 1.125 1.125 1.125m0 0h17.25m-17.25 0h7.5c.621 0 1.125.504 1.125 1.125M3.375 8.25c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125m17.25-3.75h-7.5c-.621 0-1.125.504-1.125 1.125m8.625-1.125c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125M12 10.875v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 10.875c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125M13.125 12h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125M20.625 12c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5M12 14.625v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 14.625c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125m0 0v.375"/></svg><span>Run a query to see results</span></div>';
|
|
538
775
|
el.rowCount.textContent = '';
|
|
539
776
|
|
|
@@ -544,6 +781,10 @@
|
|
|
544
781
|
function startNewChat() {
|
|
545
782
|
currentConversationId = null;
|
|
546
783
|
conversation = [];
|
|
784
|
+
destroyChart();
|
|
785
|
+
lastResultData = null;
|
|
786
|
+
const oldToggle = document.querySelector('.ql-chart-toggle');
|
|
787
|
+
if (oldToggle) oldToggle.remove();
|
|
547
788
|
el.messages.innerHTML = '<div class="ql-empty-state" id="ql-empty-state"><div class="ql-empty-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 6v12m-3-2.818.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/></svg></div><div class="ql-empty-title">Ask about your data</div><div class="ql-empty-text">Try something like:<br>"How many users signed up this month?"<br>"What\'s the total revenue by plan?"</div></div>';
|
|
548
789
|
el.sql.value = '';
|
|
549
790
|
el.results.innerHTML = '<div class="ql-results-empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 0 1-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0 1 12 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125m19.5 0v1.5c0 .621-.504 1.125-1.125 1.125M2.25 5.625v1.5c0 .621.504 1.125 1.125 1.125m0 0h17.25m-17.25 0h7.5c.621 0 1.125.504 1.125 1.125M3.375 8.25c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125m17.25-3.75h-7.5c-.621 0-1.125.504-1.125 1.125m8.625-1.125c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125M12 10.875v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 10.875c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125M13.125 12h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125M20.625 12c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5M12 14.625v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 14.625c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125m0 0v.375"/></svg><span>Run a query to see results</span></div>';
|
data/lib/query_lens/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: query_lens
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bryan Beshore
|
|
@@ -114,13 +114,13 @@ files:
|
|
|
114
114
|
- lib/query_lens/configuration.rb
|
|
115
115
|
- lib/query_lens/engine.rb
|
|
116
116
|
- lib/query_lens/version.rb
|
|
117
|
-
homepage: https://
|
|
117
|
+
homepage: https://querylens.ai
|
|
118
118
|
licenses:
|
|
119
119
|
- MIT
|
|
120
120
|
metadata:
|
|
121
|
-
homepage_uri: https://
|
|
121
|
+
homepage_uri: https://querylens.ai
|
|
122
122
|
source_code_uri: https://github.com/bryanbeshore/query_lens
|
|
123
|
-
changelog_uri: https://
|
|
123
|
+
changelog_uri: https://querylens.ai/blob/main/CHANGELOG.md
|
|
124
124
|
rdoc_options: []
|
|
125
125
|
require_paths:
|
|
126
126
|
- lib
|