diff --git a/app/web/static/js/app.js b/app/web/static/js/app.js index 82e306c..d4456ac 100644 --- a/app/web/static/js/app.js +++ b/app/web/static/js/app.js @@ -302,12 +302,19 @@ class LLMMonitorApp { detailsModal.setAttribute("aria-hidden", "false"); if (!showData) { - detailsContent.textContent = "Loading show details..."; + detailsContent.innerHTML = ` +
+ + + + + Loading details… +
`; this.loadModelShowDetails(modelName, detailsContent); return; } - detailsContent.textContent = JSON.stringify(showData, null, 2); + detailsContent.innerHTML = this.buildAccordionHTML(showData); } async loadModelShowDetails(modelName, detailsContent) { @@ -329,16 +336,171 @@ class LLMMonitorApp { this.lastData.models.showByModel[modelName] = showData; if (this.selectedModelName === modelName) { - detailsContent.textContent = JSON.stringify(showData, null, 2); + detailsContent.innerHTML = this.buildAccordionHTML(showData); } } catch (error) { console.error(error); if (this.selectedModelName === modelName) { - detailsContent.textContent = "Show details are not available for this model."; + detailsContent.innerHTML = '

Show details are not available for this model.

'; } } } + // ── Accordion helpers ──────────────────────────────────────────────────── + + buildAccordionHTML(showData) { + if (!showData || typeof showData !== "object") { + return '

No details available.

'; + } + + const sectionOrder = ["details", "model_info", "parameters", "template", "modelfile", "license"]; + const allKeys = Object.keys(showData); + const orderedKeys = [ + ...sectionOrder.filter(k => k in showData), + ...allKeys.filter(k => !sectionOrder.includes(k)) + ]; + + let html = '
'; + + orderedKeys.forEach((key, index) => { + const value = showData[key]; + const isFirst = index === 0; + const contentId = `acc-${key.replace(/[^a-z0-9]/gi, "-")}`; + const label = this.formatAccordionLabel(key); + const body = this.renderAccordionBody(key, value); + + html += ` +
+ +
+
${body}
+
+
`; + }); + + html += "
"; + return html; + } + + formatAccordionLabel(key) { + const labels = { + details: "Details", + model_info: "Model Info", + parameters: "Parameters", + template: "Template", + modelfile: "Modelfile", + license: "License" + }; + const icons = { + details: "▦", + model_info: "🧠", + parameters: "⚙", + template: "📄", + modelfile: "📦", + license: "📜" + }; + const icon = icons[key] || "▸"; + const text = labels[key] || key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()); + return `${icon}${text}`; + } + + renderAccordionBody(key, value) { + if (key === "details" && value && typeof value === "object" && !Array.isArray(value)) { + return this.renderDetailsGrid(value); + } + if (key === "model_info" && value && typeof value === "object" && !Array.isArray(value)) { + return this.renderModelInfoTable(value); + } + if (typeof value === "string") { + return `
${this.escapeHtml(value)}
`; + } + if (value && typeof value === "object") { + return this.renderKeyValueList(value); + } + return `${this.escapeHtml(String(value))}`; + } + + renderDetailsGrid(details) { + const labelMap = { + family: "Family", + families: "Families", + parameter_size: "Parameters", + quantization_level: "Quantization", + format: "Format", + parent_model: "Parent Model" + }; + let html = '
'; + for (const [k, v] of Object.entries(details)) { + const label = labelMap[k] || k.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()); + const display = Array.isArray(v) ? v.join(", ") : String(v); + html += ` +
+ ${this.escapeHtml(label)} + ${this.escapeHtml(display)} +
`; + } + html += "
"; + return html; + } + + renderModelInfoTable(modelInfo) { + let html = '
'; + for (const [k, v] of Object.entries(modelInfo)) { + const display = typeof v === "object" ? JSON.stringify(v) : String(v); + html += ` +
+
${this.escapeHtml(k)}
+
${this.escapeHtml(display)}
+
`; + } + html += "
"; + return html; + } + + renderKeyValueList(obj) { + let html = '
'; + for (const [k, v] of Object.entries(obj)) { + const display = typeof v === "object" ? JSON.stringify(v) : String(v); + html += ` +
+
${this.escapeHtml(k)}
+
${this.escapeHtml(display)}
+
`; + } + html += "
"; + return html; + } + + escapeHtml(str) { + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + toggleAccordion(contentId, btn) { + const content = document.getElementById(contentId); + if (!content) return; + const isHidden = content.classList.contains("hidden"); + content.classList.toggle("hidden", !isHidden); + const chevron = btn.querySelector(".accordion-chevron"); + if (chevron) chevron.classList.toggle("rotate-180", isHidden); + btn.setAttribute("aria-expanded", String(isHidden)); + } + + // ── Fine accordion helpers ─────────────────────────────────────────────── + hideModelDetails() { const detailsModal = document.getElementById("model-details-modal"); const detailsDialog = document.getElementById("model-details-dialog"); diff --git a/app/web/templates/index.html b/app/web/templates/index.html index 4402524..7a32d30 100644 --- a/app/web/templates/index.html +++ b/app/web/templates/index.html @@ -140,8 +140,8 @@ -