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> |