tts-lmstudio / index.html
Freefall's picture
Add 3 files
4c3ec3a verified
<!DOCTYPE html>
<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>