anycoder-524ca236 / index.html
d310h's picture
Upload folder using huggingface_hub
eaabd07 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ollama Chat Interface</title>
<style>
/*
* GLOBAL VARIABLES & RESET
*/
:root {
--bg-body: #121212;
--bg-panel: #1e1e1e;
--bg-input: #2c2c2c;
--text-main: #e0e0e0;
--text-muted: #a0a0a0;
--accent-color: #008080;
--accent-hover: #009999;
--border-color: #333;
--msg-user-bg: #008080;
--msg-ai-bg: #2c2c2c;
--font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.4);
--sidebar-width: 280px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-family);
background-color: var(--bg-body);
color: var(--text-main);
height: 100vh;
display: flex;
overflow: hidden;
}
/*
* SIDEBAR
* Settings and Model Selection
*/
aside {
width: var(--sidebar-width);
background-color: var(--bg-panel);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 1.5rem;
gap: 2rem;
z-index: 10;
transition: transform 0.3s ease;
}
aside h2 {
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
label {
font-size: 0.85rem;
color: var(--text-muted);
}
select,
textarea {
background-color: var(--bg-input);
border: 1px solid var(--border-color);
color: var(--text-main);
padding: 0.75rem;
border-radius: 6px;
font-family: inherit;
font-size: 0.9rem;
outline: none;
resize: none;
}
select:focus,
textarea:focus {
border-color: var(--accent-color);
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85rem;
padding: 0.5rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #555;
transition: background-color 0.3s;
}
.status-dot.online {
background-color: #2e7d32;
box-shadow: 0 0 5px #2e7d32;
}
.status-dot.offline {
background-color: #c62828;
}
/*
* MAIN CHAT AREA
*/
main {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
#chat-container {
flex: 1;
overflow-y: auto;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
scroll-behavior: smooth;
}
/* Scrollbar Styling */
#chat-container::-webkit-scrollbar {
width: 8px;
}
#chat-container::-webkit-scrollbar-track {
background: var(--bg-body);
}
#chat-container::-webkit-scrollbar-thumb {
background: #444;
border-radius: 4px;
}
/* Message Bubbles */
.message-row {
display: flex;
width: 100%;
opacity: 0;
animation: fadeIn 0.3s forwards;
}
.message-row.user {
justify-content: flex-end;
}
.message-row.ai {
justify-content: flex-start;
}
.message-bubble {
max-width: 80%;
padding: 1rem 1.25rem;
border-radius: 12px;
font-size: 1rem;
line-height: 1.6;
position: relative;
word-wrap: break-word;
}
.message-row.user .message-bubble {
background-color: var(--msg-user-bg);
color: white;
border-bottom-right-radius: 2px;
box-shadow: var(--shadow-sm);
}
.message-row.ai .message-bubble {
background-color: var(--msg-ai-bg);
color: var(--text-main);
border-bottom-left-radius: 2px;
border: 1px solid var(--border-color);
box-shadow: var(--shadow-sm);
}
.timestamp {
font-size: 0.7rem;
opacity: 0.7;
margin-bottom: 4px;
display: block;
}
/* Typing Indicator */
.typing-indicator {
display: flex;
gap: 4px;
padding: 12px 16px;
background-color: var(--msg-ai-bg);
border-radius: 12px;
width: fit-content;
margin-left: 1rem;
display: none;
}
.typing-indicator span {
width: 8px;
height: 8px;
background-color: var(--text-muted);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(1) {
animation-delay: -0.32s;
}
.typing-indicator span:nth-child(2) {
animation-delay: -0.16s;
}
/*
* INPUT AREA
*/
footer {
background-color: var(--bg-panel);
padding: 1.5rem;
border-top: 1px solid var(--border-color);
display: flex;
gap: 10px;
align-items: flex-end;
}
#user-input {
flex: 1;
background-color: var(--bg-input);
border: 1px solid var(--border-color);
color: var(--text-main);
padding: 12px 16px;
border-radius: 8px;
font-size: 1rem;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
min-height: 50px;
max-height: 150px;
overflow-y: auto;
}
#user-input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(0, 128, 128, 0.2);
}
#send-btn {
background-color: var(--accent-color);
color: white;
border: none;
padding: 0 24px;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
height: 50px;
display: flex;
align-items: center;
gap: 8px;
}
#send-btn:hover {
background-color: var(--accent-hover);
}
#send-btn:active {
transform: scale(0.98);
}
#send-btn:disabled {
background-color: #444;
cursor: not-allowed;
opacity: 0.7;
}
/*
* ANIMATIONS
*/
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes bounce {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
/*
* MODAL / TOAST
*/
.toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%) translateY(-100px);
background-color: #333;
color: white;
padding: 12px 24px;
border-radius: 8px;
box-shadow: var(--shadow-md);
opacity: 0;
transition: all 0.4s ease;
z-index: 100;
border-left: 4px solid var(--accent-color);
display: flex;
align-items: center;
gap: 10px;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* Responsive */
@media (max-width: 768px) {
aside {
position: fixed;
height: 100%;
left: -280px;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5);
}
aside.open {
left: 0;
}
.mobile-menu-btn {
display: block !important;
}
.message-bubble {
max-width: 90%;
}
}
.mobile-menu-btn {
display: none;
position: absolute;
top: 1rem;
left: 1rem;
z-index: 20;
background: var(--bg-panel);
border: 1px solid var(--border-color);
color: var(--text-main);
padding: 0.5rem;
border-radius: 4px;
}
</style>
</head>
<body>
<!-- Mobile Menu Toggle -->
<button class="mobile-menu-btn" id="menu-toggle">⚙️</button>
<!-- Sidebar -->
<aside id="sidebar">
<div class="status-indicator">
<div class="status-dot" id="status-dot"></div>
<span id="status-text">Checking...</span>
</div>
<div class="control-group">
<h2>Settings</h2>
<label for="model-select">Ollama Model</label>
<select id="model-select">
<option value="llama3.2">llama3.2 (Default)</option>
<option value="codellama">CodeLlama</option>
<option value="mistral">Mistral</option>
<option value="llava">LLaVA (Vision)</option>
</select>
</div>
<div class="control-group">
<label for="system-prompt">System Prompt</label>
<textarea id="system-prompt" rows="4">You are a helpful AI assistant.</textarea>
</div>
<div class="control-group" style="margin-top: auto;">
<button id="refresh-models-btn" style="background: var(--bg-input); border: 1px solid var(--border-color); color: var(--text-main); padding: 8px; border-radius: 4px; cursor: pointer;">Refresh Models</button>
</div>
</aside>
<!-- Main Chat -->
<main>
<header
style="padding: 1rem 1.5rem; border-bottom: 1px solid var(--border-color); display: flex; align-items: center;">
<h1 style="font-size: 1.2rem; font-weight: 600; display: flex; align-items: center; gap: 10px;">
<span>🤖</span> Ollama Chat
</h1>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank"
style="margin-left: auto; font-size: 0.8rem; color: var(--text-muted); text-decoration: none;">Built with
anycoder</a>
</header>
<div id="chat-container">
<!-- Initial Welcome Message -->
<div class="message-row ai">
<div class="message-bubble">
<span class="timestamp">System</span>
Welcome! Ensure Ollama is running locally on port 11434. I am connected and ready to chat.
</div>
</div>
<!-- Typing Indicator -->
<div class="typing-indicator" id="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
<footer>
<textarea id="user-input" placeholder="Type a message... (Shift+Enter for new line)" rows="1"></textarea>
<button id="send-btn">
<span>Send</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
</button>
</footer>
</main>
<div id="toast" class="toast">
<span id="toast-icon">ℹ️</span>
<span id="toast-message">Notification</span>
</div>
<script>
/**
* OLLAMA CHAT LOGIC
* Handles API communication, state management, and UI updates.
*/
// --- Configuration ---
const OLLAMA_API_URL = 'http://localhost:11434/api/chat';
const OLLAMA_TAGS_URL = 'http://localhost:11434/api/tags';
// --- DOM Elements ---
const chatContainer = document.getElementById('chat-container');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const typingIndicator = document.getElementById('typing-indicator');
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
const toast = document.getElementById('toast');
const modelSelect = document.getElementById('model-select');
const systemPrompt = document.getElementById('system-prompt');
const refreshModelsBtn = document.getElementById('refresh-models-btn');
const menuToggle = document.getElementById('menu-toggle');
const sidebar = document.getElementById('sidebar');
// --- State ---
let isGenerating = false;
let messages = []; // Conversation history
// --- Initialization ---
window.addEventListener('DOMContentLoaded', () => {
checkConnection();
userInput.focus();
// Auto-resize textarea
userInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
if(this.value === '') this.style.height = 'auto';
});
// Sidebar toggle for mobile
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('open');
});
});
// --- Event Listeners ---
sendBtn.addEventListener('click', handleSend);
userInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
});
refreshModelsBtn.addEventListener('click', fetchModels);
// --- Core Functions ---
async function checkConnection() {
try {
const response = await fetch(OLLAMA_TAGS_URL);
if (response.ok) {
updateStatus(true);
showToast('Connected to Ollama');
// Only fetch models if we haven't populated them yet
if(modelSelect.options.length === 1) {
fetchModels();
}
} else {
throw new Error('Not connected');
}
} catch (error) {
updateStatus(false);
showToast('Ollama is not running.', true);
}
}
async function fetchModels() {
try {
const response = await fetch(OLLAMA_TAGS_URL);
const data = await response.json();
// Clear existing options except default
modelSelect.innerHTML = '<option value="">Loading...</option>';
if (data.models && data.models.length > 0) {
modelSelect.innerHTML = '';
data.models.forEach(model => {
const option = document.createElement('option');
option.value = model.name;
option.textContent = model.name;
modelSelect.appendChild(option);
});
showToast(`Loaded ${data.models.length} models`);
} else {
modelSelect.innerHTML = '<option value="">No models found</option>';
}
} catch (error) {
console.error(error);
showToast('Failed to fetch models', true);
}
}
async function handleSend() {
if (isGenerating) return;
const text = userInput.value.trim();
if (!text) return;
// 1. UI Updates
addMessageToUI('user', text);
userInput.value = '';
userInput.style.height = 'auto'; // Reset height
isGenerating = true;
toggleLoading(true);
// 2. Prepare Payload
// We prepend the system prompt to the history for context
const history = [
{ role: 'system', content: systemPrompt.value },
...messages
];
try {
const response = await fetch(OLLAMA_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: modelSelect.value || 'llama3.2',
messages: history,
stream: true
})
});
if (!response.ok) throw new Error('API request failed');
// 3. Handle Streaming
const reader = response.body.getReader();
const decoder = new TextDecoder();
let aiResponseText = '';
// Create a placeholder AI message
const aiBubble = addMessageToUI('ai', '', true);
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.slice(6);
if (jsonStr === '[DONE]') break;
try {
const data = JSON.parse(jsonStr);
const content = data.message?.content || '';
aiResponseText += content;
aiBubble.querySelector('.content-text').innerHTML = escapeHtml(aiResponseText);
chatContainer.scrollTop = chatContainer.scrollHeight;
} catch (e) {
console.error('Error parsing stream', e);
}
}
}
}
// 4. Save to history
messages.push({ role: 'user', content: text });
messages.push({ role: 'assistant', content: aiResponseText });
} catch (error) {
console.error(error);
addMessageToUI('ai', 'Error: Could not connect to Ollama. Is it running on port 11434?');
toggleLoading(false);
isGenerating = false;
}
}
// --- UI Helpers ---
function addMessageToUI(role, text, isPlaceholder = false) {
const row = document.createElement('div');
row.className = `message-row ${role}`;
const bubble = document.createElement('div');
bubble.className = 'message-bubble';
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const timestamp = document.createElement('span');
timestamp.className = 'timestamp';
timestamp.textContent = time;
bubble.appendChild(timestamp);
if (isPlaceholder) {
const contentSpan = document.createElement('span');
contentSpan.className = 'content-text';
bubble.appendChild(contentSpan);
} else {
const contentSpan = document.createElement('span');
contentSpan.className = 'content-text';
contentSpan.innerHTML = escapeHtml(text);
bubble.appendChild(contentSpan);
}
row.appendChild(bubble);
// Insert before the typing indicator
chatContainer.insertBefore(row, typingIndicator);
chatContainer.scrollTop = chatContainer.scrollHeight;
return bubble;
}
function toggleLoading(show) {
if (show) {
typingIndicator.style.display = 'flex';
sendBtn.disabled = true;
sendBtn.innerHTML = '<span>...</span>';
sidebar.style.opacity = '0.5';
sidebar.style.pointerEvents = 'none';
} else {
typingIndicator.style.display = 'none';
sendBtn.disabled = false;
sendBtn.innerHTML = '<span>Send</span><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>';
userInput.focus();
sidebar.style.opacity = '1';
sidebar.style.pointerEvents = 'auto';
}
}
function showToast(msg, isError = false) {
const toastEl = document.getElementById('toast');
const toastMsg = document.getElementById('toast-message');
const toastIcon = document.getElementById('toast-icon');
toastMsg.textContent = msg;
toastIcon.textContent = isError ? '⚠️' : '✅';
toastEl.style.borderLeftColor = isError ? '#c62828' : 'var(--accent-color)';
toastEl.classList.add('show');
setTimeout(() => {
toastEl.classList.remove('show');
}, 3000);
}
function updateStatus(isOnline) {
if (isOnline) {
statusDot.className = 'status-dot online';
statusText.textContent = 'Online';
statusText.style.color = '#2e7d32';
} else {
statusDot.className = 'status-dot offline';
statusText.textContent = 'Offline';
statusText.style.color = '#c62828';
}
}
function escapeHtml(text) {
if (!text) return text;
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
</script>
</body>
</html>