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:
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user