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> |