${entry.content.replace(/
const healthButton = document.getElementById("healthButton"); const ingestButton = document.getElementById("ingestButton"); const cleanupButton = document.getElementById("cleanupButton"); const bootstrapButton = document.getElementById("bootstrapButton"); const replaceBootstrapButton = document.getElementById("replaceBootstrapButton"); const clearBootstrapButton = document.getElementById("clearBootstrapButton"); const sendChatButton = document.getElementById("sendChatButton"); const clearChatButton = document.getElementById("clearChatButton"); const manualLogButton = document.getElementById("manualLogButton"); const presetDocs = document.getElementById("presetDocs"); const presetRagDocs = document.getElementById("presetRagDocs"); const presetCode = document.getElementById("presetCode"); const healthResult = document.getElementById("healthResult"); const ingestResult = document.getElementById("ingestResult"); const cleanupResult = document.getElementById("cleanupResult"); const bootstrapResult = document.getElementById("bootstrapResult"); const mainResult = document.getElementById("mainResult"); const compareResult = document.getElementById("compareResult"); const bootstrapContextResult = document.getElementById("bootstrapContextResult"); const chatMessages = document.getElementById("chatMessages"); const contextIndicator = document.getElementById("contextIndicator"); const contextStatusText = document.getElementById("contextStatusText"); const contextScopeText = document.getElementById("contextScopeText"); const logsResult = document.getElementById("logsResult"); const logCounterValue = document.getElementById("logCounterValue"); const ingestSourceType = document.getElementById("ingestSourceType"); const ingestScopeMode = document.getElementById("ingestScopeMode"); const ingestSourceIdWrapper = document.getElementById("ingestSourceIdWrapper"); const ingestSourceId = document.getElementById("ingestSourceId"); const ingestSourceRef = document.getElementById("ingestSourceRef"); const ingestUploadFile = document.getElementById("ingestUploadFile"); const ingestUploadFolder = document.getElementById("ingestUploadFolder"); const btnUploadFile = document.getElementById("btnUploadFile"); const btnUploadFolder = document.getElementById("btnUploadFolder"); const btnClearUpload = document.getElementById("btnClearUpload"); const uploadStatusText = document.getElementById("uploadStatusText"); const ingestMode = document.getElementById("ingestMode"); const ingestTags = document.getElementById("ingestTags"); const ingestModeHint = document.getElementById("ingestModeHint"); const cleanupScopeSelect = document.getElementById("cleanupScopeSelect"); const cleanupSourceId = document.getElementById("cleanupSourceId"); const cleanupSourceRef = document.getElementById("cleanupSourceRef"); const cleanupTags = document.getElementById("cleanupTags"); const bootstrapQuery = document.getElementById("bootstrapQuery"); const bootstrapMode = document.getElementById("bootstrapMode"); const answerModel = document.getElementById("answerModel"); const useModelInRetrieve = document.getElementById("useModelInRetrieve"); const reuseBootstrapContext = document.getElementById("reuseBootstrapContext"); const allowAdditionalRetrieve = document.getElementById("allowAdditionalRetrieve"); const scopePresetSelect = document.getElementById("scopePresetSelect"); const selectedSourceId = document.getElementById("selectedSourceId"); const scopeSourceRef = document.getElementById("scopeSourceRef"); const scopeTags = document.getElementById("scopeTags"); const scopeEditMode = document.getElementById("scopeEditMode"); const compareWithoutRag = document.getElementById("compareWithoutRag"); const chatMode = document.getElementById("chatMode"); const chatScopeInfo = document.getElementById("chatScopeInfo"); const chatInput = document.getElementById("chatInput"); const manualLogNote = document.getElementById("manualLogNote"); let lastBootstrapContext = null; let lastBootstrapMeta = null; let chatHistory = []; let availableScopes = []; let lastInteraction = null; let currentUploadType = null; // 'file' o 'folder' function format(value) { return JSON.stringify(value, null, 2); } function splitTags(value) { return value.split(",").map((entry) => entry.trim()).filter(Boolean); } function buildScopeLabel(scope) { const modes = scope.chunkModes.join(", ") || "sin modo"; return `${scope.sourceRef} [${modes}]`; } let previousSourceRef = ""; // Para guardar el valor original de SourceRef function updateIngestUiState() { const hasFile = Boolean(ingestUploadFile.files && ingestUploadFile.files.length > 0); const hasFolder = Boolean(ingestUploadFolder.files && ingestUploadFolder.files.length > 0); const hasUpload = hasFile || hasFolder; ingestSourceType.value = hasUpload ? "file" : ingestSourceType.value; ingestSourceType.disabled = hasUpload; ingestSourceRef.disabled = hasUpload; ingestSourceIdWrapper.style.display = ingestScopeMode.value === "custom" ? "block" : "none"; btnClearUpload.style.display = hasUpload ? "block" : "none"; if (hasUpload && !previousSourceRef) { previousSourceRef = ingestSourceRef.value; } if (hasFile) { const fileName = ingestUploadFile.files[0].name; uploadStatusText.textContent = `Archivo seleccionado: ${fileName}`; ingestSourceRef.value = `[Ignorado: se usara nombre local "${fileName}"]`; ingestModeHint.textContent = `Upload directo activo: se ingerira el archivo local "${fileName}" y se ignorara la ruta remota.`; ingestModeHint.classList.add("strong"); } else if (hasFolder) { const firstPath = ingestUploadFolder.files[0].webkitRelativePath || ""; const folderName = firstPath.split('/')[0] || "Carpeta"; uploadStatusText.textContent = `Carpeta seleccionada: ${folderName} (${ingestUploadFolder.files.length} archivos totales, se filtraran ignorados)`; ingestSourceRef.value = `[Ignorado: se usara nombre local "${folderName}"]`; ingestModeHint.textContent = `Upload directo activo: se comprimira y subira la carpeta local "${folderName}" y se ignorara la ruta remota.`; ingestModeHint.classList.add("strong"); } else { uploadStatusText.textContent = "Ningun elemento seleccionado"; ingestModeHint.textContent = "Si seleccionas un archivo o carpeta local, el playground lo subira directamente y podras aislarlo con un `sourceId` propio."; ingestModeHint.classList.remove("strong"); if (previousSourceRef) { ingestSourceRef.value = previousSourceRef; previousSourceRef = ""; } } } function applySelectedScope(scope) { selectedSourceId.value = scope?.sourceId || ""; scopeSourceRef.value = scope?.sourceRef || ""; scopeTags.value = (scope?.tags || []).join(", "); chatScopeInfo.value = [scope?.sourceId, scope?.sourceRef].filter(Boolean).join(" | "); if (scope?.chunkModes?.includes("codigo")) { bootstrapMode.value = "codigo"; chatMode.value = "codigo"; } else if (scope?.chunkModes?.includes("documental")) { bootstrapMode.value = "documental"; chatMode.value = "documental"; } } function applySelectedCleanupScope(scope) { cleanupSourceId.value = scope?.sourceId || ""; cleanupSourceRef.value = scope?.sourceRef || ""; cleanupTags.value = (scope?.tags || []).join(", "); } function updateScopeEditState() { const locked = scopeEditMode.value !== "manual"; scopeSourceRef.readOnly = locked; scopeTags.readOnly = locked; } function request(url, payload, method = "POST") { return fetch(url, { method, headers: { "Content-Type": "application/json" }, body: payload ? JSON.stringify(payload) : undefined }).then(async (response) => { const data = await response.json(); if (!response.ok) { throw new Error(data.error || `HTTP ${response.status}`); } return data; }); } function renderBootstrapContext() { if (!lastBootstrapContext) { bootstrapContextResult.textContent = "Aun no hay bootstrap cargado."; contextIndicator.className = "indicator indicator-off"; contextStatusText.textContent = "Sin contexto cargado"; contextScopeText.textContent = "No hay bootstrap activo."; chatScopeInfo.value = [selectedSourceId.value, scopeSourceRef.value].filter(Boolean).join(" | ") || "Sin scope seleccionado"; return; } bootstrapContextResult.textContent = format(lastBootstrapContext); contextIndicator.className = "indicator indicator-on"; contextStatusText.textContent = "Contexto bootstrap activo"; contextScopeText.textContent = lastBootstrapContext.scope?.sourceRef || "Scope no especificado"; chatScopeInfo.value = [lastBootstrapContext.scope?.sourceId, lastBootstrapContext.scope?.sourceRef].filter(Boolean).join(" | "); } function renderChatHistory() { if (chatHistory.length === 0) { chatMessages.innerHTML = '
Aun no hay conversacion. Carga un bootstrap y empieza a preguntar.
'; return; } chatMessages.innerHTML = chatHistory.map((entry) => ` `).join(""); chatMessages.scrollTop = chatMessages.scrollHeight; } function buildScopeFromInputs() { if (scopeEditMode.value !== "manual" && scopePresetSelect.value) { const scope = JSON.parse(scopePresetSelect.value); return { sourceId: scope.sourceId, sourceRef: scope.sourceRef, tags: scope.tags || [] }; } return { sourceId: selectedSourceId.value || undefined, sourceRef: scopeSourceRef.value, tags: splitTags(scopeTags.value) }; } function applyPreset(mode, query, sourceRef, useRetrieveModel = false) { bootstrapMode.value = mode; chatMode.value = mode; bootstrapQuery.value = query; scopeSourceRef.value = sourceRef; useModelInRetrieve.checked = useRetrieveModel; } async function loadScopes() { try { const scopes = await fetch("/sources").then((response) => response.json()); availableScopes = scopes; scopePresetSelect.innerHTML = ""; cleanupScopeSelect.innerHTML = ""; const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = "Selecciona un scope disponible"; scopePresetSelect.appendChild(placeholder.cloneNode(true)); cleanupScopeSelect.appendChild(placeholder.cloneNode(true)); for (const scope of scopes) { const option = document.createElement("option"); option.value = JSON.stringify(scope); option.textContent = buildScopeLabel(scope); scopePresetSelect.appendChild(option.cloneNode(true)); cleanupScopeSelect.appendChild(option.cloneNode(true)); } if (scopes.length === 0) { const emptyMsg = "No hay scopes detectados"; scopePresetSelect.options[0].textContent = emptyMsg; cleanupScopeSelect.options[0].textContent = emptyMsg; } else if (!scopePresetSelect.value) { scopePresetSelect.value = JSON.stringify(scopes[0]); cleanupScopeSelect.value = JSON.stringify(scopes[0]); applySelectedScope(scopes[0]); applySelectedCleanupScope(scopes[0]); } } catch (error) { scopePresetSelect.innerHTML = ``; } } async function loadAnswerModels() { try { const payload = await fetch("/models/answer").then((response) => response.json()); answerModel.innerHTML = ""; for (const model of payload.models || []) { const option = document.createElement("option"); option.value = model; option.textContent = model; answerModel.appendChild(option); } if (payload.defaultModel) { answerModel.value = payload.defaultModel; } } catch { answerModel.innerHTML = ''; } } async function loadRecentLogs() { try { const logs = await fetch("/logs/recent").then((response) => response.json()); logsResult.textContent = format(logs); logCounterValue.textContent = Array.isArray(logs) ? String(logs.length) : "0"; } catch (error) { logsResult.textContent = String(error); logCounterValue.textContent = "0"; } } document.querySelectorAll(".tab-button").forEach((button) => { button.addEventListener("click", () => { document.querySelectorAll(".tab-button").forEach((entry) => entry.classList.remove("active")); document.querySelectorAll(".tab-panel").forEach((panel) => panel.classList.remove("active")); button.classList.add("active"); document.getElementById(`tab-${button.dataset.tab}`).classList.add("active"); }); }); scopePresetSelect.addEventListener("change", () => { if (!scopePresetSelect.value) { return; } const scope = JSON.parse(scopePresetSelect.value); applySelectedScope(scope); }); cleanupScopeSelect.addEventListener("change", () => { if (!cleanupScopeSelect.value) { applySelectedCleanupScope(null); return; } const scope = JSON.parse(cleanupScopeSelect.value); applySelectedCleanupScope(scope); }); btnUploadFile.addEventListener("click", () => { ingestUploadFolder.value = ""; currentUploadType = 'file'; ingestTags.value = ""; // Limpiamos tags para evitar arrastrar basura ingestUploadFile.click(); }); btnUploadFolder.addEventListener("click", () => { ingestUploadFile.value = ""; currentUploadType = 'folder'; ingestTags.value = ""; // Limpiamos tags para evitar arrastrar basura ingestUploadFolder.click(); }); btnClearUpload.addEventListener("click", () => { ingestUploadFile.value = ""; ingestUploadFolder.value = ""; currentUploadType = null; updateIngestUiState(); }); ingestUploadFile.addEventListener("change", updateIngestUiState); ingestUploadFolder.addEventListener("change", updateIngestUiState); ingestScopeMode.addEventListener("change", updateIngestUiState); scopeEditMode.addEventListener("change", updateScopeEditState); healthButton.addEventListener("click", async () => { healthResult.textContent = "Comprobando..."; try { const data = await fetch("/health").then((response) => response.json()); healthResult.textContent = format(data); } catch (error) { healthResult.textContent = String(error); } }); ingestButton.addEventListener("click", async () => { ingestResult.textContent = "Ejecutando ingesta..."; try { let data; if (currentUploadType === 'file' && ingestUploadFile.files && ingestUploadFile.files[0]) { const formData = new FormData(); formData.append("file", ingestUploadFile.files[0]); formData.append("mode", ingestMode.value); formData.append("tags", splitTags(ingestTags.value).join(",")); if (ingestScopeMode.value === "custom" && ingestSourceId.value.trim()) { formData.append("sourceId", ingestSourceId.value.trim()); } const response = await fetch("/ingest/upload", { method: "POST", body: formData }); data = await response.json(); if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`); } else if (currentUploadType === 'folder' && ingestUploadFolder.files && ingestUploadFolder.files.length > 0) { ingestResult.textContent = "Empaquetando carpeta local (esto puede tardar unos segundos)..."; const zip = new JSZip(); let addedCount = 0; for (const file of ingestUploadFolder.files) { // webkitRelativePath format: "FolderName/path/to/file.ext" // Le quitamos el primer segmento (FolderName) para la ruta interna const relativePath = file.webkitRelativePath.split('/').slice(1).join('/'); // Filtro nativo simple sin depender de la libreria ignore const isIgnored = relativePath.startsWith('node_modules/') || relativePath.startsWith('.git/') || relativePath.startsWith('.venv/') || relativePath.startsWith('dist/') || relativePath.startsWith('build/') || relativePath.includes('/node_modules/') || relativePath.includes('/.git/'); if (relativePath && !isIgnored) { zip.file(relativePath, file); addedCount++; } } if (addedCount === 0) { throw new Error("La carpeta esta vacia o todos sus archivos fueron ignorados (.gitignore, node_modules, etc)."); } ingestResult.textContent = `Subiendo paquete comprimido con ${addedCount} archivos...`; const zipBlob = await zip.generateAsync({ type: "blob", compression: "STORE" }); const folderName = ingestUploadFolder.files[0].webkitRelativePath.split('/')[0] || "upload"; const formData = new FormData(); formData.append("file", zipBlob, `${folderName}.zip`); formData.append("isZipFolder", "true"); formData.append("mode", ingestMode.value); formData.append("tags", splitTags(ingestTags.value).join(",")); if (ingestScopeMode.value === "custom" && ingestSourceId.value.trim()) { formData.append("sourceId", ingestSourceId.value.trim()); } const response = await fetch("/ingest/upload", { method: "POST", body: formData }); data = await response.json(); if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`); } else { data = await request("/ingest", { sourceId: ingestScopeMode.value === "custom" ? (ingestSourceId.value.trim() || undefined) : undefined, sourceType: ingestSourceType.value, sourceRef: ingestSourceRef.value, mode: ingestMode.value, tags: splitTags(ingestTags.value) }); } ingestResult.textContent = format(data); await loadScopes(); await loadRecentLogs(); updateIngestUiState(); } catch (error) { ingestResult.textContent = String(error); } }); cleanupButton.addEventListener("click", async () => { if (!cleanupScopeSelect.value) { cleanupResult.textContent = "Error: Debes seleccionar un scope primero."; return; } const scope = JSON.parse(cleanupScopeSelect.value); const identifier = scope.sourceId || scope.sourceRef || "scope desconocido"; if (!window.confirm(`¿Seguro que quieres eliminar TODO el contexto de:\n\n${identifier}\n\nEsto borrara todos los vectores de Qdrant asociados y no se puede deshacer.`)) { return; } cleanupResult.textContent = "Eliminando contexto..."; try { const data = await request("/cleanup", { scope }); cleanupResult.textContent = format(data); await loadScopes(); } catch (error) { cleanupResult.textContent = String(error); } }); async function executeBootstrap() { bootstrapResult.textContent = "Cargando bootstrap..."; try { const data = await request("/retrieve", { mode: bootstrapMode.value, intent: "bootstrap", query: bootstrapQuery.value, model: answerModel.value, useModelInRetrieve: useModelInRetrieve.checked, scope: buildScopeFromInputs() }); lastBootstrapContext = data; lastBootstrapMeta = { query: bootstrapQuery.value, mode: bootstrapMode.value, scope: buildScopeFromInputs(), model: answerModel.value, usedModelSummary: useModelInRetrieve.checked }; bootstrapResult.textContent = format(data); renderBootstrapContext(); lastInteraction = { operation: "retrieve", query: bootstrapQuery.value, mode: bootstrapMode.value, intent: "bootstrap", model: answerModel.value, scope: buildScopeFromInputs(), usedBootstrapContext: false, bootstrapMeta: lastBootstrapMeta, usedAdditionalRetrieve: useModelInRetrieve.checked, responseSummary: data.modelSummary || data.summary, retrievedItems: data.items || [] }; await loadRecentLogs(); } catch (error) { bootstrapResult.textContent = String(error); } } bootstrapButton.addEventListener("click", executeBootstrap); replaceBootstrapButton.addEventListener("click", executeBootstrap); clearBootstrapButton.addEventListener("click", () => { lastBootstrapContext = null; lastBootstrapMeta = null; renderBootstrapContext(); }); sendChatButton.addEventListener("click", async () => { const message = chatInput.value.trim(); if (!message) { return; } chatHistory.push({ role: "user", content: message }); renderChatHistory(); mainResult.textContent = "Consultando..."; compareResult.textContent = compareWithoutRag.checked ? "Comparando..." : "Desactivada."; try { const response = await request("/chat", { message, history: chatHistory, mode: chatMode.value, model: answerModel.value, preloadedContext: reuseBootstrapContext.checked && lastBootstrapContext ? (lastBootstrapContext.modelSummary || lastBootstrapContext.summary || "") : undefined, bootstrapMeta: reuseBootstrapContext.checked ? lastBootstrapMeta : undefined, allowAdditionalRetrieve: allowAdditionalRetrieve.checked, scope: buildScopeFromInputs() }); chatHistory.push({ role: "assistant", content: response.answer }); renderChatHistory(); mainResult.textContent = format(response); lastInteraction = { operation: "chat", query: message, mode: chatMode.value, intent: "specific", model: answerModel.value, scope: buildScopeFromInputs(), usedBootstrapContext: response.usedBootstrapContext, bootstrapMeta: reuseBootstrapContext.checked ? lastBootstrapMeta : undefined, usedAdditionalRetrieve: response.usedAdditionalRetrieve, responseSummary: response.answer, retrievedItems: response.retrieved?.items || [] }; await loadRecentLogs(); if (compareWithoutRag.checked) { const comparison = await request("/answer/direct", { query: message, model: answerModel.value, preloadedContext: reuseBootstrapContext.checked && lastBootstrapContext ? (lastBootstrapContext.modelSummary || lastBootstrapContext.summary || "") : undefined, bootstrapMeta: reuseBootstrapContext.checked ? lastBootstrapMeta : undefined }); compareResult.textContent = format(comparison); } chatInput.value = ""; } catch (error) { mainResult.textContent = String(error); compareResult.textContent = compareWithoutRag.checked ? String(error) : "Desactivada."; } }); clearChatButton.addEventListener("click", () => { chatHistory = []; renderChatHistory(); mainResult.textContent = "Sin ejecutar aun."; compareResult.textContent = "Desactivada."; }); manualLogButton.addEventListener("click", async () => { if (!lastInteraction) { mainResult.textContent = "No hay una consulta previa para registrar en logs."; return; } try { const entry = await request("/logs/manual", { ...lastInteraction, reason: "manual_review_requested", note: manualLogNote.value.trim() || undefined }); logsResult.textContent = format(entry); await loadRecentLogs(); } catch (error) { logsResult.textContent = String(error); } }); presetDocs.addEventListener("click", () => { applyPreset( "documental", "dame un mapa inicial del workspace, sus lineas de trabajo principales, reglas, documentacion base y puntos importantes a tener presentes", "/home/pancho/Documentos/Empresa/Desarrollo/IA/docs", true ); }); presetRagDocs.addEventListener("click", () => { applyPreset( "documental", "dame un mapa inicial del modulo RAG, su arquitectura, decisiones, estado actual y documentos clave", "/home/pancho/Documentos/Empresa/Desarrollo/IA/RAG/docs", true ); }); presetCode.addEventListener("click", () => { applyPreset( "codigo", "dame un mapa inicial del codigo del modulo RAG, sus modulos principales, flujo interno y piezas clave", "/home/pancho/Documentos/Empresa/Desarrollo/IA/RAG/src", true ); }); loadScopes(); loadAnswerModels(); loadRecentLogs(); renderBootstrapContext(); renderChatHistory(); updateIngestUiState(); updateScopeEditState();