| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Local LLM Chat</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
| <script src="https://unpkg.com/feather-icons"></script> |
| <style> |
| :root { |
| --primary: #6366f1; |
| --primary-dark: #4f46e5; |
| --bg: #0f172a; |
| --text: #e2e8f0; |
| --text-light: #94a3b8; |
| --border: #1e293b; |
| } |
| |
| .dark { |
| --bg: #0f172a; |
| --text: #e2e8f0; |
| --text-light: #94a3b8; |
| --border: #1e293b; |
| } |
| |
| .light { |
| --bg: #f9fafb; |
| --text: #111827; |
| --text-light: #6b7280; |
| --border: #e5e7eb; |
| } |
| |
| body { |
| width: 400px; |
| height: 600px; |
| font-family: 'Inter', sans-serif; |
| background-color: var(--bg); |
| color: var(--text); |
| transition: background-color 0.3s, color 0.3s; |
| } |
| |
| .chat-container { |
| height: calc(100% - 60px); |
| } |
| |
| .message-user { |
| background-color: var(--primary); |
| color: white; |
| border-radius: 12px 12px 0 12px; |
| } |
| |
| .message-ai { |
| background-color: #1e293b; |
| border: 1px solid var(--border); |
| border-radius: 12px 12px 12px 0; |
| } |
| |
| .rounded-12px { |
| border-radius: 12px !important; |
| } |
| |
| .rounded-none { |
| border-radius: 0 !important; |
| } |
| |
| .rounded-full { |
| border-radius: 9999px !important; |
| } |
| |
| .typing-indicator span { |
| display: inline-block; |
| width: 8px; |
| height: 8px; |
| background-color: var(--primary); |
| border-radius: 50%; |
| margin: 0 2px; |
| opacity: 0.4; |
| } |
| |
| .typing-indicator span:nth-child(1) { |
| animation: pulse 1s infinite; |
| } |
| |
| .typing-indicator span:nth-child(2) { |
| animation: pulse 1s infinite 0.2s; |
| } |
| |
| .typing-indicator span:nth-child(3) { |
| animation: pulse 1s infinite 0.4s; |
| } |
| |
| @keyframes pulse { |
| 0%, 100% { |
| opacity: 0.4; |
| transform: scale(1); |
| } |
| 50% { |
| opacity: 1; |
| transform: scale(1.2); |
| } |
| } |
| |
| .input-box { |
| box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); |
| background-color: #1e293b; |
| } |
| |
| .header-dark { |
| background-color: #1e293b; |
| border-color: #334155; |
| } |
| |
| .modal-dark { |
| background-color: #1e293b; |
| color: #e2e8f0; |
| } |
| |
| .modal-dark input, |
| .modal-dark select { |
| background-color: #334155; |
| color: #e2e8f0; |
| border-color: #475569; |
| } |
| |
| .modal-dark .border-gray-200 { |
| border-color: #334155; |
| } |
| </style> |
| </head> |
| <body class="flex flex-col"> |
| <header class="header-dark px-4 py-3 border-b border-gray-800 flex items-center justify-between sticky top-0 z-10"> |
| <div class="flex items-center space-x-2"> |
| <div class="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center"> |
| <i data-feather="cpu" class="text-indigo-500 w-4 h-4"></i> |
| </div> |
| <h1 class="font-semibold text-white">Local LLM</h1> |
| </div> |
| <div class="flex items-center space-x-2"> |
| <button id="settings-btn" class="p-1 rounded-full hover:bg-gray-700"> |
| <i data-feather="settings" class="w-5 h-5 text-white"></i> |
| </button> |
| <button id="clear-btn" class="p-1 rounded-full hover:bg-gray-700"> |
| <i data-feather="trash-2" class="w-5 h-5 text-white"></i> |
| </button> |
| </div> |
| </header> |
|
|
| <div class="chat-container overflow-y-auto flex-1 px-4 py-3 space-y-3" id="chat-messages"> |
| <div class="message-ai p-3 max-w-[80%]"> |
| <p class="text-sm">Hello! I'm your local AI assistant. How can I help you today?</p> |
| </div> |
| </div> |
|
|
| <div class="input-box bg-white px-4 py-3 border-t border-gray-200 sticky bottom-0"> |
| <div class="flex items-center space-x-2"> |
| <input |
| type="text" |
| id="message-input" |
| placeholder="Type your message..." |
| class="flex-1 border border-gray-300 rounded-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm bg-white text-gray-900" |
| autocomplete="off" |
| > |
| <button id="send-btn" class="bg-indigo-500 hover:bg-indigo-600 text-white rounded-full p-2 transition-colors"> |
| <i data-feather="send" class="w-4 h-4"></i> |
| </button> |
| </div> |
| <div class="flex items-center justify-between mt-2 text-xs text-gray-500"> |
| <div class="flex items-center space-x-2"> |
| <button id="model-selector" class="flex items-center space-x-1 hover:text-indigo-500"> |
| <span>Ollama</span> |
| <i data-feather="chevron-down" class="w-3 h-3"></i> |
| </button> |
| </div> |
| <div class="flex items-center space-x-2"> |
| <button id="api-settings" class="hover:text-indigo-500 flex items-center"> |
| <i data-feather="link" class="w-3 h-3 mr-1"></i> |
| <span>API Settings</span> |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="settings-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden"> |
| <div class="modal-dark bg-gray-900 rounded-lg w-full max-w-md mx-4"> |
| <div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center"> |
| <h3 class="text-lg font-medium text-gray-900">Settings</h3> |
| <button id="close-settings" class="text-gray-400 hover:text-gray-500"> |
| <i data-feather="x" class="w-5 h-5"></i> |
| </button> |
| </div> |
| <div class="px-6 py-4 space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">AI Role</label> |
| <textarea id="ai-role" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm" placeholder="You are a helpful AI assistant..."></textarea> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Prompt Library</label> |
| <select id="prompt-library" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm"> |
| <option value="">Select a prompt...</option> |
| <option value="creative">Creative Writer</option> |
| <option value="coder">Code Assistant</option> |
| <option value="analyst">Data Analyst</option> |
| <option value="tutor">Learning Tutor</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Ollama API URL</label> |
| <input type="text" id="api-url" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm" placeholder="http://localhost:11434"> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Default Model</label> |
| <select id="default-model" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm"> |
| <option value="llama2">Llama 2</option> |
| <option value="mistral">Mistral</option> |
| <option value="gemma">Gemma</option> |
| <option value="phi">Phi</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Theme</label> |
| <select id="theme-selector" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm"> |
| <option value="light">Light</option> |
| <option value="dark">Dark</option> |
| <option value="system">System</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Message Style</label> |
| <select id="message-style" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm"> |
| <option value="rounded">Rounded</option> |
| <option value="square">Square</option> |
| <option value="bubble">Bubble</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Font Size</label> |
| <select id="font-size" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm"> |
| <option value="small">Small</option> |
| <option value="medium">Medium</option> |
| <option value="large">Large</option> |
| </select> |
| </div> |
| <div class="flex items-center justify-between"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Enable Markdown</label> |
| <label class="relative inline-flex items-center cursor-pointer"> |
| <input type="checkbox" id="markdown-toggle" class="sr-only peer"> |
| <div class="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-indigo-600"></div> |
| </label> |
| </div> |
| <div class="flex items-center justify-between"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Enable Animations</label> |
| <label class="relative inline-flex items-center cursor-pointer"> |
| <input type="checkbox" id="animations-toggle" class="sr-only peer"> |
| <div class="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-indigo-600"></div> |
| </label> |
| </div> |
| </div> |
| <div class="px-6 py-4 border-t border-gray-200 flex justify-end"> |
| <button id="save-settings" class="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-md text-sm font-medium">Save</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| feather.replace(); |
| |
| |
| const messageInput = document.getElementById('message-input'); |
| const sendBtn = document.getElementById('send-btn'); |
| const chatMessages = document.getElementById('chat-messages'); |
| const settingsBtn = document.getElementById('settings-btn'); |
| const clearBtn = document.getElementById('clear-btn'); |
| const settingsModal = document.getElementById('settings-modal'); |
| const closeSettings = document.getElementById('close-settings'); |
| const saveSettings = document.getElementById('save-settings'); |
| const apiSettings = document.getElementById('api-settings'); |
| const modelSelector = document.getElementById('model-selector'); |
| |
| |
| document.documentElement.classList.add('dark'); |
| |
| |
| chrome.storage.sync.get([ |
| 'apiUrl', |
| 'defaultModel', |
| 'theme', |
| 'messageStyle', |
| 'fontSize', |
| 'markdownEnabled', |
| 'animationsEnabled', |
| 'aiRole', |
| 'selectedPrompt' |
| ], (result) => { |
| if (result.apiUrl) document.getElementById('api-url').value = result.apiUrl; |
| if (result.defaultModel) document.getElementById('default-model').value = result.defaultModel; |
| if (result.theme) applyTheme(result.theme); |
| if (result.messageStyle) document.getElementById('message-style').value = result.messageStyle; |
| if (result.fontSize) document.getElementById('font-size').value = result.fontSize; |
| if (result.markdownEnabled) document.getElementById('markdown-toggle').checked = result.markdownEnabled; |
| if (result.animationsEnabled) document.getElementById('animations-toggle').checked = result.animationsEnabled; |
| |
| applyMessageStyle(result.messageStyle || 'rounded'); |
| applyFontSize(result.fontSize || 'medium'); |
| }); |
| |
| |
| sendBtn.addEventListener('click', sendMessage); |
| messageInput.addEventListener('keypress', (e) => { |
| if (e.key === 'Enter') { |
| sendMessage(); |
| } |
| }); |
| |
| settingsBtn.addEventListener('click', () => { |
| settingsModal.classList.remove('hidden'); |
| }); |
| |
| closeSettings.addEventListener('click', () => { |
| settingsModal.classList.add('hidden'); |
| }); |
| |
| saveSettings.addEventListener('click', () => { |
| const apiUrl = document.getElementById('api-url').value; |
| const defaultModel = document.getElementById('default-model').value; |
| const theme = document.getElementById('theme-selector').value; |
| const messageStyle = document.getElementById('message-style').value; |
| const fontSize = document.getElementById('font-size').value; |
| const markdownEnabled = document.getElementById('markdown-toggle').checked; |
| const animationsEnabled = document.getElementById('animations-toggle').checked; |
| const aiRole = document.getElementById('ai-role').value; |
| const selectedPrompt = document.getElementById('prompt-library').value; |
| |
| chrome.storage.sync.set({ |
| apiUrl, |
| defaultModel, |
| theme, |
| messageStyle, |
| fontSize, |
| markdownEnabled, |
| animationsEnabled, |
| aiRole, |
| selectedPrompt |
| }, () => { |
| applyTheme(theme); |
| applyMessageStyle(messageStyle); |
| applyFontSize(fontSize); |
| settingsModal.classList.add('hidden'); |
| showNotification('Settings saved successfully'); |
| }); |
| }); |
| |
| clearBtn.addEventListener('click', () => { |
| chatMessages.innerHTML = ` |
| <div class="message-ai p-3 max-w-[80%]"> |
| <p class="text-sm">Hello! I'm your local AI assistant. How can I help you today?</p> |
| </div> |
| `; |
| }); |
| |
| apiSettings.addEventListener('click', () => { |
| settingsModal.classList.remove('hidden'); |
| }); |
| |
| |
| function sendMessage() { |
| const message = messageInput.value.trim(); |
| if (!message) return; |
| |
| |
| chrome.storage.sync.get(['aiRole', 'selectedPrompt'], (result) => { |
| let fullMessage = message; |
| |
| if (result.aiRole) { |
| fullMessage = `${result.aiRole}\n\n${message}`; |
| } |
| |
| if (result.selectedPrompt) { |
| const prompts = { |
| 'creative': 'You are a creative writer. Respond with imaginative and vivid descriptions.', |
| 'coder': 'You are a coding assistant. Respond with clean, efficient code examples.', |
| 'analyst': 'You are a data analyst. Respond with clear, structured analysis.', |
| 'tutor': 'You are a learning tutor. Explain concepts clearly with examples.' |
| }; |
| fullMessage = `${prompts[result.selectedPrompt]}\n\n${fullMessage}`; |
| } |
| |
| |
| addMessage(message, 'user'); |
| messageInput.value = ''; |
| |
| |
| const typingIndicator = document.createElement('div'); |
| typingIndicator.className = 'typing-indicator p-3 max-w-[80%]'; |
| typingIndicator.innerHTML = ` |
| <div class="flex space-x-1"> |
| <span></span> |
| <span></span> |
| <span></span> |
| </div> |
| `; |
| chatMessages.appendChild(typingIndicator); |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| |
| |
| chrome.storage.sync.get(['apiUrl', 'defaultModel'], async (result) => { |
| const apiUrl = result.apiUrl || 'http://localhost:11434'; |
| const model = result.defaultModel || 'llama2'; |
| |
| try { |
| |
| const response = await fetch(`${apiUrl}/api/generate`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ |
| model: model, |
| prompt: fullMessage, |
| stream: false |
| }) |
| }); |
| |
| if (!response.ok) { |
| throw new Error(`API request failed: ${response.status}`); |
| } |
| |
| const data = await response.json(); |
| |
| |
| chatMessages.removeChild(typingIndicator); |
| |
| |
| addMessage(data.response, 'ai'); |
| } catch (error) { |
| console.error('Error:', error); |
| chatMessages.removeChild(typingIndicator); |
| addMessage(`Error: ${error.message}`, 'ai'); |
| } |
| }); |
| } |
| |
| function addMessage(text, sender) { |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = `message-${sender} p-3 max-w-[80%] ${sender === 'user' ? 'ml-auto' : ''}`; |
| |
| |
| chrome.storage.sync.get(['markdownEnabled'], (result) => { |
| if (result.markdownEnabled) { |
| |
| text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); |
| text = text.replace(/\*(.*?)\*/g, '<em>$1</em>'); |
| text = text.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" class="text-indigo-400 hover:underline" target="_blank">$1</a>'); |
| } |
| |
| messageDiv.innerHTML = `<p class="text-sm">${text}</p>`; |
| chatMessages.appendChild(messageDiv); |
| |
| |
| chrome.storage.sync.get(['animationsEnabled'], (result) => { |
| if (result.animationsEnabled) { |
| messageDiv.style.opacity = '0'; |
| messageDiv.style.transform = 'translateY(10px)'; |
| messageDiv.style.transition = 'all 0.3s ease'; |
| setTimeout(() => { |
| messageDiv.style.opacity = '1'; |
| messageDiv.style.transform = 'translateY(0)'; |
| }, 10); |
| } |
| }); |
| |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| }); |
| } |
| |
| function applyMessageStyle(style) { |
| const messages = document.querySelectorAll('.message-user, .message-ai'); |
| |
| messages.forEach(msg => { |
| msg.classList.remove('rounded-12px', 'rounded-none', 'rounded-full'); |
| |
| switch(style) { |
| case 'rounded': |
| msg.classList.add('rounded-[12px]'); |
| break; |
| case 'square': |
| msg.classList.add('rounded-none'); |
| break; |
| case 'bubble': |
| msg.classList.add('rounded-full'); |
| break; |
| } |
| }); |
| } |
| |
| function applyFontSize(size) { |
| const messages = document.querySelectorAll('.message-user p, .message-ai p'); |
| |
| messages.forEach(msg => { |
| msg.classList.remove('text-xs', 'text-sm', 'text-base'); |
| |
| switch(size) { |
| case 'small': |
| msg.classList.add('text-xs'); |
| break; |
| case 'medium': |
| msg.classList.add('text-sm'); |
| break; |
| case 'large': |
| msg.classList.add('text-base'); |
| break; |
| } |
| }); |
| } |
| |
| function applyTheme(theme) { |
| if (theme === 'system') { |
| theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; |
| } |
| |
| document.documentElement.classList.remove('light', 'dark'); |
| document.documentElement.classList.add(theme); |
| |
| |
| const header = document.querySelector('header'); |
| const modal = document.querySelector('#settings-modal > div'); |
| |
| if (theme === 'dark') { |
| header.classList.add('header-dark'); |
| header.classList.remove('bg-white'); |
| modal.classList.add('modal-dark'); |
| modal.classList.remove('bg-white'); |
| } else { |
| header.classList.remove('header-dark'); |
| header.classList.add('bg-white'); |
| modal.classList.remove('modal-dark'); |
| modal.classList.add('bg-white'); |
| } |
| } |
| |
| function showNotification(message) { |
| const notification = document.createElement('div'); |
| notification.className = 'fixed bottom-4 right-4 bg-green-600 text-white px-4 py-2 rounded-md text-sm shadow-lg'; |
| notification.textContent = message; |
| document.body.appendChild(notification); |
| |
| setTimeout(() => { |
| notification.classList.add('opacity-0', 'transition-opacity', 'duration-300'); |
| setTimeout(() => { |
| document.body.removeChild(notification); |
| }, 300); |
| }, 3000); |
| } |
| |
| |
| document.getElementById('prompt-library').addEventListener('change', function() { |
| if (this.value) { |
| document.getElementById('ai-role').value = ''; |
| } |
| }); |
| |
| document.getElementById('ai-role').addEventListener('input', function() { |
| if (this.value) { |
| document.getElementById('prompt-library').value = ''; |
| } |
| }); |
| |
| |
| chrome.storage.sync.get(['aiRole', 'selectedPrompt'], (result) => { |
| if (result.aiRole) { |
| document.getElementById('ai-role').value = result.aiRole; |
| } |
| if (result.selectedPrompt) { |
| document.getElementById('prompt-library').value = result.selectedPrompt; |
| } |
| }); |
| </script> |
| </body> |
| </html> |
|
|