we have a lot of models so group them nicely

This commit is contained in:
Alex Cheema
2025-01-24 18:02:00 +00:00
parent cfdaaef8e6
commit 59174bdc62
3 changed files with 175 additions and 44 deletions

View File

@@ -778,4 +778,55 @@ main {
border-top-color: transparent;
border-radius: 50%;
animation: thinking-spin 1s linear infinite;
}
.model-group {
margin-bottom: 12px;
}
.model-group-header,
.model-subgroup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: var(--primary-bg-color);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 8px;
}
.model-group-header:hover,
.model-subgroup-header:hover {
background-color: var(--secondary-color-transparent);
}
.model-group-content {
padding-left: 12px;
}
.model-subgroup {
margin-bottom: 8px;
}
.model-subgroup-header {
font-size: 0.9em;
background-color: rgba(255, 255, 255, 0.05);
}
.model-subgroup-content {
padding-left: 12px;
}
.group-header-content {
display: flex;
align-items: center;
gap: 8px;
}
.model-count {
font-size: 0.8em;
color: var(--secondary-color-transparent);
font-family: monospace;
}

View File

@@ -50,50 +50,78 @@
<span>Loading models...</span>
</div>
<template x-for="(model, key) in models" :key="key">
<div class="model-option"
:class="{ 'selected': cstate.selectedModel === key }"
@click="cstate.selectedModel = key">
<div class="model-header">
<div class="model-name" x-text="model.name"></div>
<button
@click.stop="deleteModel(key, model)"
class="model-delete-button"
x-show="model.download_percentage > 0">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="model-info">
<div class="model-progress">
<template x-if="model.loading">
<span><i class="fas fa-spinner fa-spin"></i> Checking download status...</span>
</template>
<div class="model-progress-info">
<template x-if="!model.loading && model.download_percentage != null">
<span>
<!-- Check if there's an active download for this model -->
<template x-if="downloadProgress?.some(p =>
p.repo_id && p.repo_id.toLowerCase().includes(key.toLowerCase()) && !p.isComplete
)">
<i class="fas fa-circle-notch fa-spin"></i>
</template>
<span x-text="model.downloaded ? 'Downloaded' : `${Math.round(model.download_percentage)}% downloaded`"></span>
</span>
</template>
<template x-if="!model.loading && (model.download_percentage === null || model.download_percentage < 100) && !downloadProgress?.some(p => !p.isComplete)">
<button
@click.stop="handleDownload(key)"
class="model-download-button">
<i class="fas fa-download"></i>
<span x-text="(model.download_percentage > 0 && model.download_percentage < 100) ? 'Continue Downloading' : 'Download'"></span>
</button>
</template>
</div>
<!-- Group models by prefix -->
<template x-for="[mainPrefix, subGroups] in Object.entries(groupModelsByPrefix(models))" :key="mainPrefix">
<div class="model-group">
<div class="model-group-header" @click="toggleGroup(mainPrefix)">
<div class="group-header-content">
<span x-text="mainPrefix"></span>
<span class="model-count" x-text="getGroupCounts(Object.values(subGroups).flatMap(group => Object.values(group)))"></span>
</div>
<template x-if="model.total_size">
<div class="model-size" x-text="model.total_downloaded ?
`${formatBytes(model.total_downloaded)} / ${formatBytes(model.total_size)}` :
formatBytes(model.total_size)">
<i class="fas" :class="isGroupExpanded(mainPrefix) ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
</div>
<div class="model-group-content" x-show="isGroupExpanded(mainPrefix)" x-transition>
<template x-for="[subPrefix, groupModels] in Object.entries(subGroups)" :key="subPrefix">
<div class="model-subgroup">
<div class="model-subgroup-header" @click.stop="toggleGroup(mainPrefix, subPrefix)">
<div class="group-header-content">
<span x-text="subPrefix"></span>
<span class="model-count" x-text="getGroupCounts(groupModels)"></span>
</div>
<i class="fas" :class="isGroupExpanded(mainPrefix, subPrefix) ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
</div>
<div class="model-subgroup-content" x-show="isGroupExpanded(mainPrefix, subPrefix)" x-transition>
<template x-for="(model, key) in groupModels" :key="key">
<div class="model-option"
:class="{ 'selected': cstate.selectedModel === key }"
@click="cstate.selectedModel = key">
<div class="model-header">
<div class="model-name" x-text="model.name"></div>
<button
@click.stop="deleteModel(key, model)"
class="model-delete-button"
x-show="model.download_percentage > 0">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="model-info">
<div class="model-progress">
<template x-if="model.loading">
<span><i class="fas fa-spinner fa-spin"></i> Checking download status...</span>
</template>
<div class="model-progress-info">
<template x-if="!model.loading && model.download_percentage != null">
<span>
<template x-if="downloadProgress?.some(p =>
p.repo_id && p.repo_id.toLowerCase().includes(key.toLowerCase()) && !p.isComplete
)">
<i class="fas fa-circle-notch fa-spin"></i>
</template>
<span x-text="model.downloaded ? 'Downloaded' : `${Math.round(model.download_percentage)}% downloaded`"></span>
</span>
</template>
<template x-if="!model.loading && (model.download_percentage === null || model.download_percentage < 100) && !downloadProgress?.some(p => !p.isComplete)">
<button
@click.stop="handleDownload(key)"
class="model-download-button">
<i class="fas fa-download"></i>
<span x-text="(model.download_percentage > 0 && model.download_percentage < 100) ? 'Continue Downloading' : 'Download'"></span>
</button>
</template>
</div>
</div>
<template x-if="model.total_size">
<div class="model-size" x-text="model.total_downloaded ?
`${formatBytes(model.total_downloaded)} / ${formatBytes(model.total_size)}` :
formatBytes(model.total_size)">
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
@@ -178,6 +206,7 @@
</template>
</div>
</div>
</div>
<button
@click="
home = 0;

