mirror of
https://github.com/mudler/LocalAI.git
synced 2025-12-30 09:59:36 -05:00
588 lines
28 KiB
HTML
588 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
{{template "views/partials/head" .}}
|
|
|
|
<body class="bg-[#101827] text-[#E5E7EB]">
|
|
<div class="flex flex-col min-h-screen">
|
|
|
|
{{template "views/partials/navbar" .}}
|
|
|
|
<!-- Main Content - ChatGPT-style minimal interface -->
|
|
<div class="flex-1 flex flex-col items-center justify-center px-4 py-12">
|
|
<div class="w-full max-w-3xl mx-auto">
|
|
{{ if eq (len .ModelsConfig) 0 }}
|
|
<!-- No Models - Wizard Guide -->
|
|
<div class="bg-[#1E293B] border border-[#8B5CF6]/20 rounded-xl p-12">
|
|
<div class="text-center max-w-4xl mx-auto">
|
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[#8B5CF6]/10 border border-[#8B5CF6]/20 mb-6">
|
|
<i class="text-[#8B5CF6] text-2xl fas fa-robot"></i>
|
|
</div>
|
|
<h2 class="text-3xl md:text-4xl font-bold text-[#E5E7EB] mb-4">
|
|
<span class="bg-clip-text text-transparent bg-gradient-to-r from-[#38BDF8] to-[#8B5CF6]">
|
|
No Models Installed
|
|
</span>
|
|
</h2>
|
|
<p class="text-xl text-[#94A3B8] mb-8">
|
|
Get started with LocalAI by installing your first model. Choose from our gallery, import your own, or use the API to download models.
|
|
</p>
|
|
|
|
<!-- Features Preview -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-10">
|
|
<div class="bg-[#101827] border border-[#38BDF8]/20 rounded-lg p-4">
|
|
<div class="w-10 h-10 bg-blue-500/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
|
<i class="fas fa-images text-[#38BDF8] text-xl"></i>
|
|
</div>
|
|
<h3 class="text-sm font-semibold text-[#E5E7EB] mb-2">Model Gallery</h3>
|
|
<p class="text-xs text-[#94A3B8]">Browse and install pre-configured models</p>
|
|
</div>
|
|
<div class="bg-[#101827] border border-[#8B5CF6]/20 rounded-lg p-4">
|
|
<div class="w-10 h-10 bg-purple-500/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
|
<i class="fas fa-upload text-[#8B5CF6] text-xl"></i>
|
|
</div>
|
|
<h3 class="text-sm font-semibold text-[#E5E7EB] mb-2">Import Models</h3>
|
|
<p class="text-xs text-[#94A3B8]">Upload your own model files</p>
|
|
</div>
|
|
<div class="bg-[#101827] border border-green-500/20 rounded-lg p-4">
|
|
<div class="w-10 h-10 bg-green-500/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
|
<i class="fas fa-code text-green-400 text-xl"></i>
|
|
</div>
|
|
<h3 class="text-sm font-semibold text-[#E5E7EB] mb-2">API Download</h3>
|
|
<p class="text-xs text-[#94A3B8]">Use the API to download models programmatically</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Setup Instructions -->
|
|
<div class="bg-[#101827] border border-[#8B5CF6]/20 rounded-xl p-6 mb-8 text-left">
|
|
<h3 class="text-lg font-bold text-[#E5E7EB] mb-4 flex items-center">
|
|
<i class="fas fa-rocket text-[#8B5CF6] mr-2"></i>
|
|
How to Get Started
|
|
</h3>
|
|
<div class="space-y-4">
|
|
<div class="flex items-start">
|
|
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-[#8B5CF6]/20 flex items-center justify-center mr-3 mt-0.5">
|
|
<span class="text-[#8B5CF6] font-bold text-sm">1</span>
|
|
</div>
|
|
<div class="flex-1">
|
|
<p class="text-[#E5E7EB] font-medium mb-2">Browse the Model Gallery</p>
|
|
<p class="text-[#94A3B8] text-sm">Explore our curated collection of pre-configured models. Find models for chat, image generation, audio processing, and more.</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-start">
|
|
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-[#8B5CF6]/20 flex items-center justify-center mr-3 mt-0.5">
|
|
<span class="text-[#8B5CF6] font-bold text-sm">2</span>
|
|
</div>
|
|
<div class="flex-1">
|
|
<p class="text-[#E5E7EB] font-medium mb-2">Install a Model</p>
|
|
<p class="text-[#94A3B8] text-sm">Click on a model from the gallery to install it, or use the import feature to upload your own model files.</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-start">
|
|
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-[#8B5CF6]/20 flex items-center justify-center mr-3 mt-0.5">
|
|
<span class="text-[#8B5CF6] font-bold text-sm">3</span>
|
|
</div>
|
|
<div class="flex-1">
|
|
<p class="text-[#E5E7EB] font-medium mb-2">Start Chatting</p>
|
|
<p class="text-[#94A3B8] text-sm">Once installed, return to this page to start chatting with your model or use the API to interact programmatically.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap justify-center gap-4">
|
|
<a href="/browse/"
|
|
class="inline-flex items-center bg-[#8B5CF6] hover:bg-[#8B5CF6]/90 text-white py-3 px-6 rounded-lg font-semibold transition-colors">
|
|
<i class="fas fa-images mr-2"></i>
|
|
Browse Model Gallery
|
|
</a>
|
|
<a href="/import-model"
|
|
class="inline-flex items-center bg-[#38BDF8] hover:bg-[#38BDF8]/90 text-white py-3 px-6 rounded-lg font-semibold transition-colors">
|
|
<i class="fas fa-upload mr-2"></i>
|
|
Import Model
|
|
</a>
|
|
<a href="https://localai.io/basics/getting_started/" target="_blank"
|
|
class="inline-flex items-center bg-[#1E293B] hover:bg-[#1E293B]/80 border border-[#8B5CF6]/20 text-[#E5E7EB] py-3 px-6 rounded-lg font-semibold transition-colors">
|
|
<i class="fas fa-graduation-cap mr-2"></i>
|
|
Getting Started
|
|
<i class="fas fa-external-link-alt ml-2 text-sm"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ else }}
|
|
<!-- Welcome Message -->
|
|
<div class="text-center mb-12">
|
|
<div class="mb-6 flex justify-center">
|
|
<img src="static/logo.png" alt="LocalAI Logo" class="h-24 md:h-32 drop-shadow-[0_0_15px_rgba(56,189,248,0.3)]">
|
|
</div>
|
|
|
|
<p class="text-lg text-[#94A3B8]">How can I help you today?</p>
|
|
</div>
|
|
|
|
<!-- Chat Input Form -->
|
|
<div class="mb-8" x-data="{
|
|
selectedModel: '',
|
|
inputValue: '',
|
|
shiftPressed: false,
|
|
fileName: '',
|
|
imageFiles: [],
|
|
audioFiles: [],
|
|
textFiles: [],
|
|
currentPlaceholder: 'Send a message...',
|
|
placeholderIndex: 0,
|
|
charIndex: 0,
|
|
isTyping: false,
|
|
typingTimeout: null,
|
|
displayTimeout: null,
|
|
placeholderMessages: [
|
|
'What is Nuclear fusion?',
|
|
'How does a combustion engine work?',
|
|
'Explain quantum computing',
|
|
'What causes climate change?',
|
|
'How do neural networks learn?',
|
|
'What is the theory of relativity?',
|
|
'How does photosynthesis work?',
|
|
'Explain the water cycle',
|
|
'What is machine learning?',
|
|
'How do black holes form?',
|
|
'What is DNA and how does it work?',
|
|
'Explain the greenhouse effect',
|
|
'How does the immune system work?',
|
|
'What is artificial intelligence?',
|
|
'How do solar panels generate electricity?',
|
|
'Explain the process of evolution',
|
|
'What is the difference between weather and climate?',
|
|
'How does the human brain process information?',
|
|
'What is the structure of an atom?',
|
|
'How do vaccines work?',
|
|
'Explain the concept of entropy',
|
|
'What is the speed of light?',
|
|
'How does gravity work?',
|
|
'What is the difference between mass and weight?'
|
|
],
|
|
init() {
|
|
window.currentPlaceholderText = this.currentPlaceholder;
|
|
this.startTypingAnimation();
|
|
// Select first model by default
|
|
this.$nextTick(() => {
|
|
const select = this.$el.querySelector('select');
|
|
if (select && select.options.length > 1) {
|
|
// Skip the first option (disabled placeholder) and select the first real option
|
|
const firstModelOption = select.options[1];
|
|
if (firstModelOption && firstModelOption.value) {
|
|
this.selectedModel = firstModelOption.value;
|
|
}
|
|
}
|
|
});
|
|
},
|
|
startTypingAnimation() {
|
|
if (this.isTyping) return;
|
|
this.typeNextPlaceholder();
|
|
},
|
|
typeNextPlaceholder() {
|
|
if (this.isTyping) return;
|
|
this.isTyping = true;
|
|
this.charIndex = 0;
|
|
const message = this.placeholderMessages[this.placeholderIndex];
|
|
this.currentPlaceholder = '';
|
|
window.currentPlaceholderText = '';
|
|
|
|
const typeChar = () => {
|
|
if (this.charIndex < message.length) {
|
|
this.currentPlaceholder = message.substring(0, this.charIndex + 1);
|
|
window.currentPlaceholderText = this.currentPlaceholder;
|
|
this.charIndex++;
|
|
this.typingTimeout = setTimeout(typeChar, 30);
|
|
} else {
|
|
// Finished typing, wait 2 seconds then move to next
|
|
this.isTyping = false;
|
|
window.currentPlaceholderText = this.currentPlaceholder;
|
|
this.displayTimeout = setTimeout(() => {
|
|
this.placeholderIndex = (this.placeholderIndex + 1) % this.placeholderMessages.length;
|
|
this.typeNextPlaceholder();
|
|
}, 2000);
|
|
}
|
|
};
|
|
|
|
typeChar();
|
|
},
|
|
pauseTyping() {
|
|
if (this.typingTimeout) {
|
|
clearTimeout(this.typingTimeout);
|
|
this.typingTimeout = null;
|
|
}
|
|
if (this.displayTimeout) {
|
|
clearTimeout(this.displayTimeout);
|
|
this.displayTimeout = null;
|
|
}
|
|
this.isTyping = false;
|
|
},
|
|
resumeTyping() {
|
|
if (!this.inputValue.trim() && !this.isTyping) {
|
|
this.startTypingAnimation();
|
|
}
|
|
},
|
|
handleFocus() {
|
|
// Complete the current placeholder instantly if typing
|
|
if (this.isTyping && this.placeholderIndex < this.placeholderMessages.length) {
|
|
const fullMessage = this.placeholderMessages[this.placeholderIndex];
|
|
this.currentPlaceholder = fullMessage;
|
|
window.currentPlaceholderText = fullMessage;
|
|
}
|
|
this.pauseTyping();
|
|
},
|
|
handleBlur() {
|
|
if (!this.inputValue.trim()) {
|
|
this.resumeTyping();
|
|
}
|
|
},
|
|
handleInput() {
|
|
if (this.inputValue.trim()) {
|
|
this.pauseTyping();
|
|
} else {
|
|
this.resumeTyping();
|
|
}
|
|
}
|
|
}">
|
|
<!-- Model Selector -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-[#94A3B8] mb-2">Select Model</label>
|
|
<select
|
|
x-model="selectedModel"
|
|
class="w-full bg-[#1E293B] text-[#E5E7EB] border border-[#38BDF8]/20 focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50 rounded-lg p-3 appearance-none"
|
|
required
|
|
>
|
|
<option value="" disabled class="text-[#94A3B8]">Select a model to chat with...</option>
|
|
{{ range .ModelsConfig }}
|
|
{{ $cfg := . }}
|
|
{{ range .KnownUsecaseStrings }}
|
|
{{ if eq . "FLAG_CHAT" }}
|
|
<option value="{{$cfg.Name}}" class="bg-[#1E293B] text-[#E5E7EB]">{{$cfg.Name}}</option>
|
|
{{ end }}
|
|
{{ end }}
|
|
{{ end }}
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Input Bar -->
|
|
<form @submit.prevent="startChat($event)" class="relative w-full">
|
|
<div class="relative w-full bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl focus-within:ring-2 focus-within:ring-[#38BDF8]/50 focus-within:border-[#38BDF8] transition-all duration-200">
|
|
<textarea
|
|
x-model="inputValue"
|
|
:placeholder="currentPlaceholder"
|
|
class="p-3 pr-16 w-full bg-[#1E293B] text-[#E5E7EB] placeholder-[#94A3B8] focus:outline-none resize-none border-0 rounded-xl transition-colors duration-200"
|
|
required
|
|
@keydown.shift="shiftPressed = true"
|
|
@keyup.shift="shiftPressed = false"
|
|
@keydown.enter.prevent="if (!shiftPressed && selectedModel && (inputValue.trim() || currentPlaceholder.trim())) { startChat($event); }"
|
|
@focus="handleFocus()"
|
|
@blur="handleBlur()"
|
|
@input="handleInput()"
|
|
rows="2"
|
|
></textarea>
|
|
<span x-show="fileName" x-text="fileName" class="absolute right-16 top-3 text-[#94A3B8] text-xs mr-2"></span>
|
|
|
|
<!-- Attachment Buttons -->
|
|
<button
|
|
type="button"
|
|
@click="document.getElementById('index_input_image').click()"
|
|
class="fa-solid fa-image text-[#94A3B8] absolute right-12 top-3 text-base p-1.5 hover:text-[#38BDF8] transition-colors duration-200"
|
|
title="Attach images"
|
|
></button>
|
|
<button
|
|
type="button"
|
|
@click="document.getElementById('index_input_audio').click()"
|
|
class="fa-solid fa-microphone text-[#94A3B8] absolute right-20 top-3 text-base p-1.5 hover:text-[#38BDF8] transition-colors duration-200"
|
|
title="Attach an audio file"
|
|
></button>
|
|
<button
|
|
type="button"
|
|
@click="document.getElementById('index_input_file').click()"
|
|
class="fa-solid fa-file text-[#94A3B8] absolute right-28 top-3 text-base p-1.5 hover:text-[#38BDF8] transition-colors duration-200"
|
|
title="Upload text, markdown or PDF file"
|
|
></button>
|
|
|
|
<!-- Send Button -->
|
|
<button
|
|
type="submit"
|
|
:disabled="!selectedModel || (!inputValue.trim() && !currentPlaceholder.trim())"
|
|
:class="!selectedModel || (!inputValue.trim() && !currentPlaceholder.trim()) ? 'opacity-50 cursor-not-allowed' : ''"
|
|
class="text-lg p-2 text-[#94A3B8] hover:text-[#38BDF8] transition-colors duration-200 absolute right-3 top-3"
|
|
title="Send message (Enter)"
|
|
>
|
|
<i class="fa-solid fa-paper-plane"></i>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Hidden File Inputs -->
|
|
<input
|
|
id="index_input_image"
|
|
type="file"
|
|
multiple
|
|
accept="image/*"
|
|
style="display: none;"
|
|
@change="imageFiles = Array.from($event.target.files); fileName = imageFiles.length > 0 ? imageFiles.length + ' image(s) selected' : ''"
|
|
/>
|
|
<input
|
|
id="index_input_audio"
|
|
type="file"
|
|
multiple
|
|
accept="audio/*"
|
|
style="display: none;"
|
|
@change="audioFiles = Array.from($event.target.files); fileName = audioFiles.length > 0 ? audioFiles.length + ' audio file(s) selected' : ''"
|
|
/>
|
|
<input
|
|
id="index_input_file"
|
|
type="file"
|
|
multiple
|
|
accept=".txt,.md,.pdf"
|
|
style="display: none;"
|
|
@change="textFiles = Array.from($event.target.files); fileName = textFiles.length > 0 ? textFiles.length + ' file(s) selected' : ''"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Quick Links -->
|
|
<div class="flex flex-wrap justify-center gap-3 mb-8">
|
|
<a href="/manage"
|
|
class="inline-flex items-center text-sm text-[#94A3B8] hover:text-[#E5E7EB] px-4 py-2 rounded-lg hover:bg-[#1E293B] transition-colors">
|
|
<i class="fas fa-cog mr-2"></i>
|
|
Installed Models and Backends
|
|
</a>
|
|
<a href="/import-model" class="inline-flex items-center text-sm text-[#94A3B8] hover:text-[#E5E7EB] px-4 py-2 rounded-lg hover:bg-[#1E293B] transition-colors">
|
|
<i class="fas fa-upload mr-2"></i>
|
|
Import Model
|
|
</a>
|
|
<a href="/browse/"
|
|
class="inline-flex items-center text-sm text-[#94A3B8] hover:text-[#E5E7EB] px-4 py-2 rounded-lg hover:bg-[#1E293B] transition-colors">
|
|
<i class="fas fa-images mr-2"></i>
|
|
Browse Gallery
|
|
</a>
|
|
<a href="https://localai.io" target="_blank"
|
|
class="inline-flex items-center text-sm text-[#94A3B8] hover:text-[#E5E7EB] px-4 py-2 rounded-lg hover:bg-[#1E293B] transition-colors">
|
|
<i class="fas fa-book mr-2"></i>
|
|
Documentation
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Model Status Summary - Subtle -->
|
|
{{ $loadedModels := .LoadedModels }}
|
|
<div class="mb-8 flex items-center justify-center gap-2 text-xs text-[#94A3B8]"
|
|
x-data="{ stoppingAll: false, stopAllModels() { window.stopAllModels(this); }, stopModel(name) { window.stopModel(name); }, getLoadedCount() { return document.querySelectorAll('[data-loaded-model]').length; } }"
|
|
x-show="getLoadedCount() > 0"
|
|
style="display: none;">
|
|
<span class="flex items-center gap-1.5">
|
|
<i class="fas fa-circle text-green-500 text-[10px]"></i>
|
|
<span x-text="`${getLoadedCount()} model(s) loaded`"></span>
|
|
</span>
|
|
<span class="text-[#38BDF8]/40">•</span>
|
|
{{ range .ModelsConfig }}
|
|
{{ if index $loadedModels .Name }}
|
|
<span class="inline-flex items-center gap-1 text-[#94A3B8] hover:text-[#E5E7EB] transition-colors" data-loaded-model>
|
|
<span class="truncate max-w-[100px]">{{.Name}}</span>
|
|
<button
|
|
@click="stopModel('{{.Name}}')"
|
|
class="text-red-400/60 hover:text-red-400 transition-colors ml-0.5"
|
|
title="Stop {{.Name}}"
|
|
>
|
|
<i class="fas fa-times text-[10px]"></i>
|
|
</button>
|
|
</span>
|
|
{{ end }}
|
|
{{ end }}
|
|
<span class="text-[#38BDF8]/40">•</span>
|
|
<button
|
|
@click="stopAllModels()"
|
|
:disabled="stoppingAll"
|
|
:class="stoppingAll ? 'opacity-50 cursor-not-allowed' : ''"
|
|
class="text-red-400/60 hover:text-red-400 transition-colors text-xs"
|
|
title="Stop all loaded models"
|
|
>
|
|
<span x-text="stoppingAll ? 'Stopping...' : 'Stop all'"></span>
|
|
</button>
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
|
|
{{template "views/partials/footer" .}}
|
|
</div>
|
|
|
|
<script>
|
|
// Handle form submission - redirect to chat with message
|
|
function startChat(event) {
|
|
if (event) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
// Get form data directly from form elements (Alpine x-model binds to value)
|
|
const form = event ? event.target.closest('form') : document.querySelector('form');
|
|
if (!form) return;
|
|
|
|
const alpineComponent = form.closest('[x-data]');
|
|
const select = alpineComponent ? alpineComponent.querySelector('select') : null;
|
|
const textarea = form.querySelector('textarea');
|
|
|
|
const selectedModel = select ? select.value : '';
|
|
let message = textarea ? textarea.value : '';
|
|
|
|
// If message is empty, use the current placeholder text
|
|
if (!message.trim() && window.currentPlaceholderText) {
|
|
message = window.currentPlaceholderText;
|
|
}
|
|
|
|
if (!selectedModel || !message.trim()) {
|
|
return;
|
|
}
|
|
|
|
// Store message and files in localStorage for chat page to pick up
|
|
const chatData = {
|
|
message: message,
|
|
imageFiles: [],
|
|
audioFiles: [],
|
|
textFiles: []
|
|
};
|
|
|
|
// Convert files to base64 for storage
|
|
const imageInput = document.getElementById('index_input_image');
|
|
const audioInput = document.getElementById('index_input_audio');
|
|
const fileInput = document.getElementById('index_input_file');
|
|
|
|
const filePromises = [
|
|
...Array.from(imageInput.files || []).map(file =>
|
|
new Promise(resolve => {
|
|
const reader = new FileReader();
|
|
reader.onload = e => resolve({ name: file.name, data: e.target.result, type: file.type });
|
|
reader.readAsDataURL(file);
|
|
})
|
|
),
|
|
...Array.from(audioInput.files || []).map(file =>
|
|
new Promise(resolve => {
|
|
const reader = new FileReader();
|
|
reader.onload = e => resolve({ name: file.name, data: e.target.result, type: file.type });
|
|
reader.readAsDataURL(file);
|
|
})
|
|
),
|
|
...Array.from(fileInput.files || []).map(file =>
|
|
new Promise(resolve => {
|
|
const reader = new FileReader();
|
|
reader.onload = e => resolve({ name: file.name, data: e.target.result, type: file.type });
|
|
reader.readAsText(file);
|
|
})
|
|
)
|
|
];
|
|
|
|
if (filePromises.length > 0) {
|
|
Promise.all(filePromises).then(files => {
|
|
files.forEach(file => {
|
|
if (file.type.startsWith('image/')) {
|
|
chatData.imageFiles.push(file);
|
|
} else if (file.type.startsWith('audio/')) {
|
|
chatData.audioFiles.push(file);
|
|
} else {
|
|
chatData.textFiles.push(file);
|
|
}
|
|
});
|
|
|
|
// Store in localStorage
|
|
localStorage.setItem('localai_index_chat_data', JSON.stringify(chatData));
|
|
|
|
// Redirect to chat page
|
|
window.location.href = `/chat/${selectedModel}`;
|
|
}).catch(err => {
|
|
console.error('Error processing files:', err);
|
|
// Still redirect even if file processing fails
|
|
localStorage.setItem('localai_index_chat_data', JSON.stringify({ message: message, imageFiles: [], audioFiles: [], textFiles: [] }));
|
|
window.location.href = `/chat/${selectedModel}`;
|
|
});
|
|
} else {
|
|
// No files, just store message and redirect
|
|
localStorage.setItem('localai_index_chat_data', JSON.stringify(chatData));
|
|
window.location.href = `/chat/${selectedModel}`;
|
|
}
|
|
}
|
|
|
|
// Make startChat available globally
|
|
window.startChat = startChat;
|
|
|
|
// Stop individual model
|
|
async function stopModel(modelName) {
|
|
if (!confirm(`Are you sure you want to stop "${modelName}"?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/backend/shutdown', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ model: modelName })
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Reload page after short delay to reflect changes
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 500);
|
|
} else {
|
|
alert('Failed to stop model');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error stopping model:', error);
|
|
alert('Failed to stop model');
|
|
}
|
|
}
|
|
|
|
// Stop all loaded models
|
|
async function stopAllModels(component) {
|
|
const loadedModelNamesStr = '{{ $loadedModels := .LoadedModels }}{{ range .ModelsConfig }}{{ if index $loadedModels .Name }}{{.Name}},{{ end }}{{ end }}';
|
|
const loadedModelNames = loadedModelNamesStr.split(',').filter(name => name.length > 0);
|
|
|
|
if (loadedModelNames.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (!confirm(`Are you sure you want to stop all ${loadedModelNames.length} loaded model(s)?`)) {
|
|
return;
|
|
}
|
|
|
|
// Set loading state
|
|
if (component) {
|
|
component.stoppingAll = true;
|
|
}
|
|
|
|
try {
|
|
// Stop all models in parallel
|
|
const stopPromises = loadedModelNames.map(modelName =>
|
|
fetch('/backend/shutdown', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ model: modelName })
|
|
})
|
|
);
|
|
|
|
await Promise.all(stopPromises);
|
|
|
|
// Reload page after short delay to reflect changes
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1000);
|
|
} catch (error) {
|
|
console.error('Error stopping models:', error);
|
|
alert('Failed to stop some models');
|
|
if (component) {
|
|
component.stoppingAll = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make functions available globally for Alpine.js
|
|
window.stopModel = stopModel;
|
|
window.stopAllModels = stopAllModels;
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|