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);
+39
View File
@@ -54,10 +54,49 @@ async function fetchModels() {
}
}
// Recuperare dettagli show per un modello
async function fetchModelShow(modelName) {
try {
const response = await fetch(`${API_BASE}/models/${encodeURIComponent(modelName)}/show`);
if (!response.ok) {
return null;
}
return await response.json();
} catch (error) {
console.error(`Error loading show data for model ${modelName}:`, error);
return null;
}
}
// Recuperare dettagli show per tutti i modelli
async function fetchAllModelsShow(models) {
const showByModel = {};
const results = await Promise.allSettled(
models.map(async (model) => {
const showData = await fetchModelShow(model.name);
return { name: model.name, showData };
})
);
results.forEach((result) => {
if (result.status === "fulfilled" && result.value.showData) {
showByModel[result.value.name] = result.value.showData;
}
});
return showByModel;
}
// Sincronizzare i dati
async function syncData() {
const health = await fetchHealth();
const modelsData = await fetchModels();
if (modelsData && modelsData.models.length > 0) {
modelsData.showByModel = await fetchAllModelsShow(modelsData.models);
} else if (modelsData) {
modelsData.showByModel = {};
}
// Notificare il main thread
// (il main thread gestisce localStorage)
+9
View File
@@ -77,6 +77,15 @@
</div>
</div>
<!-- Model Show Details -->
<div id="model-details-section" class="mt-8 bg-gray-800 rounded-lg border border-gray-700 p-6 hidden">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold">Dettagli Modello</h3>
<span id="model-details-name" class="text-sm text-purple-300 font-medium"></span>
</div>
<pre id="model-details-content" class="bg-gray-900 text-gray-200 text-xs p-4 rounded-lg overflow-auto max-h-96 whitespace-pre-wrap"></pre>
</div>
<!-- API Documentation Section -->
<div class="mt-8 bg-blue-900 bg-opacity-20 border border-blue-700 rounded-lg p-6">
<h3 class="text-lg font-bold mb-4">📚 Documentazione API</h3>