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 += `
+
`;
+ });
+
+ 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 @@
-