| | <!DOCTYPE html> |
| | <html lang="en"> |
| | {{template "views/partials/head" .}} |
| |
|
| | <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]"> |
| | <div class="flex flex-col min-h-screen"> |
| |
|
| | {{template "views/partials/navbar" .}} |
| |
|
| | |
| | <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 }} |
| | |
| | <div class="hero-section"> |
| | <div class="hero-content"> |
| | <h2 class="hero-title"> |
| | No Models Installed |
| | </h2> |
| | <p class="hero-subtitle"> |
| | Get started with LocalAI by installing your first model. Choose from our gallery, import your own, or use the API to download models. |
| | </p> |
| | </div> |
| | </div> |
| | |
| | |
| | <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"> |
| | <div class="card card-animate"> |
| | <div class="w-10 h-10 bg-[var(--color-primary-light)] rounded-lg flex items-center justify-center mx-auto mb-3"> |
| | <i class="fas fa-images text-[var(--color-primary)] text-xl"></i> |
| | </div> |
| | <h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">Model Gallery</h3> |
| | <p class="text-xs text-[var(--color-text-secondary)]">Browse and install pre-configured models</p> |
| | </div> |
| | <div class="card card-animate"> |
| | <div class="w-10 h-10 bg-[var(--color-accent-light)] rounded-lg flex items-center justify-center mx-auto mb-3"> |
| | <i class="fas fa-upload text-[var(--color-accent)] text-xl"></i> |
| | </div> |
| | <h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">Import Models</h3> |
| | <p class="text-xs text-[var(--color-text-secondary)]">Upload your own model files</p> |
| | </div> |
| | <div class="card card-animate"> |
| | <div class="w-10 h-10 bg-[var(--color-success-light)] rounded-lg flex items-center justify-center mx-auto mb-3"> |
| | <i class="fas fa-code text-[var(--color-success)] text-xl"></i> |
| | </div> |
| | <h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">API Download</h3> |
| | <p class="text-xs text-[var(--color-text-secondary)]">Use the API to download models programmatically</p> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div class="card mb-6 text-left"> |
| | <h3 class="text-lg font-bold text-[var(--color-text-primary)] mb-4 flex items-center"> |
| | <i class="fas fa-rocket text-[var(--color-accent)] 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-[var(--color-accent-light)] flex items-center justify-center mr-3 mt-0.5"> |
| | <span class="text-[var(--color-accent)] font-bold text-sm">1</span> |
| | </div> |
| | <div class="flex-1"> |
| | <p class="text-[var(--color-text-primary)] font-medium mb-2">Browse the Model Gallery</p> |
| | <p class="text-[var(--color-text-secondary)] 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-[var(--color-accent-light)] flex items-center justify-center mr-3 mt-0.5"> |
| | <span class="text-[var(--color-accent)] font-bold text-sm">2</span> |
| | </div> |
| | <div class="flex-1"> |
| | <p class="text-[var(--color-text-primary)] font-medium mb-2">Install a Model</p> |
| | <p class="text-[var(--color-text-secondary)] 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-[var(--color-accent-light)] flex items-center justify-center mr-3 mt-0.5"> |
| | <span class="text-[var(--color-accent)] font-bold text-sm">3</span> |
| | </div> |
| | <div class="flex-1"> |
| | <p class="text-[var(--color-text-primary)] font-medium mb-2">Start Chatting</p> |
| | <p class="text-[var(--color-text-secondary)] 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 mb-8"> |
| | <a href="/browse/" class="btn-primary"> |
| | <i class="fas fa-images mr-2"></i> |
| | Browse Model Gallery |
| | </a> |
| | <a href="/import-model" class="btn-primary"> |
| | <i class="fas fa-upload mr-2"></i> |
| | Import Model |
| | </a> |
| | <a href="https://localai.io/basics/getting_started/" target="_blank" class="btn-secondary"> |
| | <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> |
| | {{ else }} |
| | |
| | <div class="hero-section"> |
| | <div class="hero-content"> |
| | <div class="mb-4 flex justify-center"> |
| | <img src="static/logo.png" alt="LocalAI Logo" class="h-16 md:h-20"> |
| | </div> |
| | <h1 class="hero-title">How can I help you today?</h1> |
| | <p class="hero-subtitle">Ask me anything, and I'll do my best to assist you.</p> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div class="mb-8" x-data="{ |
| | selectedModel: '', |
| | inputValue: '', |
| | shiftPressed: false, |
| | fileName: '', |
| | imageFiles: [], |
| | audioFiles: [], |
| | textFiles: [], |
| | attachedFiles: [], |
| | mcpMode: false, |
| | mcpAvailable: false, |
| | mcpModels: {}, |
| | 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(); |
| | // Build MCP models map from data attributes |
| | this.buildMCPModelsMap(); |
| | // 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; |
| | this.checkMCPAvailability(); |
| | } |
| | } |
| | }); |
| | // Watch for changes to selectedModel to update MCP availability |
| | this.$watch('selectedModel', () => { |
| | this.checkMCPAvailability(); |
| | }); |
| | }, |
| | buildMCPModelsMap() { |
| | const select = this.$el.querySelector('select'); |
| | if (!select) return; |
| | this.mcpModels = {}; |
| | for (let i = 0; i < select.options.length; i++) { |
| | const option = select.options[i]; |
| | if (option.value) { |
| | const hasMcpAttr = option.getAttribute('data-has-mcp'); |
| | this.mcpModels[option.value] = hasMcpAttr === 'true'; |
| | } |
| | } |
| | // Debug: uncomment to see the MCP models map |
| | // console.log('MCP Models Map:', this.mcpModels); |
| | }, |
| | checkMCPAvailability() { |
| | if (!this.selectedModel) { |
| | this.mcpAvailable = false; |
| | this.mcpMode = false; |
| | return; |
| | } |
| | // Check MCP availability from the map |
| | const hasMCP = this.mcpModels[this.selectedModel] === true; |
| | this.mcpAvailable = hasMCP; |
| | // Debug: uncomment to see what's happening |
| | // console.log('MCP Check:', { model: this.selectedModel, hasMCP, mcpAvailable: this.mcpAvailable, map: this.mcpModels }); |
| | if (!hasMCP) { |
| | this.mcpMode = false; |
| | } |
| | }, |
| | 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(); |
| | } |
| | }, |
| | handleFileSelection(files, fileType) { |
| | Array.from(files).forEach(file => { |
| | // Check if file already exists |
| | const exists = this.attachedFiles.some(f => f.name === file.name && f.type === fileType); |
| | if (!exists) { |
| | this.attachedFiles.push({ name: file.name, type: fileType }); |
| | } |
| | }); |
| | }, |
| | removeAttachedFile(fileType, fileName) { |
| | // Remove from attachedFiles array |
| | const index = this.attachedFiles.findIndex(f => f.name === fileName && f.type === fileType); |
| | if (index !== -1) { |
| | this.attachedFiles.splice(index, 1); |
| | } |
| | // Remove from corresponding file array |
| | if (fileType === 'image') { |
| | this.imageFiles = this.imageFiles.filter(f => f.name !== fileName); |
| | } else if (fileType === 'audio') { |
| | this.audioFiles = this.audioFiles.filter(f => f.name !== fileName); |
| | } else if (fileType === 'file') { |
| | this.textFiles = this.textFiles.filter(f => f.name !== fileName); |
| | } |
| | } |
| | }"> |
| | |
| | <div class="mb-4"> |
| | <label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">Select Model</label> |
| | <div class="flex items-center gap-3"> |
| | <select |
| | x-model="selectedModel" |
| | @change="$nextTick(() => checkMCPAvailability())" |
| | class="input flex-1" |
| | required |
| | > |
| | <option value="" disabled class="text-[var(--color-text-secondary)]">Select a model to chat with...</option> |
| | {{ range .ModelsConfig }} |
| | {{ $cfg := . }} |
| | {{ $hasMCP := or (ne $cfg.MCP.Servers "") (ne $cfg.MCP.Stdio "") }} |
| | {{ range .KnownUsecaseStrings }} |
| | {{ if eq . "FLAG_CHAT" }} |
| | <option value="{{$cfg.Name}}" data-has-mcp="{{if $hasMCP}}true{{else}}false{{end}}" class="bg-[var(--color-bg-secondary)] text-[var(--color-text-primary)]">{{$cfg.Name}}</option> |
| | {{ end }} |
| | {{ end }} |
| | {{ end }} |
| | </select> |
| | |
| | |
| | <div |
| | x-show="mcpAvailable" |
| | class="flex items-center gap-2 px-3 py-2 text-xs rounded text-[var(--color-text-primary)] bg-[var(--color-bg-secondary)] border border-[var(--color-primary-border)] whitespace-nowrap"> |
| | <i class="fa-solid fa-plug text-[var(--color-primary)] text-sm"></i> |
| | <span class="text-[var(--color-text-secondary)]">MCP</span> |
| | <label class="relative inline-flex items-center cursor-pointer ml-1"> |
| | <input type="checkbox" id="index_mcp_toggle" class="sr-only peer" x-model="mcpMode"> |
| | <div class="w-9 h-5 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-[var(--color-primary-border)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-bg-secondary)] after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-[var(--color-primary)]"></div> |
| | </label> |
| | </div> |
| | </div> |
| | |
| | |
| | <div |
| | x-show="mcpMode && mcpAvailable" |
| | class="mt-2 p-2 bg-[var(--color-primary-light)] border border-[var(--color-primary-border)] rounded text-[var(--color-text-secondary)] text-xs"> |
| | <div class="flex items-start space-x-2"> |
| | <i class="fa-solid fa-info-circle text-[var(--color-primary)] mt-0.5 text-xs"></i> |
| | <p class="text-[var(--color-text-secondary)]">Non-streaming mode active. Responses may take longer to process.</p> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <form @submit.prevent="startChat($event)" class="relative w-full"> |
| | |
| | <div x-show="attachedFiles.length > 0" class="mb-3 flex flex-wrap gap-2 items-center"> |
| | <template x-for="(file, index) in attachedFiles" :key="index"> |
| | <div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm bg-[var(--color-primary-light)] border border-[var(--color-primary-border)] text-[var(--color-text-primary)]"> |
| | <i :class="file.type === 'image' ? 'fa-solid fa-image' : file.type === 'audio' ? 'fa-solid fa-microphone' : 'fa-solid fa-file'" class="text-[var(--color-primary)]"></i> |
| | <span x-text="file.name" class="max-w-[200px] truncate"></span> |
| | <button |
| | type="button" |
| | @click="attachedFiles.splice(index, 1); removeAttachedFile(file.type, file.name)" |
| | class="ml-1 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors" |
| | title="Remove attachment" |
| | > |
| | <i class="fa-solid fa-times text-xs"></i> |
| | </button> |
| | </div> |
| | </template> |
| | </div> |
| |
|
| | <div class="relative w-full"> |
| | <textarea |
| | x-model="inputValue" |
| | :placeholder="currentPlaceholder" |
| | class="input p-3 pr-16 w-full resize-none border-0" |
| | 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> |
| | |
| | |
| | <button |
| | type="button" |
| | @click="document.getElementById('index_input_image').click()" |
| | class="fa-solid fa-image text-[var(--color-text-secondary)] absolute right-12 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200" |
| | title="Attach images" |
| | ></button> |
| | <button |
| | type="button" |
| | @click="document.getElementById('index_input_audio').click()" |
| | class="fa-solid fa-microphone text-[var(--color-text-secondary)] absolute right-20 top-3 text-base p-1.5 hover:text-[var(--color-primary)] 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-[var(--color-text-secondary)] absolute right-28 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200" |
| | title="Upload text, markdown or PDF file" |
| | ></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-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors duration-200 absolute right-3 top-3" |
| | title="Send message (Enter)" |
| | > |
| | <i class="fa-solid fa-paper-plane"></i> |
| | </button> |
| | </div> |
| | </form> |
| |
|
| | |
| | <input |
| | id="index_input_image" |
| | type="file" |
| | multiple |
| | accept="image/*" |
| | style="display: none;" |
| | @change="imageFiles = Array.from($event.target.files); handleFileSelection($event.target.files, 'image')" |
| | /> |
| | <input |
| | id="index_input_audio" |
| | type="file" |
| | multiple |
| | accept="audio/*" |
| | style="display: none;" |
| | @change="audioFiles = Array.from($event.target.files); handleFileSelection($event.target.files, 'audio')" |
| | /> |
| | <input |
| | id="index_input_file" |
| | type="file" |
| | multiple |
| | accept=".txt,.md,.pdf" |
| | style="display: none;" |
| | @change="textFiles = Array.from($event.target.files); handleFileSelection($event.target.files, 'file')" |
| | /> |
| | </div> |
| |
|
| | |
| | <div class="flex flex-wrap justify-center gap-3 mb-8"> |
| | <a href="/manage" class="btn-tertiary"> |
| | <i class="fas fa-cog mr-2"></i> |
| | Installed Models and Backends |
| | </a> |
| | <a href="/import-model" class="btn-tertiary"> |
| | <i class="fas fa-upload mr-2"></i> |
| | Import Model |
| | </a> |
| | <a href="/browse/" class="btn-tertiary"> |
| | <i class="fas fa-images mr-2"></i> |
| | Browse Gallery |
| | </a> |
| | <a href="https://localai.io" target="_blank" class="btn-tertiary"> |
| | <i class="fas fa-book mr-2"></i> |
| | Documentation |
| | </a> |
| | </div> |
| |
|
| | |
| | <div class="mb-4" x-data="resourceMonitor()" x-init="startPolling()"> |
| | <template x-if="resourceData && resourceData.available"> |
| | <div class="flex items-center justify-center gap-3 text-xs text-[var(--color-text-secondary)]"> |
| | <div class="flex items-center gap-2 px-3 py-1.5 rounded-full bg-[var(--color-bg-secondary)] border border-[var(--color-primary-border)]/20"> |
| | <i :class="resourceData.type === 'gpu' ? 'fas fa-microchip' : 'fas fa-memory'" |
| | :class="resourceData.aggregate.usage_percent > 90 ? 'text-red-400' : resourceData.aggregate.usage_percent > 70 ? 'text-yellow-400' : 'text-green-400'"></i> |
| | <span class="text-[var(--color-text-secondary)]" x-text="resourceData.type === 'gpu' ? 'GPU' : 'RAM'"></span> |
| | <span class="font-mono" |
| | :class="resourceData.aggregate.usage_percent > 90 ? 'text-red-400' : resourceData.aggregate.usage_percent > 70 ? 'text-yellow-400' : 'text-green-400'" |
| | x-text="`${resourceData.aggregate.usage_percent.toFixed(0)}%`"></span> |
| | <div class="w-16 bg-[var(--color-bg-primary)] rounded-full h-1.5 overflow-hidden"> |
| | <div class="h-full rounded-full transition-all duration-300" |
| | :class="resourceData.aggregate.usage_percent > 90 ? 'bg-red-500' : resourceData.aggregate.usage_percent > 70 ? 'bg-yellow-500' : 'bg-[var(--color-success)]'" |
| | :style="`width: ${resourceData.aggregate.usage_percent}%`"></div> |
| | </div> |
| | </div> |
| | </div> |
| | </template> |
| | </div> |
| |
|
| | |
| | {{ $loadedModels := .LoadedModels }} |
| | <div class="mb-8 flex items-center justify-center gap-2 text-xs text-[var(--color-text-secondary)]" |
| | 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-[var(--color-primary)] opacity-40">•</span> |
| | {{ range .ModelsConfig }} |
| | {{ if index $loadedModels .Name }} |
| | <span class="inline-flex items-center gap-1 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] 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-[var(--color-primary)] opacity-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> |
| | |
| | function startChat(event) { |
| | if (event) { |
| | event.preventDefault(); |
| | } |
| | |
| | |
| | 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.trim() && window.currentPlaceholderText) { |
| | message = window.currentPlaceholderText; |
| | } |
| | |
| | if (!selectedModel || !message.trim()) { |
| | return; |
| | } |
| | |
| | |
| | let mcpMode = false; |
| | const mcpToggle = document.getElementById('index_mcp_toggle'); |
| | if (mcpToggle && mcpToggle.checked) { |
| | mcpMode = true; |
| | } |
| | |
| | |
| | const chatData = { |
| | message: message, |
| | imageFiles: [], |
| | audioFiles: [], |
| | textFiles: [], |
| | mcpMode: mcpMode |
| | }; |
| | |
| | |
| | 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); |
| | } |
| | }); |
| | |
| | |
| | localStorage.setItem('localai_index_chat_data', JSON.stringify(chatData)); |
| | |
| | |
| | window.location.href = `/chat/${selectedModel}`; |
| | }).catch(err => { |
| | console.error('Error processing files:', err); |
| | |
| | localStorage.setItem('localai_index_chat_data', JSON.stringify({ message: message, imageFiles: [], audioFiles: [], textFiles: [] })); |
| | window.location.href = `/chat/${selectedModel}`; |
| | }); |
| | } else { |
| | |
| | localStorage.setItem('localai_index_chat_data', JSON.stringify(chatData)); |
| | window.location.href = `/chat/${selectedModel}`; |
| | } |
| | } |
| | |
| | |
| | window.startChat = startChat; |
| | |
| | |
| | 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) { |
| | |
| | setTimeout(() => { |
| | window.location.reload(); |
| | }, 500); |
| | } else { |
| | alert('Failed to stop model'); |
| | } |
| | } catch (error) { |
| | console.error('Error stopping model:', error); |
| | alert('Failed to stop model'); |
| | } |
| | } |
| | |
| | |
| | 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; |
| | } |
| | |
| | |
| | if (component) { |
| | component.stoppingAll = true; |
| | } |
| | |
| | try { |
| | |
| | const stopPromises = loadedModelNames.map(modelName => |
| | fetch('/backend/shutdown', { |
| | method: 'POST', |
| | headers: { |
| | 'Content-Type': 'application/json', |
| | }, |
| | body: JSON.stringify({ model: modelName }) |
| | }) |
| | ); |
| | |
| | await Promise.all(stopPromises); |
| | |
| | |
| | setTimeout(() => { |
| | window.location.reload(); |
| | }, 1000); |
| | } catch (error) { |
| | console.error('Error stopping models:', error); |
| | alert('Failed to stop some models'); |
| | if (component) { |
| | component.stoppingAll = false; |
| | } |
| | } |
| | } |
| | |
| | |
| | window.stopModel = stopModel; |
| | window.stopAllModels = stopAllModels; |
| | |
| | |
| | function resourceMonitor() { |
| | return { |
| | resourceData: null, |
| | pollInterval: null, |
| | |
| | async fetchResourceData() { |
| | try { |
| | const response = await fetch('/api/resources'); |
| | if (response.ok) { |
| | this.resourceData = await response.json(); |
| | } |
| | } catch (error) { |
| | console.error('Error fetching resource data:', error); |
| | } |
| | }, |
| | |
| | startPolling() { |
| | |
| | this.fetchResourceData(); |
| | |
| | this.pollInterval = setInterval(() => this.fetchResourceData(), 5000); |
| | }, |
| | |
| | stopPolling() { |
| | if (this.pollInterval) { |
| | clearInterval(this.pollInterval); |
| | } |
| | } |
| | } |
| | } |
| | </script> |
| |
|
| | </body> |
| | </html> |
| |
|