batch-ollamanator / index.html
Meroar's picture
Add 3 files
75a0618 verified
<!DOCTYPE html>
<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>