View File

@@ -42,6 +42,9 @@ document.addEventListener("alpine:init", () => {
topology: null,
topologyInterval: null,
// Add these new properties
expandedGroups: {},
init() {
// Clean up any pending messages
localStorage.removeItem("pendingMessage");
@@ -664,7 +667,55 @@ document.addEventListener("alpine:init", () => {
`;
vizElement.appendChild(nodeElement);
});
}
},
// Add these helper methods
countDownloadedModels(models) {
return Object.values(models).filter(model => model.downloaded).length;
},
getGroupCounts(groupModels) {
const total = Object.keys(groupModels).length;
const downloaded = this.countDownloadedModels(groupModels);
return `[${downloaded}/${total}]`;
},
// Update the existing groupModelsByPrefix method to include counts
groupModelsByPrefix(models) {
const groups = {};
Object.entries(models).forEach(([key, model]) => {
const parts = key.split('-');
const mainPrefix = parts[0].toUpperCase();
let subPrefix;
if (parts.length === 2) {
subPrefix = parts[1].toUpperCase();
} else if (parts.length > 2) {
subPrefix = parts[1].toUpperCase();
} else {
subPrefix = 'OTHER';
}
if (!groups[mainPrefix]) {
groups[mainPrefix] = {};
}
if (!groups[mainPrefix][subPrefix]) {
groups[mainPrefix][subPrefix] = {};
}
groups[mainPrefix][subPrefix][key] = model;
});
return groups;
},
toggleGroup(prefix, subPrefix = null) {
const key = subPrefix ? `${prefix}-${subPrefix}` : prefix;
this.expandedGroups[key] = !this.expandedGroups[key];
},
isGroupExpanded(prefix, subPrefix = null) {
const key = subPrefix ? `${prefix}-${subPrefix}` : prefix;
return this.expandedGroups[key] || false;
},
}));
});