2025-08-14 21:48:09 +00:00
<!DOCTYPE html>
< html lang = "en" >
{{template "views/partials/head" .}}
2026-02-17 23:14:39 +00:00
< body class = "bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]" >
< div class = "app-layout" >
{{template "views/partials/navbar" .}}
< main class = "main-content" >
< div class = "main-content-inner" x-data = "importModel()" x-init = "init()" >
2025-11-12 19:48:56 +00:00
{{template "views/partials/inprogress" .}}
2025-08-14 21:48:09 +00:00
< div class = "container mx-auto px-4 py-8 flex-grow" >
2025-08-16 05:44:50 +00:00
<!-- Hero Header -->
2025-11-29 21:11:44 +00:00
< div class = "hero-section" >
< div class = "hero-content" >
2025-08-16 05:44:50 +00:00
< div class = "flex flex-col md:flex-row md:items-center md:justify-between" >
2025-11-29 21:11:44 +00:00
< div >
< h1 class = "hero-title" >
{{if .ModelName}}Edit Model: {{.ModelName}}{{else}}Import New Model{{end}}
2025-08-16 05:44:50 +00:00
< / h1 >
2025-11-29 21:11:44 +00:00
< p class = "hero-subtitle" x-text = "isAdvancedMode ? 'Configure your model settings using YAML' : 'Import a model from URI with preferences'" > < / p >
2025-08-16 05:44:50 +00:00
< / div >
< div class = "flex gap-3" >
2025-11-12 19:48:56 +00:00
<!-- Mode Toggle (only show when not in edit mode) -->
< template x-if = "!isEditMode" >
2026-02-19 22:41:05 +00:00
< button type = "button" @ click = "toggleMode()" class = "inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors" >
< i class = "fas" :class = "isAdvancedMode ? 'fa-magic' : 'fa-code'" > < / i >
2025-11-12 19:48:56 +00:00
< span x-text = "isAdvancedMode ? 'Simple Mode' : 'Advanced Mode'" > < / span >
< / button >
< / template >
<!-- Advanced Mode Buttons -->
< template x-if = "isAdvancedMode" >
2026-02-19 22:41:05 +00:00
< div class = "flex gap-2" >
< button type = "button" id = "validateBtn" class = "inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors" >
< i class = "fas fa-check" > < / i >
2025-11-12 19:48:56 +00:00
< span > Validate< / span >
< / button >
2026-02-19 22:41:05 +00:00
< button type = "button" id = "saveBtn" class = "inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors" >
< i class = "fas fa-save" > < / i >
2025-11-12 19:48:56 +00:00
< span > {{if .ModelName}}Update{{else}}Create{{end}}< / span >
< / button >
< / div >
< / template >
<!-- Simple Mode Button -->
< template x-if = "!isAdvancedMode && !isEditMode" >
2026-02-19 22:41:05 +00:00
< button type = "button" @ click = "submitImport()"
2025-11-12 19:48:56 +00:00
:disabled="isSubmitting || !importUri.trim()"
2026-02-19 22:41:05 +00:00
class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--color-border-subtle)]">
< i class = "fas text-[10px]" :class = "isSubmitting ? 'fa-spinner fa-spin' : 'fa-upload'" > < / i >
2025-11-12 19:48:56 +00:00
< span x-text = "isSubmitting ? 'Importing...' : 'Import Model'" > < / span >
< / button >
< / template >
2025-08-16 05:44:50 +00:00
< / div >
2025-08-14 21:48:09 +00:00
< / div >
< / div >
< / div >
<!-- Alert Messages -->
< div id = "alertContainer" class = "mb-6" > < / div >
2026-02-28 22:03:47 +00:00
<!-- Persistent estimate (stays visible so user can see size/VRAM even if alert is replaced) -->
< div x-show = "!isAdvancedMode && !isEditMode && lastEstimate && ((lastEstimate.sizeDisplay && lastEstimate.sizeDisplay !== '0 B') || (lastEstimate.vramDisplay && lastEstimate.vramDisplay !== '0 B'))"
x-transition
class="mb-6 p-4 rounded-xl border border-[var(--color-primary)]/30 bg-[var(--color-primary-light)]/30">
< h3 class = "text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2" >
< i class = "fas fa-memory text-[var(--color-primary)]" > < / i >
Estimated requirements
< / h3 >
< div class = "flex flex-wrap gap-4 text-sm text-[var(--color-text-secondary)]" >
< span x-show = "lastEstimate && lastEstimate.sizeDisplay && lastEstimate.sizeDisplay !== '0 B'" >
< i class = "fas fa-download mr-1.5 text-[var(--color-primary)]" > < / i >
Download size: < span class = "font-medium text-[var(--color-text-primary)]" x-text = "lastEstimate?.sizeDisplay" > < / span >
< / span >
< span x-show = "lastEstimate && lastEstimate.vramDisplay && lastEstimate.vramDisplay !== '0 B'" >
< i class = "fas fa-microchip mr-1.5 text-[var(--color-primary)]" > < / i >
VRAM: < span class = "font-medium text-[var(--color-text-primary)]" x-text = "lastEstimate?.vramDisplay" > < / span >
< / span >
< / div >
< / div >
2025-11-12 19:48:56 +00:00
<!-- Simple Import Mode -->
< div x-show = "!isAdvancedMode && !isEditMode"
2025-11-16 10:01:05 +00:00
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
2025-11-29 21:11:44 +00:00
class="card p-8">
2025-11-16 10:01:05 +00:00
< div class = "space-y-6" >
2026-02-17 23:14:39 +00:00
< h2 class = "text-2xl font-semibold text-[var(--color-text-primary)] flex items-center gap-3 mb-6" >
< div class = "w-10 h-10 rounded-lg bg-[var(--color-success-light)] flex items-center justify-center" >
< i class = "fas fa-link text-[var(--color-success)]" > < / i >
2025-11-12 19:48:56 +00:00
< / div >
Import from URI
< / h2 >
<!-- URI Input -->
< div >
2025-11-19 19:52:11 +00:00
< div class = "flex items-center justify-between mb-2" >
2026-02-17 23:14:39 +00:00
< label class = "block text-sm font-medium text-[var(--color-text-secondary)]" >
2025-11-19 19:52:11 +00:00
< i class = "fas fa-link mr-2" > < / i > Model URI
< / label >
< div class = "flex gap-2" >
< a href = "https://huggingface.co/models?search=gguf&sort=trending"
target="_blank"
2026-02-17 23:14:39 +00:00
class="text-xs px-3 py-1.5 rounded-lg bg-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-[var(--color-accent)] border border-[var(--color-accent)]/30 transition-all flex items-center gap-1.5">
2025-11-19 19:52:11 +00:00
< i class = "fab fa-huggingface" > < / i >
< span > Search GGUF Models on Hugging Face< / span >
< i class = "fas fa-external-link-alt text-xs" > < / i >
< / a >
< a href = "https://huggingface.co/models?sort=trending"
target="_blank"
2026-02-17 23:14:39 +00:00
class="text-xs px-3 py-1.5 rounded-lg bg-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-[var(--color-accent)] border border-[var(--color-accent)]/30 transition-all flex items-center gap-1.5">
2025-11-19 19:52:11 +00:00
< i class = "fab fa-huggingface" > < / i >
< span > Browse All Models on Hugging Face< / span >
< i class = "fas fa-external-link-alt text-xs" > < / i >
< / a >
< / div >
< / div >
2025-11-12 19:48:56 +00:00
< input
x-model="importUri"
type="text"
2025-11-19 19:52:11 +00:00
placeholder="huggingface://TheBloke/Llama-2-7B-Chat-GGUF or https://example.com/model.gguf"
2025-11-29 21:11:44 +00:00
class="input w-full"
2025-11-12 19:48:56 +00:00
:disabled="isSubmitting">
2026-02-17 23:14:39 +00:00
< p class = "mt-2 text-xs text-[var(--color-text-secondary)]" >
2025-11-12 19:48:56 +00:00
Enter the URI or path to the model file you want to import
< / p >
2025-11-19 19:52:11 +00:00
<!-- URI Format Guide -->
< div class = "mt-4" x-data = "{ showGuide: false }" >
< button @ click = "showGuide = !showGuide"
2026-02-17 23:14:39 +00:00
class="flex items-center gap-2 text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors">
2025-11-19 19:52:11 +00:00
< i class = "fas" :class = "showGuide ? 'fa-chevron-down' : 'fa-chevron-right'" > < / i >
< i class = "fas fa-info-circle" > < / i >
< span > Supported URI Formats< / span >
< / button >
< div x-show = "showGuide"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform -translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
2026-02-17 23:14:39 +00:00
class="mt-3 p-4 bg-[var(--color-bg-primary)] border border-[var(--color-border-subtle)] rounded-lg space-y-4">
2025-11-19 19:52:11 +00:00
<!-- HuggingFace -->
< div >
2026-02-17 23:14:39 +00:00
< h4 class = "text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2" >
< i class = "fab fa-huggingface text-[var(--color-accent)]" > < / i >
2025-11-19 19:52:11 +00:00
HuggingFace
< / h4 >
2026-02-17 23:14:39 +00:00
< div class = "space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6" >
2025-11-19 19:52:11 +00:00
< div class = "flex items-start gap-2" >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-success)]" > •< / span >
2025-11-19 19:52:11 +00:00
< div >
2026-02-17 23:14:39 +00:00
< code class = "text-[var(--color-success)]" > huggingface://< / code > < span class = "text-[var(--color-text-secondary)]" > TheBloke/Llama-2-7B-Chat-GGUF< / span >
< p class = "text-[var(--color-text-muted)] mt-0.5" > Standard HuggingFace format< / p >
2025-11-19 19:52:11 +00:00
< / div >
< / div >
< div class = "flex items-start gap-2" >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-success)]" > •< / span >
2025-11-19 19:52:11 +00:00
< div >
2026-02-17 23:14:39 +00:00
< code class = "text-[var(--color-success)]" > hf://< / code > < span class = "text-[var(--color-text-secondary)]" > TheBloke/Llama-2-7B-Chat-GGUF< / span >
< p class = "text-[var(--color-text-muted)] mt-0.5" > Short HuggingFace format< / p >
2025-11-19 19:52:11 +00:00
< / div >
< / div >
< div class = "flex items-start gap-2" >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-success)]" > •< / span >
2025-11-19 19:52:11 +00:00
< div >
2026-02-17 23:14:39 +00:00
< code class = "text-[var(--color-success)]" > https://huggingface.co/< / code > < span class = "text-[var(--color-text-secondary)]" > TheBloke/Llama-2-7B-Chat-GGUF< / span >
< p class = "text-[var(--color-text-muted)] mt-0.5" > Full HuggingFace URL< / p >
2025-11-19 19:52:11 +00:00
< / div >
< / div >
< / div >
< / div >
<!-- HTTP/HTTPS -->
< div >
2026-02-17 23:14:39 +00:00
< h4 class = "text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2" >
< i class = "fas fa-globe text-[var(--color-primary)]" > < / i >
2025-11-19 19:52:11 +00:00
HTTP/HTTPS URLs
< / h4 >
2026-02-17 23:14:39 +00:00
< div class = "space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6" >
2025-11-19 19:52:11 +00:00
< div class = "flex items-start gap-2" >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-success)]" > •< / span >
2025-11-19 19:52:11 +00:00
< div >
2026-02-17 23:14:39 +00:00
< code class = "text-[var(--color-success)]" > https://< / code > < span class = "text-[var(--color-text-secondary)]" > example.com/model.gguf< / span >
< p class = "text-[var(--color-text-muted)] mt-0.5" > Direct download from any HTTPS URL< / p >
2025-11-19 19:52:11 +00:00
< / div >
< / div >
< / div >
< / div >
<!-- Local Files -->
< div >
2026-02-17 23:14:39 +00:00
< h4 class = "text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2" >
< i class = "fas fa-file text-[var(--color-warning)]" > < / i >
2025-11-19 19:52:11 +00:00
Local Files
< / h4 >
2026-02-17 23:14:39 +00:00
< div class = "space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6" >
2025-11-19 19:52:11 +00:00
< div class = "flex items-start gap-2" >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-success)]" > •< / span >
2025-11-19 19:52:11 +00:00
< div >
2026-02-17 23:14:39 +00:00
< code class = "text-[var(--color-success)]" > file://< / code > < span class = "text-[var(--color-text-secondary)]" > /path/to/model.gguf< / span >
< p class = "text-[var(--color-text-muted)] mt-0.5" > Local file path (absolute)< / p >
2025-11-19 19:52:11 +00:00
< / div >
< / div >
< div class = "flex items-start gap-2" >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-success)]" > •< / span >
2025-11-19 19:52:11 +00:00
< div >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-text-secondary)]" > /path/to/model.yaml< / span >
< p class = "text-[var(--color-text-muted)] mt-0.5" > Direct local YAML config file< / p >
2025-11-19 19:52:11 +00:00
< / div >
< / div >
< / div >
< / div >
<!-- OCI -->
< div >
2026-02-17 23:14:39 +00:00
< h4 class = "text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2" >
2025-11-19 19:52:11 +00:00
< i class = "fas fa-box text-cyan-400" > < / i >
OCI Registry
< / h4 >
2026-02-17 23:14:39 +00:00
< div class = "space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6" >
2025-11-19 19:52:11 +00:00
< div class = "flex items-start gap-2" >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-success)]" > •< / span >
2025-11-19 19:52:11 +00:00
< div >
2026-02-17 23:14:39 +00:00
< code class = "text-[var(--color-success)]" > oci://< / code > < span class = "text-[var(--color-text-secondary)]" > registry.example.com/model:tag< / span >
< p class = "text-[var(--color-text-muted)] mt-0.5" > OCI container registry< / p >
2025-11-19 19:52:11 +00:00
< / div >
< / div >
< div class = "flex items-start gap-2" >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-success)]" > •< / span >
2025-11-19 19:52:11 +00:00
< div >
2026-02-17 23:14:39 +00:00
< code class = "text-[var(--color-success)]" > ocifile://< / code > < span class = "text-[var(--color-text-secondary)]" > /path/to/image.tar< / span >
< p class = "text-[var(--color-text-muted)] mt-0.5" > Local OCI tarball file< / p >
2025-11-19 19:52:11 +00:00
< / div >
< / div >
< / div >
< / div >
<!-- Ollama -->
< div >
2026-02-17 23:14:39 +00:00
< h4 class = "text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2" >
2025-11-19 19:52:11 +00:00
< i class = "fas fa-cube text-indigo-400" > < / i >
Ollama
< / h4 >
2026-02-17 23:14:39 +00:00
< div class = "space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6" >
2025-11-19 19:52:11 +00:00
< div class = "flex items-start gap-2" >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-success)]" > •< / span >
2025-11-19 19:52:11 +00:00
< div >
2026-02-17 23:14:39 +00:00
< code class = "text-[var(--color-success)]" > ollama://< / code > < span class = "text-[var(--color-text-secondary)]" > llama2:7b< / span >
< p class = "text-[var(--color-text-muted)] mt-0.5" > Ollama model format< / p >
2025-11-19 19:52:11 +00:00
< / div >
< / div >
< / div >
< / div >
<!-- YAML Config Files -->
< div >
2026-02-17 23:14:39 +00:00
< h4 class = "text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2" >
2025-11-19 19:52:11 +00:00
< i class = "fas fa-code text-pink-400" > < / i >
YAML Configuration Files
< / h4 >
2026-02-17 23:14:39 +00:00
< div class = "space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6" >
2025-11-19 19:52:11 +00:00
< div class = "flex items-start gap-2" >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-success)]" > •< / span >
2025-11-19 19:52:11 +00:00
< div >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-text-secondary)]" > https://example.com/model.yaml< / span >
< p class = "text-[var(--color-text-muted)] mt-0.5" > Remote YAML config file< / p >
2025-11-19 19:52:11 +00:00
< / div >
< / div >
< div class = "flex items-start gap-2" >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-success)]" > •< / span >
2025-11-19 19:52:11 +00:00
< div >
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-text-secondary)]" > file:///path/to/config.yaml< / span >
< p class = "text-[var(--color-text-muted)] mt-0.5" > Local YAML config file< / p >
2025-11-19 19:52:11 +00:00
< / div >
< / div >
< / div >
< / div >
2026-02-17 23:14:39 +00:00
< div class = "pt-2 mt-3 border-t border-[var(--color-border-subtle)]" >
< p class = "text-xs text-[var(--color-text-muted)] italic" >
< i class = "fas fa-lightbulb mr-1.5 text-[var(--color-warning)]" > < / i >
2025-11-19 19:52:11 +00:00
Tip: For HuggingFace models, you can use any of the three formats. The system will automatically detect and download the appropriate model files.
< / p >
< / div >
< / div >
< / div >
2025-11-12 19:48:56 +00:00
< / div >
<!-- Preferences Section -->
< div >
< div class = "flex items-center justify-between mb-4" >
2026-02-17 23:14:39 +00:00
< label class = "block text-sm font-medium text-[var(--color-text-secondary)]" >
2025-11-12 19:48:56 +00:00
< i class = "fas fa-cog mr-2" > < / i > Preferences (Optional)
< / label >
< / div >
<!-- Common Preferences -->
2026-02-17 23:14:39 +00:00
< div class = "space-y-4 mb-6 p-4 bg-[var(--color-bg-primary)]/50 rounded-xl border border-[var(--color-border-subtle)]/50" >
< h3 class = "text-sm font-semibold text-[var(--color-text-secondary)] mb-3 flex items-center" >
< i class = "fas fa-star mr-2 text-[var(--color-warning)]" > < / i > Common Preferences
2025-11-12 19:48:56 +00:00
< / h3 >
<!-- Backend Selection -->
< div >
2026-02-17 23:14:39 +00:00
< label class = "block text-sm font-medium text-[var(--color-text-secondary)] mb-2" >
2025-11-12 19:48:56 +00:00
< i class = "fas fa-server mr-2" > < / i > Backend
< / label >
< select
x-model="commonPreferences.backend"
2026-02-17 23:14:39 +00:00
class="input w-full px-4 py-2"
2025-11-12 19:48:56 +00:00
:disabled="isSubmitting">
< option value = "" > Auto-detect (based on URI)< / option >
< option value = "llama-cpp" > llama-cpp< / option >
< option value = "mlx" > mlx< / option >
< option value = "mlx-vlm" > mlx-vlm< / option >
2025-11-15 21:47:09 +00:00
< option value = "transformers" > transformers< / option >
< option value = "vllm" > vllm< / option >
2025-11-20 21:38:30 +00:00
< option value = "diffusers" > diffusers< / option >
2025-11-12 19:48:56 +00:00
< / select >
2026-02-17 23:14:39 +00:00
< p class = "mt-1 text-xs text-[var(--color-text-muted)]" >
2025-11-12 19:48:56 +00:00
Force a specific backend. Leave empty to auto-detect from URI.
< / p >
< / div >
<!-- Model Name -->
< div >
2026-02-17 23:14:39 +00:00
< label class = "block text-sm font-medium text-[var(--color-text-secondary)] mb-2" >
2025-11-12 19:48:56 +00:00
< i class = "fas fa-tag mr-2" > < / i > Model Name
< / label >
< input
x-model="commonPreferences.name"
type="text"
placeholder="Leave empty to use filename"
2026-02-17 23:14:39 +00:00
class="input w-full px-4 py-2"
2025-11-12 19:48:56 +00:00
:disabled="isSubmitting">
2026-02-17 23:14:39 +00:00
< p class = "mt-1 text-xs text-[var(--color-text-muted)]" >
2025-11-12 19:48:56 +00:00
Custom name for the model. If empty, the filename will be used.
< / p >
< / div >
<!-- Description -->
< div >
2026-02-17 23:14:39 +00:00
< label class = "block text-sm font-medium text-[var(--color-text-secondary)] mb-2" >
2025-11-12 19:48:56 +00:00
< i class = "fas fa-align-left mr-2" > < / i > Description
< / label >
< textarea
x-model="commonPreferences.description"
rows="3"
placeholder="Leave empty to use default description"
2025-11-29 21:11:44 +00:00
class="input w-full resize-none"
2025-11-12 19:48:56 +00:00
:disabled="isSubmitting">< / textarea >
2026-02-17 23:14:39 +00:00
< p class = "mt-1 text-xs text-[var(--color-text-muted)]" >
2025-11-12 19:48:56 +00:00
Custom description for the model. If empty, a default description will be generated.
< / p >
< / div >
<!-- Quantizations -->
< div >
2026-02-17 23:14:39 +00:00
< label class = "block text-sm font-medium text-[var(--color-text-secondary)] mb-2" >
2025-11-12 19:48:56 +00:00
< i class = "fas fa-layer-group mr-2" > < / i > Quantizations
< / label >
< input
x-model="commonPreferences.quantizations"
type="text"
placeholder="q4_k_m,q4_k_s,q3_k_m (comma-separated)"
2026-02-17 23:14:39 +00:00
class="input w-full px-4 py-2"
2025-11-12 19:48:56 +00:00
:disabled="isSubmitting">
2026-02-17 23:14:39 +00:00
< p class = "mt-1 text-xs text-[var(--color-text-muted)]" >
2025-11-12 19:48:56 +00:00
Preferred quantizations (comma-separated). Examples: q4_k_m, q4_k_s, q3_k_m, q2_k. Leave empty to use default (q4_k_m).
< / p >
< / div >
<!-- MMProj Quantizations -->
< div >
2026-02-17 23:14:39 +00:00
< label class = "block text-sm font-medium text-[var(--color-text-secondary)] mb-2" >
2025-11-12 19:48:56 +00:00
< i class = "fas fa-image mr-2" > < / i > MMProj Quantizations
< / label >
< input
x-model="commonPreferences.mmproj_quantizations"
type="text"
placeholder="fp16,fp32 (comma-separated)"
2026-02-17 23:14:39 +00:00
class="input w-full px-4 py-2"
2025-11-12 19:48:56 +00:00
:disabled="isSubmitting">
2026-02-17 23:14:39 +00:00
< p class = "mt-1 text-xs text-[var(--color-text-muted)]" >
2025-11-12 19:48:56 +00:00
Preferred MMProj quantizations (comma-separated). Examples: fp16, fp32. Leave empty to use default (fp16).
< / p >
< / div >
2025-11-15 21:47:09 +00:00
<!-- Embeddings -->
< div >
< label class = "flex items-center cursor-pointer" >
< input
x-model="commonPreferences.embeddings"
type="checkbox"
2026-02-17 23:14:39 +00:00
class="w-5 h-5 rounded bg-[var(--color-bg-primary)] border-[var(--color-border-subtle)] text-[var(--color-success)] focus:ring-2 focus:ring-[var(--color-success)]/50 focus:outline-none transition-all cursor-pointer"
2025-11-15 21:47:09 +00:00
:disabled="isSubmitting">
2026-02-17 23:14:39 +00:00
< span class = "ml-3 text-sm font-medium text-[var(--color-text-secondary)]" >
2025-11-15 21:47:09 +00:00
< i class = "fas fa-vector-square mr-2" > < / i > Embeddings
< / span >
< / label >
2026-02-17 23:14:39 +00:00
< p class = "mt-1 ml-8 text-xs text-[var(--color-text-muted)]" >
2025-11-15 21:47:09 +00:00
Enable embeddings support for this model.
< / p >
< / div >
<!-- Model Type -->
< div >
2026-02-17 23:14:39 +00:00
< label class = "block text-sm font-medium text-[var(--color-text-secondary)] mb-2" >
2025-11-15 21:47:09 +00:00
< i class = "fas fa-tag mr-2" > < / i > Model Type
< / label >
< input
x-model="commonPreferences.type"
type="text"
placeholder="AutoModelForCausalLM (for transformers backend)"
2026-02-17 23:14:39 +00:00
class="input w-full px-4 py-2"
2025-11-15 21:47:09 +00:00
:disabled="isSubmitting">
2026-02-17 23:14:39 +00:00
< p class = "mt-1 text-xs text-[var(--color-text-muted)]" >
2025-11-15 21:47:09 +00:00
Model type for transformers backend. Examples: AutoModelForCausalLM, SentenceTransformer, Mamba, MusicgenForConditionalGeneration. Leave empty to use default (AutoModelForCausalLM).
< / p >
< / div >
2025-11-20 21:38:30 +00:00
<!-- Pipeline Type (Diffusers) -->
< div x-show = "commonPreferences.backend === 'diffusers'" >
2026-02-17 23:14:39 +00:00
< label class = "block text-sm font-medium text-[var(--color-text-secondary)] mb-2" >
2025-11-20 21:38:30 +00:00
< i class = "fas fa-stream mr-2" > < / i > Pipeline Type
< / label >
< input
x-model="commonPreferences.pipeline_type"
type="text"
placeholder="StableDiffusionPipeline (for diffusers backend)"
2026-02-17 23:14:39 +00:00
class="input w-full px-4 py-2"
2025-11-20 21:38:30 +00:00
:disabled="isSubmitting">
2026-02-17 23:14:39 +00:00
< p class = "mt-1 text-xs text-[var(--color-text-muted)]" >
2025-11-20 21:38:30 +00:00
Pipeline type for diffusers backend. Examples: StableDiffusionPipeline, StableDiffusion3Pipeline, FluxPipeline. Leave empty to use default (StableDiffusionPipeline).
< / p >
< / div >
<!-- Scheduler Type (Diffusers) -->
< div x-show = "commonPreferences.backend === 'diffusers'" >
2026-02-17 23:14:39 +00:00
< label class = "block text-sm font-medium text-[var(--color-text-secondary)] mb-2" >
2025-11-20 21:38:30 +00:00
< i class = "fas fa-clock mr-2" > < / i > Scheduler Type
< / label >
< input
x-model="commonPreferences.scheduler_type"
type="text"
placeholder="k_dpmpp_2m (optional)"
2026-02-17 23:14:39 +00:00
class="input w-full px-4 py-2"
2025-11-20 21:38:30 +00:00
:disabled="isSubmitting">
2026-02-17 23:14:39 +00:00
< p class = "mt-1 text-xs text-[var(--color-text-muted)]" >
2025-11-20 21:38:30 +00:00
Scheduler type for diffusers backend. Examples: k_dpmpp_2m, euler_a, ddim. Leave empty to use model default.
< / p >
< / div >
<!-- Enable Parameters (Diffusers) -->
< div x-show = "commonPreferences.backend === 'diffusers'" >
2026-02-17 23:14:39 +00:00
< label class = "block text-sm font-medium text-[var(--color-text-secondary)] mb-2" >
2025-11-20 21:38:30 +00:00
< i class = "fas fa-cogs mr-2" > < / i > Enable Parameters
< / label >
< input
x-model="commonPreferences.enable_parameters"
type="text"
placeholder="negative_prompt,num_inference_steps (comma-separated)"
2026-02-17 23:14:39 +00:00
class="input w-full px-4 py-2"
2025-11-20 21:38:30 +00:00
:disabled="isSubmitting">
2026-02-17 23:14:39 +00:00
< p class = "mt-1 text-xs text-[var(--color-text-muted)]" >
2025-11-20 21:38:30 +00:00
Enabled parameters for diffusers backend (comma-separated). Leave empty to use default (negative_prompt,num_inference_steps).
< / p >
< / div >
<!-- CUDA (Diffusers) -->
< div x-show = "commonPreferences.backend === 'diffusers'" >
< label class = "flex items-center cursor-pointer" >
< input
x-model="commonPreferences.cuda"
type="checkbox"
2026-02-17 23:14:39 +00:00
class="w-5 h-5 rounded bg-[var(--color-bg-primary)] border-[var(--color-border-subtle)] text-[var(--color-success)] focus:ring-2 focus:ring-[var(--color-success)]/50 focus:outline-none transition-all cursor-pointer"
2025-11-20 21:38:30 +00:00
:disabled="isSubmitting">
2026-02-17 23:14:39 +00:00
< span class = "ml-3 text-sm font-medium text-[var(--color-text-secondary)]" >
2025-11-20 21:38:30 +00:00
< i class = "fas fa-microchip mr-2" > < / i > CUDA
< / span >
< / label >
2026-02-17 23:14:39 +00:00
< p class = "mt-1 ml-8 text-xs text-[var(--color-text-muted)]" >
2025-11-20 21:38:30 +00:00
Enable CUDA support for GPU acceleration with diffusers backend.
< / p >
< / div >
2025-11-12 19:48:56 +00:00
< / div >
<!-- Custom Preferences -->
< div class = "space-y-3" >
< div class = "flex items-center justify-between mb-3" >
2026-02-17 23:14:39 +00:00
< label class = "block text-sm font-medium text-[var(--color-text-secondary)]" >
2025-11-12 19:48:56 +00:00
< i class = "fas fa-sliders-h mr-2" > < / i > Custom Preferences
< / label >
< button @ click = "addPreference()"
:disabled="isSubmitting"
2026-02-17 23:14:39 +00:00
class="text-sm px-3 py-1.5 rounded-lg bg-[var(--color-success-light)] hover:bg-[var(--color-success)]/30 text-[var(--color-success)] border border-[var(--color-success)]/30 transition-all">
2025-11-12 19:48:56 +00:00
< i class = "fas fa-plus mr-1" > < / i > Add Custom
< / button >
< / div >
< div class = "space-y-3" x-show = "preferences.length > 0" >
< template x-for = "(pref, index) in preferences" :key = "index" >
< div class = "flex gap-3 items-center" >
< input
x-model="pref.key"
type="text"
placeholder="Key"
2026-02-17 23:14:39 +00:00
class="flex-1 input px-4 py-2"
2025-11-12 19:48:56 +00:00
:disabled="isSubmitting">
2026-02-17 23:14:39 +00:00
< span class = "text-[var(--color-text-secondary)]" > :< / span >
2025-11-12 19:48:56 +00:00
< input
x-model="pref.value"
type="text"
placeholder="Value"
2026-02-17 23:14:39 +00:00
class="flex-1 input px-4 py-2"
2025-11-12 19:48:56 +00:00
:disabled="isSubmitting">
< button @ click = "removePreference(index)"
:disabled="isSubmitting"
2026-02-17 23:14:39 +00:00
class="px-3 py-2 rounded-lg bg-[var(--color-error-light)] hover:bg-[var(--color-error)]/30 text-[var(--color-error)] border border-[var(--color-error)]/30 transition-all">
2025-11-12 19:48:56 +00:00
< i class = "fas fa-trash" > < / i >
< / button >
< / div >
< / template >
< / div >
2026-02-17 23:14:39 +00:00
< p class = "mt-2 text-xs text-[var(--color-text-muted)]" >
2025-11-12 19:48:56 +00:00
Add custom key-value pairs for advanced configuration
< / p >
< / div >
< / div >
< / div >
< / div >
<!-- Advanced YAML Editor Panel -->
< div x-show = "isAdvancedMode || isEditMode"
2025-11-16 10:01:05 +00:00
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
2026-02-17 23:14:39 +00:00
class="bg-[var(--color-bg-secondary)] border border-[var(--color-accent)]/20 rounded-xl overflow-hidden h-[calc(100vh-250px)]">
< div class = "sticky top-0 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border-subtle)] p-6 flex items-center justify-between z-10" >
< h2 class = "text-xl font-semibold text-[var(--color-text-primary)] flex items-center gap-3" >
2025-11-16 10:01:05 +00:00
< div class = "w-8 h-8 rounded-lg bg-fuchsia-500/10 flex items-center justify-center" >
2025-10-10 13:10:13 +00:00
< i class = "fas fa-code text-fuchsia-400" > < / i >
2025-08-14 21:48:09 +00:00
< / div >
2025-10-10 13:10:13 +00:00
YAML Configuration Editor
< / h2 >
< div class = "flex items-center gap-3" >
2026-02-17 23:14:39 +00:00
< button id = "formatYamlBtn" class = "text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] text-sm px-3 py-1.5 rounded-lg hover:bg-[var(--color-bg-primary)] transition-colors" >
2025-11-16 10:01:05 +00:00
< i class = "fas fa-indent mr-1.5" > < / i > Format
2025-10-10 13:10:13 +00:00
< / button >
2026-02-17 23:14:39 +00:00
< button id = "copyYamlBtn" class = "text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] text-sm px-3 py-1.5 rounded-lg hover:bg-[var(--color-bg-primary)] transition-colors" >
2025-11-16 10:01:05 +00:00
< i class = "fas fa-copy mr-1.5" > < / i > Copy
2025-10-10 13:10:13 +00:00
< / button >
2025-08-14 21:48:09 +00:00
< / div >
2025-08-16 05:44:50 +00:00
< / div >
2025-10-10 13:10:13 +00:00
< div class = "relative" style = "height: calc(100% - 88px);" >
< div id = "yamlCodeMirror" class = "h-full" > < / div >
2025-08-14 21:48:09 +00:00
< / div >
< / div >
< / div >
< / div >
<!-- Include JS - YAML library -->
2025-11-15 20:49:52 +00:00
< script src = "static/assets/js-yaml.min.js" > < / script >
2025-08-14 21:48:09 +00:00
<!-- Include CodeMirror for syntax highlighting -->
2025-11-15 20:49:52 +00:00
< link rel = "stylesheet" href = "static/assets/codemirror.min.css" >
< script src = "static/assets/codemirror.min.js" > < / script >
< script src = "static/assets/yaml.min.js" > < / script >
< script src = "static/assets/autorefresh.min.js" > < / script >
2025-08-14 21:48:09 +00:00
< style >
2025-08-16 05:44:50 +00:00
/* Enhanced CodeMirror styling */
2025-08-14 21:48:09 +00:00
.CodeMirror {
2026-02-17 23:14:39 +00:00
background: var(--color-bg-primary) !important;
color: var(--color-text-primary) !important;
2025-08-14 21:48:09 +00:00
border: none !important;
height: 100% !important;
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace !important;
font-size: 14px !important;
border-radius: 0 !important;
2025-08-16 05:44:50 +00:00
line-height: 1.5 !important;
2025-08-14 21:48:09 +00:00
}
2025-08-16 05:44:50 +00:00
2025-08-14 21:48:09 +00:00
.CodeMirror-cursor {
2026-02-17 23:14:39 +00:00
border-left: 2px solid var(--color-accent) !important;
2025-08-16 05:44:50 +00:00
animation: blink 1s infinite;
2025-08-14 21:48:09 +00:00
}
2025-08-16 05:44:50 +00:00
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
2025-08-14 21:48:09 +00:00
.CodeMirror-gutters {
2026-02-17 23:14:39 +00:00
background: var(--color-bg-secondary) !important;
border-right: 1px solid var(--color-border-subtle) !important;
color: var(--color-text-secondary) !important;
2025-08-16 05:44:50 +00:00
padding-right: 8px !important;
2025-08-14 21:48:09 +00:00
}
2025-08-16 05:44:50 +00:00
2025-08-14 21:48:09 +00:00
.CodeMirror-linenumber {
2026-02-17 23:14:39 +00:00
color: var(--color-text-muted) !important;
2025-08-16 05:44:50 +00:00
padding: 0 8px 0 4px !important;
font-size: 12px !important;
2025-08-14 21:48:09 +00:00
}
2025-08-16 05:44:50 +00:00
2025-08-14 21:48:09 +00:00
.CodeMirror-activeline-background {
2026-02-17 23:14:39 +00:00
background: var(--color-accent-light) !important;
2025-08-14 21:48:09 +00:00
}
2025-08-16 05:44:50 +00:00
2025-08-14 21:48:09 +00:00
.CodeMirror-selected {
2025-08-16 05:44:50 +00:00
background: rgba(139, 92, 246, 0.25) !important;
2025-08-14 21:48:09 +00:00
}
2025-08-16 05:44:50 +00:00
2025-08-14 21:48:09 +00:00
.CodeMirror-selectedtext {
2025-08-16 05:44:50 +00:00
background: rgba(139, 92, 246, 0.25) !important;
2025-08-14 21:48:09 +00:00
}
2025-08-16 05:44:50 +00:00
2025-08-14 21:48:09 +00:00
.CodeMirror-focused .CodeMirror-selected {
2025-08-16 05:44:50 +00:00
background: rgba(139, 92, 246, 0.3) !important;
2025-08-14 21:48:09 +00:00
}
2025-08-16 05:44:50 +00:00
2025-08-14 21:48:09 +00:00
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection {
2025-08-16 05:44:50 +00:00
background: rgba(139, 92, 246, 0.3) !important;
2025-08-14 21:48:09 +00:00
}
2025-08-16 05:44:50 +00:00
2025-08-14 21:48:09 +00:00
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection {
2025-08-16 05:44:50 +00:00
background: rgba(139, 92, 246, 0.3) !important;
}
/* Enhanced YAML Syntax Highlighting */
2025-11-12 19:48:56 +00:00
.cm-keyword { color: #8b5cf6 !important; font-weight: 600 !important; }
.cm-string { color: #10b981 !important; }
.cm-number { color: #f59e0b !important; }
2026-02-17 23:14:39 +00:00
.cm-comment { color: var(--color-text-muted) !important; font-style: italic !important; }
2025-11-12 19:48:56 +00:00
.cm-property { color: #ec4899 !important; }
.cm-operator { color: #ef4444 !important; }
.cm-variable { color: #06b6d4 !important; }
.cm-tag { color: #8b5cf6 !important; font-weight: 600 !important; }
.cm-attribute { color: #f59e0b !important; }
.cm-def { color: #ec4899 !important; font-weight: 600 !important; }
2026-02-17 23:14:39 +00:00
.cm-bracket { color: var(--color-text-secondary) !important; }
.cm-punctuation { color: var(--color-text-secondary) !important; }
2025-11-12 19:48:56 +00:00
.cm-quote { color: #10b981 !important; }
2026-02-17 23:14:39 +00:00
.cm-meta { color: var(--color-text-muted) !important; }
2025-11-12 19:48:56 +00:00
.cm-builtin { color: #f472b6 !important; }
.cm-atom { color: #f59e0b !important; }
2025-08-14 21:48:09 +00:00
2025-08-16 05:44:50 +00:00
/* Enhanced scrollbar styling */
2025-08-14 21:48:09 +00:00
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
2026-02-17 23:14:39 +00:00
background: var(--color-bg-secondary) !important;
2025-08-14 21:48:09 +00:00
}
2025-08-16 05:44:50 +00:00
2025-08-14 21:48:09 +00:00
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar {
2026-02-17 23:14:39 +00:00
background: var(--color-bg-secondary) !important;
2025-08-14 21:48:09 +00:00
}
2025-08-16 05:44:50 +00:00
2025-08-14 21:48:09 +00:00
.CodeMirror-vscrollbar::-webkit-scrollbar, .CodeMirror-hscrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
2025-08-16 05:44:50 +00:00
2025-08-14 21:48:09 +00:00
.CodeMirror-vscrollbar::-webkit-scrollbar-track, .CodeMirror-hscrollbar::-webkit-scrollbar-track {
2026-02-17 23:14:39 +00:00
background: var(--color-bg-secondary);
2025-08-16 05:44:50 +00:00
border-radius: 4px;
2025-08-14 21:48:09 +00:00
}
2025-08-16 05:44:50 +00:00
2025-08-14 21:48:09 +00:00
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb {
2026-02-17 23:14:39 +00:00
background: var(--color-text-muted);
2025-08-14 21:48:09 +00:00
border-radius: 4px;
}
2025-08-16 05:44:50 +00:00
2025-08-14 21:48:09 +00:00
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb:hover, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb:hover {
2026-02-17 23:14:39 +00:00
background: var(--color-text-secondary);
2025-08-14 21:48:09 +00:00
}
/* Focus ring styling */
.CodeMirror-focused {
2025-08-16 05:44:50 +00:00
outline: 2px solid rgba(139, 92, 246, 0.5) !important;
2025-08-14 21:48:09 +00:00
outline-offset: -2px !important;
2025-08-16 05:44:50 +00:00
border-radius: 0.5rem !important;
}
/* Alert styling */
.alert {
border-radius: 1rem;
padding: 1rem 1.5rem;
backdrop-filter: blur(8px);
border: 1px solid;
animation: slideInFromTop 0.3s ease-out;
}
@keyframes slideInFromTop {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.alert-success {
2026-02-17 23:14:39 +00:00
background: var(--color-success-light);
border-color: var(--color-success);
color: var(--color-success);
2025-08-16 05:44:50 +00:00
}
.alert-error {
2026-02-17 23:14:39 +00:00
background: var(--color-error-light);
border-color: var(--color-error);
color: var(--color-error);
2025-08-16 05:44:50 +00:00
}
.alert-warning {
2026-02-17 23:14:39 +00:00
background: var(--color-warning-light);
border-color: var(--color-warning);
color: var(--color-warning);
2025-08-16 05:44:50 +00:00
}
.alert-info {
2026-02-17 23:14:39 +00:00
background: var(--color-info-light);
border-color: var(--color-info);
color: var(--color-info);
2025-08-14 21:48:09 +00:00
}
< / style >
< script >
2025-11-12 19:48:56 +00:00
function importModel() {
return {
isAdvancedMode: false,
isEditMode: {{if .ModelName}}true{{else}}false{{end}},
importUri: '',
preferences: [],
commonPreferences: {
backend: '',
name: '',
description: '',
quantizations: '',
2025-11-15 21:47:09 +00:00
mmproj_quantizations: '',
embeddings: false,
2025-11-20 21:38:30 +00:00
type: '',
pipeline_type: '',
scheduler_type: '',
enable_parameters: '',
cuda: false
2025-11-12 19:48:56 +00:00
},
isSubmitting: false,
currentJobId: null,
jobPollInterval: null,
yamlEditor: null,
modelEditor: null,
2026-02-28 22:03:47 +00:00
lastEstimate: null,
2025-08-14 21:48:09 +00:00
2025-11-12 19:48:56 +00:00
init() {
// If in edit mode, always show advanced mode
if (this.isEditMode) {
this.isAdvancedMode = true;
}
// Initialize YAML editor if in advanced mode
if (this.isAdvancedMode || this.isEditMode) {
this.$nextTick(() => {
this.initializeCodeMirror();
this.bindAdvancedEvents();
});
}
},
toggleMode() {
this.isAdvancedMode = !this.isAdvancedMode;
if (this.isAdvancedMode) {
this.$nextTick(() => {
this.initializeCodeMirror();
this.bindAdvancedEvents();
});
}
},
addPreference() {
this.preferences.push({ key: '', value: '' });
},
removePreference(index) {
this.preferences.splice(index, 1);
},
async submitImport() {
if (!this.importUri.trim()) {
this.showAlert('error', 'Please enter a model URI');
return;
}
this.isSubmitting = true;
try {
// Build preferences object starting with common preferences
const prefsObj = {};
// Add common preferences (only non-empty values)
if (this.commonPreferences.backend & & this.commonPreferences.backend.trim()) {
prefsObj.backend = this.commonPreferences.backend.trim();
}
if (this.commonPreferences.name & & this.commonPreferences.name.trim()) {
prefsObj.name = this.commonPreferences.name.trim();
}
if (this.commonPreferences.description & & this.commonPreferences.description.trim()) {
prefsObj.description = this.commonPreferences.description.trim();
}
if (this.commonPreferences.quantizations & & this.commonPreferences.quantizations.trim()) {
prefsObj.quantizations = this.commonPreferences.quantizations.trim();
}
if (this.commonPreferences.mmproj_quantizations & & this.commonPreferences.mmproj_quantizations.trim()) {
prefsObj.mmproj_quantizations = this.commonPreferences.mmproj_quantizations.trim();
}
2025-11-15 21:47:09 +00:00
if (this.commonPreferences.embeddings) {
prefsObj.embeddings = 'true';
}
if (this.commonPreferences.type & & this.commonPreferences.type.trim()) {
prefsObj.type = this.commonPreferences.type.trim();
}
2025-11-20 21:38:30 +00:00
if (this.commonPreferences.pipeline_type & & this.commonPreferences.pipeline_type.trim()) {
prefsObj.pipeline_type = this.commonPreferences.pipeline_type.trim();
}
if (this.commonPreferences.scheduler_type & & this.commonPreferences.scheduler_type.trim()) {
prefsObj.scheduler_type = this.commonPreferences.scheduler_type.trim();
}
if (this.commonPreferences.enable_parameters & & this.commonPreferences.enable_parameters.trim()) {
prefsObj.enable_parameters = this.commonPreferences.enable_parameters.trim();
}
if (this.commonPreferences.cuda) {
prefsObj.cuda = true;
}
2025-11-12 19:48:56 +00:00
// Add custom preferences (can override common ones)
this.preferences.forEach(pref => {
if (pref.key & & pref.value) {
prefsObj[pref.key.trim()] = pref.value.trim();
}
});
const requestBody = {
uri: this.importUri.trim(),
preferences: Object.keys(prefsObj).length > 0 ? prefsObj : null
};
const response = await fetch('/models/import-uri', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
2025-12-25 09:43:12 +00:00
const errorData = await response.json().catch(() => ({ message: 'Failed to start import' }));
// Extract error message from various possible formats
// Handle nested error object: {"error": {"message": "...", "code": 500}}
let errorMessage = 'Failed to start import';
if (errorData.error) {
if (typeof errorData.error === 'object' & & errorData.error.message) {
errorMessage = errorData.error.message;
} else if (typeof errorData.error === 'string') {
errorMessage = errorData.error;
}
} else if (errorData.message) {
errorMessage = errorData.message;
} else if (errorData.Error) {
errorMessage = errorData.Error;
} else {
errorMessage = JSON.stringify(errorData);
}
throw new Error(errorMessage);
2025-11-12 19:48:56 +00:00
}
const result = await response.json();
2026-02-28 22:03:47 +00:00
const hasSize = result.estimated_size_display & & result.estimated_size_display !== '0 B';
const hasVram = result.estimated_vram_display & & result.estimated_vram_display !== '0 B';
if (hasSize || hasVram) {
this.lastEstimate = {
sizeDisplay: result.estimated_size_display || '',
vramDisplay: result.estimated_vram_display || '',
sizeBytes: result.estimated_size_bytes || 0,
vramBytes: result.estimated_vram_bytes || 0
};
} else {
this.lastEstimate = null;
}
let successMsg = 'Import started! Tracking progress...';
if (hasSize || hasVram) {
const parts = [];
if (hasSize) parts.push('Size: ' + result.estimated_size_display);
if (hasVram) parts.push('VRAM: ' + result.estimated_vram_display);
successMsg += ' (' + parts.join(' · ') + ')';
}
2025-11-12 19:48:56 +00:00
if (result.uuid) {
this.currentJobId = result.uuid;
2026-02-28 22:03:47 +00:00
this.showAlert('success', successMsg);
2025-11-12 19:48:56 +00:00
this.startJobPolling();
} else if (result.ID) {
// Fallback for different response format
this.currentJobId = result.ID;
2026-02-28 22:03:47 +00:00
this.showAlert('success', successMsg);
2025-11-12 19:48:56 +00:00
this.startJobPolling();
} else {
throw new Error('No job ID returned from server');
}
} catch (error) {
this.showAlert('error', 'Failed to start import: ' + error.message);
this.isSubmitting = false;
}
},
startJobPolling() {
if (this.jobPollInterval) {
clearInterval(this.jobPollInterval);
}
this.jobPollInterval = setInterval(async () => {
if (!this.currentJobId) {
clearInterval(this.jobPollInterval);
return;
}
try {
const response = await fetch(`/models/jobs/${this.currentJobId}`);
if (!response.ok) {
return;
}
const jobData = await response.json();
if (jobData.completed) {
clearInterval(this.jobPollInterval);
this.isSubmitting = false;
this.currentJobId = null;
this.showAlert('success', 'Model imported successfully! Refreshing page...');
// Refresh the page after a short delay
setTimeout(() => {
window.location.reload();
}, 2000);
2025-11-19 19:52:11 +00:00
} else if (jobData.error || (jobData.message & & jobData.message.startsWith('error:'))) {
2025-11-12 19:48:56 +00:00
clearInterval(this.jobPollInterval);
this.isSubmitting = false;
this.currentJobId = null;
2025-11-19 19:52:11 +00:00
// Extract error message - handle both string and object errors
let errorMessage = 'Unknown error';
if (typeof jobData.error === 'string') {
errorMessage = jobData.error;
} else if (jobData.error & & typeof jobData.error === 'object') {
// Check if error object has any properties
const errorKeys = Object.keys(jobData.error);
if (errorKeys.length > 0) {
// Try common error object properties
errorMessage = jobData.error.message || jobData.error.error || jobData.error.Error || JSON.stringify(jobData.error);
} else {
// Empty object {}, fall back to message field
errorMessage = jobData.message || 'Unknown error';
}
} else if (jobData.message) {
// Use message field if error is not present or is empty
errorMessage = jobData.message;
}
// Remove "error: " prefix if present
if (errorMessage.startsWith('error: ')) {
errorMessage = errorMessage.substring(7);
}
this.showAlert('error', 'Import failed: ' + errorMessage);
2025-11-12 19:48:56 +00:00
}
} catch (error) {
console.error('Error polling job status:', error);
}
}, 1000);
},
initializeCodeMirror() {
if (this.yamlEditor) {
return; // Already initialized
}
const initialValue = {{if .ConfigYAML}}`{{.ConfigYAML}}`{{else}}this.getDefaultConfig(){{end}};
this.yamlEditor = CodeMirror(document.getElementById('yamlCodeMirror'), {
mode: 'yaml',
theme: 'default',
lineNumbers: true,
autoRefresh: true,
indentUnit: 2,
tabSize: 2,
indentWithTabs: false,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
autoCloseBrackets: true,
value: initialValue
});
},
bindAdvancedEvents() {
if (!this.yamlEditor) return;
// Button events
const saveBtn = document.getElementById('saveBtn');
const validateBtn = document.getElementById('validateBtn');
const formatYamlBtn = document.getElementById('formatYamlBtn');
const copyYamlBtn = document.getElementById('copyYamlBtn');
if (saveBtn) {
saveBtn.addEventListener('click', () => this.saveConfig());
}
if (validateBtn) {
validateBtn.addEventListener('click', () => this.validateConfig());
}
if (formatYamlBtn) {
formatYamlBtn.addEventListener('click', () => this.formatYaml());
}
if (copyYamlBtn) {
copyYamlBtn.addEventListener('click', () => this.copyYaml());
}
},
getDefaultConfig() {
return `# Model Configuration
2025-10-10 13:10:13 +00:00
name: my-model
backend: llama-cpp
parameters:
model: path/to/model.gguf
temperature: 0.7
top_p: 0.9
top_k: 40
max_tokens: 2048
# Uncomment and configure as needed:
# context_size: 4096
# gpu_layers: 35
# threads: 8
# f16: true
# mmap: true
# Template configuration
# template:
# chat: |
# {{"{{"}}.Input}}
# completion: |
# {{"{{"}}.Input}}
# Use cases
# known_usecases:
# - chat
# - completion
`;
2025-11-12 19:48:56 +00:00
},
2025-10-10 13:10:13 +00:00
2025-11-12 19:48:56 +00:00
validateConfig() {
try {
const yamlContent = this.yamlEditor.getValue();
const config = jsyaml.load(yamlContent);
if (!config || typeof config !== 'object') {
throw new Error('Invalid YAML structure');
}
if (!config.name) {
throw new Error('Model name is required');
}
2026-02-11 13:18:05 +00:00
const isPipeline = config.pipeline & & (config.pipeline.vad || config.pipeline.transcription || config.pipeline.tts || config.pipeline.llm);
if (!isPipeline & & !config.backend) {
2025-11-12 19:48:56 +00:00
throw new Error('Backend is required');
}
2026-02-12 12:57:34 +00:00
if (!isPipeline & & (!config.parameters || !config.parameters.model)) {
2025-11-12 19:48:56 +00:00
throw new Error('Model file/path is required in parameters.model');
}
this.showAlert('success', 'Configuration is valid!');
} catch (error) {
this.showAlert('error', 'Validation failed: ' + error.message);
2025-08-14 21:48:09 +00:00
}
2025-11-12 19:48:56 +00:00
},
async saveConfig() {
try {
const yamlContent = this.yamlEditor.getValue();
const config = jsyaml.load(yamlContent);
if (!config || typeof config !== 'object') {
throw new Error('Invalid YAML structure');
}
if (!config.name) {
throw new Error('Model name is required');
}
2026-02-11 13:18:05 +00:00
const isPipeline = config.pipeline & & (config.pipeline.vad || config.pipeline.transcription || config.pipeline.tts || config.pipeline.llm);
if (!isPipeline & & !config.backend) {
2025-11-12 19:48:56 +00:00
throw new Error('Backend is required');
}
2026-02-12 12:57:34 +00:00
if (!isPipeline & & (!config.parameters || !config.parameters.model)) {
2025-11-12 19:48:56 +00:00
throw new Error('Model file/path is required in parameters.model');
}
const endpoint = this.isEditMode ? `/models/edit/{{.ModelName}}` : '/models/import';
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-yaml',
},
body: yamlContent
});
2025-12-25 09:43:12 +00:00
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Failed to save configuration' }));
// Extract error message from various possible formats
// Handle nested error object: {"error": {"message": "...", "code": 500}}
let errorMessage = 'Failed to save configuration';
if (errorData.error) {
if (typeof errorData.error === 'object' & & errorData.error.message) {
errorMessage = errorData.error.message;
} else if (typeof errorData.error === 'string') {
errorMessage = errorData.error;
}
} else if (errorData.message) {
errorMessage = errorData.message;
} else if (errorData.Error) {
errorMessage = errorData.Error;
} else {
errorMessage = JSON.stringify(errorData);
}
throw new Error(errorMessage);
}
2025-11-12 19:48:56 +00:00
const result = await response.json();
if (result.success) {
this.showAlert('success', result.message || (this.isEditMode ? 'Model updated successfully!' : 'Model created successfully!'));
if (!this.isEditMode & & config.name) {
setTimeout(() => {
window.location.href = `/models/edit/${config.name}`;
}, 2000);
}
} else {
2025-12-25 09:43:12 +00:00
const errorMessage = result.message || result.error || result.Error || 'Failed to save configuration';
this.showAlert('error', errorMessage);
2025-11-12 19:48:56 +00:00
}
} catch (error) {
this.showAlert('error', 'Failed to save: ' + error.message);
2025-08-14 21:48:09 +00:00
}
2025-11-12 19:48:56 +00:00
},
formatYaml() {
try {
const yamlContent = this.yamlEditor.getValue();
const parsed = jsyaml.load(yamlContent);
const formatted = jsyaml.dump(parsed, {
indent: 2,
lineWidth: 120,
noRefs: true,
sortKeys: false
});
this.yamlEditor.setValue(formatted);
this.showAlert('success', 'YAML formatted successfully');
} catch (error) {
this.showAlert('error', 'Failed to format YAML: ' + error.message);
2025-08-14 21:48:09 +00:00
}
2025-11-12 19:48:56 +00:00
},
copyYaml() {
2025-08-14 21:48:09 +00:00
const yamlContent = this.yamlEditor.getValue();
2025-11-12 19:48:56 +00:00
navigator.clipboard.writeText(yamlContent).then(() => {
this.showAlert('success', 'YAML copied to clipboard');
}).catch(err => {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = yamlContent;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
this.showAlert('success', 'YAML copied to clipboard');
});
},
showAlert(type, message) {
const container = document.getElementById('alertContainer');
const alertClasses = {
success: 'alert alert-success',
error: 'alert alert-error',
warning: 'alert alert-warning',
info: 'alert alert-info'
};
2025-10-10 13:10:13 +00:00
2025-11-12 19:48:56 +00:00
const alertIcons = {
success: 'fas fa-check-circle',
error: 'fas fa-exclamation-triangle',
warning: 'fas fa-exclamation-circle',
info: 'fas fa-info-circle'
};
2025-10-10 13:10:13 +00:00
2025-11-12 19:48:56 +00:00
container.innerHTML = `
< div class = "${alertClasses[type]}" >
< div class = "flex items-center" >
< i class = "${alertIcons[type]} mr-3 text-lg" > < / i >
< span class = "flex-1" > ${message}< / span >
< button onclick = "this.parentElement.parentElement.remove()" class = "ml-4 text-current hover:opacity-70 transition-opacity" >
< i class = "fas fa-times" > < / i >
< / button >
< / div >
< / div >
`;
2025-10-10 13:10:13 +00:00
2025-11-12 19:48:56 +00:00
if (type === 'success' || type === 'info') {
setTimeout(() => {
const alert = container.querySelector('div');
if (alert) alert.remove();
}, 5000);
2025-08-14 21:48:09 +00:00
}
}
}
}
< / script >
2026-02-17 23:14:39 +00:00
{{template "views/partials/footer" .}}
< / div >
< / main >
< / div >
2025-08-14 21:48:09 +00:00
< / body >
2026-02-17 23:14:39 +00:00
< / html >