Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Ollama Workstation</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .message-stream { | |
| height: calc(100vh - 300px); | |
| } | |
| .sidebar { | |
| width: 300px; | |
| transition: all 0.3s; | |
| } | |
| .sidebar.collapsed { | |
| transform: translateX(-280px); | |
| } | |
| .model-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| } | |
| .typing-indicator::after { | |
| content: '...'; | |
| animation: typing 1.5s infinite; | |
| } | |
| @keyframes typing { | |
| 0% { content: '.'; } | |
| 33% { content: '..'; } | |
| 66% { content: '...'; } | |
| } | |
| .response-area { | |
| scroll-behavior: smooth; | |
| } | |
| .tab-active { | |
| border-bottom: 2px solid #3b82f6; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 font-sans flex h-screen overflow-hidden"> | |
| <!-- Sidebar --> | |
| <div class="sidebar bg-gray-800 text-white h-full flex flex-col border-r border-gray-700"> | |
| <div class="p-4 border-b border-gray-700 flex justify-between items-center"> | |
| <h1 class="text-xl font-bold">Ollama Workstation</h1> | |
| <button id="toggle-sidebar" class="text-gray-400 hover:text-white"> | |
| <i class="fas fa-chevron-left"></i> | |
| </button> | |
| </div> | |
| <div class="flex-1 overflow-y-auto"> | |
| <!-- Model Management --> | |
| <div class="p-4"> | |
| <h2 class="text-lg font-semibold mb-2">Models</h2> | |
| <div class="space-y-2"> | |
| <button id="list-models" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded flex items-center justify-between"> | |
| <span>Available Models</span> | |
| <i class="fas fa-list"></i> | |
| </button> | |
| <button id="pull-model" class="w-full bg-gray-700 hover:bg-gray-600 text-white py-2 px-4 rounded flex items-center justify-between"> | |
| <span>Pull Model</span> | |
| <i class="fas fa-download"></i> | |
| </button> | |
| <button id="delete-model" class="w-full bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded flex items-center justify-between"> | |
| <span>Delete Model</span> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Model List --> | |
| <div id="model-list" class="p-4 border-t border-gray-700 hidden"> | |
| <h3 class="font-medium mb-2">Installed Models</h3> | |
| <div id="models-container" class="space-y-2"> | |
| <!-- Models will be populated here --> | |
| </div> | |
| </div> | |
| <!-- Pull Model Form --> | |
| <div id="pull-model-form" class="p-4 border-t border-gray-700 hidden"> | |
| <h3 class="font-medium mb-2">Pull Model</h3> | |
| <div class="space-y-2"> | |
| <input id="model-to-pull" type="text" placeholder="Model name (e.g. llama2)" class="w-full bg-gray-700 text-white px-3 py-2 rounded border border-gray-600"> | |
| <button id="confirm-pull" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded"> | |
| Pull Model | |
| </button> | |
| </div> | |
| <div id="pull-progress" class="mt-2 hidden"> | |
| <div class="flex justify-between text-sm"> | |
| <span>Downloading...</span> | |
| <span id="pull-percentage">0%</span> | |
| </div> | |
| <div class="w-full bg-gray-700 rounded-full h-2.5 mt-1"> | |
| <div id="pull-progress-bar" class="bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Delete Model Form --> | |
| <div id="delete-model-form" class="p-4 border-t border-gray-700 hidden"> | |
| <h3 class="font-medium mb-2">Delete Model</h3> | |
| <div class="space-y-2"> | |
| <select id="model-to-delete" class="w-full bg-gray-700 text-white px-3 py-2 rounded border border-gray-600"> | |
| <option value="">Select a model</option> | |
| </select> | |
| <button id="confirm-delete" class="w-full bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded"> | |
| Delete Model | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Settings --> | |
| <div class="p-4 border-t border-gray-700"> | |
| <h2 class="text-lg font-semibold mb-2">Settings</h2> | |
| <div class="space-y-3"> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Ollama Host</label> | |
| <input id="ollama-host" type="text" value="http://localhost:11434" class="w-full bg-gray-700 text-white px-3 py-2 rounded border border-gray-600"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Temperature</label> | |
| <input id="temperature" type="range" min="0" max="1" step="0.1" value="0.7" class="w-full"> | |
| <span id="temperature-value" class="text-sm">0.7</span> | |
| </div> | |
| <div> | |
| <label class="flex items-center space-x-2"> | |
| <input id="stream-responses" type="checkbox" checked class="rounded bg-gray-700 border-gray-600 text-blue-600"> | |
| <span class="text-sm">Stream Responses</span> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-4 border-t border-gray-700"> | |
| <div class="flex items-center space-x-2"> | |
| <div class="h-3 w-3 rounded-full bg-green-500"></div> | |
| <span id="connection-status" class="text-sm">Connected</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="flex-1 flex flex-col h-full overflow-hidden"> | |
| <!-- Model Info Bar --> | |
| <div class="bg-white border-b border-gray-200 p-3 flex items-center justify-between"> | |
| <div class="flex items-center space-x-3"> | |
| <div id="current-model" class="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm font-medium"> | |
| No model selected | |
| </div> | |
| <div id="model-loading" class="hidden"> | |
| <div class="flex items-center space-x-2 text-gray-500"> | |
| <div class="animate-spin"> | |
| <i class="fas fa-spinner"></i> | |
| </div> | |
| <span>Loading model...</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex space-x-2"> | |
| <button id="new-chat" class="bg-gray-100 hover:bg-gray-200 text-gray-800 px-3 py-1 rounded text-sm flex items-center space-x-1"> | |
| <i class="fas fa-plus"></i> | |
| <span>New Chat</span> | |
| </button> | |
| <button id="export-chat" class="bg-gray-100 hover:bg-gray-200 text-gray-800 px-3 py-1 rounded text-sm flex items-center space-x-1"> | |
| <i class="fas fa-file-export"></i> | |
| <span>Export</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Chat Area --> | |
| <div class="flex-1 overflow-hidden flex flex-col"> | |
| <!-- Response Area --> | |
| <div id="response-area" class="response-area flex-1 overflow-y-auto p-4 space-y-6 bg-white"> | |
| <div class="text-center text-gray-500 py-10"> | |
| <i class="fas fa-robot text-4xl mb-2"></i> | |
| <p class="text-lg">Select a model and start chatting</p> | |
| </div> | |
| </div> | |
| <!-- Input Area --> | |
| <div class="border-t border-gray-200 bg-gray-50 p-4"> | |
| <div class="flex space-x-2 mb-2"> | |
| <button id="chat-tab" class="tab-active px-3 py-1 text-sm font-medium">Chat</button> | |
| <button id="generate-tab" class="px-3 py-1 text-sm font-medium text-gray-500 hover:text-gray-700">Generate</button> | |
| <button id="structured-tab" class="px-3 py-1 text-sm font-medium text-gray-500 hover:text-gray-700">Structured</button> | |
| </div> | |
| <!-- Chat Tab --> | |
| <div id="chat-input" class="space-y-2"> | |
| <div class="flex space-x-2"> | |
| <select id="message-role" class="bg-gray-200 text-gray-800 px-3 py-2 rounded text-sm"> | |
| <option value="user">User</option> | |
| <option value="system">System</option> | |
| <option value="assistant">Assistant</option> | |
| </select> | |
| <button id="add-image" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-3 py-2 rounded text-sm flex items-center"> | |
| <i class="fas fa-image mr-1"></i> | |
| <span>Image</span> | |
| </button> | |
| </div> | |
| <div class="relative"> | |
| <textarea id="message-input" rows="3" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none" placeholder="Type your message here..."></textarea> | |
| <button id="send-message" class="absolute right-3 bottom-3 bg-blue-600 hover:bg-blue-700 text-white p-2 rounded-full"> | |
| <i class="fas fa-paper-plane"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Generate Tab --> | |
| <div id="generate-input" class="space-y-2 hidden"> | |
| <div class="flex space-x-2"> | |
| <input id="generate-prompt" type="text" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg" placeholder="Enter your prompt..."> | |
| <button id="send-generate" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg"> | |
| Generate | |
| </button> | |
| </div> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">System Prompt</label> | |
| <textarea id="system-prompt" rows="2" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"></textarea> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Template</label> | |
| <textarea id="generate-template" rows="2" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Structured Tab --> | |
| <div id="structured-input" class="space-y-2 hidden"> | |
| <div class="flex space-x-2"> | |
| <input id="structured-prompt" type="text" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg" placeholder="Enter your prompt..."> | |
| <button id="send-structured" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg"> | |
| Process | |
| </button> | |
| </div> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">JSON Schema</label> | |
| <textarea id="json-schema" rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono text-sm">{ | |
| "type": "object", | |
| "properties": { | |
| "name": { "type": "string" }, | |
| "age": { "type": "number" } | |
| }, | |
| "required": ["name", "age"] | |
| }</textarea> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Response Format</label> | |
| <select id="response-format" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"> | |
| <option value="json">JSON</option> | |
| <option value="yaml">YAML</option> | |
| <option value="xml">XML</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Import Ollama browser module | |
| const ollama = { | |
| chat: async (options) => { | |
| // Mock implementation for demo | |
| if (options.stream) { | |
| return (async function*() { | |
| const words = "This is a simulated streaming response from the Ollama model. It demonstrates how text would appear word by word when streaming is enabled.".split(" "); | |
| for (const word of words) { | |
| await new Promise(resolve => setTimeout(resolve, 50)); | |
| yield { message: { content: word + " " } }; | |
| } | |
| })(); | |
| } else { | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| return { | |
| message: { | |
| content: "This is a simulated response from the Ollama model. In a real implementation, this would be the actual response from the API." | |
| } | |
| }; | |
| } | |
| }, | |
| generate: async (options) => { | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| return { | |
| response: options.prompt + " (generated response)" | |
| }; | |
| }, | |
| list: async () => { | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| return { | |
| models: [ | |
| { name: "llama3.1", modified_at: "2023-06-15T10:30:00Z" }, | |
| { name: "mistral", modified_at: "2023-07-20T14:45:00Z" }, | |
| { name: "codellama", modified_at: "2023-08-05T09:15:00Z" } | |
| ] | |
| }; | |
| }, | |
| pull: async (options) => { | |
| return (async function*() { | |
| for (let i = 0; i <= 100; i += 5) { | |
| await new Promise(resolve => setTimeout(resolve, 200)); | |
| yield { status: "downloading", completed: i, total: 100 }; | |
| } | |
| })(); | |
| }, | |
| delete: async (options) => { | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| return { status: "success" }; | |
| } | |
| }; | |
| // DOM Elements | |
| const toggleSidebar = document.getElementById('toggle-sidebar'); | |
| const sidebar = document.querySelector('.sidebar'); | |
| const listModelsBtn = document.getElementById('list-models'); | |
| const pullModelBtn = document.getElementById('pull-model'); | |
| const deleteModelBtn = document.getElementById('delete-model'); | |
| const modelList = document.getElementById('model-list'); | |
| const pullModelForm = document.getElementById('pull-model-form'); | |
| const deleteModelForm = document.getElementById('delete-model-form'); | |
| const modelsContainer = document.getElementById('models-container'); | |
| const modelToPull = document.getElementById('model-to-pull'); | |
| const confirmPull = document.getElementById('confirm-pull'); | |
| const pullProgress = document.getElementById('pull-progress'); | |
| const pullPercentage = document.getElementById('pull-percentage'); | |
| const pullProgressBar = document.getElementById('pull-progress-bar'); | |
| const modelToDelete = document.getElementById('model-to-delete'); | |
| const confirmDelete = document.getElementById('confirm-delete'); | |
| const currentModel = document.getElementById('current-model'); | |
| const modelLoading = document.getElementById('model-loading'); | |
| const responseArea = document.getElementById('response-area'); | |
| const messageInput = document.getElementById('message-input'); | |
| const sendMessage = document.getElementById('send-message'); | |
| const messageRole = document.getElementById('message-role'); | |
| const addImage = document.getElementById('add-image'); | |
| const newChat = document.getElementById('new-chat'); | |
| const exportChat = document.getElementById('export-chat'); | |
| const chatTab = document.getElementById('chat-tab'); | |
| const generateTab = document.getElementById('generate-tab'); | |
| const structuredTab = document.getElementById('structured-tab'); | |
| const chatInput = document.getElementById('chat-input'); | |
| const generateInput = document.getElementById('generate-input'); | |
| const structuredInput = document.getElementById('structured-input'); | |
| const generatePrompt = document.getElementById('generate-prompt'); | |
| const sendGenerate = document.getElementById('send-generate'); | |
| const structuredPrompt = document.getElementById('structured-prompt'); | |
| const sendStructured = document.getElementById('send-structured'); | |
| const jsonSchema = document.getElementById('json-schema'); | |
| const responseFormat = document.getElementById('response-format'); | |
| const systemPrompt = document.getElementById('system-prompt'); | |
| const generateTemplate = document.getElementById('generate-template'); | |
| const temperature = document.getElementById('temperature'); | |
| const temperatureValue = document.getElementById('temperature-value'); | |
| const streamResponses = document.getElementById('stream-responses'); | |
| const ollamaHost = document.getElementById('ollama-host'); | |
| const connectionStatus = document.getElementById('connection-status'); | |
| // State | |
| let selectedModel = null; | |
| let chatHistory = []; | |
| let isSidebarCollapsed = false; | |
| // Event Listeners | |
| toggleSidebar.addEventListener('click', toggleSidebarCollapse); | |
| listModelsBtn.addEventListener('click', toggleModelList); | |
| pullModelBtn.addEventListener('click', togglePullModelForm); | |
| deleteModelBtn.addEventListener('click', toggleDeleteModelForm); | |
| confirmPull.addEventListener('click', handlePullModel); | |
| confirmDelete.addEventListener('click', handleDeleteModel); | |
| sendMessage.addEventListener('click', handleSendMessage); | |
| messageInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| handleSendMessage(); | |
| } | |
| }); | |
| addImage.addEventListener('click', handleAddImage); | |
| newChat.addEventListener('click', handleNewChat); | |
| exportChat.addEventListener('click', handleExportChat); | |
| chatTab.addEventListener('click', () => switchTab('chat')); | |
| generateTab.addEventListener('click', () => switchTab('generate')); | |
| structuredTab.addEventListener('click', () => switchTab('structured')); | |
| sendGenerate.addEventListener('click', handleGenerate); | |
| sendStructured.addEventListener('click', handleStructured); | |
| temperature.addEventListener('input', updateTemperature); | |
| streamResponses.addEventListener('change', updateStreamSetting); | |
| // Initialize | |
| loadModels(); | |
| checkConnection(); | |
| updateTemperature(); | |
| // Functions | |
| function toggleSidebarCollapse() { | |
| isSidebarCollapsed = !isSidebarCollapsed; | |
| sidebar.classList.toggle('collapsed'); | |
| toggleSidebar.innerHTML = isSidebarCollapsed ? | |
| '<i class="fas fa-chevron-right"></i>' : | |
| '<i class="fas fa-chevron-left"></i>'; | |
| } | |
| function toggleModelList() { | |
| const isVisible = !modelList.classList.contains('hidden'); | |
| // Hide all forms first | |
| modelList.classList.add('hidden'); | |
| pullModelForm.classList.add('hidden'); | |
| deleteModelForm.classList.add('hidden'); | |
| if (!isVisible) { | |
| modelList.classList.remove('hidden'); | |
| } | |
| } | |
| function togglePullModelForm() { | |
| const isVisible = !pullModelForm.classList.contains('hidden'); | |
| // Hide all forms first | |
| modelList.classList.add('hidden'); | |
| pullModelForm.classList.add('hidden'); | |
| deleteModelForm.classList.add('hidden'); | |
| if (!isVisible) { | |
| pullModelForm.classList.remove('hidden'); | |
| } | |
| } | |
| function toggleDeleteModelForm() { | |
| const isVisible = !deleteModelForm.classList.contains('hidden'); | |
| // Hide all forms first | |
| modelList.classList.add('hidden'); | |
| pullModelForm.classList.add('hidden'); | |
| deleteModelForm.classList.add('hidden'); | |
| if (!isVisible) { | |
| deleteModelForm.classList.remove('hidden'); | |
| populateDeleteModelDropdown(); | |
| } | |
| } | |
| async function loadModels() { | |
| try { | |
| const response = await ollama.list(); | |
| displayModels(response.models); | |
| } catch (error) { | |
| console.error("Error loading models:", error); | |
| showError("Failed to load models. Check your Ollama connection."); | |
| } | |
| } | |
| function displayModels(models) { | |
| modelsContainer.innerHTML = ''; | |
| if (models.length === 0) { | |
| modelsContainer.innerHTML = '<p class="text-gray-400 text-sm">No models installed</p>'; | |
| return; | |
| } | |
| models.forEach(model => { | |
| const modelCard = document.createElement('div'); | |
| modelCard.className = 'model-card bg-gray-700 p-3 rounded-lg cursor-pointer transition-all duration-200'; | |
| modelCard.innerHTML = ` | |
| <div class="flex justify-between items-center"> | |
| <h4 class="font-medium">${model.name}</h4> | |
| <span class="text-xs text-gray-400">${new Date(model.modified_at).toLocaleDateString()}</span> | |
| </div> | |
| `; | |
| modelCard.addEventListener('click', () => selectModel(model.name)); | |
| modelsContainer.appendChild(modelCard); | |
| }); | |
| } | |
| function populateDeleteModelDropdown() { | |
| modelToDelete.innerHTML = '<option value="">Select a model</option>'; | |
| // In a real implementation, we would fetch the models from Ollama | |
| // For demo, we'll use some sample models | |
| const sampleModels = ['llama3.1', 'mistral', 'codellama']; | |
| sampleModels.forEach(model => { | |
| const option = document.createElement('option'); | |
| option.value = model; | |
| option.textContent = model; | |
| modelToDelete.appendChild(option); | |
| }); | |
| } | |
| async function selectModel(modelName) { | |
| selectedModel = modelName; | |
| currentModel.textContent = modelName; | |
| currentModel.className = 'bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm font-medium'; | |
| // Show loading indicator | |
| modelLoading.classList.remove('hidden'); | |
| // In a real implementation, we might verify the model is loaded | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| // Hide loading indicator | |
| modelLoading.classList.add('hidden'); | |
| // Clear chat history for new model | |
| chatHistory = []; | |
| responseArea.innerHTML = ` | |
| <div class="text-center text-gray-500 py-10"> | |
| <i class="fas fa-robot text-4xl mb-2"></i> | |
| <p class="text-lg">Model ${modelName} is ready</p> | |
| <p class="text-sm mt-2">Start chatting with ${modelName}</p> | |
| </div> | |
| `; | |
| } | |
| async function handlePullModel() { | |
| const modelName = modelToPull.value.trim(); | |
| if (!modelName) { | |
| alert("Please enter a model name"); | |
| return; | |
| } | |
| pullProgress.classList.remove('hidden'); | |
| try { | |
| const pullStream = ollama.pull({ model: modelName }); | |
| for await (const progress of pullStream) { | |
| const percent = Math.floor((progress.completed / progress.total) * 100); | |
| pullPercentage.textContent = `${percent}%`; | |
| pullProgressBar.style.width = `${percent}%`; | |
| if (percent === 100) { | |
| pullProgress.innerHTML = ` | |
| <div class="text-green-500 text-sm"> | |
| <i class="fas fa-check-circle mr-1"></i> | |
| Model ${modelName} downloaded successfully | |
| </div> | |
| `; | |
| loadModels(); // Refresh model list | |
| break; | |
| } | |
| } | |
| } catch (error) { | |
| console.error("Error pulling model:", error); | |
| pullProgress.innerHTML = ` | |
| <div class="text-red-500 text-sm"> | |
| <i class="fas fa-exclamation-circle mr-1"></i> | |
| Failed to download model: ${error.message} | |
| </div> | |
| `; | |
| } | |
| } | |
| async function handleDeleteModel() { | |
| const modelName = modelToDelete.value; | |
| if (!modelName) { | |
| alert("Please select a model to delete"); | |
| return; | |
| } | |
| if (!confirm(`Are you sure you want to delete ${modelName}? This cannot be undone.`)) { | |
| return; | |
| } | |
| try { | |
| await ollama.delete({ model: modelName }); | |
| alert(`Model ${modelName} deleted successfully`); | |
| loadModels(); // Refresh model list | |
| if (selectedModel === modelName) { | |
| selectedModel = null; | |
| currentModel.textContent = "No model selected"; | |
| currentModel.className = 'bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm font-medium'; | |
| } | |
| deleteModelForm.classList.add('hidden'); | |
| } catch (error) { | |
| console.error("Error deleting model:", error); | |
| alert(`Failed to delete model: ${error.message}`); | |
| } | |
| } | |
| async function handleSendMessage() { | |
| const messageText = messageInput.value.trim(); | |
| if (!messageText) return; | |
| if (!selectedModel) { | |
| alert("Please select a model first"); | |
| return; | |
| } | |
| const role = messageRole.value; | |
| const content = messageText; | |
| // Add user message to chat history | |
| chatHistory.push({ role, content }); | |
| // Display user message | |
| displayMessage({ role, content }); | |
| // Clear input | |
| messageInput.value = ''; | |
| // Show typing indicator | |
| const typingId = showTypingIndicator(); | |
| try { | |
| const options = { | |
| model: selectedModel, | |
| messages: chatHistory, | |
| stream: streamResponses.checked, | |
| options: { | |
| temperature: parseFloat(temperature.value) | |
| } | |
| }; | |
| if (streamResponses.checked) { | |
| // Handle streaming response | |
| const stream = await ollama.chat(options); | |
| let fullResponse = ''; | |
| // Remove typing indicator | |
| removeTypingIndicator(typingId); | |
| // Create assistant message container | |
| const messageId = `msg-${Date.now()}`; | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.id = messageId; | |
| messageDiv.className = 'flex space-x-3'; | |
| messageDiv.innerHTML = ` | |
| <div class="flex-shrink-0"> | |
| <div class="bg-purple-500 text-white w-8 h-8 rounded-full flex items-center justify-center"> | |
| <i class="fas fa-robot"></i> | |
| </div> | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <div class="bg-purple-100 text-gray-800 p-3 rounded-lg"> | |
| <div class="whitespace-pre-wrap"></div> | |
| </div> | |
| </div> | |
| `; | |
| responseArea.appendChild(messageDiv); | |
| // Scroll to bottom | |
| responseArea.scrollTop = responseArea.scrollHeight; | |
| // Process stream | |
| for await (const chunk of stream) { | |
| fullResponse += chunk.message.content; | |
| const contentDiv = messageDiv.querySelector('.whitespace-pre-wrap'); | |
| contentDiv.textContent = fullResponse; | |
| // Scroll to keep visible | |
| responseArea.scrollTop = responseArea.scrollHeight; | |
| } | |
| // Add assistant response to chat history | |
| chatHistory.push({ role: 'assistant', content: fullResponse }); | |
| } else { | |
| // Handle non-streaming response | |
| const response = await ollama.chat(options); | |
| // Remove typing indicator | |
| removeTypingIndicator(typingId); | |
| // Display assistant response | |
| displayMessage({ role: 'assistant', content: response.message.content }); | |
| // Add assistant response to chat history | |
| chatHistory.push({ role: 'assistant', content: response.message.content }); | |
| } | |
| } catch (error) { | |
| console.error("Error in chat:", error); | |
| removeTypingIndicator(typingId); | |
| showError(error.message); | |
| } | |
| } | |
| function displayMessage(message) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'flex space-x-3'; | |
| if (message.role === 'user') { | |
| messageDiv.innerHTML = ` | |
| <div class="flex-shrink-0"> | |
| <div class="bg-blue-500 text-white w-8 h-8 rounded-full flex items-center justify-center"> | |
| <i class="fas fa-user"></i> | |
| </div> | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <div class="bg-blue-100 text-gray-800 p-3 rounded-lg"> | |
| <div class="whitespace-pre-wrap">${message.content}</div> | |
| </div> | |
| </div> | |
| `; | |
| } else if (message.role === 'system') { | |
| messageDiv.innerHTML = ` | |
| <div class="flex-shrink-0"> | |
| <div class="bg-yellow-500 text-white w-8 h-8 rounded-full flex items-center justify-center"> | |
| <i class="fas fa-cog"></i> | |
| </div> | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <div class="bg-yellow-100 text-gray-800 p-3 rounded-lg"> | |
| <div class="whitespace-pre-wrap">${message.content}</div> | |
| </div> | |
| </div> | |
| `; | |
| } else { // assistant | |
| messageDiv.innerHTML = ` | |
| <div class="flex-shrink-0"> | |
| <div class="bg-purple-500 text-white w-8 h-8 rounded-full flex items-center justify-center"> | |
| <i class="fas fa-robot"></i> | |
| </div> | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <div class="bg-purple-100 text-gray-800 p-3 rounded-lg"> | |
| <div class="whitespace-pre-wrap">${message.content}</div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| responseArea.appendChild(messageDiv); | |
| responseArea.scrollTop = responseArea.scrollHeight; | |
| } | |
| function showTypingIndicator() { | |
| const typingId = `typing-${Date.now()}`; | |
| const typingDiv = document.createElement('div'); | |
| typingDiv.id = typingId; | |
| typingDiv.className = 'flex space-x-3'; | |
| typingDiv.innerHTML = ` | |
| <div class="flex-shrink-0"> | |
| <div class="bg-purple-500 text-white w-8 h-8 rounded-full flex items-center justify-center"> | |
| <i class="fas fa-robot"></i> | |
| </div> | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <div class="bg-purple-100 text-gray-800 p-3 rounded-lg"> | |
| <div class="typing-indicator">Thinking</div> | |
| </div> | |
| </div> | |
| `; | |
| responseArea.appendChild(typingDiv); | |
| responseArea.scrollTop = responseArea.scrollHeight; | |
| return typingId; | |
| } | |
| function removeTypingIndicator(id) { | |
| const element = document.getElementById(id); | |
| if (element) { | |
| element.remove(); | |
| } | |
| } | |
| function showError(message) { | |
| const errorDiv = document.createElement('div'); | |
| errorDiv.className = 'bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4'; | |
| errorDiv.innerHTML = ` | |
| <div class="flex items-center"> | |
| <div class="flex-shrink-0"> | |
| <i class="fas fa-exclamation-circle text-red-500"></i> | |
| </div> | |
| <div class="ml-3"> | |
| <p class="text-sm">${message}</p> | |
| </div> | |
| </div> | |
| `; | |
| responseArea.appendChild(errorDiv); | |
| responseArea.scrollTop = responseArea.scrollHeight; | |
| } | |
| function handleAddImage() { | |
| alert("Image upload functionality would be implemented here"); | |
| // In a real implementation, this would open a file dialog | |
| // and handle image uploads to be included in the message | |
| } | |
| function handleNewChat() { | |
| if (!selectedModel) { | |
| alert("Please select a model first"); | |
| return; | |
| } | |
| if (chatHistory.length === 0) { | |
| return; | |
| } | |
| if (confirm("Start a new chat? The current chat history will be cleared.")) { | |
| chatHistory = []; | |
| responseArea.innerHTML = ` | |
| <div class="text-center text-gray-500 py-10"> | |
| <i class="fas fa-robot text-4xl mb-2"></i> | |
| <p class="text-lg">New chat started with ${selectedModel}</p> | |
| </div> | |
| `; | |
| } | |
| } | |
| function handleExportChat() { | |
| if (chatHistory.length === 0) { | |
| alert("No chat history to export"); | |
| return; | |
| } | |
| const chatText = chatHistory.map(msg => { | |
| return `${msg.role.toUpperCase()}: ${msg.content}`; | |
| }).join('\n\n'); | |
| const blob = new Blob([chatText], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `ollama-chat-${selectedModel || 'unknown'}-${new Date().toISOString().slice(0, 10)}.txt`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| function switchTab(tab) { | |
| chatInput.classList.add('hidden'); | |
| generateInput.classList.add('hidden'); | |
| structuredInput.classList.add('hidden'); | |
| chatTab.classList.remove('tab-active'); | |
| generateTab.classList.remove('tab-active'); | |
| structuredTab.classList.remove('tab-active'); | |
| if (tab === 'chat') { | |
| chatInput.classList.remove('hidden'); | |
| chatTab.classList.add('tab-active'); | |
| } else if (tab === 'generate') { | |
| generateInput.classList.remove('hidden'); | |
| generateTab.classList.add('tab-active'); | |
| } else if (tab === 'structured') { | |
| structuredInput.classList.remove('hidden'); | |
| structuredTab.classList.add('tab-active'); | |
| } | |
| } | |
| async function handleGenerate() { | |
| const prompt = generatePrompt.value.trim(); | |
| if (!prompt) return; | |
| if (!selectedModel) { | |
| alert("Please select a model first"); | |
| return; | |
| } | |
| // Show typing indicator | |
| const typingId = showTypingIndicator(); | |
| try { | |
| const options = { | |
| model: selectedModel, | |
| prompt: prompt, | |
| system: systemPrompt.value.trim() || undefined, | |
| template: generateTemplate.value.trim() || undefined, | |
| options: { | |
| temperature: parseFloat(temperature.value) | |
| } | |
| }; | |
| const response = await ollama.generate(options); | |
| // Remove typing indicator | |
| removeTypingIndicator(typingId); | |
| // Display generated response | |
| displayMessage({ | |
| role: 'assistant', | |
| content: `Generated response for prompt "${prompt}":\n\n${response.response}` | |
| }); | |
| } catch (error) { | |
| console.error("Error in generation:", error); | |
| removeTypingIndicator(typingId); | |
| showError(error.message); | |
| } | |
| } | |
| async function handleStructured() { | |
| const prompt = structuredPrompt.value.trim(); | |
| if (!prompt) return; | |
| if (!selectedModel) { | |
| alert("Please select a model first"); | |
| return; | |
| } | |
| // Show typing indicator | |
| const typingId = showTypingIndicator(); | |
| try { | |
| const schema = jsonSchema.value.trim(); | |
| const format = responseFormat.value; | |
| // In a real implementation, we would validate the JSON schema | |
| const options = { | |
| model: selectedModel, | |
| messages: [{ role: 'user', content: prompt }], | |
| format: schema, | |
| options: { | |
| temperature: parseFloat(temperature.value) | |
| } | |
| }; | |
| const response = await ollama.chat(options); | |
| // Remove typing indicator | |
| removeTypingIndicator(typingId); | |
| // Display structured response | |
| displayMessage({ | |
| role: 'assistant', | |
| content: `Structured response (${format}):\n\n${JSON.stringify(JSON.parse(response.message.content), null, 2)}` | |
| }); | |
| } catch (error) { | |
| console.error("Error in structured output:", error); | |
| removeTypingIndicator(typingId); | |
| showError(error.message); | |
| } | |
| } | |
| function updateTemperature() { | |
| temperatureValue.textContent = temperature.value; | |
| } | |
| function updateStreamSetting() { | |
| // No action needed, just update the state | |
| } | |
| async function checkConnection() { | |
| try { | |
| // In a real implementation, we would ping the Ollama server | |
| await new Promise(resolve => setTimeout(resolve, 300)); | |
| connectionStatus.textContent = "Connected"; | |
| connectionStatus.previousElementSibling.className = "h-3 w-3 rounded-full bg-green-500"; | |
| } catch (error) { | |
| connectionStatus.textContent = "Disconnected"; | |
| connectionStatus.previousElementSibling.className = "h-3 w-3 rounded-full bg-red-500"; | |
| } | |
| } | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Meroar/batch-ollamanator" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |