Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>LM Studio Chat Interface</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> | |
| /* Custom scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #f1f1f1; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #888; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #555; | |
| } | |
| /* Pulse animation for streaming indicator */ | |
| @keyframes pulse { | |
| 0%, 100% { | |
| opacity: 1; | |
| } | |
| 50% { | |
| opacity: 0.5; | |
| } | |
| } | |
| .animate-pulse { | |
| animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
| } | |
| /* Smooth transitions */ | |
| .transition-all { | |
| transition-property: all; | |
| transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | |
| transition-duration: 150ms; | |
| } | |
| /* Chat bubble styling */ | |
| .user-bubble { | |
| background-color: #3b82f6; | |
| color: white; | |
| border-radius: 18px 18px 4px 18px; | |
| } | |
| .assistant-bubble { | |
| background-color: #f3f4f6; | |
| color: #111827; | |
| border-radius: 18px 18px 18px 4px; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 h-screen flex overflow-hidden"> | |
| <!-- Sidebar --> | |
| <div class="w-64 bg-white border-r border-gray-200 flex flex-col h-full"> | |
| <div class="p-4 border-b border-gray-200"> | |
| <h1 class="text-xl font-bold text-gray-800">LM Studio Chat</h1> | |
| <button id="new-chat-btn" class="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg flex items-center justify-center"> | |
| <i class="fas fa-plus mr-2"></i> New Chat | |
| </button> | |
| </div> | |
| <div class="flex-1 overflow-y-auto" id="conversation-list"> | |
| <!-- Conversations will be loaded here --> | |
| </div> | |
| <div class="p-4 border-t border-gray-200"> | |
| <div class="flex items-center space-x-2"> | |
| <img src="https://ui-avatars.com/api/?name=User&background=3b82f6&color=fff" alt="User" class="w-8 h-8 rounded-full"> | |
| <span class="font-medium text-gray-700">User</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Chat Area --> | |
| <div class="flex-1 flex flex-col h-full"> | |
| <!-- Connection and Model Selection Bar --> | |
| <div class="bg-white border-b border-gray-200 p-3 flex items-center justify-between"> | |
| <div class="flex items-center space-x-4"> | |
| <div class="flex items-center space-x-2"> | |
| <span class="text-sm font-medium text-gray-600">Server:</span> | |
| <input id="server-url" type="text" value="http://localhost:1234" class="px-3 py-1 border border-gray-300 rounded-md text-sm w-48"> | |
| <button id="connect-btn" class="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded-md text-sm"> | |
| <i class="fas fa-plug mr-1"></i> Connect | |
| </button> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <span class="text-sm font-medium text-gray-600">Model:</span> | |
| <select id="model-select" class="px-3 py-1 border border-gray-300 rounded-md text-sm w-48" disabled> | |
| <option value="">Not connected</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <div class="flex items-center space-x-2"> | |
| <span class="text-sm font-medium text-gray-600">TTS Voice:</span> | |
| <select id="voice-select" class="px-3 py-1 border border-gray-300 rounded-md text-sm w-48"> | |
| <option value="">Select Voice</option> | |
| </select> | |
| </div> | |
| <button id="tts-toggle" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md text-sm"> | |
| <i class="fas fa-volume-up mr-1"></i> Enable TTS | |
| </button> | |
| </div> | |
| </div> | |
| <!-- System Prompt Area --> | |
| <div class="bg-gray-100 border-b border-gray-200 p-3"> | |
| <div class="flex items-center justify-between"> | |
| <span class="text-sm font-medium text-gray-600">System Prompt:</span> | |
| <button id="edit-system-prompt" class="text-blue-600 hover:text-blue-800 text-sm"> | |
| <i class="fas fa-edit mr-1"></i> Edit | |
| </button> | |
| </div> | |
| <div id="system-prompt-display" class="mt-1 text-sm text-gray-700 bg-white p-2 rounded border border-gray-200"> | |
| You are a helpful AI assistant. Be concise and helpful. | |
| </div> | |
| <div id="system-prompt-edit" class="hidden mt-1"> | |
| <textarea id="system-prompt-input" class="w-full p-2 border border-gray-300 rounded-md text-sm h-20">You are a helpful AI assistant. Be concise and helpful.</textarea> | |
| <div class="flex justify-end space-x-2 mt-2"> | |
| <button id="cancel-system-prompt" class="px-3 py-1 border border-gray-300 rounded-md text-sm">Cancel</button> | |
| <button id="save-system-prompt" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md text-sm">Save</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Chat Messages --> | |
| <div id="chat-messages" class="flex-1 overflow-y-auto p-4 space-y-4"> | |
| <div class="flex justify-center items-center h-full text-gray-400" id="empty-state"> | |
| <div class="text-center"> | |
| <i class="fas fa-comments text-4xl mb-2"></i> | |
| <p>Start a new conversation</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Input Area --> | |
| <div class="p-4 border-t border-gray-200 bg-white"> | |
| <div class="relative"> | |
| <textarea id="message-input" rows="2" class="w-full p-3 pr-16 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none" placeholder="Type your message..."></textarea> | |
| <div class="absolute right-3 bottom-3 flex space-x-2"> | |
| <button id="send-btn" class="bg-blue-600 hover:bg-blue-700 text-white p-2 rounded-full"> | |
| <i class="fas fa-paper-plane"></i> | |
| </button> | |
| <button id="stop-btn" class="bg-red-600 hover:bg-red-700 text-white p-2 rounded-full hidden"> | |
| <i class="fas fa-stop"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="flex items-center justify-between mt-2 text-xs text-gray-500"> | |
| <div> | |
| <span id="connection-status" class="flex items-center"> | |
| <span class="w-2 h-2 rounded-full bg-red-500 mr-1"></span> | |
| Disconnected | |
| </span> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <span id="streaming-indicator" class="hidden items-center"> | |
| <span class="w-2 h-2 rounded-full bg-green-500 mr-1 animate-pulse"></span> | |
| Streaming | |
| </span> | |
| <span id="tts-indicator" class="hidden items-center"> | |
| <span class="w-2 h-2 rounded-full bg-purple-500 mr-1 animate-pulse"></span> | |
| Speaking | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Conversation Settings Modal --> | |
| <div id="conversation-settings-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50"> | |
| <div class="bg-white rounded-lg p-6 w-96"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-lg font-medium">Conversation Settings</h3> | |
| <button id="close-settings-modal" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Title</label> | |
| <input type="text" id="conversation-title" class="w-full p-2 border border-gray-300 rounded-md"> | |
| </div> | |
| <div class="flex justify-end space-x-2"> | |
| <button id="delete-conversation" class="text-red-600 hover:text-red-800">Delete</button> | |
| <button id="save-conversation-settings" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md">Save</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // State management | |
| const state = { | |
| currentConversationId: null, | |
| conversations: [], | |
| isConnected: false, | |
| models: [], | |
| currentModel: null, | |
| voices: [], | |
| ttsEnabled: false, | |
| currentVoice: null, | |
| systemPrompt: "You are a helpful AI assistant. Be concise and helpful.", | |
| abortController: null, | |
| isStreaming: false, | |
| isSpeaking: false | |
| }; | |
| // DOM elements | |
| const elements = { | |
| chatMessages: document.getElementById('chat-messages'), | |
| messageInput: document.getElementById('message-input'), | |
| sendBtn: document.getElementById('send-btn'), | |
| stopBtn: document.getElementById('stop-btn'), | |
| serverUrl: document.getElementById('server-url'), | |
| connectBtn: document.getElementById('connect-btn'), | |
| modelSelect: document.getElementById('model-select'), | |
| voiceSelect: document.getElementById('voice-select'), | |
| ttsToggle: document.getElementById('tts-toggle'), | |
| conversationList: document.getElementById('conversation-list'), | |
| newChatBtn: document.getElementById('new-chat-btn'), | |
| connectionStatus: document.getElementById('connection-status'), | |
| streamingIndicator: document.getElementById('streaming-indicator'), | |
| ttsIndicator: document.getElementById('tts-indicator'), | |
| emptyState: document.getElementById('empty-state'), | |
| systemPromptDisplay: document.getElementById('system-prompt-display'), | |
| systemPromptEdit: document.getElementById('system-prompt-edit'), | |
| systemPromptInput: document.getElementById('system-prompt-input'), | |
| editSystemPrompt: document.getElementById('edit-system-prompt'), | |
| cancelSystemPrompt: document.getElementById('cancel-system-prompt'), | |
| saveSystemPrompt: document.getElementById('save-system-prompt'), | |
| conversationSettingsModal: document.getElementById('conversation-settings-modal'), | |
| closeSettingsModal: document.getElementById('close-settings-modal'), | |
| conversationTitle: document.getElementById('conversation-title'), | |
| deleteConversation: document.getElementById('delete-conversation'), | |
| saveConversationSettings: document.getElementById('save-conversation-settings') | |
| }; | |
| // Speech synthesis | |
| const synth = window.speechSynthesis; | |
| let utterance = null; | |
| // Initialize the app | |
| function init() { | |
| loadVoices(); | |
| loadConversations(); | |
| setupEventListeners(); | |
| // Check if voices are already loaded (sometimes they are) | |
| if (synth.getVoices().length > 0) { | |
| populateVoiceList(); | |
| } | |
| // Listen for voices changed event | |
| synth.onvoiceschanged = populateVoiceList; | |
| } | |
| // Load available voices for TTS | |
| function loadVoices() { | |
| state.voices = synth.getVoices(); | |
| populateVoiceList(); | |
| } | |
| // Populate the voice select dropdown | |
| function populateVoiceList() { | |
| state.voices = synth.getVoices(); | |
| elements.voiceSelect.innerHTML = '<option value="">Select Voice</option>'; | |
| state.voices.forEach(voice => { | |
| const option = document.createElement('option'); | |
| option.textContent = `${voice.name} (${voice.lang})${voice.default ? ' - DEFAULT' : ''}`; | |
| option.setAttribute('data-name', voice.name); | |
| option.setAttribute('data-lang', voice.lang); | |
| elements.voiceSelect.appendChild(option); | |
| }); | |
| } | |
| // Load conversations from localStorage | |
| function loadConversations() { | |
| const savedConversations = localStorage.getItem('lmStudioConversations'); | |
| if (savedConversations) { | |
| state.conversations = JSON.parse(savedConversations); | |
| renderConversationList(); | |
| // If there are conversations, select the first one | |
| if (state.conversations.length > 0) { | |
| loadConversation(state.conversations[0].id); | |
| } | |
| } else { | |
| // Create a default conversation if none exist | |
| createNewConversation(); | |
| } | |
| } | |
| // Save conversations to localStorage | |
| function saveConversations() { | |
| localStorage.setItem('lmStudioConversations', JSON.stringify(state.conversations)); | |
| } | |
| // Create a new conversation | |
| function createNewConversation() { | |
| const newConversation = { | |
| id: Date.now().toString(), | |
| title: `New Conversation ${state.conversations.length + 1}`, | |
| messages: [], | |
| createdAt: new Date().toISOString(), | |
| updatedAt: new Date().toISOString() | |
| }; | |
| state.conversations.unshift(newConversation); | |
| saveConversations(); | |
| renderConversationList(); | |
| loadConversation(newConversation.id); | |
| // Clear the chat messages and hide empty state | |
| elements.chatMessages.innerHTML = ''; | |
| elements.emptyState.classList.add('hidden'); | |
| } | |
| // Load a conversation by ID | |
| function loadConversation(conversationId) { | |
| const conversation = state.conversations.find(c => c.id === conversationId); | |
| if (!conversation) return; | |
| state.currentConversationId = conversationId; | |
| renderConversationMessages(conversation.messages); | |
| // Update active conversation in the list | |
| document.querySelectorAll('.conversation-item').forEach(item => { | |
| item.classList.remove('bg-blue-50', 'border-blue-200'); | |
| if (item.dataset.id === conversationId) { | |
| item.classList.add('bg-blue-50', 'border-blue-200'); | |
| } | |
| }); | |
| // Hide empty state if there are messages | |
| if (conversation.messages.length > 0) { | |
| elements.emptyState.classList.add('hidden'); | |
| } else { | |
| elements.emptyState.classList.remove('hidden'); | |
| } | |
| } | |
| // Render conversation list in sidebar | |
| function renderConversationList() { | |
| elements.conversationList.innerHTML = ''; | |
| state.conversations.forEach(conversation => { | |
| const conversationElement = document.createElement('div'); | |
| conversationElement.className = `p-3 border-b border-gray-200 cursor-pointer hover:bg-gray-50 conversation-item ${state.currentConversationId === conversation.id ? 'bg-blue-50 border-blue-200' : ''}`; | |
| conversationElement.dataset.id = conversation.id; | |
| conversationElement.innerHTML = ` | |
| <div class="flex justify-between items-start"> | |
| <div class="flex-1 min-w-0"> | |
| <p class="text-sm font-medium text-gray-800 truncate">${conversation.title}</p> | |
| <p class="text-xs text-gray-500">${new Date(conversation.updatedAt).toLocaleString()}</p> | |
| </div> | |
| <button class="text-gray-400 hover:text-gray-600 conversation-settings-btn" data-id="${conversation.id}"> | |
| <i class="fas fa-ellipsis-v"></i> | |
| </button> | |
| </div> | |
| `; | |
| conversationElement.addEventListener('click', () => loadConversation(conversation.id)); | |
| elements.conversationList.appendChild(conversationElement); | |
| }); | |
| // Add event listeners for settings buttons | |
| document.querySelectorAll('.conversation-settings-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| openConversationSettingsModal(btn.dataset.id); | |
| }); | |
| }); | |
| } | |
| // Render messages in the chat area | |
| function renderConversationMessages(messages) { | |
| elements.chatMessages.innerHTML = ''; | |
| if (messages.length === 0) { | |
| elements.emptyState.classList.remove('hidden'); | |
| return; | |
| } | |
| messages.forEach(message => { | |
| const messageElement = document.createElement('div'); | |
| messageElement.className = `flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`; | |
| const bubbleClass = message.role === 'user' ? 'user-bubble' : 'assistant-bubble'; | |
| messageElement.innerHTML = ` | |
| <div class="max-w-3/4 ${message.role === 'user' ? 'ml-16' : 'mr-16'}"> | |
| <div class="${bubbleClass} p-3 inline-block"> | |
| <div class="whitespace-pre-wrap">${message.content}</div> | |
| </div> | |
| <div class="text-xs text-gray-500 mt-1 ${message.role === 'user' ? 'text-right' : 'text-left'}"> | |
| ${new Date(message.timestamp).toLocaleTimeString()} | |
| ${message.role === 'assistant' ? ` | |
| <button class="ml-2 text-blue-500 hover:text-blue-700 tts-play-btn" data-content="${encodeURIComponent(message.content)}"> | |
| <i class="fas fa-volume-up"></i> | |
| </button> | |
| ` : ''} | |
| </div> | |
| </div> | |
| `; | |
| elements.chatMessages.appendChild(messageElement); | |
| }); | |
| // Scroll to bottom | |
| elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; | |
| // Add event listeners for TTS buttons | |
| document.querySelectorAll('.tts-play-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const content = decodeURIComponent(btn.dataset.content); | |
| speak(content); | |
| }); | |
| }); | |
| } | |
| // Connect to LM Studio server | |
| async function connectToServer() { | |
| const serverUrl = elements.serverUrl.value; | |
| if (!serverUrl) { | |
| showAlert('Please enter a server URL', 'error'); | |
| return; | |
| } | |
| try { | |
| elements.connectBtn.disabled = true; | |
| elements.connectBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Connecting...'; | |
| // Test connection | |
| const response = await fetch(`${serverUrl}/v1/models`, { | |
| method: 'GET', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| if (!response.ok) { | |
| throw new Error('Failed to connect to server'); | |
| } | |
| const data = await response.json(); | |
| state.models = data.data; | |
| state.isConnected = true; | |
| // Update UI | |
| elements.modelSelect.disabled = false; | |
| elements.modelSelect.innerHTML = '<option value="">Select a model</option>'; | |
| state.models.forEach(model => { | |
| const option = document.createElement('option'); | |
| option.value = model.id; | |
| option.textContent = model.id; | |
| elements.modelSelect.appendChild(option); | |
| }); | |
| elements.connectBtn.innerHTML = '<i class="fas fa-plug mr-1"></i> Connected'; | |
| elements.connectBtn.className = 'bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded-md text-sm'; | |
| elements.connectionStatus.innerHTML = '<span class="w-2 h-2 rounded-full bg-green-500 mr-1"></span> Connected'; | |
| showAlert('Successfully connected to server', 'success'); | |
| } catch (error) { | |
| console.error('Connection error:', error); | |
| showAlert(`Connection failed: ${error.message}`, 'error'); | |
| elements.connectBtn.disabled = false; | |
| elements.connectBtn.innerHTML = '<i class="fas fa-plug mr-1"></i> Connect'; | |
| } | |
| } | |
| // Send message to LM Studio | |
| async function sendMessage() { | |
| const message = elements.messageInput.value.trim(); | |
| if (!message || !state.isConnected || !state.currentModel) return; | |
| // Add user message to conversation | |
| const userMessage = { | |
| role: 'user', | |
| content: message, | |
| timestamp: new Date().toISOString() | |
| }; | |
| addMessageToCurrentConversation(userMessage); | |
| elements.messageInput.value = ''; | |
| // Create assistant message placeholder | |
| const assistantMessage = { | |
| role: 'assistant', | |
| content: '', | |
| timestamp: new Date().toISOString() | |
| }; | |
| const messageId = addMessageToCurrentConversation(assistantMessage); | |
| // Prepare the request | |
| const messages = [ | |
| { role: 'system', content: state.systemPrompt }, | |
| ...getCurrentConversation().messages | |
| .filter(m => m.role !== 'system') | |
| .map(m => ({ role: m.role, content: m.content })) | |
| ]; | |
| state.abortController = new AbortController(); | |
| state.isStreaming = true; | |
| elements.streamingIndicator.classList.remove('hidden'); | |
| elements.stopBtn.classList.remove('hidden'); | |
| elements.sendBtn.classList.add('hidden'); | |
| try { | |
| const response = await fetch(`${elements.serverUrl.value}/v1/chat/completions`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| model: state.currentModel, | |
| messages: messages, | |
| stream: true, | |
| temperature: 0.7, | |
| max_tokens: 1000 | |
| }), | |
| signal: state.abortController.signal | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Server responded with ${response.status}`); | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let assistantMessageContent = ''; | |
| 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) { | |
| if (line.startsWith('data: ') && !line.includes('[DONE]')) { | |
| try { | |
| const data = JSON.parse(line.substring(6)); | |
| if (data.choices && data.choices[0].delta && data.choices[0].delta.content) { | |
| assistantMessageContent += data.choices[0].delta.content; | |
| updateMessageContent(messageId, assistantMessageContent); | |
| } | |
| } catch (e) { | |
| console.error('Error parsing stream data:', e); | |
| } | |
| } | |
| } | |
| } | |
| // If TTS is enabled, speak the response | |
| if (state.ttsEnabled && state.currentVoice) { | |
| speak(assistantMessageContent); | |
| } | |
| } catch (error) { | |
| if (error.name !== 'AbortError') { | |
| console.error('Error streaming response:', error); | |
| showAlert(`Error: ${error.message}`, 'error'); | |
| } | |
| } finally { | |
| state.isStreaming = false; | |
| elements.streamingIndicator.classList.add('hidden'); | |
| elements.stopBtn.classList.add('hidden'); | |
| elements.sendBtn.classList.remove('hidden'); | |
| state.abortController = null; | |
| } | |
| } | |
| // Stop streaming | |
| function stopStreaming() { | |
| if (state.abortController) { | |
| state.abortController.abort(); | |
| state.isStreaming = false; | |
| elements.streamingIndicator.classList.add('hidden'); | |
| elements.stopBtn.classList.add('hidden'); | |
| elements.sendBtn.classList.remove('hidden'); | |
| } | |
| if (state.isSpeaking) { | |
| synth.cancel(); | |
| state.isSpeaking = false; | |
| elements.ttsIndicator.classList.add('hidden'); | |
| } | |
| } | |
| // Add message to current conversation | |
| function addMessageToCurrentConversation(message) { | |
| const conversation = getCurrentConversation(); | |
| if (!conversation) return null; | |
| const messageId = Date.now().toString(); | |
| conversation.messages.push({ | |
| ...message, | |
| id: messageId | |
| }); | |
| conversation.updatedAt = new Date().toISOString(); | |
| saveConversations(); | |
| renderConversationMessages(conversation.messages); | |
| return messageId; | |
| } | |
| // Update message content | |
| function updateMessageContent(messageId, content) { | |
| const conversation = getCurrentConversation(); | |
| if (!conversation) return; | |
| const message = conversation.messages.find(m => m.id === messageId); | |
| if (message) { | |
| message.content = content; | |
| message.timestamp = new Date().toISOString(); | |
| conversation.updatedAt = new Date().toISOString(); | |
| // Update the UI | |
| const messageElement = document.querySelector(`[data-id="${messageId}"]`); | |
| if (messageElement) { | |
| messageElement.querySelector('.whitespace-pre-wrap').textContent = content; | |
| } | |
| // Scroll to bottom | |
| elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; | |
| } | |
| } | |
| // Get current conversation | |
| function getCurrentConversation() { | |
| return state.conversations.find(c => c.id === state.currentConversationId); | |
| } | |
| // Speak text using TTS | |
| function speak(text) { | |
| if (synth.speaking) { | |
| synth.cancel(); | |
| } | |
| if (!state.currentVoice) { | |
| showAlert('Please select a voice first', 'error'); | |
| return; | |
| } | |
| utterance = new SpeechSynthesisUtterance(text); | |
| utterance.voice = state.currentVoice; | |
| utterance.rate = 1.0; | |
| utterance.pitch = 1.0; | |
| utterance.onstart = () => { | |
| state.isSpeaking = true; | |
| elements.ttsIndicator.classList.remove('hidden'); | |
| }; | |
| utterance.onend = () => { | |
| state.isSpeaking = false; | |
| elements.ttsIndicator.classList.add('hidden'); | |
| }; | |
| utterance.onerror = (event) => { | |
| console.error('Speech synthesis error:', event); | |
| state.isSpeaking = false; | |
| elements.ttsIndicator.classList.add('hidden'); | |
| showAlert('Error with speech synthesis', 'error'); | |
| }; | |
| synth.speak(utterance); | |
| } | |
| // Toggle TTS | |
| function toggleTTS() { | |
| state.ttsEnabled = !state.ttsEnabled; | |
| if (state.ttsEnabled) { | |
| elements.ttsToggle.innerHTML = '<i class="fas fa-volume-up mr-1"></i> Disable TTS'; | |
| elements.ttsToggle.className = 'bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md text-sm'; | |
| showAlert('Text-to-speech enabled', 'success'); | |
| } else { | |
| elements.ttsToggle.innerHTML = '<i class="fas fa-volume-up mr-1"></i> Enable TTS'; | |
| elements.ttsToggle.className = 'bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md text-sm'; | |
| // Stop any ongoing speech | |
| if (synth.speaking) { | |
| synth.cancel(); | |
| } | |
| } | |
| } | |
| // Open conversation settings modal | |
| function openConversationSettingsModal(conversationId) { | |
| const conversation = state.conversations.find(c => c.id === conversationId); | |
| if (!conversation) return; | |
| elements.conversationTitle.value = conversation.title; | |
| elements.conversationSettingsModal.dataset.id = conversationId; | |
| elements.conversationSettingsModal.classList.remove('hidden'); | |
| } | |
| // Save conversation settings | |
| function saveConversationSettings() { | |
| const conversationId = elements.conversationSettingsModal.dataset.id; | |
| const conversation = state.conversations.find(c => c.id === conversationId); | |
| if (!conversation) return; | |
| conversation.title = elements.conversationTitle.value.trim() || conversation.title; | |
| conversation.updatedAt = new Date().toISOString(); | |
| saveConversations(); | |
| renderConversationList(); | |
| elements.conversationSettingsModal.classList.add('hidden'); | |
| } | |
| // Delete conversation | |
| function deleteConversation() { | |
| const conversationId = elements.conversationSettingsModal.dataset.id; | |
| // Confirm deletion | |
| if (!confirm('Are you sure you want to delete this conversation?')) { | |
| return; | |
| } | |
| // Remove the conversation | |
| state.conversations = state.conversations.filter(c => c.id !== conversationId); | |
| // If we deleted the current conversation, select another one or create a new one | |
| if (state.currentConversationId === conversationId) { | |
| if (state.conversations.length > 0) { | |
| loadConversation(state.conversations[0].id); | |
| } else { | |
| createNewConversation(); | |
| } | |
| } | |
| saveConversations(); | |
| renderConversationList(); | |
| elements.conversationSettingsModal.classList.add('hidden'); | |
| } | |
| // Show alert message | |
| function showAlert(message, type) { | |
| const alert = document.createElement('div'); | |
| alert.className = `fixed top-4 right-4 p-4 rounded-md shadow-md text-white ${ | |
| type === 'error' ? 'bg-red-500' : | |
| type === 'success' ? 'bg-green-500' : 'bg-blue-500' | |
| }`; | |
| alert.textContent = message; | |
| document.body.appendChild(alert); | |
| setTimeout(() => { | |
| alert.classList.add('opacity-0', 'transition-opacity', 'duration-300'); | |
| setTimeout(() => alert.remove(), 300); | |
| }, 3000); | |
| } | |
| // Setup event listeners | |
| function setupEventListeners() { | |
| // Send message on Enter (Shift+Enter for new line) | |
| elements.messageInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| // Send button | |
| elements.sendBtn.addEventListener('click', sendMessage); | |
| // Stop button | |
| elements.stopBtn.addEventListener('click', stopStreaming); | |
| // Connect button | |
| elements.connectBtn.addEventListener('click', connectToServer); | |
| // Model selection | |
| elements.modelSelect.addEventListener('change', (e) => { | |
| state.currentModel = e.target.value; | |
| }); | |
| // Voice selection | |
| elements.voiceSelect.addEventListener('change', (e) => { | |
| const selectedOption = e.target.selectedOptions[0]; | |
| if (selectedOption.value === '') { | |
| state.currentVoice = null; | |
| } else { | |
| const voiceName = selectedOption.dataset.name; | |
| state.currentVoice = state.voices.find(v => v.name === voiceName); | |
| } | |
| }); | |
| // TTS toggle | |
| elements.ttsToggle.addEventListener('click', toggleTTS); | |
| // New conversation button | |
| elements.newChatBtn.addEventListener('click', createNewConversation); | |
| // System prompt edit | |
| elements.editSystemPrompt.addEventListener('click', () => { | |
| elements.systemPromptDisplay.classList.add('hidden'); | |
| elements.systemPromptEdit.classList.remove('hidden'); | |
| }); | |
| // Cancel system prompt edit | |
| elements.cancelSystemPrompt.addEventListener('click', () => { | |
| elements.systemPromptInput.value = state.systemPrompt; | |
| elements.systemPromptEdit.classList.add('hidden'); | |
| elements.systemPromptDisplay.classList.remove('hidden'); | |
| }); | |
| // Save system prompt | |
| elements.saveSystemPrompt.addEventListener('click', () => { | |
| state.systemPrompt = elements.systemPromptInput.value.trim() || state.systemPrompt; | |
| elements.systemPromptDisplay.textContent = state.systemPrompt; | |
| elements.systemPromptEdit.classList.add('hidden'); | |
| elements.systemPromptDisplay.classList.remove('hidden'); | |
| showAlert('System prompt updated', 'success'); | |
| }); | |
| // Conversation settings modal | |
| elements.closeSettingsModal.addEventListener('click', () => { | |
| elements.conversationSettingsModal.classList.add('hidden'); | |
| }); | |
| // Save conversation settings | |
| elements.saveConversationSettings.addEventListener('click', saveConversationSettings); | |
| // Delete conversation | |
| elements.deleteConversation.addEventListener('click', deleteConversation); | |
| // Close modal when clicking outside | |
| elements.conversationSettingsModal.addEventListener('click', (e) => { | |
| if (e.target === elements.conversationSettingsModal) { | |
| elements.conversationSettingsModal.classList.add('hidden'); | |
| } | |
| }); | |
| } | |
| // Initialize the app when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', init); | |
| </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=Freefall/tts-lmstudio" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |