up
This commit is contained in:
@@ -48,10 +48,10 @@
|
||||
<thead class="border-b border-gray-200 text-left text-xs font-semibold text-gray-500 uppercase">
|
||||
<tr>
|
||||
<th class="py-2 px-3">Nombre</th>
|
||||
<th class="py-2 px-3">Módulo</th>
|
||||
<th class="py-2 px-3">Provider</th>
|
||||
<th class="py-2 px-3">Modelo</th>
|
||||
<th class="py-2 px-3">API Key</th>
|
||||
<th class="py-2 px-3">Base URL</th>
|
||||
<th class="py-2 px-3">Estado</th>
|
||||
<th class="py-2 px-3 text-right">Acciones</th>
|
||||
</tr>
|
||||
@@ -63,6 +63,16 @@
|
||||
<template x-for="item in items" :key="item.ID">
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="py-2 px-3 font-medium" x-text="item.nombre"></td>
|
||||
<td class="py-2 px-3">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold"
|
||||
:class="{
|
||||
'bg-blue-100 text-blue-700': item.modulo === 'landing',
|
||||
'bg-[#e9f0cf] text-[#5a7a1e]': item.modulo === 'query_runner',
|
||||
'bg-gray-100 text-gray-500': !item.modulo
|
||||
}"
|
||||
x-text="item.modulo === 'landing' ? 'Landing' : item.modulo === 'query_runner' ? 'Query Runner' : 'Global'">
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2 px-3">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold"
|
||||
:class="{
|
||||
@@ -74,7 +84,6 @@
|
||||
</td>
|
||||
<td class="py-2 px-3 text-gray-500 font-mono text-xs" x-text="item.model_name || '—'"></td>
|
||||
<td class="py-2 px-3 font-mono text-xs text-gray-400" x-text="item.api_key_hint"></td>
|
||||
<td class="py-2 px-3 text-xs text-gray-400" x-text="item.base_url || 'default'"></td>
|
||||
<td class="py-2 px-3">
|
||||
<span :class="item.is_active ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'"
|
||||
class="px-2 py-0.5 rounded-full text-xs font-semibold"
|
||||
@@ -161,7 +170,20 @@
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input x-model="form.is_active" type="checkbox" id="ai_is_active" class="rounded" />
|
||||
<label for="ai_is_active" class="text-sm text-gray-700">Activo (usado por Landing Generator)</label>
|
||||
<label for="ai_is_active" class="text-sm text-gray-700">Activo</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Módulo / Servicio</label>
|
||||
<select x-model="form.modulo"
|
||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
|
||||
<option value="">Global (usado por todos como fallback)</option>
|
||||
<option value="landing">Landing Generator</option>
|
||||
<option value="query_runner">Query Runner SQL</option>
|
||||
</select>
|
||||
<p class="text-[10px] text-gray-400 mt-1">
|
||||
Asigna esta IA a un servicio específico. "Global" actúa de fallback cuando un servicio no tiene IA propia.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -209,7 +231,7 @@ function aiConfigApp() {
|
||||
search: '',
|
||||
showModal: false, editItem: null, deleteId: null,
|
||||
errorMsg: '', successMsg: '', formError: '',
|
||||
form: { nombre: '', provider: '', api_key: '', base_url: '', model_name: '', is_active: true, notes: '' },
|
||||
form: { nombre: '', provider: '', api_key: '', base_url: '', model_name: '', is_active: true, notes: '', modulo: '' },
|
||||
|
||||
async init() { await this.load() },
|
||||
|
||||
@@ -229,14 +251,14 @@ function aiConfigApp() {
|
||||
|
||||
openAdd() {
|
||||
this.editItem = null
|
||||
this.form = { nombre: '', provider: 'qwen', api_key: '', base_url: '', model_name: 'qwen2.5-72b-instruct', is_active: true, notes: '' }
|
||||
this.form = { nombre: '', provider: 'qwen', api_key: '', base_url: '', model_name: 'qwen2.5-72b-instruct', is_active: true, notes: '', modulo: '' }
|
||||
this.formError = ''
|
||||
this.showModal = true
|
||||
},
|
||||
|
||||
openEdit(item) {
|
||||
this.editItem = item
|
||||
this.form = { nombre: item.nombre, provider: item.provider, api_key: '', base_url: item.base_url, model_name: item.model_name, is_active: item.is_active, notes: item.notes }
|
||||
this.form = { nombre: item.nombre, provider: item.provider, api_key: '', base_url: item.base_url, model_name: item.model_name, is_active: item.is_active, notes: item.notes, modulo: item.modulo || '' }
|
||||
this.formError = ''
|
||||
this.showModal = true
|
||||
},
|
||||
|
||||
@@ -181,6 +181,108 @@
|
||||
<div class="flex-1"></div>
|
||||
<span x-show="selectedDb" class="text-xs text-[#4ade80] font-mono" x-text="selectedDb"></span>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<textarea id="sql-editor" x-model="sqlText"
|
||||
@input.debounce.150ms="onSqlInput()"
|
||||
@keydown="onSqlKeydown($event)"
|
||||
@keydown.ctrl.enter.prevent="runQuery()"
|
||||
@keydown.meta.enter.prevent="runQuery()"
|
||||
@scroll="closeAutocomplete()"
|
||||
@click="closeAutocomplete()"
|
||||
:placeholder="isMongo ? 'db.coleccion.find({})\n// Ctrl+Enter para ejecutar\n// db.coleccion.insertOne({campo: \'valor\'})' : isRedis ? '# Comandos Redis (uno por línea)\nKEYS *\nGET mi_clave\nHGETALL mi_hash\n# Ctrl+Enter para ejecutar' : '-- Escribe tu consulta SQL aquí\n-- Ctrl+Enter para ejecutar\nSELECT * FROM tabla LIMIT 100;'"
|
||||
class="w-full bg-gray-900 text-gray-100 font-mono text-sm p-4 resize-none outline-none leading-relaxed"
|
||||
style="height:180px; tab-size:2;"></textarea>
|
||||
<!-- Autocomplete dropdown -->
|
||||
<div x-show="autocompleteVisible" x-cloak
|
||||
:style="'position:absolute;left:'+autocompletePos.left+'px;top:'+autocompletePos.top+'px;z-index:100;'"
|
||||
class="bg-white border border-gray-300 rounded-lg shadow-xl max-h-48 overflow-y-auto min-w-[180px]">
|
||||
<template x-for="(item, i) in autocompleteItems" :key="i">
|
||||
<div @mousedown.prevent="selectAutocomplete(i)"
|
||||
class="px-3 py-1.5 text-xs font-mono cursor-pointer flex items-center gap-2"
|
||||
:class="i === autocompleteIdx ? 'bg-[#8eb02f] text-white' : 'hover:bg-gray-100 text-gray-700'">
|
||||
<!-- Icono por tipo -->
|
||||
<span class="text-[9px] shrink-0 w-12 truncate opacity-60 font-sans"
|
||||
:class="{
|
||||
'text-blue-500': item.type === 'keyword',
|
||||
'text-amber-600': item.type === 'table',
|
||||
'text-green-600': item.type.startsWith('col')
|
||||
}"
|
||||
x-text="item.type === 'keyword' ? 'KW' : item.type === 'table' ? 'tabla' : item.type.replace('col·','')">
|
||||
</span>
|
||||
<span class="flex-1 truncate" x-text="item.label"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- Typo suggestions -->
|
||||
<div x-show="typoSuggestions.length > 0" x-cloak
|
||||
class="absolute bottom-0 left-0 right-0 px-3 py-1.5 bg-yellow-900 bg-opacity-90 flex flex-wrap gap-1.5 items-center">
|
||||
<span class="text-yellow-300 text-[10px] shrink-0">¿Quisiste decir?</span>
|
||||
<template x-for="(s, i) in typoSuggestions" :key="i">
|
||||
<button @mousedown.prevent="applyTypoFix(s)"
|
||||
class="text-[10px] px-2 py-0.5 rounded bg-yellow-400 text-yellow-900 hover:bg-yellow-300 transition font-mono font-semibold">
|
||||
<span x-text="s"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ BARRA NL2SQL (cool) ═══ -->
|
||||
<div x-show="!isMongo && !isRedis && selectedConxId" class="mt-2 rounded-xl overflow-hidden"
|
||||
style="background: linear-gradient(135deg, #1e1b4b 0%, #312e81 40%, #1e3a5f 100%); border: 1px solid #4c1d95">
|
||||
<div class="flex items-center gap-2 px-3 py-2">
|
||||
<!-- Icono mágico animado -->
|
||||
<div class="shrink-0 flex items-center justify-center w-7 h-7 rounded-lg"
|
||||
style="background: linear-gradient(135deg,#7c3aed,#2563eb)"
|
||||
:class="nl2sqlLoading ? 'animate-pulse' : ''">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<input id="nl2sql-input" x-model="nl2sqlText"
|
||||
@keydown.enter="generateNL2SQL()"
|
||||
:disabled="nl2sqlLoading"
|
||||
type="text"
|
||||
placeholder="Describe en español lo que quieres consultar... ej: trae los usuarios activos creados este mes"
|
||||
class="flex-1 bg-transparent text-white text-xs placeholder-indigo-300 outline-none font-sans">
|
||||
<button @click="generateNL2SQL()" :disabled="nl2sqlLoading || !nl2sqlText.trim()"
|
||||
class="shrink-0 flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition disabled:opacity-40"
|
||||
style="background: linear-gradient(135deg,#7c3aed,#2563eb); color:white"
|
||||
onmouseover="this.style.opacity='0.85'"
|
||||
onmouseout="this.style.opacity='1'">
|
||||
<svg x-show="!nl2sqlLoading" class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<svg x-show="nl2sqlLoading" class="w-3.5 h-3.5 animate-spin" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
<span x-text="nl2sqlLoading ? 'Generando…' : 'Generar SQL'"></span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Respuesta inline -->
|
||||
<div x-show="nl2sqlResult" x-cloak class="px-3 pb-2">
|
||||
<div class="rounded-lg overflow-hidden border border-indigo-600">
|
||||
<div class="flex items-center justify-between px-3 py-1.5 bg-indigo-900 bg-opacity-60">
|
||||
<span class="text-[10px] text-indigo-200 font-semibold">SQL generado</span>
|
||||
<div class="flex gap-1.5">
|
||||
<button @click="applyNL2SQL()"
|
||||
class="text-[10px] px-2 py-0.5 rounded font-semibold transition"
|
||||
style="background:#8eb02f;color:white"
|
||||
onmouseover="this.style.background='#6d8c24'"
|
||||
onmouseout="this.style.background='#8eb02f'">
|
||||
Aplicar al editor
|
||||
</button>
|
||||
<button @click="nl2sqlResult=''"
|
||||
class="text-[10px] px-1.5 text-indigo-300 hover:text-white transition">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="px-3 py-2 text-xs font-mono text-green-300 bg-gray-950 whitespace-pre-wrap max-h-24 overflow-y-auto" x-text="nl2sqlResult"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<textarea id="sql-editor" x-model="sqlText"
|
||||
@input.debounce.150ms="onSqlInput()"
|
||||
@@ -679,10 +781,13 @@ document.addEventListener('alpine:init', () => {
|
||||
editValue: '',
|
||||
editSaving: false,
|
||||
|
||||
// ── NL2SQL ──
|
||||
nl2sqlText: '',
|
||||
nl2sqlLoading: false,
|
||||
nl2sqlResult: '',
|
||||
|
||||
// ── Typo detection ──
|
||||
typoSuggestions: [],
|
||||
|
||||
// ── AI ──
|
||||
aiLoading: false,
|
||||
aiModalOpen: false,
|
||||
aiModalSql: '',
|
||||
@@ -763,7 +868,7 @@ document.addEventListener('alpine:init', () => {
|
||||
},
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// AUTOCOMPLETE
|
||||
// AUTOCOMPLETE — con contexto WHERE/SET/ON/HAVING
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
async onSqlInput() {
|
||||
if (!this.sqlText || this.isMongo || this.isRedis) {
|
||||
@@ -774,71 +879,127 @@ document.addEventListener('alpine:init', () => {
|
||||
const ta = document.getElementById('sql-editor');
|
||||
if (!ta) return;
|
||||
const cursorPos = ta.selectionStart;
|
||||
const text = this.sqlText.substring(0, cursorPos);
|
||||
const words = text.split(/[\s,;()\n]+/);
|
||||
const currentWord = words[words.length - 1] || '';
|
||||
const fullText = this.sqlText;
|
||||
const textBefore = this.sqlText.substring(0, cursorPos);
|
||||
const wordMatch = textBefore.match(/[\w.]+$/);
|
||||
const currentWord = wordMatch ? wordMatch[0] : '';
|
||||
|
||||
// ── Autocomplete ──
|
||||
if (currentWord.length >= 1) {
|
||||
const upper = currentWord.toUpperCase();
|
||||
const suggestions = [];
|
||||
// Prefetch columnas de TODAS las tablas de la query en paralelo
|
||||
this.prefetchTableColumns(this.sqlText);
|
||||
|
||||
// Keywords
|
||||
for (const kw of this.sqlKeywords) {
|
||||
if (kw.startsWith(upper) && kw !== upper) {
|
||||
suggestions.push({ label: kw, type: 'keyword' });
|
||||
}
|
||||
}
|
||||
if (currentWord.length < 1) {
|
||||
this.closeAutocomplete();
|
||||
this.typoSuggestions = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Table names
|
||||
for (const t of this.tables) {
|
||||
const tLower = t.toLowerCase();
|
||||
const cLower = currentWord.toLowerCase();
|
||||
if (tLower.startsWith(cLower) && t !== currentWord) {
|
||||
suggestions.push({ label: t, type: 'table' });
|
||||
}
|
||||
}
|
||||
const upper = currentWord.toUpperCase();
|
||||
|
||||
// Column names from loaded columns
|
||||
const tableContext = this.detectTableContext(fullText, cursorPos);
|
||||
if (tableContext && this.columnCache[tableContext]) {
|
||||
for (const col of this.columnCache[tableContext]) {
|
||||
// ── Detectar contexto del cursor ──
|
||||
// Contexto columna: cursor después de WHERE, AND, OR, ON, SET, HAVING, SELECT campo
|
||||
const contextUpper = textBefore.replace(/\s+/g, ' ').toUpperCase();
|
||||
const columnContextRe = /(?:WHERE|AND|OR|HAVING|ON|SET|,)\s+[\w.]*$/;
|
||||
const selectContextRe = /SELECT\s+(?:[\w.,\s]+,\s*)?[\w.]*$/;
|
||||
const isColumnContext = columnContextRe.test(contextUpper) || selectContextRe.test(contextUpper);
|
||||
|
||||
const suggestions = [];
|
||||
|
||||
if (isColumnContext) {
|
||||
// Prioridad 1: columnas de TODAS las tablas en el query
|
||||
const allTables = this.extractAllTablesFromQuery(this.sqlText);
|
||||
for (const tableName of allTables) {
|
||||
const cols = this.columnCache[tableName] || [];
|
||||
for (const col of cols) {
|
||||
const colLower = col.name.toLowerCase();
|
||||
const cLower2 = currentWord.toLowerCase();
|
||||
if (colLower.startsWith(cLower2) && col.name !== currentWord) {
|
||||
suggestions.push({ label: col.name, type: 'col·' + (col.type || '').substring(0, 12) });
|
||||
if (colLower.startsWith(currentWord.toLowerCase()) && col.name !== currentWord) {
|
||||
suggestions.push({
|
||||
label: col.name,
|
||||
type: 'col·' + (col.type || '').substring(0, 10),
|
||||
priority: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
this.autocompleteItems = suggestions.slice(0, 12);
|
||||
this.autocompleteIdx = 0;
|
||||
this.autocompleteVisible = true;
|
||||
// Position below cursor
|
||||
const lineHeight = 24;
|
||||
const charWidth = 8.4;
|
||||
const lines = text.split('\n');
|
||||
const currentLine = lines.length;
|
||||
const currentCol = lines[lines.length - 1].length;
|
||||
this.autocompletePos = {
|
||||
left: Math.min(currentCol * charWidth, 600),
|
||||
top: currentLine * lineHeight + 4
|
||||
};
|
||||
} else {
|
||||
this.closeAutocomplete();
|
||||
// Prioridad 2: también keywords que aplican en este contexto
|
||||
const whereKws = ['IS', 'NOT', 'NULL', 'IN', 'LIKE', 'ILIKE', 'BETWEEN', 'AND', 'OR', 'EXISTS', 'TRUE', 'FALSE'];
|
||||
for (const kw of whereKws) {
|
||||
if (kw.startsWith(upper) && kw !== upper) {
|
||||
suggestions.push({ label: kw, type: 'keyword', priority: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Typo detection ──
|
||||
this.detectTypos(currentWord);
|
||||
} else {
|
||||
this.closeAutocomplete();
|
||||
this.typoSuggestions = [];
|
||||
// Contexto general: keywords primero
|
||||
for (const kw of this.sqlKeywords) {
|
||||
if (kw.startsWith(upper) && kw !== upper) {
|
||||
suggestions.push({ label: kw, type: 'keyword', priority: 0 });
|
||||
}
|
||||
}
|
||||
// Luego tablas (contexto FROM / JOIN / UPDATE / INTO)
|
||||
const tableContextRe = /(?:FROM|JOIN|UPDATE|INTO|TABLE)\s+[\w.]*$/i;
|
||||
if (tableContextRe.test(textBefore)) {
|
||||
for (const t of this.tables) {
|
||||
if (t.toLowerCase().startsWith(currentWord.toLowerCase()) && t !== currentWord) {
|
||||
suggestions.push({ label: t, type: 'table', priority: 0 });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fuera de contexto claro: tablas de menor prioridad
|
||||
for (const t of this.tables) {
|
||||
if (t.toLowerCase().startsWith(currentWord.toLowerCase()) && t !== currentWord) {
|
||||
suggestions.push({ label: t, type: 'table', priority: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Columnas de tabla detectada en contexto
|
||||
const tableCtx = this.detectTableContext(this.sqlText, cursorPos);
|
||||
if (tableCtx && this.columnCache[tableCtx]) {
|
||||
for (const col of this.columnCache[tableCtx]) {
|
||||
if (col.name.toLowerCase().startsWith(currentWord.toLowerCase()) && col.name !== currentWord) {
|
||||
suggestions.push({ label: col.name, type: 'col·' + (col.type || '').substring(0, 10), priority: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch columns for tables mentioned in the query
|
||||
this.prefetchTableColumns(fullText);
|
||||
// Deduplicar y limitar
|
||||
const seen = new Set();
|
||||
const unique = suggestions.filter(s => {
|
||||
if (seen.has(s.label)) return false;
|
||||
seen.add(s.label);
|
||||
return true;
|
||||
}).slice(0, 14);
|
||||
|
||||
if (unique.length > 0) {
|
||||
this.autocompleteItems = unique;
|
||||
this.autocompleteIdx = 0;
|
||||
this.autocompleteVisible = true;
|
||||
// Calcular posición debajo del cursor
|
||||
const lineHeight = 24;
|
||||
const charWidth = 8.4;
|
||||
const lines = textBefore.split('\n');
|
||||
const lineNum = lines.length;
|
||||
const colNum = lines[lines.length - 1].length;
|
||||
this.autocompletePos = {
|
||||
left: Math.min(colNum * charWidth, 560),
|
||||
top: lineNum * lineHeight + 4
|
||||
};
|
||||
} else {
|
||||
this.closeAutocomplete();
|
||||
}
|
||||
|
||||
// Typo detection
|
||||
this.detectTypos(currentWord);
|
||||
},
|
||||
|
||||
// Extrae TODAS las tablas mencionadas en FROM, JOIN, UPDATE, INTO del query
|
||||
extractAllTablesFromQuery(sql) {
|
||||
const found = [];
|
||||
const re = /(?:FROM|JOIN|UPDATE|INTO|TABLE)\s+`?(\w[\w\d_]*)`?/gi;
|
||||
let m;
|
||||
while ((m = re.exec(sql)) !== null) {
|
||||
const t = m[1];
|
||||
if (!found.includes(t) && this.tables.includes(t)) found.push(t);
|
||||
}
|
||||
return found;
|
||||
},
|
||||
|
||||
onSqlKeydown(e) {
|
||||
@@ -1129,6 +1290,64 @@ document.addEventListener('alpine:init', () => {
|
||||
}
|
||||
},
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// NL2SQL — Lenguaje natural a SQL
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
async generateNL2SQL() {
|
||||
if (!this.nl2sqlText.trim() || this.nl2sqlLoading) return;
|
||||
this.nl2sqlLoading = true;
|
||||
this.nl2sqlResult = '';
|
||||
|
||||
// Construir schema de tablas disponibles para dar contexto a la IA
|
||||
const schemaPayload = [];
|
||||
for (const tableName of this.tables) {
|
||||
const cols = this.columnCache[tableName] || [];
|
||||
schemaPayload.push({
|
||||
table: tableName,
|
||||
columns: cols.map(c => ({ name: c.name, type: c.type || '' }))
|
||||
});
|
||||
// Si no tenemos columnas cargadas aún, intentar cargarlas
|
||||
if (cols.length === 0 && this.selectedConxId && this.selectedDb) {
|
||||
try {
|
||||
const res = await axios.get('/app/query-runner/columns', {
|
||||
params: { conx_db_id: this.selectedConxId, db: this.selectedDb, table: tableName }
|
||||
});
|
||||
const loaded = res.data.data || [];
|
||||
this.columnCache[tableName] = loaded;
|
||||
schemaPayload[schemaPayload.length - 1].columns = loaded.map(c => ({ name: c.name, type: c.type || '' }));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios.post('/app/query-runner/nl2sql', {
|
||||
conx_db_id: parseInt(this.selectedConxId) || 0,
|
||||
database: this.selectedDb,
|
||||
text: this.nl2sqlText,
|
||||
schema: schemaPayload
|
||||
});
|
||||
if (res.data.suggestion) {
|
||||
this.nl2sqlResult = res.data.suggestion;
|
||||
} else if (res.data.error) {
|
||||
this.showToast(res.data.error, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e.response?.data?.error || e.message;
|
||||
this.showToast('Error NL2SQL: ' + msg, 'error');
|
||||
}
|
||||
this.nl2sqlLoading = false;
|
||||
},
|
||||
|
||||
applyNL2SQL() {
|
||||
if (this.nl2sqlResult) {
|
||||
this.sqlText = this.nl2sqlResult;
|
||||
this.nl2sqlResult = '';
|
||||
this.nl2sqlText = '';
|
||||
document.getElementById('sql-editor')?.focus();
|
||||
this.showToast('SQL aplicado al editor');
|
||||
}
|
||||
},
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CORE QUERY RUNNER
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user