Spaces:
Running
Running
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Ollama Interface - Modell-Steuerung</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <style> | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); | |
| min-height: 100vh; | |
| } | |
| .code-font { | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| .glass-panel { | |
| background: rgba(30, 41, 59, 0.7); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .message-bubble { | |
| max-width: 85%; | |
| word-wrap: break-word; | |
| } | |
| .typing-indicator span { | |
| animation: blink 1.4s infinite both; | |
| } | |
| .typing-indicator span:nth-child(2) { animation-delay: 0.2s; } | |
| .typing-indicator span:nth-child(3) { animation-delay: 0.4s; } | |
| @keyframes blink { | |
| 0%, 100% { opacity: 0.2; } | |
| 50% { opacity: 1; } | |
| } | |
| .custom-scrollbar::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-track { | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb { | |
| background: rgba(255, 255, 255, 0.2); | |
| border-radius: 4px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb:hover { | |
| background: rgba(255, 255, 255, 0.3); | |
| } | |
| .gradient-text { | |
| background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| </style> | |
| </head> | |
| <body class="text-slate-200 overflow-hidden"> | |
| <!-- Header --> | |
| <header class="glass-panel border-b border-slate-700/50 px-6 py-4 flex justify-between items-center z-50 relative"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20"> | |
| <i data-lucide="cpu" class="w-6 h-6 text-white"></i> | |
| </div> | |
| <div> | |
| <h1 class="text-xl font-bold tracking-tight">Ollama<span class="gradient-text">Control</span></h1> | |
| <p class="text-xs text-slate-400">Lokales LLM Interface</p> | |
| </div> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="flex items-center gap-2 px-4 py-2 bg-slate-800/50 hover:bg-slate-700/50 rounded-lg border border-slate-700 transition-all text-sm font-medium group"> | |
| <span>Built with</span> | |
| <span class="text-blue-400 group-hover:text-blue-300">anycoder</span> | |
| <i data-lucide="external-link" class="w-3 h-3 opacity-50"></i> | |
| </a> | |
| </header> | |
| <div class="flex h-[calc(100vh-80px)]"> | |
| <!-- Sidebar Settings --> | |
| <aside class="w-96 glass-panel border-r border-slate-700/50 flex flex-col overflow-hidden"> | |
| <div class="p-6 border-b border-slate-700/50"> | |
| <h2 class="text-sm font-semibold text-slate-300 uppercase tracking-wider mb-4 flex items-center gap-2"> | |
| <i data-lucide="settings" class="w-4 h-4"></i> | |
| Konfiguration | |
| </h2> | |
| <!-- Connection --> | |
| <div class="space-y-3 mb-6"> | |
| <label class="text-xs text-slate-400 font-medium">Ollama Server URL</label> | |
| <div class="flex gap-2"> | |
| <input type="text" id="serverUrl" value="http://localhost:11434" | |
| class="flex-1 bg-slate-900/50 border border-slate-700 rounded-lg px-3 py-2 text-sm code-font focus:outline-none focus:border-blue-500 transition-colors"> | |
| <button onclick="refreshModels()" class="p-2 bg-slate-800 hover:bg-slate-700 rounded-lg border border-slate-700 transition-colors" title="Verbinden"> | |
| <i data-lucide="refresh-cw" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Model Selection --> | |
| <div class="space-y-3 mb-6"> | |
| <label class="text-xs text-slate-400 font-medium">Modell auswählen</label> | |
| <select id="modelSelect" class="w-full bg-slate-900/50 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-blue-500 transition-colors"> | |
| <option value="">Lade Modelle...</option> | |
| </select> | |
| <p class="text-xs text-slate-500 leading-relaxed"> | |
| Hinweis: Für "uncensored" Verhalten benötigen Sie entsprechende Modelle wie | |
| <span class="text-blue-400">dolphin-mixtral</span>, | |
| <span class="text-blue-400">llama2-uncensored</span> oder | |
| <span class="text-blue-400">nous-hermes</span>. | |
| </p> | |
| </div> | |
| </div> | |
| <!-- System Prompt --> | |
| <div class="flex-1 overflow-y-auto custom-scrollbar p-6"> | |
| <label class="text-xs text-slate-400 font-medium mb-2 block flex items-center gap-2"> | |
| <i data-lucide="terminal" class="w-4 h-4"></i> | |
| System Prompt (Steuerung) | |
| </label> | |
| <textarea id="systemPrompt" rows="8" | |
| class="w-full bg-slate-900/50 border border-slate-700 rounded-lg px-3 py-2 text-sm code-font resize-none focus:outline-none focus:border-blue-500 transition-colors mb-2" | |
| placeholder="Sie sind ein hilfreicher Assistent...">Sie sind ein präziser und direkter Assistent. Sie antworten wahrheitsgemäß und ohne unnötige Einschränkungen. Sie helfen bei technischen Fragen, Programmierung und allgemeinem Wissen.</textarea> | |
| <div class="space-y-2 mt-4"> | |
| <label class="text-xs text-slate-400 font-medium">Temperatur: <span id="tempValue" class="text-blue-400">0.7</span></label> | |
| <input type="range" id="temperature" min="0" max="2" step="0.1" value="0.7" | |
| class="w-full accent-blue-500 h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer" | |
| oninput="document.getElementById('tempValue').textContent = this.value"> | |
| </div> | |
| <button onclick="clearChat()" class="mt-6 w-full py-2 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/30 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2"> | |
| <i data-lucide="trash-2" class="w-4 h-4"></i> | |
| Chatverlauf löschen | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- Main Chat Area --> | |
| <main class="flex-1 flex flex-col bg-slate-950/30 relative"> | |
| <!-- Messages Container --> | |
| <div id="chatContainer" class="flex-1 overflow-y-auto custom-scrollbar p-6 space-y-6"> | |
| <!-- Welcome Message --> | |
| <div class="flex justify-center"> | |
| <div class="glass-panel rounded-2xl p-8 max-w-2xl text-center border border-slate-700/50 shadow-2xl"> | |
| <div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl mx-auto mb-4 flex items-center justify-center shadow-lg shadow-blue-500/20"> | |
| <i data-lucide="bot" class="w-8 h-8 text-white"></i> | |
| </div> | |
| <h2 class="text-2xl font-bold mb-2">Willkommen bei OllamaControl</h2> | |
| <p class="text-slate-400 mb-6"> | |
| Stellen Sie sicher, dass Ollama läuft (<code class="bg-slate-800 px-2 py-1 rounded text-sm">ollama serve</code>) | |
| und wählen Sie ein Modell aus der Seitenleiste. | |
| </p> | |
| <div class="grid grid-cols-2 gap-3 text-left"> | |
| <div class="bg-slate-800/50 p-3 rounded-lg border border-slate-700/50"> | |
| <i data-lucide="zap" class="w-4 h-4 text-yellow-400 mb-1"></i> | |
| <p class="text-xs text-slate-300">Lokale Verarbeitung</p> | |
| </div> | |
| <div class="bg-slate-800/50 p-3 rounded-lg border border-slate-700/50"> | |
| <i data-lucide="shield" class="w-4 h-4 text-green-400 mb-1"></i> | |
| <p class="text-xs text-slate-300">Datenschutz-fokussiert</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Input Area --> | |
| <div class="border-t border-slate-700/50 bg-slate-900/50 backdrop-blur-xl p-4"> | |
| <div class="max-w-4xl mx-auto relative"> | |
| <textarea id="userInput" rows="1" | |
| class="w-full bg-slate-800 border border-slate-700 rounded-xl pl-4 pr-12 py-3 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all resize-none text-sm" | |
| placeholder="Nachricht eingeben... (Shift+Enter für neue Zeile)" | |
| onkeydown="handleKeyDown(event)"></textarea> | |
| <button onclick="sendMessage()" id="sendBtn" | |
| class="absolute right-2 bottom-2 p-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"> | |
| <i data-lucide="send" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| <div class="max-w-4xl mx-auto mt-2 flex justify-between items-center text-xs text-slate-500"> | |
| <span>Enter zum Senden</span> | |
| <span id="statusIndicator" class="flex items-center gap-2"> | |
| <span class="w-2 h-2 rounded-full bg-slate-600"></span> | |
| Bereit | |
| </span> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <script> | |
| // Initialize Lucide icons | |
| lucide.createIcons(); | |
| let currentModel = ''; | |
| let conversationHistory = []; | |
| let isGenerating = false; | |
| // Auto-resize textarea | |
| const textarea = document.getElementById('userInput'); | |
| textarea.addEventListener('input', function() { | |
| this.style.height = 'auto'; | |
| this.style.height = Math.min(this.scrollHeight, 200) + 'px'; | |
| }); | |
| async function refreshModels() { | |
| const url = document.getElementById('serverUrl').value; | |
| const select = document.getElementById('modelSelect'); | |
| const status = document.getElementById('statusIndicator'); | |
| status.innerHTML = '<span class="w-2 h-2 rounded-full bg-yellow-500 animate-pulse"></span> Verbinde...'; | |
| try { | |
| const response = await fetch(`${url}/api/tags`); | |
| if (!response.ok) throw new Error('Verbindung fehlgeschlagen'); | |
| const data = await response.json(); | |
| select.innerHTML = '<option value="">Modell wählen...</option>'; | |
| data.models.forEach(model => { | |
| const option = document.createElement('option'); | |
| option.value = model.name; | |
| option.textContent = model.name; | |
| select.appendChild(option); | |
| }); | |
| status.innerHTML = '<span class="w-2 h-2 rounded-full bg-green-500"></span> Verbunden'; | |
| setTimeout(() => { | |
| status.innerHTML = '<span class="w-2 h-2 rounded-full bg-slate-600"></span> Bereit'; | |
| }, 2000); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| status.innerHTML = '<span class="w-2 h-2 rounded-full bg-red-500"></span> Fehler'; | |
| alert('Verbindung zu Ollama fehlgeschlagen. Stellen Sie sicher, dass Ollama läuft.'); | |
| } | |
| } | |
| select.addEventListener('change', (e) => { | |
| currentModel = e.target.value; | |
| }); | |
| function handleKeyDown(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| } | |
| function clearChat() { | |
| conversationHistory = []; | |
| document.getElementById('chatContainer').innerHTML = ` | |
| <div class="flex justify-center"> | |
| <div class="glass-panel rounded-2xl p-8 max-w-2xl text-center border border-slate-700/50"> | |
| <p class="text-slate-400">Chat zurückgesetzt. Bereit für neue Konversation.</p> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function appendMessage(role, content, isStreaming = false) { | |
| const container = document.getElementById('chatContainer'); | |
| const isUser = role === 'user'; | |
| if (isStreaming && document.getElementById('streaming-message')) { | |
| const existing = document.getElementById('streaming-message'); | |
| existing.querySelector('.content').innerHTML = formatMessage(content); | |
| hljs.highlightAll(); | |
| container.scrollTop = container.scrollHeight; | |
| return; | |
| } | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `flex ${isUser ? 'justify-end' : 'justify-start'} animate-fade-in`; | |
| if (isStreaming) messageDiv.id = 'streaming-message'; | |
| const bubble = document.createElement('div'); | |
| bubble.className = `message-bubble rounded-2xl px-5 py-3 ${ | |
| isUser | |
| ? 'bg-blue-600 text-white rounded-br-md' | |
| : 'glass-panel text-slate-200 rounded-bl-md border border-slate-700/50' | |
| }`; | |
| if (!isUser) { | |
| bubble.innerHTML = ` | |
| <div class="flex items-center gap-2 mb-2 text-xs text-slate-400 border-b border-slate-700/50 pb-2"> | |
| <i data-lucide="bot" class="w-3 h-3"></i> | |
| <span>Assistant</span> | |
| </div> | |
| <div class="content prose prose-invert max-w-none text-sm leading-relaxed"> | |
| ${formatMessage(content)} | |
| </div> | |
| `; | |
| } else { | |
| bubble.innerHTML = `<div class="text-sm">${escapeHtml(content)}</div>`; | |
| } | |
| messageDiv.appendChild(bubble); | |
| container.appendChild(messageDiv); | |
| container.scrollTop = container.scrollHeight; | |
| if (!isUser && !isStreaming) { | |
| lucide.createIcons(); | |
| hljs.highlightAll(); | |
| } | |
| } | |
| function formatMessage(text) { | |
| // Escape HTML | |
| text = escapeHtml(text); | |
| // Code blocks | |
| text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { | |
| return `<pre class="bg-slate-950 rounded-lg p-3 my-2 overflow-x-auto"><code class="language-${lang || 'text'}">${code.trim()}</code></pre>`; | |
| }); | |
| // Inline code | |
| text = text.replace(/`([^`]+)`/g, '<code class="bg-slate-800 px-1.5 py-0.5 rounded text-xs code-font text-blue-300">$1</code>'); | |
| // Bold | |
| text = text.replace(/\*\*(.*?)\*\*/g, '<strong class="text-white">$1</strong>'); | |
| // Italic | |
| text = text.replace(/\*(.*?)\*/g, '<em>$1</em>'); | |
| // Line breaks | |
| text = text.replace(/\n/g, '<br>'); | |
| return text; | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| async function sendMessage() { | |
| const input = document.getElementById('userInput'); | |
| const message = input.value.trim(); | |
| const model = document.getElementById('modelSelect').value; | |
| const url = document.getElementById('serverUrl').value; | |
| if (!message || isGenerating) return; | |
| if (!model) { | |
| alert('Bitte wählen Sie zuerst ein Modell aus.'); | |
| return; | |
| } | |
| // Add user message | |
| appendMessage('user', message); | |
| conversationHistory.push({ role: 'user', content: message }); | |
| input.value = ''; | |
| input.style.height = 'auto'; | |
| isGenerating = true; | |
| document.getElementById('sendBtn').disabled = true; | |
| const systemPrompt = document.getElementById('systemPrompt').value; | |
| const temperature = parseFloat(document.getElementById('temperature').value); | |
| try { | |
| const response = await fetch(`${url}/api/generate`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| model: model, | |
| prompt: message, | |
| system: systemPrompt, | |
| temperature: temperature, | |
| stream: true, | |
| context: [] // Ollama handles context internally, but for chat-like behavior we could implement full context passing | |
| }) | |
| }); | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let fullResponse = ''; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| const chunk = decoder.decode(value); | |
| const lines = chunk.split('\n').filter(line => line.trim()); | |
| for (const line of lines) { | |
| try { | |
| const data = JSON.parse(line); | |
| if (data.response) { | |
| fullResponse += data.response; | |
| appendMessage('assistant', fullResponse, true); | |
| } | |
| } catch (e) { | |
| console.error('Parse error:', e); | |
| } | |
| } | |
| } | |
| conversationHistory.push({ role: 'assistant', content: fullResponse }); | |
| document.getElementById('streaming-message')?.removeAttribute('id'); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| appendMessage('assistant', 'Fehler bei der Kommunikation mit Ollama. Bitte überprüfen Sie die Verbindung.'); | |
| } finally { | |
| isGenerating = false; | |
| document.getElementById('sendBtn').disabled = false; | |
| lucide.createIcons(); | |
| hljs.highlightAll(); | |
| } | |
| } | |
| // Load models on startup | |
| window.addEventListener('DOMContentLoaded', () => { | |
| setTimeout(refreshModels, 500); | |
| }); | |
| // CSS Animation for fade in | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .animate-fade-in { | |
| animation: fadeIn 0.3s ease-out forwards; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| </script> | |
| </body> | |
| </html> |