537 lines
19 KiB
JavaScript
537 lines
19 KiB
JavaScript
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 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;
|
|
|
|
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}]`;
|
|
}
|
|
|
|
function updateIngestUiState() {
|
|
const hasUpload = Boolean(ingestUploadFile.files && ingestUploadFile.files[0]);
|
|
ingestSourceType.value = hasUpload ? "file" : ingestSourceType.value;
|
|
ingestSourceType.disabled = hasUpload;
|
|
ingestSourceRef.disabled = hasUpload;
|
|
ingestSourceIdWrapper.style.display = ingestScopeMode.value === "custom" ? "block" : "none";
|
|
|
|
if (hasUpload) {
|
|
ingestModeHint.textContent = `Upload directo activo: se ingerira el archivo local "${ingestUploadFile.files[0].name}" y se ignorara la ruta manual.`;
|
|
ingestModeHint.classList.add("strong");
|
|
} else {
|
|
ingestModeHint.textContent = "Si seleccionas un archivo local, el playground usara upload directo y podras aislarlo con un `sourceId` propio para no mezclarlo con otros scopes.";
|
|
ingestModeHint.classList.remove("strong");
|
|
}
|
|
}
|
|
|
|
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 = '<p class="empty-chat">Aun no hay conversacion. Carga un bootstrap y empieza a preguntar.</p>';
|
|
return;
|
|
}
|
|
|
|
chatMessages.innerHTML = chatHistory.map((entry) => `
|
|
<article class="message ${entry.role}">
|
|
<header>${entry.role === "user" ? "Usuario" : "Modelo"}</header>
|
|
<p>${entry.content.replace(/</g, "<")}</p>
|
|
</article>
|
|
`).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 = `<option value="">Error cargando scopes: ${error}</option>`;
|
|
}
|
|
}
|
|
|
|
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 = '<option value="openai/gpt-4.1-mini">openai/gpt-4.1-mini</option>';
|
|
}
|
|
}
|
|
|
|
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);
|
|
});
|
|
|
|
ingestUploadFile.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 (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 {
|
|
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();
|