471 lines
18 KiB
JavaScript
471 lines
18 KiB
JavaScript
import { api } from "../lib/api.js";
|
|
import { on } from "../lib/bus.js";
|
|
import { modal } from "../lib/modal.js";
|
|
|
|
const PROMPT_LABELS = {
|
|
router: "Router (clasificador de dominio)",
|
|
greeting: "Saludos",
|
|
orders: "Pedidos",
|
|
shipping: "Envio/Retiro",
|
|
payment: "Pago",
|
|
browse: "Consultas de catalogo",
|
|
};
|
|
|
|
class PromptsCrud extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.attachShadow({ mode: "open" });
|
|
this.items = [];
|
|
this.selected = null;
|
|
this.loading = false;
|
|
this.versions = [];
|
|
this.availableVariables = [];
|
|
this.availableModels = [];
|
|
this.currentSettings = {}; // Valores actuales de las variables
|
|
this.testResult = null;
|
|
this.testLoading = false;
|
|
|
|
this.shadowRoot.innerHTML = `
|
|
<style>
|
|
:host { display:block; height:100%; padding:16px; overflow:hidden; }
|
|
* { box-sizing:border-box; font-family:system-ui,Segoe UI,Roboto,Arial; }
|
|
.container { display:grid; grid-template-columns:280px 1fr; gap:16px; height:100%; }
|
|
.panel { background:#121823; border:1px solid #1e2a3a; border-radius:10px; padding:16px; overflow:hidden; display:flex; flex-direction:column; }
|
|
.panel-title { font-size:14px; font-weight:700; color:#8aa0b5; text-transform:uppercase; letter-spacing:.4px; margin-bottom:12px; }
|
|
|
|
input, select, textarea { background:#0f1520; color:#e7eef7; border:1px solid #253245; border-radius:8px; padding:8px 12px; font-size:13px; width:100%; }
|
|
input:focus, select:focus, textarea:focus { outline:none; border-color:#1f6feb; }
|
|
textarea { resize:vertical; min-height:200px; font-family:monospace; font-size:12px; line-height:1.5; }
|
|
button { cursor:pointer; background:#1f6feb; color:#fff; border:none; border-radius:8px; padding:8px 16px; font-size:13px; }
|
|
button:hover { background:#1a5fd0; }
|
|
button:disabled { opacity:.5; cursor:not-allowed; }
|
|
button.secondary { background:#253245; }
|
|
button.secondary:hover { background:#2d3e52; }
|
|
button.danger { background:#e74c3c; }
|
|
button.danger:hover { background:#c0392b; }
|
|
button.small { padding:4px 8px; font-size:11px; }
|
|
|
|
.list { flex:1; overflow-y:auto; }
|
|
.item { background:#0f1520; border:1px solid #253245; border-radius:8px; padding:12px; margin-bottom:8px; cursor:pointer; transition:all .15s; }
|
|
.item:hover { border-color:#1f6feb; }
|
|
.item.active { border-color:#1f6feb; background:#111b2a; }
|
|
.item-name { font-weight:600; color:#e7eef7; margin-bottom:4px; font-size:14px; }
|
|
.item-meta { font-size:11px; color:#8aa0b5; }
|
|
.item-meta .default { color:#2ecc71; }
|
|
.item-meta .custom { color:#f39c12; }
|
|
|
|
.form { flex:1; overflow-y:auto; display:flex; flex-direction:column; gap:16px; }
|
|
.form-empty { color:#8aa0b5; text-align:center; padding:40px; }
|
|
.field { }
|
|
.field label { display:block; font-size:12px; color:#8aa0b5; margin-bottom:4px; text-transform:uppercase; letter-spacing:.4px; }
|
|
.field-hint { font-size:11px; color:#8aa0b5; margin-top:4px; }
|
|
|
|
.actions { display:flex; gap:8px; flex-wrap:wrap; }
|
|
.loading { text-align:center; padding:40px; color:#8aa0b5; }
|
|
|
|
.variables-list { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
|
|
.var-item { display:inline-flex; align-items:center; gap:4px; background:#0f1520; border:1px solid #253245; border-radius:4px; padding:2px 4px 2px 2px; }
|
|
.var-btn { background:#253245; border:none; color:#8aa0b5; padding:4px 8px; border-radius:3px; font-size:11px; cursor:pointer; font-family:monospace; }
|
|
.var-btn:hover { background:#1f6feb; color:#fff; }
|
|
.var-value { font-size:10px; color:#6c7a89; max-width:120px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
|
|
.versions-list { max-height:150px; overflow-y:auto; margin-top:8px; }
|
|
.version-item { display:flex; justify-content:space-between; align-items:center; padding:6px 8px; background:#0f1520; border-radius:4px; margin-bottom:4px; font-size:12px; }
|
|
.version-item.active { border-left:3px solid #2ecc71; }
|
|
.version-item .ver { color:#e7eef7; }
|
|
.version-item .date { color:#8aa0b5; font-size:10px; }
|
|
|
|
.test-section { background:#0f1520; border:1px solid #253245; border-radius:8px; padding:12px; margin-top:16px; }
|
|
.test-section h4 { margin:0 0 12px; font-size:13px; color:#8aa0b5; }
|
|
.test-result { background:#0a0e14; border:1px solid #1e2a3a; border-radius:6px; padding:12px; margin-top:12px; font-family:monospace; font-size:11px; white-space:pre-wrap; max-height:200px; overflow-y:auto; }
|
|
.test-result.error { border-color:#e74c3c; color:#e74c3c; }
|
|
.test-result.success { border-color:#2ecc71; }
|
|
.test-meta { font-size:10px; color:#8aa0b5; margin-top:8px; }
|
|
|
|
.row { display:flex; gap:12px; align-items:flex-end; }
|
|
.row .field { flex:1; }
|
|
</style>
|
|
|
|
<div class="container">
|
|
<div class="panel">
|
|
<div class="panel-title">Prompts del Sistema</div>
|
|
<div class="list" id="list">
|
|
<div class="loading">Cargando...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-title" id="formTitle">Editor de Prompt</div>
|
|
<div class="form" id="form">
|
|
<div class="form-empty">Selecciona un prompt para editarlo</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.load();
|
|
|
|
// Refrescar settings cuando se vuelve a esta vista (por si cambiaron en Config)
|
|
this._unsubRouter = on("router:viewChanged", ({ view }) => {
|
|
if (view === "prompts") {
|
|
this.refreshSettings();
|
|
}
|
|
});
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
this._unsubRouter?.();
|
|
}
|
|
|
|
async refreshSettings() {
|
|
try {
|
|
const settings = await api.getSettings();
|
|
this.currentSettings = {
|
|
store_name: settings.store_name || "",
|
|
store_hours: this.formatStoreHours(settings),
|
|
store_address: settings.store_address || "",
|
|
store_phone: settings.store_phone || "",
|
|
bot_name: settings.bot_name || "",
|
|
current_date: new Date().toLocaleDateString("es-AR"),
|
|
customer_name: "(nombre del cliente)",
|
|
state: "(estado actual)",
|
|
};
|
|
// Re-renderizar el form si hay uno seleccionado
|
|
if (this.selected) {
|
|
this.renderForm();
|
|
}
|
|
} catch (e) {
|
|
console.debug("Error refreshing settings:", e);
|
|
}
|
|
}
|
|
|
|
async load() {
|
|
this.loading = true;
|
|
this.renderList();
|
|
|
|
try {
|
|
// Cargar prompts y settings en paralelo
|
|
const [data, settings] = await Promise.all([
|
|
api.prompts(),
|
|
api.getSettings().catch(() => ({})),
|
|
]);
|
|
|
|
this.items = data.items || [];
|
|
this.availableVariables = data.available_variables || [];
|
|
this.availableModels = data.available_models || [];
|
|
|
|
// Mapear settings a variables
|
|
this.currentSettings = {
|
|
store_name: settings.store_name || "",
|
|
store_hours: this.formatStoreHours(settings),
|
|
store_address: settings.store_address || "",
|
|
store_phone: settings.store_phone || "",
|
|
bot_name: settings.bot_name || "",
|
|
current_date: new Date().toLocaleDateString("es-AR"),
|
|
customer_name: "(nombre del cliente)",
|
|
state: "(estado actual)",
|
|
};
|
|
|
|
this.loading = false;
|
|
this.renderList();
|
|
} catch (e) {
|
|
console.error("Error loading prompts:", e);
|
|
this.items = [];
|
|
this.loading = false;
|
|
this.renderList();
|
|
}
|
|
}
|
|
|
|
formatStoreHours(settings) {
|
|
if (!settings.pickup_days) return "";
|
|
|
|
// Mapeo de días cortos a nombres legibles
|
|
const dayNames = {
|
|
lun: "Lun", mar: "Mar", mie: "Mié", jue: "Jue",
|
|
vie: "Vie", sab: "Sáb", dom: "Dom"
|
|
};
|
|
|
|
const days = settings.pickup_days.split(",").map(d => dayNames[d.trim()] || d).join(", ");
|
|
const start = (settings.pickup_hours_start || "08:00").slice(0, 5);
|
|
const end = (settings.pickup_hours_end || "20:00").slice(0, 5);
|
|
|
|
return `${days} de ${start} a ${end}`;
|
|
}
|
|
|
|
renderList() {
|
|
const list = this.shadowRoot.getElementById("list");
|
|
|
|
if (this.loading) {
|
|
list.innerHTML = `<div class="loading">Cargando...</div>`;
|
|
return;
|
|
}
|
|
|
|
if (!this.items.length) {
|
|
list.innerHTML = `<div class="loading">No se encontraron prompts</div>`;
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = "";
|
|
for (const item of this.items) {
|
|
const el = document.createElement("div");
|
|
el.className = "item" + (this.selected?.prompt_key === item.prompt_key ? " active" : "");
|
|
|
|
const label = PROMPT_LABELS[item.prompt_key] || item.prompt_key;
|
|
const statusClass = item.is_default ? "default" : "custom";
|
|
const statusText = item.is_default ? "Default" : `v${item.version}`;
|
|
|
|
el.innerHTML = `
|
|
<div class="item-name">${label}</div>
|
|
<div class="item-meta">
|
|
<span class="${statusClass}">${statusText}</span>
|
|
${item.model ? ` | ${item.model}` : ""}
|
|
</div>
|
|
`;
|
|
|
|
el.onclick = () => this.selectPrompt(item);
|
|
list.appendChild(el);
|
|
}
|
|
}
|
|
|
|
async selectPrompt(item) {
|
|
this.selected = item;
|
|
this.testResult = null;
|
|
this.renderList();
|
|
|
|
// Cargar detalles con versiones
|
|
try {
|
|
const details = await api.getPrompt(item.prompt_key);
|
|
this.selected = { ...item, ...details.current };
|
|
this.versions = details.versions || [];
|
|
this.availableVariables = details.available_variables || this.availableVariables;
|
|
this.availableModels = details.available_models || this.availableModels;
|
|
this.renderForm();
|
|
} catch (e) {
|
|
console.error("Error loading prompt details:", e);
|
|
this.renderForm();
|
|
}
|
|
}
|
|
|
|
renderForm() {
|
|
const form = this.shadowRoot.getElementById("form");
|
|
const title = this.shadowRoot.getElementById("formTitle");
|
|
|
|
if (!this.selected) {
|
|
title.textContent = "Editor de Prompt";
|
|
form.innerHTML = `<div class="form-empty">Selecciona un prompt para editarlo</div>`;
|
|
return;
|
|
}
|
|
|
|
const label = PROMPT_LABELS[this.selected.prompt_key] || this.selected.prompt_key;
|
|
title.textContent = `Editar: ${label}`;
|
|
|
|
const content = this.selected.content || "";
|
|
const model = this.selected.model || "gpt-4-turbo";
|
|
|
|
form.innerHTML = `
|
|
<div class="row">
|
|
<div class="field">
|
|
<label>Modelo LLM</label>
|
|
<select id="modelSelect">
|
|
${this.availableModels.map(m => `<option value="${m}" ${m === model ? "selected" : ""}>${m}</option>`).join("")}
|
|
</select>
|
|
</div>
|
|
<div class="field" style="flex:0">
|
|
<label> </label>
|
|
<button id="resetBtn" class="secondary">Reset a Default</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field" style="flex:1; display:flex; flex-direction:column;">
|
|
<label>Contenido del Prompt</label>
|
|
<textarea id="contentInput" style="flex:1; min-height:250px;">${this.escapeHtml(content)}</textarea>
|
|
<div class="field-hint">
|
|
Variables disponibles (click para insertar):
|
|
<div class="variables-list" id="variablesList">
|
|
${this.availableVariables.map(v => {
|
|
const key = typeof v === 'string' ? v : v.key;
|
|
const desc = typeof v === 'string' ? '' : (v.description || '');
|
|
const value = this.currentSettings[key] || '';
|
|
const displayValue = value ? `= ${value}` : '(vacío)';
|
|
return `<span class="var-item">
|
|
<button class="var-btn" data-var="${key}" title="${desc}">{{${key}}}</button>
|
|
<span class="var-value" title="${this.escapeHtml(value)}">${this.escapeHtml(displayValue)}</span>
|
|
</span>`;
|
|
}).join("")}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
${this.versions.length > 0 ? `
|
|
<div class="field">
|
|
<label>Historial de Versiones</label>
|
|
<div class="versions-list" id="versionsList">
|
|
${this.versions.map(v => `
|
|
<div class="version-item ${v.is_active ? "active" : ""}">
|
|
<span class="ver">v${v.version} ${v.is_active ? "(activa)" : ""}</span>
|
|
<span class="date">${this.formatDate(v.created_at)}</span>
|
|
${!v.is_active ? `<button class="small secondary" data-version="${v.version}">Restaurar</button>` : ""}
|
|
</div>
|
|
`).join("")}
|
|
</div>
|
|
</div>
|
|
` : ""}
|
|
|
|
<div class="actions">
|
|
<button id="saveBtn">Guardar Cambios</button>
|
|
<button id="testBtn" class="secondary">Probar Prompt</button>
|
|
</div>
|
|
|
|
<div class="test-section" id="testSection" style="display:none;">
|
|
<h4>Probar Prompt</h4>
|
|
<div class="field">
|
|
<label>Mensaje de prueba</label>
|
|
<input type="text" id="testMessage" placeholder="Ej: Hola, quiero 2kg de asado" />
|
|
</div>
|
|
<button id="runTestBtn" style="margin-top:8px;">Ejecutar Prueba</button>
|
|
<div id="testResultContainer"></div>
|
|
</div>
|
|
`;
|
|
|
|
// Event listeners
|
|
this.shadowRoot.getElementById("saveBtn").onclick = () => this.save();
|
|
this.shadowRoot.getElementById("resetBtn").onclick = () => this.reset();
|
|
this.shadowRoot.getElementById("testBtn").onclick = () => this.toggleTestSection();
|
|
this.shadowRoot.getElementById("runTestBtn").onclick = () => this.runTest();
|
|
|
|
// Variable buttons
|
|
this.shadowRoot.querySelectorAll(".var-btn").forEach(btn => {
|
|
btn.onclick = () => this.insertVariable(btn.dataset.var);
|
|
});
|
|
|
|
// Version restore buttons
|
|
this.shadowRoot.querySelectorAll(".versions-list button").forEach(btn => {
|
|
btn.onclick = () => this.rollback(parseInt(btn.dataset.version, 10));
|
|
});
|
|
}
|
|
|
|
escapeHtml(str) {
|
|
return (str || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
}
|
|
|
|
formatDate(dateStr) {
|
|
if (!dateStr) return "";
|
|
const d = new Date(dateStr);
|
|
return d.toLocaleDateString("es-AR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
}
|
|
|
|
insertVariable(varName) {
|
|
const textarea = this.shadowRoot.getElementById("contentInput");
|
|
const start = textarea.selectionStart;
|
|
const end = textarea.selectionEnd;
|
|
const text = textarea.value;
|
|
const insertion = `{{${varName}}}`;
|
|
textarea.value = text.slice(0, start) + insertion + text.slice(end);
|
|
textarea.selectionStart = textarea.selectionEnd = start + insertion.length;
|
|
textarea.focus();
|
|
}
|
|
|
|
toggleTestSection() {
|
|
const section = this.shadowRoot.getElementById("testSection");
|
|
section.style.display = section.style.display === "none" ? "block" : "none";
|
|
}
|
|
|
|
async save() {
|
|
const content = this.shadowRoot.getElementById("contentInput").value;
|
|
const model = this.shadowRoot.getElementById("modelSelect").value;
|
|
|
|
if (!content.trim()) {
|
|
modal.warn("El contenido no puede estar vacío");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await api.savePrompt(this.selected.prompt_key, { content, model });
|
|
modal.success("Prompt guardado correctamente");
|
|
await this.load();
|
|
// Re-seleccionar el prompt actual
|
|
const updated = this.items.find(i => i.prompt_key === this.selected.prompt_key);
|
|
if (updated) this.selectPrompt(updated);
|
|
} catch (e) {
|
|
console.error("Error saving prompt:", e);
|
|
modal.error("Error guardando: " + (e.message || e));
|
|
}
|
|
}
|
|
|
|
async reset() {
|
|
const confirmed = await modal.confirm("Esto desactivará todas las versiones custom y volverá al prompt por defecto. ¿Continuar?");
|
|
if (!confirmed) return;
|
|
|
|
try {
|
|
await api.resetPrompt(this.selected.prompt_key);
|
|
modal.success("Prompt reseteado a default");
|
|
await this.load();
|
|
const updated = this.items.find(i => i.prompt_key === this.selected.prompt_key);
|
|
if (updated) this.selectPrompt(updated);
|
|
} catch (e) {
|
|
console.error("Error resetting prompt:", e);
|
|
modal.error("Error: " + (e.message || e));
|
|
}
|
|
}
|
|
|
|
async rollback(version) {
|
|
const confirmed = await modal.confirm(`¿Restaurar versión ${version}? Se creará una nueva versión con ese contenido.`);
|
|
if (!confirmed) return;
|
|
|
|
try {
|
|
await api.rollbackPrompt(this.selected.prompt_key, version);
|
|
modal.success("Versión restaurada");
|
|
await this.load();
|
|
const updated = this.items.find(i => i.prompt_key === this.selected.prompt_key);
|
|
if (updated) this.selectPrompt(updated);
|
|
} catch (e) {
|
|
console.error("Error rolling back:", e);
|
|
modal.error("Error: " + (e.message || e));
|
|
}
|
|
}
|
|
|
|
async runTest() {
|
|
const testMessage = this.shadowRoot.getElementById("testMessage").value;
|
|
if (!testMessage.trim()) {
|
|
modal.warn("Ingresa un mensaje de prueba");
|
|
return;
|
|
}
|
|
|
|
const content = this.shadowRoot.getElementById("contentInput").value;
|
|
const container = this.shadowRoot.getElementById("testResultContainer");
|
|
container.innerHTML = `<div class="test-result">Ejecutando prueba...</div>`;
|
|
|
|
try {
|
|
const result = await api.testPrompt(this.selected.prompt_key, {
|
|
content,
|
|
test_message: testMessage,
|
|
store_config: { store_name: "Carniceria Demo", bot_name: "Piaf" },
|
|
});
|
|
|
|
if (result.ok) {
|
|
let parsed = result.response;
|
|
try {
|
|
parsed = JSON.stringify(JSON.parse(result.response), null, 2);
|
|
} catch (e) { /* no es JSON */ }
|
|
|
|
container.innerHTML = `
|
|
<div class="test-result success">${this.escapeHtml(parsed)}</div>
|
|
<div class="test-meta">
|
|
Modelo: ${result.model} | Latencia: ${result.latency_ms}ms |
|
|
Tokens: ${result.usage?.total_tokens || "?"}
|
|
</div>
|
|
`;
|
|
} else {
|
|
container.innerHTML = `<div class="test-result error">Error: ${result.error || "Unknown"}</div>`;
|
|
}
|
|
} catch (e) {
|
|
console.error("Error testing prompt:", e);
|
|
container.innerHTML = `<div class="test-result error">Error: ${e.message || e}</div>`;
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define("prompts-crud", PromptsCrud);
|