mirror of
https://github.com/mudler/LocalAI.git
synced 2025-12-30 22:20:20 -06:00
* wip * Simplify stop Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Improve UI Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Show installed backends at the index Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Imporve UI Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1196 lines
54 KiB
HTML
1196 lines
54 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
{{template "views/partials/head" .}}
|
|
|
|
<body class="bg-gradient-to-br from-gray-900 via-gray-950 to-black text-gray-200">
|
|
<div class="flex flex-col min-h-screen">
|
|
|
|
{{template "views/partials/navbar" .}}
|
|
|
|
<div class="container mx-auto px-4 py-8 flex-grow">
|
|
<!-- Hero Header -->
|
|
<div class="relative bg-gradient-to-r from-violet-900/40 via-purple-900/30 to-fuchsia-900/40 rounded-3xl shadow-2xl p-8 mb-8 overflow-hidden">
|
|
<!-- Background Pattern -->
|
|
<div class="absolute inset-0 opacity-10">
|
|
<div class="absolute inset-0 bg-gradient-to-r from-violet-500/20 to-fuchsia-500/20"></div>
|
|
<div class="absolute top-0 left-0 w-full h-full" style="background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.15) 1px, transparent 0); background-size: 20px 20px;"></div>
|
|
</div>
|
|
|
|
<div class="relative max-w-5xl mx-auto">
|
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
|
<div class="mb-4 md:mb-0">
|
|
<h1 class="text-3xl md:text-4xl font-bold text-white mb-2">
|
|
<span class="bg-clip-text text-transparent bg-gradient-to-r from-violet-400 via-purple-400 to-fuchsia-400">
|
|
{{if .ModelName}}Edit Model: {{.ModelName}}{{else}}Import New Model{{end}}
|
|
</span>
|
|
</h1>
|
|
<p class="text-lg text-gray-300 font-light">Configure your model settings using the form or YAML editor</p>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button id="validateBtn" class="group relative inline-flex items-center bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white py-3 px-6 rounded-xl font-semibold transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:shadow-blue-500/25">
|
|
<i class="fas fa-check mr-2 group-hover:animate-pulse"></i>
|
|
<span>Validate</span>
|
|
<div class="absolute inset-0 rounded-xl bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
|
</button>
|
|
<button id="saveBtn" class="group relative inline-flex items-center bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white py-3 px-6 rounded-xl font-semibold transition-all duration-300 ease-in-out transform hover:scale-105 hover:shadow-xl hover:shadow-green-500/25">
|
|
<i class="fas fa-save mr-2 group-hover:animate-pulse"></i>
|
|
<span>{{if .ModelName}}Update{{else}}Create{{end}}</span>
|
|
<div class="absolute inset-0 rounded-xl bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Alert Messages -->
|
|
<div id="alertContainer" class="mb-6"></div>
|
|
|
|
<!-- Main Content Grid -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 h-[calc(100vh-250px)]">
|
|
|
|
<!-- Form Panel (Left) -->
|
|
<div class="relative bg-gradient-to-br from-gray-800/90 to-gray-900/90 border border-gray-700/50 rounded-2xl overflow-hidden shadow-xl backdrop-blur-sm">
|
|
<div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-violet-500/5 to-purple-500/5"></div>
|
|
|
|
<div class="relative sticky top-0 bg-gray-800/95 border-b border-gray-700/50 p-6 flex items-center justify-between z-10 backdrop-blur-sm">
|
|
<h2 class="text-xl font-semibold text-white flex items-center gap-3">
|
|
<div class="w-8 h-8 rounded-lg bg-violet-500/20 flex items-center justify-center">
|
|
<i class="fas fa-edit text-violet-400"></i>
|
|
</div>
|
|
Configuration Form
|
|
</h2>
|
|
<div class="flex items-center gap-3">
|
|
<button id="resetFormBtn" class="group text-gray-400 hover:text-gray-200 text-sm px-3 py-1.5 rounded-lg hover:bg-gray-700/50 transition-all duration-200">
|
|
<i class="fas fa-undo mr-1.5 group-hover:animate-spin"></i> Reset
|
|
</button>
|
|
<button id="expandAllBtn" class="group text-gray-400 hover:text-gray-200 text-sm px-3 py-1.5 rounded-lg hover:bg-gray-700/50 transition-all duration-200">
|
|
<i class="fas fa-expand-alt mr-1.5 group-hover:animate-pulse"></i> Expand All
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="relative p-6 overflow-y-auto" style="height: calc(100% - 88px);">
|
|
<form id="configForm" class="space-y-6">
|
|
<!-- Form will be dynamically generated here -->
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- YAML Editor Panel (Right) -->
|
|
<div class="relative bg-gradient-to-br from-gray-800/90 to-gray-900/90 border border-gray-700/50 rounded-2xl overflow-hidden shadow-xl backdrop-blur-sm">
|
|
<div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-fuchsia-500/5 to-purple-500/5"></div>
|
|
|
|
<div class="relative sticky top-0 bg-gray-800/95 border-b border-gray-700/50 p-6 flex items-center justify-between z-10 backdrop-blur-sm">
|
|
<h2 class="text-xl font-semibold text-white flex items-center gap-3">
|
|
<div class="w-8 h-8 rounded-lg bg-fuchsia-500/20 flex items-center justify-center">
|
|
<i class="fas fa-code text-fuchsia-400"></i>
|
|
</div>
|
|
YAML Editor
|
|
</h2>
|
|
<div class="flex items-center gap-3">
|
|
<button id="formatYamlBtn" class="group text-gray-400 hover:text-gray-200 text-sm px-3 py-1.5 rounded-lg hover:bg-gray-700/50 transition-all duration-200">
|
|
<i class="fas fa-indent mr-1.5 group-hover:animate-pulse"></i> Format
|
|
</button>
|
|
<button id="copyYamlBtn" class="group text-gray-400 hover:text-gray-200 text-sm px-3 py-1.5 rounded-lg hover:bg-gray-700/50 transition-all duration-200">
|
|
<i class="fas fa-copy mr-1.5 group-hover:animate-bounce"></i> Copy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="relative" style="height: calc(100% - 88px);">
|
|
<div id="yamlCodeMirror" class="h-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{template "views/partials/footer" .}}
|
|
</div>
|
|
|
|
<!-- Include JS-YAML library -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
|
|
|
|
<!-- Include CodeMirror for syntax highlighting -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css">
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/mode/yaml/yaml.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/display/autorefresh.min.js"></script>
|
|
|
|
<style>
|
|
/* Enhanced CodeMirror styling */
|
|
.CodeMirror {
|
|
background: linear-gradient(135deg, #111827 0%, #1f2937 100%) !important;
|
|
color: #e5e7eb !important;
|
|
border: none !important;
|
|
height: 100% !important;
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace !important;
|
|
font-size: 14px !important;
|
|
border-radius: 0 !important;
|
|
line-height: 1.5 !important;
|
|
}
|
|
|
|
.CodeMirror-cursor {
|
|
border-left: 2px solid #a78bfa !important;
|
|
animation: blink 1s infinite;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 50% { opacity: 1; }
|
|
51%, 100% { opacity: 0; }
|
|
}
|
|
|
|
.CodeMirror-gutters {
|
|
background: linear-gradient(135deg, #1f2937 0%, #374151 100%) !important;
|
|
border-right: 1px solid rgba(75, 85, 99, 0.5) !important;
|
|
color: #9ca3af !important;
|
|
padding-right: 8px !important;
|
|
}
|
|
|
|
.CodeMirror-linenumber {
|
|
color: #6b7280 !important;
|
|
padding: 0 8px 0 4px !important;
|
|
font-size: 12px !important;
|
|
}
|
|
|
|
.CodeMirror-activeline-background {
|
|
background: rgba(139, 92, 246, 0.1) !important;
|
|
}
|
|
|
|
.CodeMirror-selected {
|
|
background: rgba(139, 92, 246, 0.25) !important;
|
|
}
|
|
|
|
.CodeMirror-selectedtext {
|
|
background: rgba(139, 92, 246, 0.25) !important;
|
|
}
|
|
|
|
.CodeMirror-focused .CodeMirror-selected {
|
|
background: rgba(139, 92, 246, 0.3) !important;
|
|
}
|
|
|
|
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection {
|
|
background: rgba(139, 92, 246, 0.3) !important;
|
|
}
|
|
|
|
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection {
|
|
background: rgba(139, 92, 246, 0.3) !important;
|
|
}
|
|
|
|
/* Enhanced YAML Syntax Highlighting */
|
|
.cm-keyword { color: #8b5cf6 !important; font-weight: 600 !important; } /* Purple for YAML keys */
|
|
.cm-string { color: #10b981 !important; } /* Emerald for strings */
|
|
.cm-number { color: #f59e0b !important; } /* Amber for numbers */
|
|
.cm-comment { color: #6b7280 !important; font-style: italic !important; } /* Gray for comments */
|
|
.cm-property { color: #ec4899 !important; } /* Pink for properties */
|
|
.cm-operator { color: #ef4444 !important; } /* Red for operators */
|
|
.cm-variable { color: #06b6d4 !important; } /* Cyan for variables */
|
|
.cm-tag { color: #8b5cf6 !important; font-weight: 600 !important; } /* Purple for tags */
|
|
.cm-attribute { color: #f59e0b !important; } /* Amber for attributes */
|
|
.cm-def { color: #ec4899 !important; font-weight: 600 !important; } /* Pink for definitions */
|
|
.cm-bracket { color: #d1d5db !important; } /* Light gray for brackets */
|
|
.cm-punctuation { color: #d1d5db !important; } /* Light gray for punctuation */
|
|
.cm-quote { color: #10b981 !important; } /* Emerald for quotes */
|
|
.cm-meta { color: #6b7280 !important; } /* Gray for meta */
|
|
.cm-builtin { color: #f472b6 !important; } /* Pink for builtins */
|
|
.cm-atom { color: #f59e0b !important; } /* Amber for atoms like true/false/null */
|
|
|
|
/* Enhanced scrollbar styling */
|
|
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
|
background: #1f2937 !important;
|
|
}
|
|
|
|
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar {
|
|
background: #1f2937 !important;
|
|
}
|
|
|
|
.CodeMirror-vscrollbar::-webkit-scrollbar, .CodeMirror-hscrollbar::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
|
|
.CodeMirror-vscrollbar::-webkit-scrollbar-track, .CodeMirror-hscrollbar::-webkit-scrollbar-track {
|
|
background: #1f2937;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb {
|
|
background: linear-gradient(135deg, #6b7280 0%, #9ca3af 100%);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb:hover, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb:hover {
|
|
background: linear-gradient(135deg, #9ca3af 0%, #d1d5db 100%);
|
|
}
|
|
|
|
/* Focus ring styling */
|
|
.CodeMirror-focused {
|
|
outline: 2px solid rgba(139, 92, 246, 0.5) !important;
|
|
outline-offset: -2px !important;
|
|
border-radius: 0.5rem !important;
|
|
}
|
|
|
|
/* Enhanced form styling */
|
|
.form-section {
|
|
background: linear-gradient(135deg, rgba(55, 65, 81, 0.3) 0%, rgba(75, 85, 99, 0.3) 100%);
|
|
border: 1px solid rgba(107, 114, 128, 0.3);
|
|
border-radius: 1rem;
|
|
backdrop-filter: blur(8px);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.form-section:hover {
|
|
border-color: rgba(139, 92, 246, 0.3);
|
|
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.1);
|
|
}
|
|
|
|
.form-section.expanded {
|
|
border-color: rgba(139, 92, 246, 0.4);
|
|
box-shadow: 0 8px 25px rgba(139, 92, 246, 0.15);
|
|
}
|
|
|
|
.section-header {
|
|
background: linear-gradient(135deg, rgba(75, 85, 99, 0.4) 0%, rgba(107, 114, 128, 0.4) 100%);
|
|
border-bottom: 1px solid rgba(107, 114, 128, 0.3);
|
|
padding: 1rem 1.5rem;
|
|
border-radius: 1rem 1rem 0 0;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.section-header:hover {
|
|
background: linear-gradient(135deg, rgba(107, 114, 128, 0.4) 0%, rgba(139, 92, 246, 0.2) 100%);
|
|
}
|
|
|
|
.form-input {
|
|
background: linear-gradient(135deg, rgba(17, 24, 39, 0.8) 0%, rgba(31, 41, 55, 0.8) 100%) !important;
|
|
border: 1px solid rgba(107, 114, 128, 0.4) !important;
|
|
border-radius: 0.75rem !important;
|
|
padding: 0.75rem 1rem !important;
|
|
color: #e5e7eb !important;
|
|
transition: all 0.3s ease !important;
|
|
backdrop-filter: blur(4px) !important;
|
|
}
|
|
|
|
.form-input:focus {
|
|
border-color: rgba(139, 92, 246, 0.6) !important;
|
|
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1) !important;
|
|
background: linear-gradient(135deg, rgba(31, 41, 55, 0.9) 0%, rgba(55, 65, 81, 0.9) 100%) !important;
|
|
}
|
|
|
|
.form-input:hover {
|
|
border-color: rgba(139, 92, 246, 0.4) !important;
|
|
}
|
|
|
|
/* Enhanced button styling */
|
|
.action-button {
|
|
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
border-radius: 0.75rem;
|
|
padding: 0.5rem 1rem;
|
|
font-weight: 600;
|
|
transition: all 0.3s ease;
|
|
border: 1px solid rgba(139, 92, 246, 0.3);
|
|
}
|
|
|
|
.action-button:hover {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 8px 25px rgba(139, 92, 246, 0.3);
|
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
|
|
}
|
|
|
|
.danger-button {
|
|
background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
|
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
}
|
|
|
|
.danger-button:hover {
|
|
background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
|
|
box-shadow: 0 8px 25px rgba(239, 68, 68, 0.3);
|
|
}
|
|
|
|
/* 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 {
|
|
transform: translateY(-20px);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateY(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.alert-success {
|
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%);
|
|
border-color: rgba(16, 185, 129, 0.3);
|
|
color: #10b981;
|
|
}
|
|
|
|
.alert-error {
|
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%);
|
|
border-color: rgba(239, 68, 68, 0.3);
|
|
color: #ef4444;
|
|
}
|
|
|
|
.alert-warning {
|
|
background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(217, 119, 6, 0.1) 100%);
|
|
border-color: rgba(245, 158, 11, 0.3);
|
|
color: #f59e0b;
|
|
}
|
|
|
|
.alert-info {
|
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%);
|
|
border-color: rgba(59, 130, 246, 0.3);
|
|
color: #3b82f6;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
class ModelEditor {
|
|
constructor() {
|
|
this.config = {};
|
|
this.isUpdatingFromYaml = false;
|
|
this.isUpdatingFromForm = false;
|
|
this.modelName = '{{.ModelName}}';
|
|
this.isEditMode = !!this.modelName;
|
|
this.yamlEditor = null;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
{{if .ConfigYAML}}
|
|
try {
|
|
this.config = jsyaml.load(`{{.ConfigYAML}}`);
|
|
} catch (e) {
|
|
console.error('Failed to parse initial YAML:', e);
|
|
this.config = {};
|
|
}
|
|
{{else}}
|
|
this.config = this.getDefaultConfig();
|
|
{{end}}
|
|
|
|
this.generateForm();
|
|
this.initializeCodeMirror();
|
|
this.bindEvents();
|
|
}
|
|
|
|
getDefaultConfig() {
|
|
return {
|
|
name: '',
|
|
backend: '',
|
|
parameters: {
|
|
model: ''
|
|
},
|
|
template: {},
|
|
diffusers: {},
|
|
grpc: {
|
|
attempts: 1,
|
|
attempts_sleep_time: 1000
|
|
},
|
|
tts: {},
|
|
pipeline: {},
|
|
function: {},
|
|
feature_flags: {},
|
|
download_files: [],
|
|
known_usecases: [],
|
|
options: [],
|
|
overrides: [],
|
|
stopwords: [],
|
|
cutstrings: [],
|
|
extract_regex: [],
|
|
trimspace: [],
|
|
trimsuffix: [],
|
|
lora_adapters: [],
|
|
lora_scales: [],
|
|
roles: {}
|
|
};
|
|
}
|
|
|
|
generateForm() {
|
|
const form = document.getElementById('configForm');
|
|
form.innerHTML = '';
|
|
|
|
const formSections = [
|
|
{
|
|
title: 'Basic Configuration',
|
|
icon: 'fas fa-cog',
|
|
fields: [
|
|
{ key: 'name', label: 'Model Name', type: 'text', required: true, description: 'Unique identifier for this model', readonly: this.isEditMode },
|
|
{ key: 'backend', label: 'Backend', type: 'text', required: true,
|
|
description: 'Backend engine to use (e.g., llama-cpp, transformers, diffusers, whisper, piper, bark-cpp, vllm, rerankers, exllama2, faster-whisper, stablediffusion-ggml)' },
|
|
{ key: 'parameters.model', label: 'Model File/Path', type: 'text', required: true, description: 'Path to model file or URL' },
|
|
{ key: 'description', label: 'Description', type: 'textarea', description: 'Description of the model' },
|
|
{ key: 'usage', label: 'Usage Instructions', type: 'textarea', description: 'How to use this model' }
|
|
]
|
|
},
|
|
{
|
|
title: 'Model Parameters',
|
|
icon: 'fas fa-sliders-h',
|
|
collapsible: true,
|
|
fields: [
|
|
{ key: 'parameters.temperature', label: 'Temperature', type: 'number', step: 0.01, min: 0, max: 2, description: 'Controls randomness (0-2)' },
|
|
{ key: 'parameters.top_p', label: 'Top P', type: 'number', step: 0.01, min: 0, max: 1, description: 'Nucleus sampling (0-1)' },
|
|
{ key: 'parameters.top_k', label: 'Top K', type: 'number', min: 1, description: 'Top-k sampling' },
|
|
{ key: 'parameters.max_tokens', label: 'Max Tokens', type: 'number', min: 1, description: 'Maximum tokens to generate' },
|
|
{ key: 'parameters.seed', label: 'Seed', type: 'number', description: 'Random seed for reproducible results' },
|
|
{ key: 'parameters.repeat_penalty', label: 'Repeat Penalty', type: 'number', step: 0.01, min: 0, description: 'Penalty for repetition' },
|
|
{ key: 'parameters.frequency_penalty', label: 'Frequency Penalty', type: 'number', step: 0.01, description: 'Frequency-based penalty' },
|
|
{ key: 'parameters.presence_penalty', label: 'Presence Penalty', type: 'number', step: 0.01, description: 'Presence-based penalty' },
|
|
{ key: 'parameters.n', label: 'N (Number of results)', type: 'number', min: 1, description: 'Number of results to return' },
|
|
{ key: 'parameters.echo', label: 'Echo', type: 'checkbox', description: 'Echo back the prompt' },
|
|
{ key: 'parameters.batch', label: 'Batch Size', type: 'number', min: 1, description: 'Batch size for processing' },
|
|
{ key: 'parameters.ignore_eos', label: 'Ignore EOS', type: 'checkbox', description: 'Ignore end-of-sequence tokens' },
|
|
{ key: 'parameters.repeat_last_n', label: 'Repeat Last N', type: 'number', min: 0, description: 'Consider last N tokens for repeat penalty' },
|
|
{ key: 'parameters.n_keep', label: 'N Keep', type: 'number', min: 0, description: 'Number of tokens to keep from prompt' },
|
|
{ key: 'parameters.tfz', label: 'TFZ (Tail Free Sampling)', type: 'number', step: 0.01, min: 0, max: 1, description: 'Tail free sampling parameter' },
|
|
{ key: 'parameters.typical_p', label: 'Typical P', type: 'number', step: 0.01, min: 0, max: 1, description: 'Typical sampling parameter' },
|
|
{ key: 'parameters.negative_prompt', label: 'Negative Prompt', type: 'textarea', description: 'Negative prompt for image generation' },
|
|
{ key: 'parameters.rope_freq_base', label: 'RoPE Freq Base', type: 'number', step: 0.01, description: 'RoPE frequency base' },
|
|
{ key: 'parameters.rope_freq_scale', label: 'RoPE Freq Scale', type: 'number', step: 0.01, description: 'RoPE frequency scale' },
|
|
{ key: 'parameters.negative_prompt_scale', label: 'Negative Prompt Scale', type: 'number', step: 0.01, description: 'Scale for negative prompt' },
|
|
{ key: 'parameters.clip_skip', label: 'CLIP Skip', type: 'number', min: 0, description: 'CLIP layers to skip' },
|
|
{ key: 'parameters.language', label: 'Language', type: 'text', description: 'Language for transcription/TTS' },
|
|
{ key: 'parameters.translate', label: 'Translate', type: 'checkbox', description: 'Enable translation for audio transcription' },
|
|
{ key: 'parameters.tokenizer', label: 'Tokenizer', type: 'text', description: 'Tokenizer to use' }
|
|
]
|
|
},
|
|
{
|
|
title: 'Advanced Configuration',
|
|
icon: 'fas fa-tools',
|
|
collapsible: true,
|
|
fields: [
|
|
{ key: 'f16', label: 'Use F16', type: 'checkbox', description: 'Use 16-bit floating point precision' },
|
|
{ key: 'threads', label: 'Threads', type: 'number', min: 1, description: 'Number of CPU threads to use' },
|
|
{ key: 'debug', label: 'Debug Mode', type: 'checkbox', description: 'Enable debug logging' },
|
|
{ key: 'embeddings', label: 'Embeddings Mode', type: 'checkbox', description: 'Enable embeddings generation' },
|
|
{ key: 'cuda', label: 'Enable CUDA', type: 'checkbox', description: 'Use CUDA acceleration' },
|
|
{ key: 'mmap', label: 'Memory Mapping', type: 'checkbox', description: 'Use memory mapping for model loading' },
|
|
{ key: 'mmlock', label: 'Memory Lock', type: 'checkbox', description: 'Lock model in memory' },
|
|
{ key: 'low_vram', label: 'Low VRAM Mode', type: 'checkbox', description: 'Optimize for low VRAM usage' },
|
|
{ key: 'numa', label: 'NUMA', type: 'checkbox', description: 'Enable NUMA support' },
|
|
{ key: 'context_size', label: 'Context Size', type: 'number', min: 1, description: 'Maximum context size' },
|
|
{ key: 'gpu_layers', label: 'GPU Layers', type: 'number', min: 0, description: 'Number of layers to offload to GPU' },
|
|
{ key: 'reranking', label: 'Reranking', type: 'checkbox', description: 'Enable reranking' }
|
|
]
|
|
},
|
|
{
|
|
title: 'Templates',
|
|
icon: 'fas fa-file-alt',
|
|
collapsible: true,
|
|
fields: [
|
|
{ key: 'template.chat', label: 'Chat Template', type: 'textarea', description: 'Template for chat completion' },
|
|
{ key: 'template.completion', label: 'Completion Template', type: 'textarea', description: 'Template for completion requests' },
|
|
{ key: 'template.edit', label: 'Edit Template', type: 'textarea', description: 'Template for edit requests' }
|
|
]
|
|
},
|
|
{
|
|
title: 'Use Cases',
|
|
icon: 'fas fa-tags',
|
|
collapsible: true,
|
|
fields: [
|
|
{ key: 'known_usecases', label: 'Known Use Cases', type: 'multiselect',
|
|
options: ['chat', 'completion', 'edit', 'embeddings', 'rerank', 'image', 'transcript', 'tts', 'sound_generation', 'tokenize', 'vad', 'video', 'detection'],
|
|
description: 'Supported model capabilities' }
|
|
]
|
|
},
|
|
{
|
|
title: 'LLM Configuration',
|
|
icon: 'fas fa-brain',
|
|
collapsible: true,
|
|
fields: [
|
|
{ key: 'system_prompt', label: 'System Prompt', type: 'textarea', description: 'System prompt for the model' },
|
|
{ key: 'tensor_split', label: 'Tensor Split', type: 'text', description: 'GPU tensor split configuration' },
|
|
{ key: 'main_gpu', label: 'Main GPU', type: 'text', description: 'Primary GPU to use' },
|
|
{ key: 'mirostat', label: 'Mirostat', type: 'number', min: 0, max: 2, description: 'Mirostat sampling mode (0=disabled, 1=mirostat, 2=mirostat2)' },
|
|
{ key: 'mirostat_eta', label: 'Mirostat ETA', type: 'number', step: 0.01, description: 'Mirostat learning rate' },
|
|
{ key: 'mirostat_tau', label: 'Mirostat TAU', type: 'number', step: 0.01, description: 'Mirostat target entropy' },
|
|
{ key: 'lora_adapter', label: 'LoRA Adapter', type: 'text', description: 'Path to LoRA adapter' },
|
|
{ key: 'lora_base', label: 'LoRA Base', type: 'text', description: 'Base model for LoRA' },
|
|
{ key: 'lora_scale', label: 'LoRA Scale', type: 'number', step: 0.01, description: 'LoRA scaling factor' },
|
|
{ key: 'lora_adapters', label: 'LoRA Adapters', type: 'array', description: 'Multiple LoRA adapters' },
|
|
{ key: 'lora_scales', label: 'LoRA Scales', type: 'array', description: 'Scaling factors for multiple LoRA adapters' },
|
|
{ key: 'rope_scaling', label: 'RoPE Scaling', type: 'text', description: 'RoPE position scaling method' },
|
|
{ key: 'yarn_ext_factor', label: 'YARN Ext Factor', type: 'number', step: 0.01, description: 'YARN extension factor' },
|
|
{ key: 'yarn_attn_factor', label: 'YARN Attn Factor', type: 'number', step: 0.01, description: 'YARN attention factor' },
|
|
{ key: 'yarn_beta_fast', label: 'YARN Beta Fast', type: 'number', step: 0.01, description: 'YARN beta fast parameter' },
|
|
{ key: 'yarn_beta_slow', label: 'YARN Beta Slow', type: 'number', step: 0.01, description: 'YARN beta slow parameter' },
|
|
{ key: 'cfg_scale', label: 'CFG Scale', type: 'number', step: 0.01, description: 'Classifier-free guidance scale' },
|
|
{ key: 'grammar', label: 'Grammar', type: 'textarea', description: 'Grammar constraints for generation' },
|
|
{ key: 'quantization', label: 'Quantization', type: 'text', description: 'Quantization method (e.g., q4_0, q8_0)' },
|
|
{ key: 'load_format', label: 'Load Format', type: 'text', description: 'Model loading format' },
|
|
{ key: 'type', label: 'Model Type', type: 'text', description: 'Model architecture type' }
|
|
]
|
|
},
|
|
{
|
|
title: 'vLLM Configuration',
|
|
icon: 'fas fa-server',
|
|
collapsible: true,
|
|
fields: [
|
|
{ key: 'gpu_memory_utilization', label: 'GPU Memory Utilization', type: 'number', step: 0.01, min: 0, max: 1, description: 'GPU memory utilization ratio (0-1)' },
|
|
{ key: 'trust_remote_code', label: 'Trust Remote Code', type: 'checkbox', description: 'Trust remote code execution' },
|
|
{ key: 'enforce_eager', label: 'Enforce Eager', type: 'checkbox', description: 'Enforce eager execution' },
|
|
{ key: 'swap_space', label: 'Swap Space', type: 'number', min: 0, description: 'Swap space in GB' },
|
|
{ key: 'max_model_len', label: 'Max Model Length', type: 'number', min: 1, description: 'Maximum model context length' },
|
|
{ key: 'tensor_parallel_size', label: 'Tensor Parallel Size', type: 'number', min: 1, description: 'Number of GPUs for tensor parallelism' },
|
|
{ key: 'disable_log_stats', label: 'Disable Log Stats', type: 'checkbox', description: 'Disable logging of statistics' },
|
|
{ key: 'dtype', label: 'Data Type', type: 'text', description: 'Model data type (e.g., float16, bfloat16)' }
|
|
]
|
|
},
|
|
{
|
|
title: 'Text Processing',
|
|
icon: 'fas fa-text-width',
|
|
collapsible: true,
|
|
fields: [
|
|
{ key: 'stopwords', label: 'Stop Words', type: 'array', description: 'List of stop words' },
|
|
{ key: 'cutstrings', label: 'Cut Strings', type: 'array', description: 'Strings to cut from output' },
|
|
{ key: 'extract_regex', label: 'Extract Regex', type: 'array', description: 'Regex patterns for extraction' },
|
|
{ key: 'trimspace', label: 'Trim Space', type: 'array', description: 'Characters to trim' },
|
|
{ key: 'trimsuffix', label: 'Trim Suffix', type: 'array', description: 'Suffixes to trim' },
|
|
{ key: 'options', label: 'Options', type: 'array', description: 'Additional options' },
|
|
{ key: 'roles', label: 'Roles', type: 'keyvalue', description: 'Role mappings' }
|
|
]
|
|
}
|
|
];
|
|
|
|
formSections.forEach(section => {
|
|
const sectionEl = this.createFormSection(section);
|
|
form.appendChild(sectionEl);
|
|
});
|
|
}
|
|
|
|
createFormSection(section) {
|
|
const sectionDiv = document.createElement('div');
|
|
sectionDiv.className = 'form-section';
|
|
|
|
const isCollapsible = section.collapsible;
|
|
const sectionId = section.title.toLowerCase().replace(/\s+/g, '-');
|
|
|
|
sectionDiv.innerHTML = `
|
|
<div class="section-header ${isCollapsible ? 'cursor-pointer' : ''}" ${isCollapsible ? `onclick="this.nextElementSibling.classList.toggle('hidden'); this.querySelector('.collapse-icon').classList.toggle('rotate-180'); this.closest('.form-section').classList.toggle('expanded')"` : ''}>
|
|
<h3 class="text-lg font-semibold text-white flex items-center gap-3">
|
|
<div class="w-8 h-8 rounded-lg bg-violet-500/20 flex items-center justify-center">
|
|
<i class="${section.icon} text-violet-400"></i>
|
|
</div>
|
|
${section.title}
|
|
${isCollapsible ? '<i class="fas fa-chevron-down text-sm ml-auto collapse-icon transition-transform duration-300"></i>' : ''}
|
|
</h3>
|
|
</div>
|
|
<div class="p-6 space-y-6 ${isCollapsible ? 'hidden' : ''}" id="${sectionId}-content">
|
|
${section.fields.map(field => this.createFormField(field)).join('')}
|
|
</div>
|
|
`;
|
|
|
|
return sectionDiv;
|
|
}
|
|
|
|
createFormField(field) {
|
|
const value = this.getNestedValue(this.config, field.key) || '';
|
|
const fieldId = field.key.replace(/\./g, '_');
|
|
|
|
let inputHtml = '';
|
|
|
|
switch (field.type) {
|
|
case 'text':
|
|
const readonlyAttr = field.readonly ? 'readonly' : '';
|
|
const readonlyClass = field.readonly ? 'bg-gray-700 cursor-not-allowed opacity-60' : '';
|
|
inputHtml = `<input type="text" id="${fieldId}" name="${field.key}" value="${value}" class="form-input w-full ${readonlyClass}" ${field.required ? 'required' : ''} ${readonlyAttr}>`;
|
|
break;
|
|
|
|
case 'textarea':
|
|
inputHtml = `<textarea id="${fieldId}" name="${field.key}" rows="3" class="form-input w-full resize-vertical">${value}</textarea>`;
|
|
break;
|
|
|
|
case 'number':
|
|
const step = field.step || '1';
|
|
const min = field.min !== undefined ? `min="${field.min}"` : '';
|
|
const max = field.max !== undefined ? `max="${field.max}"` : '';
|
|
inputHtml = `<input type="number" id="${fieldId}" name="${field.key}" value="${value}" step="${step}" ${min} ${max} class="form-input w-full">`;
|
|
break;
|
|
|
|
case 'checkbox':
|
|
const checked = value === true || value === 'true' ? 'checked' : '';
|
|
inputHtml = `
|
|
<div class="flex items-center">
|
|
<input type="checkbox" id="${fieldId}" name="${field.key}" ${checked} class="w-5 h-5 text-violet-600 bg-gray-800 border-gray-600 rounded focus:ring-violet-500 focus:ring-2">
|
|
<label for="${fieldId}" class="ml-3 text-sm text-gray-300 font-medium">Enable</label>
|
|
</div>`;
|
|
break;
|
|
|
|
case 'select':
|
|
const options = field.options.map(opt => `<option value="${opt}" ${value === opt ? 'selected' : ''}>${opt || '(Select backend)'}</option>`).join('');
|
|
inputHtml = `<select id="${fieldId}" name="${field.key}" class="form-input w-full" ${field.required ? 'required' : ''}>${options}</select>`;
|
|
break;
|
|
|
|
case 'multiselect':
|
|
const currentValues = Array.isArray(value) ? value : [];
|
|
const checkboxes = field.options.map(opt => {
|
|
const isChecked = currentValues.includes(opt);
|
|
return `
|
|
<div class="flex items-center p-2 rounded-lg hover:bg-violet-500/10 transition-colors">
|
|
<input type="checkbox" id="${fieldId}_${opt}" name="${field.key}" value="${opt}" ${isChecked ? 'checked' : ''} class="w-4 h-4 text-violet-600 bg-gray-800 border-gray-600 rounded focus:ring-violet-500 focus:ring-2">
|
|
<label for="${fieldId}_${opt}" class="ml-3 text-sm text-gray-300 font-medium">${opt}</label>
|
|
</div>`;
|
|
}).join('');
|
|
inputHtml = `<div class="space-y-2 max-h-40 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-600 scrollbar-track-gray-800 p-2 border border-gray-600/50 rounded-lg bg-gray-800/30">${checkboxes}</div>`;
|
|
break;
|
|
|
|
case 'array':
|
|
const arrayValues = Array.isArray(value) ? value : [];
|
|
inputHtml = `
|
|
<div class="space-y-3">
|
|
<div id="${fieldId}_container" class="space-y-2">
|
|
${arrayValues.map((item, index) => `
|
|
<div class="flex gap-2">
|
|
<input type="text" value="${item}" class="form-input flex-1" onchange="modelEditor.updateArrayField('${field.key}', ${index}, this.value)">
|
|
<button type="button" onclick="modelEditor.removeArrayItem('${field.key}', ${index})" class="danger-button px-3 py-2 text-white">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
<button type="button" onclick="modelEditor.addArrayItem('${field.key}')" class="action-button px-4 py-2 text-white text-sm">
|
|
<i class="fas fa-plus mr-2"></i> Add Item
|
|
</button>
|
|
</div>`;
|
|
break;
|
|
|
|
case 'keyvalue':
|
|
const kvPairs = typeof value === 'object' && value !== null ? value : {};
|
|
const kvEntries = Object.entries(kvPairs);
|
|
inputHtml = `
|
|
<div class="space-y-3">
|
|
<div id="${fieldId}_container" class="space-y-2">
|
|
${kvEntries.map(([key, val], index) => `
|
|
<div class="flex gap-2">
|
|
<input type="text" value="${key}" placeholder="Key" class="form-input flex-1" onchange="modelEditor.updateKeyValueField('${field.key}', ${index}, this.value, this.nextElementSibling.value, 'key')">
|
|
<input type="text" value="${val}" placeholder="Value" class="form-input flex-1" onchange="modelEditor.updateKeyValueField('${field.key}', ${index}, this.previousElementSibling.value, this.value, 'value')">
|
|
<button type="button" onclick="modelEditor.removeKeyValueItem('${field.key}', ${index})" class="danger-button px-3 py-2 text-white">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
<button type="button" onclick="modelEditor.addKeyValueItem('${field.key}')" class="action-button px-4 py-2 text-white text-sm">
|
|
<i class="fas fa-plus mr-2"></i> Add Pair
|
|
</button>
|
|
</div>`;
|
|
break;
|
|
}
|
|
|
|
return `
|
|
<div class="space-y-3">
|
|
<label for="${fieldId}" class="block text-sm font-semibold text-gray-300">
|
|
${field.label}
|
|
${field.required ? '<span class="text-red-400 ml-1">*</span>' : ''}
|
|
</label>
|
|
${inputHtml}
|
|
${field.description ? `<p class="text-xs text-gray-500 leading-relaxed">${field.description}</p>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
initializeCodeMirror() {
|
|
this.yamlEditor = CodeMirror(document.getElementById('yamlCodeMirror'), {
|
|
mode: 'yaml',
|
|
theme: 'default',
|
|
lineNumbers: true,
|
|
autoRefresh: true,
|
|
indentUnit: 2,
|
|
tabSize: 2,
|
|
indentWithTabs: false,
|
|
lineWrapping: false,
|
|
styleActiveLine: true,
|
|
matchBrackets: true,
|
|
autoCloseBrackets: true,
|
|
value: {{if .ConfigYAML}}`{{.ConfigYAML}}`{{else}}'# Configuration will appear here...'{{end}}
|
|
});
|
|
|
|
// Update config when YAML changes
|
|
this.yamlEditor.on('change', () => {
|
|
if (!this.isUpdatingFromForm) {
|
|
this.updateConfigFromYaml();
|
|
}
|
|
});
|
|
|
|
// If no initial YAML, set the default config
|
|
{{if not .ConfigYAML}}
|
|
setTimeout(() => {
|
|
this.updateYamlEditor();
|
|
}, 100);
|
|
{{end}}
|
|
}
|
|
|
|
configToYaml() {
|
|
try {
|
|
return jsyaml.dump(this.config, {
|
|
indent: 2,
|
|
lineWidth: 120,
|
|
noRefs: true,
|
|
sortKeys: false
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to generate YAML:', error);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
bindEvents() {
|
|
// Form change events
|
|
document.getElementById('configForm').addEventListener('change', (e) => {
|
|
if (!this.isUpdatingFromYaml) {
|
|
this.updateConfigFromForm();
|
|
}
|
|
});
|
|
|
|
document.getElementById('configForm').addEventListener('input', (e) => {
|
|
if (!this.isUpdatingFromYaml && e.target.type !== 'checkbox') {
|
|
this.updateConfigFromForm();
|
|
}
|
|
});
|
|
|
|
// Button events
|
|
document.getElementById('saveBtn').addEventListener('click', () => this.saveConfig());
|
|
document.getElementById('validateBtn').addEventListener('click', () => this.validateConfig());
|
|
document.getElementById('resetFormBtn').addEventListener('click', () => this.resetForm());
|
|
document.getElementById('expandAllBtn').addEventListener('click', () => this.expandAllSections());
|
|
document.getElementById('formatYamlBtn').addEventListener('click', () => this.formatYaml());
|
|
document.getElementById('copyYamlBtn').addEventListener('click', () => this.copyYaml());
|
|
}
|
|
|
|
|
|
|
|
updateConfigFromForm() {
|
|
this.isUpdatingFromForm = true;
|
|
|
|
const formData = new FormData(document.getElementById('configForm'));
|
|
const newConfig = {};
|
|
|
|
for (let [key, value] of formData.entries()) {
|
|
this.setNestedValue(newConfig, key, value);
|
|
}
|
|
|
|
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
|
|
checkboxes.forEach(checkbox => {
|
|
if (checkbox.name && !checkbox.name.includes('known_usecases')) {
|
|
this.setNestedValue(newConfig, checkbox.name, checkbox.checked);
|
|
}
|
|
});
|
|
|
|
const multiselectContainers = document.querySelectorAll('input[type="checkbox"][name*="known_usecases"]');
|
|
const usecases = [];
|
|
multiselectContainers.forEach(checkbox => {
|
|
if (checkbox.checked) {
|
|
usecases.push(checkbox.value);
|
|
}
|
|
});
|
|
if (usecases.length > 0) {
|
|
newConfig.known_usecases = usecases;
|
|
}
|
|
|
|
this.config = this.deepMerge(this.config, newConfig);
|
|
|
|
this.updateYamlEditor();
|
|
this.isUpdatingFromForm = false;
|
|
}
|
|
|
|
updateConfigFromYaml() {
|
|
if (this.isUpdatingFromForm) return;
|
|
|
|
this.isUpdatingFromYaml = true;
|
|
|
|
try {
|
|
const yamlContent = this.yamlEditor.getValue();
|
|
this.config = jsyaml.load(yamlContent) || {};
|
|
this.updateFormFromConfig();
|
|
this.clearAlert();
|
|
} catch (error) {
|
|
this.showAlert('error', 'YAML Syntax Error: ' + error.message);
|
|
}
|
|
|
|
this.isUpdatingFromYaml = false;
|
|
}
|
|
|
|
updateFormFromConfig() {
|
|
const form = document.getElementById('configForm');
|
|
const inputs = form.querySelectorAll('input, textarea, select');
|
|
|
|
inputs.forEach(input => {
|
|
if (input.name && !input.name.includes('known_usecases')) {
|
|
const value = this.getNestedValue(this.config, input.name);
|
|
|
|
if (input.type === 'checkbox') {
|
|
input.checked = value === true || value === 'true';
|
|
} else {
|
|
input.value = value || '';
|
|
}
|
|
}
|
|
});
|
|
|
|
this.updateMultiselectFromConfig();
|
|
this.updateArraysFromConfig();
|
|
this.updateKeyValueFromConfig();
|
|
}
|
|
|
|
updateMultiselectFromConfig() {
|
|
const usecases = this.config.known_usecases || [];
|
|
const checkboxes = document.querySelectorAll('input[type="checkbox"][name="known_usecases"]');
|
|
checkboxes.forEach(checkbox => {
|
|
checkbox.checked = usecases.includes(checkbox.value);
|
|
});
|
|
}
|
|
|
|
updateArraysFromConfig() {
|
|
const arrayFields = ['stopwords', 'cutstrings', 'extract_regex', 'trimspace', 'trimsuffix', 'options', 'lora_adapters', 'lora_scales'];
|
|
|
|
arrayFields.forEach(field => {
|
|
const container = document.getElementById(field.replace(/\./g, '_') + '_container');
|
|
if (container) {
|
|
const values = this.getNestedValue(this.config, field) || [];
|
|
this.regenerateArrayField(field, values);
|
|
}
|
|
});
|
|
}
|
|
|
|
updateKeyValueFromConfig() {
|
|
const kvFields = ['roles'];
|
|
|
|
kvFields.forEach(field => {
|
|
const container = document.getElementById(field.replace(/\./g, '_') + '_container');
|
|
if (container) {
|
|
const values = this.getNestedValue(this.config, field) || {};
|
|
this.regenerateKeyValueField(field, values);
|
|
}
|
|
});
|
|
}
|
|
|
|
updateYamlEditor() {
|
|
if (!this.isUpdatingFromForm) return;
|
|
|
|
try {
|
|
const yamlContent = jsyaml.dump(this.config, {
|
|
indent: 2,
|
|
lineWidth: 120,
|
|
noRefs: true,
|
|
sortKeys: false
|
|
});
|
|
this.yamlEditor.setValue(yamlContent);
|
|
} catch (error) {
|
|
console.error('Failed to generate YAML:', error);
|
|
}
|
|
}
|
|
|
|
// Array manipulation methods
|
|
addArrayItem(fieldKey) {
|
|
const values = this.getNestedValue(this.config, fieldKey) || [];
|
|
values.push('');
|
|
this.setNestedValue(this.config, fieldKey, values);
|
|
this.regenerateArrayField(fieldKey, values);
|
|
this.updateYamlEditor();
|
|
}
|
|
|
|
removeArrayItem(fieldKey, index) {
|
|
const values = this.getNestedValue(this.config, fieldKey) || [];
|
|
values.splice(index, 1);
|
|
this.setNestedValue(this.config, fieldKey, values);
|
|
this.regenerateArrayField(fieldKey, values);
|
|
this.updateYamlEditor();
|
|
}
|
|
|
|
updateArrayField(fieldKey, index, value) {
|
|
const values = this.getNestedValue(this.config, fieldKey) || [];
|
|
values[index] = value;
|
|
this.setNestedValue(this.config, fieldKey, values);
|
|
this.updateYamlEditor();
|
|
}
|
|
|
|
regenerateArrayField(fieldKey, values) {
|
|
const fieldId = fieldKey.replace(/\./g, '_');
|
|
const container = document.getElementById(fieldId + '_container');
|
|
if (!container) return;
|
|
|
|
container.innerHTML = values.map((item, index) => `
|
|
<div class="flex gap-2">
|
|
<input type="text" value="${item}" class="form-input flex-1" onchange="modelEditor.updateArrayField('${fieldKey}', ${index}, this.value)">
|
|
<button type="button" onclick="modelEditor.removeArrayItem('${fieldKey}', ${index})" class="danger-button px-3 py-2 text-white">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Key-value manipulation methods
|
|
addKeyValueItem(fieldKey) {
|
|
const values = this.getNestedValue(this.config, fieldKey) || {};
|
|
const newKey = 'new_key_' + Object.keys(values).length;
|
|
values[newKey] = '';
|
|
this.setNestedValue(this.config, fieldKey, values);
|
|
this.regenerateKeyValueField(fieldKey, values);
|
|
this.updateYamlEditor();
|
|
}
|
|
|
|
removeKeyValueItem(fieldKey, index) {
|
|
const values = this.getNestedValue(this.config, fieldKey) || {};
|
|
const keys = Object.keys(values);
|
|
delete values[keys[index]];
|
|
this.setNestedValue(this.config, fieldKey, values);
|
|
this.regenerateKeyValueField(fieldKey, values);
|
|
this.updateYamlEditor();
|
|
}
|
|
|
|
updateKeyValueField(fieldKey, index, key, value, changeType) {
|
|
const values = this.getNestedValue(this.config, fieldKey) || {};
|
|
const keys = Object.keys(values);
|
|
const oldKey = keys[index];
|
|
|
|
if (changeType === 'key' && key !== oldKey) {
|
|
delete values[oldKey];
|
|
values[key] = value;
|
|
} else {
|
|
values[key] = value;
|
|
}
|
|
|
|
this.setNestedValue(this.config, fieldKey, values);
|
|
this.updateYamlEditor();
|
|
}
|
|
|
|
regenerateKeyValueField(fieldKey, values) {
|
|
const fieldId = fieldKey.replace(/\./g, '_');
|
|
const container = document.getElementById(fieldId + '_container');
|
|
if (!container) return;
|
|
|
|
const entries = Object.entries(values);
|
|
container.innerHTML = entries.map(([key, val], index) => `
|
|
<div class="flex gap-2">
|
|
<input type="text" value="${key}" placeholder="Key" class="form-input flex-1" onchange="modelEditor.updateKeyValueField('${fieldKey}', ${index}, this.value, this.nextElementSibling.value, 'key')">
|
|
<input type="text" value="${val}" placeholder="Value" class="form-input flex-1" onchange="modelEditor.updateKeyValueField('${fieldKey}', ${index}, this.previousElementSibling.value, this.value, 'value')">
|
|
<button type="button" onclick="modelEditor.removeKeyValueItem('${fieldKey}', ${index})" class="danger-button px-3 py-2 text-white">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Utility methods
|
|
getNestedValue(obj, path) {
|
|
return path.split('.').reduce((current, key) => current && current[key], obj);
|
|
}
|
|
|
|
setNestedValue(obj, path, value) {
|
|
const keys = path.split('.');
|
|
const lastKey = keys.pop();
|
|
const target = keys.reduce((current, key) => {
|
|
if (!current[key] || typeof current[key] !== 'object') {
|
|
current[key] = {};
|
|
}
|
|
return current[key];
|
|
}, obj);
|
|
|
|
if (value === 'true') value = true;
|
|
else if (value === 'false') value = false;
|
|
else if (value === '' || value === null) value = undefined;
|
|
else if (!isNaN(value) && !isNaN(parseFloat(value)) && value !== '') {
|
|
value = parseFloat(value);
|
|
}
|
|
|
|
if (value !== undefined) {
|
|
target[lastKey] = value;
|
|
} else {
|
|
delete target[lastKey];
|
|
}
|
|
}
|
|
|
|
deepMerge(target, source) {
|
|
const result = { ...target };
|
|
|
|
for (const key in source) {
|
|
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
result[key] = this.deepMerge(result[key] || {}, source[key]);
|
|
} else {
|
|
result[key] = source[key];
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Action methods
|
|
validateConfig() {
|
|
try {
|
|
const yamlContent = this.yamlEditor.getValue();
|
|
jsyaml.load(yamlContent);
|
|
|
|
if (!this.config.name) {
|
|
throw new Error('Model name is required');
|
|
}
|
|
if (!this.config.backend) {
|
|
throw new Error('Backend is required');
|
|
}
|
|
if (!this.config.parameters || !this.config.parameters.model) {
|
|
throw new Error('Model file/path is required');
|
|
}
|
|
|
|
this.showAlert('success', 'Configuration is valid!');
|
|
} catch (error) {
|
|
this.showAlert('error', 'Validation failed: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async saveConfig() {
|
|
try {
|
|
this.validateConfig();
|
|
|
|
const yamlContent = this.yamlEditor.getValue();
|
|
const endpoint = this.isEditMode ? `/models/edit/${this.modelName}` : '/models/import';
|
|
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-yaml',
|
|
},
|
|
body: yamlContent
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
this.showAlert('success', result.message || (this.isEditMode ? 'Model updated successfully!' : 'Model created successfully!'));
|
|
if (!this.isEditMode && this.config.name) {
|
|
setTimeout(() => {
|
|
window.location.href = `/models/edit/${this.config.name}`;
|
|
}, 2000);
|
|
}
|
|
} else {
|
|
this.showAlert('error', result.error || 'Failed to save configuration');
|
|
}
|
|
} catch (error) {
|
|
this.showAlert('error', 'Failed to save: ' + error.message);
|
|
}
|
|
}
|
|
|
|
resetForm() {
|
|
if (confirm('Are you sure you want to reset the form? All changes will be lost.')) {
|
|
// Set the flag to allow YAML editor updates
|
|
this.isUpdatingFromForm = true;
|
|
|
|
{{if .ConfigYAML}}
|
|
try {
|
|
this.config = jsyaml.load(`{{.ConfigYAML}}`);
|
|
} catch (e) {
|
|
this.config = this.getDefaultConfig();
|
|
}
|
|
{{else}}
|
|
this.config = this.getDefaultConfig();
|
|
{{end}}
|
|
|
|
this.updateFormFromConfig();
|
|
this.updateYamlEditor();
|
|
|
|
// Reset the flag
|
|
this.isUpdatingFromForm = false;
|
|
|
|
this.showAlert('info', 'Form has been reset');
|
|
}
|
|
}
|
|
|
|
expandAllSections() {
|
|
const collapsibleSections = document.querySelectorAll('.collapse-icon');
|
|
collapsibleSections.forEach(icon => {
|
|
const content = icon.closest('div').nextElementSibling;
|
|
if (content.classList.contains('hidden')) {
|
|
content.classList.remove('hidden');
|
|
icon.classList.add('rotate-180');
|
|
icon.closest('.form-section').classList.add('expanded');
|
|
}
|
|
});
|
|
}
|
|
|
|
formatYaml() {
|
|
try {
|
|
const yamlContent = this.yamlEditor.getValue();
|
|
const parsed = jsyaml.load(yamlContent);
|
|
const formatted = jsyaml.dump(parsed, {
|
|
indent: 2,
|
|
lineWidth: 120,
|
|
noRefs: true,
|
|
sortKeys: true
|
|
});
|
|
this.yamlEditor.setValue(formatted);
|
|
this.showAlert('success', 'YAML formatted successfully');
|
|
} catch (error) {
|
|
this.showAlert('error', 'Failed to format YAML: ' + error.message);
|
|
}
|
|
}
|
|
|
|
copyYaml() {
|
|
const yamlContent = this.yamlEditor.getValue();
|
|
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'
|
|
};
|
|
|
|
const alertIcons = {
|
|
success: 'fas fa-check-circle',
|
|
error: 'fas fa-exclamation-triangle',
|
|
warning: 'fas fa-exclamation-circle',
|
|
info: 'fas fa-info-circle'
|
|
};
|
|
|
|
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>
|
|
`;
|
|
|
|
if (type === 'success' || type === 'info') {
|
|
setTimeout(() => {
|
|
const alert = container.querySelector('div');
|
|
if (alert) alert.remove();
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
clearAlert() {
|
|
document.getElementById('alertContainer').innerHTML = '';
|
|
}
|
|
}
|
|
|
|
// Initialize the editor when the page loads
|
|
let modelEditor;
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
modelEditor = new ModelEditor();
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|