rag-service/RAG/public/playground/app.js

482 lines
17 KiB
JavaScript

const healthButton = document.getElementById("healthButton");
const ingestButton = document.getElementById("ingestButton");
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 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 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 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 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, "&lt;")}</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 = "";
const placeholder = document.createElement("option");
placeholder.value = "";
placeholder.textContent = "Selecciona un scope disponible";
scopePresetSelect.appendChild(placeholder);
for (const scope of scopes) {
const option = document.createElement("option");
option.value = JSON.stringify(scope);
option.textContent = buildScopeLabel(scope);
scopePresetSelect.appendChild(option);
}
if (scopes.length === 0) {
placeholder.textContent = "No hay scopes detectados";
} else if (!scopePresetSelect.value) {
scopePresetSelect.value = JSON.stringify(scopes[0]);
applySelectedScope(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);
} catch (error) {
logsResult.textContent = String(error);
}
}
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);
});
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);
}
});
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();