feat: load and cache Ollama show data per model with clickable model details

- Add GET /api/v1/models/{model_name}/show endpoint (proxy to Ollama /api/show)
- Worker now fetches show data for each model during model list sync
- Persist show details in localStorage under llm_monitor_models.showByModel
- Make model cards clickable to display cached show details in a dedicated panel
- Keep UI updates incremental without full page reload
- Add tests for show endpoint and OpenAPI path
- Update README and PRD with show-flow and click-card behavior
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-24 19:41:46 +02:00
parent 32b1130632
commit 57663400ce
7 changed files with 217 additions and 4 deletions
+58 -2
View File
@@ -6,6 +6,7 @@
class LLMMonitorApp {
constructor() {
this.worker = null;
this.selectedModelName = null;
this.lastData = {
health: null,
models: null
@@ -35,6 +36,15 @@ class LLMMonitorApp {
document.getElementById("refresh-btn")?.addEventListener("click", () => {
this.requestSync();
});
// Listener click card modello
document.getElementById("models-container")?.addEventListener("click", (event) => {
const card = event.target.closest("[data-model-name]");
if (!card) {
return;
}
this.showModelDetails(card.getAttribute("data-model-name"));
});
}
// Caricare dati da localStorage
@@ -68,12 +78,17 @@ class LLMMonitorApp {
if (type === "DATA_UPDATED") {
if (health && JSON.stringify(this.lastData.health) !== JSON.stringify(health)) {
this.lastData.health = health;
localStorage.setItem("llm_monitor_health", JSON.stringify(health));
this.renderHealth(health);
}
if (modelsData && JSON.stringify(this.lastData.models) !== JSON.stringify(modelsData)) {
this.lastData.models = modelsData;
localStorage.setItem("llm_monitor_models", JSON.stringify(modelsData));
this.renderModels(modelsData);
if (this.selectedModelName) {
this.showModelDetails(this.selectedModelName);
}
}
}
}
@@ -140,10 +155,11 @@ class LLMMonitorApp {
// Renderizzare singola card modello
renderModelCard(model) {
const formattedDate = this.formatDate(model.modified_at);
const modelName = this.escapeHtml(model.name);
return `
<div class="bg-gray-700 rounded-lg p-4 border border-gray-600 hover:border-purple-500 transition">
<div data-model-name="${modelName}" class="bg-gray-700 rounded-lg p-4 border border-gray-600 hover:border-purple-500 transition cursor-pointer">
<div class="flex items-start justify-between mb-3">
<h3 class="text-lg font-semibold">${this.escapeHtml(model.name)}</h3>
<h3 class="text-lg font-semibold">${modelName}</h3>
<span class="bg-purple-600 px-3 py-1 rounded text-xs font-medium">Caricato</span>
</div>
<div class="grid grid-cols-2 gap-4 text-sm">
@@ -160,10 +176,35 @@ class LLMMonitorApp {
<p class="text-gray-400 text-xs">Digest</p>
<p class="font-mono text-xs bg-gray-800 p-2 rounded mt-1 break-all">${this.escapeHtml(model.digest.substring(0, 64))}...</p>
</div>
<p class="text-xs text-purple-300 mt-3">Clicca per vedere dettagli show</p>
</div>
`;
}
showModelDetails(modelName) {
const detailsSection = document.getElementById("model-details-section");
const detailsName = document.getElementById("model-details-name");
const detailsContent = document.getElementById("model-details-content");
if (!detailsSection || !detailsName || !detailsContent || !this.lastData.models) {
return;
}
const showByModel = this.lastData.models.showByModel || {};
const showData = showByModel[modelName];
this.selectedModelName = modelName;
detailsSection.classList.remove("hidden");
detailsName.textContent = modelName;
if (!showData) {
detailsContent.textContent = "Dettagli show non disponibili per questo modello.";
return;
}
detailsContent.textContent = JSON.stringify(showData, null, 2);
}
// Formattare bytes
formatBytes(bytes) {
if (bytes === 0) return "0 B";
@@ -220,15 +261,30 @@ class LLMMonitorApp {
if (response.ok) {
const data = await response.json();
const models = data.models || [];
const showByModel = {};
await Promise.allSettled(
models.map(async (model) => {
const showResponse = await fetch(`/api/v1/models/${encodeURIComponent(model.name)}/show`);
if (showResponse.ok) {
showByModel[model.name] = await showResponse.json();
}
})
);
const modelsData = {
models,
total: models.length,
totalSize: this.formatBytes(models.reduce((sum, m) => sum + m.size, 0)),
showByModel,
timestamp: new Date().toISOString()
};
this.lastData.models = modelsData;
localStorage.setItem("llm_monitor_models", JSON.stringify(modelsData));
this.renderModels(modelsData);
if (this.selectedModelName) {
this.showModelDetails(this.selectedModelName);
}
}
} catch (error) {
console.error("Models loading error:", error);