Freefall commited on
Commit
4c3ec3a
·
verified ·
1 Parent(s): 2b50267

Add 3 files

Browse files
Files changed (3) hide show
  1. README.md +7 -5
  2. index.html +854 -19
  3. prompts.txt +1 -0
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Tts Lmstudio
3
- emoji: 🔥
4
- colorFrom: purple
5
- colorTo: gray
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: tts-lmstudio
3
+ emoji: 🐳
4
+ colorFrom: blue
5
+ colorTo: red
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,854 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>LM Studio Chat Interface</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ /* Custom scrollbar */
11
+ ::-webkit-scrollbar {
12
+ width: 8px;
13
+ }
14
+ ::-webkit-scrollbar-track {
15
+ background: #f1f1f1;
16
+ }
17
+ ::-webkit-scrollbar-thumb {
18
+ background: #888;
19
+ border-radius: 4px;
20
+ }
21
+ ::-webkit-scrollbar-thumb:hover {
22
+ background: #555;
23
+ }
24
+
25
+ /* Pulse animation for streaming indicator */
26
+ @keyframes pulse {
27
+ 0%, 100% {
28
+ opacity: 1;
29
+ }
30
+ 50% {
31
+ opacity: 0.5;
32
+ }
33
+ }
34
+ .animate-pulse {
35
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
36
+ }
37
+
38
+ /* Smooth transitions */
39
+ .transition-all {
40
+ transition-property: all;
41
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
42
+ transition-duration: 150ms;
43
+ }
44
+
45
+ /* Chat bubble styling */
46
+ .user-bubble {
47
+ background-color: #3b82f6;
48
+ color: white;
49
+ border-radius: 18px 18px 4px 18px;
50
+ }
51
+ .assistant-bubble {
52
+ background-color: #f3f4f6;
53
+ color: #111827;
54
+ border-radius: 18px 18px 18px 4px;
55
+ }
56
+ </style>
57
+ </head>
58
+ <body class="bg-gray-50 h-screen flex overflow-hidden">
59
+ <!-- Sidebar -->
60
+ <div class="w-64 bg-white border-r border-gray-200 flex flex-col h-full">
61
+ <div class="p-4 border-b border-gray-200">
62
+ <h1 class="text-xl font-bold text-gray-800">LM Studio Chat</h1>
63
+ <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">
64
+ <i class="fas fa-plus mr-2"></i> New Chat
65
+ </button>
66
+ </div>
67
+ <div class="flex-1 overflow-y-auto" id="conversation-list">
68
+ <!-- Conversations will be loaded here -->
69
+ </div>
70
+ <div class="p-4 border-t border-gray-200">
71
+ <div class="flex items-center space-x-2">
72
+ <img src="https://ui-avatars.com/api/?name=User&background=3b82f6&color=fff" alt="User" class="w-8 h-8 rounded-full">
73
+ <span class="font-medium text-gray-700">User</span>
74
+ </div>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Main Chat Area -->
79
+ <div class="flex-1 flex flex-col h-full">
80
+ <!-- Connection and Model Selection Bar -->
81
+ <div class="bg-white border-b border-gray-200 p-3 flex items-center justify-between">
82
+ <div class="flex items-center space-x-4">
83
+ <div class="flex items-center space-x-2">
84
+ <span class="text-sm font-medium text-gray-600">Server:</span>
85
+ <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">
86
+ <button id="connect-btn" class="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded-md text-sm">
87
+ <i class="fas fa-plug mr-1"></i> Connect
88
+ </button>
89
+ </div>
90
+ <div class="flex items-center space-x-2">
91
+ <span class="text-sm font-medium text-gray-600">Model:</span>
92
+ <select id="model-select" class="px-3 py-1 border border-gray-300 rounded-md text-sm w-48" disabled>
93
+ <option value="">Not connected</option>
94
+ </select>
95
+ </div>
96
+ </div>
97
+ <div class="flex items-center space-x-4">
98
+ <div class="flex items-center space-x-2">
99
+ <span class="text-sm font-medium text-gray-600">TTS Voice:</span>
100
+ <select id="voice-select" class="px-3 py-1 border border-gray-300 rounded-md text-sm w-48">
101
+ <option value="">Select Voice</option>
102
+ </select>
103
+ </div>
104
+ <button id="tts-toggle" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md text-sm">
105
+ <i class="fas fa-volume-up mr-1"></i> Enable TTS
106
+ </button>
107
+ </div>
108
+ </div>
109
+
110
+ <!-- System Prompt Area -->
111
+ <div class="bg-gray-100 border-b border-gray-200 p-3">
112
+ <div class="flex items-center justify-between">
113
+ <span class="text-sm font-medium text-gray-600">System Prompt:</span>
114
+ <button id="edit-system-prompt" class="text-blue-600 hover:text-blue-800 text-sm">
115
+ <i class="fas fa-edit mr-1"></i> Edit
116
+ </button>
117
+ </div>
118
+ <div id="system-prompt-display" class="mt-1 text-sm text-gray-700 bg-white p-2 rounded border border-gray-200">
119
+ You are a helpful AI assistant. Be concise and helpful.
120
+ </div>
121
+ <div id="system-prompt-edit" class="hidden mt-1">
122
+ <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>
123
+ <div class="flex justify-end space-x-2 mt-2">
124
+ <button id="cancel-system-prompt" class="px-3 py-1 border border-gray-300 rounded-md text-sm">Cancel</button>
125
+ <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>
126
+ </div>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- Chat Messages -->
131
+ <div id="chat-messages" class="flex-1 overflow-y-auto p-4 space-y-4">
132
+ <div class="flex justify-center items-center h-full text-gray-400" id="empty-state">
133
+ <div class="text-center">
134
+ <i class="fas fa-comments text-4xl mb-2"></i>
135
+ <p>Start a new conversation</p>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Input Area -->
141
+ <div class="p-4 border-t border-gray-200 bg-white">
142
+ <div class="relative">
143
+ <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>
144
+ <div class="absolute right-3 bottom-3 flex space-x-2">
145
+ <button id="send-btn" class="bg-blue-600 hover:bg-blue-700 text-white p-2 rounded-full">
146
+ <i class="fas fa-paper-plane"></i>
147
+ </button>
148
+ <button id="stop-btn" class="bg-red-600 hover:bg-red-700 text-white p-2 rounded-full hidden">
149
+ <i class="fas fa-stop"></i>
150
+ </button>
151
+ </div>
152
+ </div>
153
+ <div class="flex items-center justify-between mt-2 text-xs text-gray-500">
154
+ <div>
155
+ <span id="connection-status" class="flex items-center">
156
+ <span class="w-2 h-2 rounded-full bg-red-500 mr-1"></span>
157
+ Disconnected
158
+ </span>
159
+ </div>
160
+ <div class="flex items-center space-x-2">
161
+ <span id="streaming-indicator" class="hidden items-center">
162
+ <span class="w-2 h-2 rounded-full bg-green-500 mr-1 animate-pulse"></span>
163
+ Streaming
164
+ </span>
165
+ <span id="tts-indicator" class="hidden items-center">
166
+ <span class="w-2 h-2 rounded-full bg-purple-500 mr-1 animate-pulse"></span>
167
+ Speaking
168
+ </span>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+
174
+ <!-- Conversation Settings Modal -->
175
+ <div id="conversation-settings-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
176
+ <div class="bg-white rounded-lg p-6 w-96">
177
+ <div class="flex justify-between items-center mb-4">
178
+ <h3 class="text-lg font-medium">Conversation Settings</h3>
179
+ <button id="close-settings-modal" class="text-gray-500 hover:text-gray-700">
180
+ <i class="fas fa-times"></i>
181
+ </button>
182
+ </div>
183
+ <div class="space-y-4">
184
+ <div>
185
+ <label class="block text-sm font-medium text-gray-700 mb-1">Title</label>
186
+ <input type="text" id="conversation-title" class="w-full p-2 border border-gray-300 rounded-md">
187
+ </div>
188
+ <div class="flex justify-end space-x-2">
189
+ <button id="delete-conversation" class="text-red-600 hover:text-red-800">Delete</button>
190
+ <button id="save-conversation-settings" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md">Save</button>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ <script>
197
+ // State management
198
+ const state = {
199
+ currentConversationId: null,
200
+ conversations: [],
201
+ isConnected: false,
202
+ models: [],
203
+ currentModel: null,
204
+ voices: [],
205
+ ttsEnabled: false,
206
+ currentVoice: null,
207
+ systemPrompt: "You are a helpful AI assistant. Be concise and helpful.",
208
+ abortController: null,
209
+ isStreaming: false,
210
+ isSpeaking: false
211
+ };
212
+
213
+ // DOM elements
214
+ const elements = {
215
+ chatMessages: document.getElementById('chat-messages'),
216
+ messageInput: document.getElementById('message-input'),
217
+ sendBtn: document.getElementById('send-btn'),
218
+ stopBtn: document.getElementById('stop-btn'),
219
+ serverUrl: document.getElementById('server-url'),
220
+ connectBtn: document.getElementById('connect-btn'),
221
+ modelSelect: document.getElementById('model-select'),
222
+ voiceSelect: document.getElementById('voice-select'),
223
+ ttsToggle: document.getElementById('tts-toggle'),
224
+ conversationList: document.getElementById('conversation-list'),
225
+ newChatBtn: document.getElementById('new-chat-btn'),
226
+ connectionStatus: document.getElementById('connection-status'),
227
+ streamingIndicator: document.getElementById('streaming-indicator'),
228
+ ttsIndicator: document.getElementById('tts-indicator'),
229
+ emptyState: document.getElementById('empty-state'),
230
+ systemPromptDisplay: document.getElementById('system-prompt-display'),
231
+ systemPromptEdit: document.getElementById('system-prompt-edit'),
232
+ systemPromptInput: document.getElementById('system-prompt-input'),
233
+ editSystemPrompt: document.getElementById('edit-system-prompt'),
234
+ cancelSystemPrompt: document.getElementById('cancel-system-prompt'),
235
+ saveSystemPrompt: document.getElementById('save-system-prompt'),
236
+ conversationSettingsModal: document.getElementById('conversation-settings-modal'),
237
+ closeSettingsModal: document.getElementById('close-settings-modal'),
238
+ conversationTitle: document.getElementById('conversation-title'),
239
+ deleteConversation: document.getElementById('delete-conversation'),
240
+ saveConversationSettings: document.getElementById('save-conversation-settings')
241
+ };
242
+
243
+ // Speech synthesis
244
+ const synth = window.speechSynthesis;
245
+ let utterance = null;
246
+
247
+ // Initialize the app
248
+ function init() {
249
+ loadVoices();
250
+ loadConversations();
251
+ setupEventListeners();
252
+
253
+ // Check if voices are already loaded (sometimes they are)
254
+ if (synth.getVoices().length > 0) {
255
+ populateVoiceList();
256
+ }
257
+
258
+ // Listen for voices changed event
259
+ synth.onvoiceschanged = populateVoiceList;
260
+ }
261
+
262
+ // Load available voices for TTS
263
+ function loadVoices() {
264
+ state.voices = synth.getVoices();
265
+ populateVoiceList();
266
+ }
267
+
268
+ // Populate the voice select dropdown
269
+ function populateVoiceList() {
270
+ state.voices = synth.getVoices();
271
+ elements.voiceSelect.innerHTML = '<option value="">Select Voice</option>';
272
+
273
+ state.voices.forEach(voice => {
274
+ const option = document.createElement('option');
275
+ option.textContent = `${voice.name} (${voice.lang})${voice.default ? ' - DEFAULT' : ''}`;
276
+ option.setAttribute('data-name', voice.name);
277
+ option.setAttribute('data-lang', voice.lang);
278
+ elements.voiceSelect.appendChild(option);
279
+ });
280
+ }
281
+
282
+ // Load conversations from localStorage
283
+ function loadConversations() {
284
+ const savedConversations = localStorage.getItem('lmStudioConversations');
285
+ if (savedConversations) {
286
+ state.conversations = JSON.parse(savedConversations);
287
+ renderConversationList();
288
+
289
+ // If there are conversations, select the first one
290
+ if (state.conversations.length > 0) {
291
+ loadConversation(state.conversations[0].id);
292
+ }
293
+ } else {
294
+ // Create a default conversation if none exist
295
+ createNewConversation();
296
+ }
297
+ }
298
+
299
+ // Save conversations to localStorage
300
+ function saveConversations() {
301
+ localStorage.setItem('lmStudioConversations', JSON.stringify(state.conversations));
302
+ }
303
+
304
+ // Create a new conversation
305
+ function createNewConversation() {
306
+ const newConversation = {
307
+ id: Date.now().toString(),
308
+ title: `New Conversation ${state.conversations.length + 1}`,
309
+ messages: [],
310
+ createdAt: new Date().toISOString(),
311
+ updatedAt: new Date().toISOString()
312
+ };
313
+
314
+ state.conversations.unshift(newConversation);
315
+ saveConversations();
316
+ renderConversationList();
317
+ loadConversation(newConversation.id);
318
+
319
+ // Clear the chat messages and hide empty state
320
+ elements.chatMessages.innerHTML = '';
321
+ elements.emptyState.classList.add('hidden');
322
+ }
323
+
324
+ // Load a conversation by ID
325
+ function loadConversation(conversationId) {
326
+ const conversation = state.conversations.find(c => c.id === conversationId);
327
+ if (!conversation) return;
328
+
329
+ state.currentConversationId = conversationId;
330
+ renderConversationMessages(conversation.messages);
331
+
332
+ // Update active conversation in the list
333
+ document.querySelectorAll('.conversation-item').forEach(item => {
334
+ item.classList.remove('bg-blue-50', 'border-blue-200');
335
+ if (item.dataset.id === conversationId) {
336
+ item.classList.add('bg-blue-50', 'border-blue-200');
337
+ }
338
+ });
339
+
340
+ // Hide empty state if there are messages
341
+ if (conversation.messages.length > 0) {
342
+ elements.emptyState.classList.add('hidden');
343
+ } else {
344
+ elements.emptyState.classList.remove('hidden');
345
+ }
346
+ }
347
+
348
+ // Render conversation list in sidebar
349
+ function renderConversationList() {
350
+ elements.conversationList.innerHTML = '';
351
+
352
+ state.conversations.forEach(conversation => {
353
+ const conversationElement = document.createElement('div');
354
+ 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' : ''}`;
355
+ conversationElement.dataset.id = conversation.id;
356
+
357
+ conversationElement.innerHTML = `
358
+ <div class="flex justify-between items-start">
359
+ <div class="flex-1 min-w-0">
360
+ <p class="text-sm font-medium text-gray-800 truncate">${conversation.title}</p>
361
+ <p class="text-xs text-gray-500">${new Date(conversation.updatedAt).toLocaleString()}</p>
362
+ </div>
363
+ <button class="text-gray-400 hover:text-gray-600 conversation-settings-btn" data-id="${conversation.id}">
364
+ <i class="fas fa-ellipsis-v"></i>
365
+ </button>
366
+ </div>
367
+ `;
368
+
369
+ conversationElement.addEventListener('click', () => loadConversation(conversation.id));
370
+ elements.conversationList.appendChild(conversationElement);
371
+ });
372
+
373
+ // Add event listeners for settings buttons
374
+ document.querySelectorAll('.conversation-settings-btn').forEach(btn => {
375
+ btn.addEventListener('click', (e) => {
376
+ e.stopPropagation();
377
+ openConversationSettingsModal(btn.dataset.id);
378
+ });
379
+ });
380
+ }
381
+
382
+ // Render messages in the chat area
383
+ function renderConversationMessages(messages) {
384
+ elements.chatMessages.innerHTML = '';
385
+
386
+ if (messages.length === 0) {
387
+ elements.emptyState.classList.remove('hidden');
388
+ return;
389
+ }
390
+
391
+ messages.forEach(message => {
392
+ const messageElement = document.createElement('div');
393
+ messageElement.className = `flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`;
394
+
395
+ const bubbleClass = message.role === 'user' ? 'user-bubble' : 'assistant-bubble';
396
+
397
+ messageElement.innerHTML = `
398
+ <div class="max-w-3/4 ${message.role === 'user' ? 'ml-16' : 'mr-16'}">
399
+ <div class="${bubbleClass} p-3 inline-block">
400
+ <div class="whitespace-pre-wrap">${message.content}</div>
401
+ </div>
402
+ <div class="text-xs text-gray-500 mt-1 ${message.role === 'user' ? 'text-right' : 'text-left'}">
403
+ ${new Date(message.timestamp).toLocaleTimeString()}
404
+ ${message.role === 'assistant' ? `
405
+ <button class="ml-2 text-blue-500 hover:text-blue-700 tts-play-btn" data-content="${encodeURIComponent(message.content)}">
406
+ <i class="fas fa-volume-up"></i>
407
+ </button>
408
+ ` : ''}
409
+ </div>
410
+ </div>
411
+ `;
412
+
413
+ elements.chatMessages.appendChild(messageElement);
414
+ });
415
+
416
+ // Scroll to bottom
417
+ elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight;
418
+
419
+ // Add event listeners for TTS buttons
420
+ document.querySelectorAll('.tts-play-btn').forEach(btn => {
421
+ btn.addEventListener('click', (e) => {
422
+ e.stopPropagation();
423
+ const content = decodeURIComponent(btn.dataset.content);
424
+ speak(content);
425
+ });
426
+ });
427
+ }
428
+
429
+ // Connect to LM Studio server
430
+ async function connectToServer() {
431
+ const serverUrl = elements.serverUrl.value;
432
+
433
+ if (!serverUrl) {
434
+ showAlert('Please enter a server URL', 'error');
435
+ return;
436
+ }
437
+
438
+ try {
439
+ elements.connectBtn.disabled = true;
440
+ elements.connectBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Connecting...';
441
+
442
+ // Test connection
443
+ const response = await fetch(`${serverUrl}/v1/models`, {
444
+ method: 'GET',
445
+ headers: {
446
+ 'Content-Type': 'application/json'
447
+ }
448
+ });
449
+
450
+ if (!response.ok) {
451
+ throw new Error('Failed to connect to server');
452
+ }
453
+
454
+ const data = await response.json();
455
+ state.models = data.data;
456
+ state.isConnected = true;
457
+
458
+ // Update UI
459
+ elements.modelSelect.disabled = false;
460
+ elements.modelSelect.innerHTML = '<option value="">Select a model</option>';
461
+ state.models.forEach(model => {
462
+ const option = document.createElement('option');
463
+ option.value = model.id;
464
+ option.textContent = model.id;
465
+ elements.modelSelect.appendChild(option);
466
+ });
467
+
468
+ elements.connectBtn.innerHTML = '<i class="fas fa-plug mr-1"></i> Connected';
469
+ elements.connectBtn.className = 'bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded-md text-sm';
470
+ elements.connectionStatus.innerHTML = '<span class="w-2 h-2 rounded-full bg-green-500 mr-1"></span> Connected';
471
+
472
+ showAlert('Successfully connected to server', 'success');
473
+ } catch (error) {
474
+ console.error('Connection error:', error);
475
+ showAlert(`Connection failed: ${error.message}`, 'error');
476
+ elements.connectBtn.disabled = false;
477
+ elements.connectBtn.innerHTML = '<i class="fas fa-plug mr-1"></i> Connect';
478
+ }
479
+ }
480
+
481
+ // Send message to LM Studio
482
+ async function sendMessage() {
483
+ const message = elements.messageInput.value.trim();
484
+ if (!message || !state.isConnected || !state.currentModel) return;
485
+
486
+ // Add user message to conversation
487
+ const userMessage = {
488
+ role: 'user',
489
+ content: message,
490
+ timestamp: new Date().toISOString()
491
+ };
492
+
493
+ addMessageToCurrentConversation(userMessage);
494
+ elements.messageInput.value = '';
495
+
496
+ // Create assistant message placeholder
497
+ const assistantMessage = {
498
+ role: 'assistant',
499
+ content: '',
500
+ timestamp: new Date().toISOString()
501
+ };
502
+
503
+ const messageId = addMessageToCurrentConversation(assistantMessage);
504
+
505
+ // Prepare the request
506
+ const messages = [
507
+ { role: 'system', content: state.systemPrompt },
508
+ ...getCurrentConversation().messages
509
+ .filter(m => m.role !== 'system')
510
+ .map(m => ({ role: m.role, content: m.content }))
511
+ ];
512
+
513
+ state.abortController = new AbortController();
514
+ state.isStreaming = true;
515
+ elements.streamingIndicator.classList.remove('hidden');
516
+ elements.stopBtn.classList.remove('hidden');
517
+ elements.sendBtn.classList.add('hidden');
518
+
519
+ try {
520
+ const response = await fetch(`${elements.serverUrl.value}/v1/chat/completions`, {
521
+ method: 'POST',
522
+ headers: {
523
+ 'Content-Type': 'application/json'
524
+ },
525
+ body: JSON.stringify({
526
+ model: state.currentModel,
527
+ messages: messages,
528
+ stream: true,
529
+ temperature: 0.7,
530
+ max_tokens: 1000
531
+ }),
532
+ signal: state.abortController.signal
533
+ });
534
+
535
+ if (!response.ok) {
536
+ throw new Error(`Server responded with ${response.status}`);
537
+ }
538
+
539
+ const reader = response.body.getReader();
540
+ const decoder = new TextDecoder();
541
+ let assistantMessageContent = '';
542
+
543
+ while (true) {
544
+ const { done, value } = await reader.read();
545
+ if (done) break;
546
+
547
+ const chunk = decoder.decode(value);
548
+ const lines = chunk.split('\n').filter(line => line.trim() !== '');
549
+
550
+ for (const line of lines) {
551
+ if (line.startsWith('data: ') && !line.includes('[DONE]')) {
552
+ try {
553
+ const data = JSON.parse(line.substring(6));
554
+ if (data.choices && data.choices[0].delta && data.choices[0].delta.content) {
555
+ assistantMessageContent += data.choices[0].delta.content;
556
+ updateMessageContent(messageId, assistantMessageContent);
557
+ }
558
+ } catch (e) {
559
+ console.error('Error parsing stream data:', e);
560
+ }
561
+ }
562
+ }
563
+ }
564
+
565
+ // If TTS is enabled, speak the response
566
+ if (state.ttsEnabled && state.currentVoice) {
567
+ speak(assistantMessageContent);
568
+ }
569
+
570
+ } catch (error) {
571
+ if (error.name !== 'AbortError') {
572
+ console.error('Error streaming response:', error);
573
+ showAlert(`Error: ${error.message}`, 'error');
574
+ }
575
+ } finally {
576
+ state.isStreaming = false;
577
+ elements.streamingIndicator.classList.add('hidden');
578
+ elements.stopBtn.classList.add('hidden');
579
+ elements.sendBtn.classList.remove('hidden');
580
+ state.abortController = null;
581
+ }
582
+ }
583
+
584
+ // Stop streaming
585
+ function stopStreaming() {
586
+ if (state.abortController) {
587
+ state.abortController.abort();
588
+ state.isStreaming = false;
589
+ elements.streamingIndicator.classList.add('hidden');
590
+ elements.stopBtn.classList.add('hidden');
591
+ elements.sendBtn.classList.remove('hidden');
592
+ }
593
+
594
+ if (state.isSpeaking) {
595
+ synth.cancel();
596
+ state.isSpeaking = false;
597
+ elements.ttsIndicator.classList.add('hidden');
598
+ }
599
+ }
600
+
601
+ // Add message to current conversation
602
+ function addMessageToCurrentConversation(message) {
603
+ const conversation = getCurrentConversation();
604
+ if (!conversation) return null;
605
+
606
+ const messageId = Date.now().toString();
607
+ conversation.messages.push({
608
+ ...message,
609
+ id: messageId
610
+ });
611
+
612
+ conversation.updatedAt = new Date().toISOString();
613
+ saveConversations();
614
+ renderConversationMessages(conversation.messages);
615
+
616
+ return messageId;
617
+ }
618
+
619
+ // Update message content
620
+ function updateMessageContent(messageId, content) {
621
+ const conversation = getCurrentConversation();
622
+ if (!conversation) return;
623
+
624
+ const message = conversation.messages.find(m => m.id === messageId);
625
+ if (message) {
626
+ message.content = content;
627
+ message.timestamp = new Date().toISOString();
628
+ conversation.updatedAt = new Date().toISOString();
629
+
630
+ // Update the UI
631
+ const messageElement = document.querySelector(`[data-id="${messageId}"]`);
632
+ if (messageElement) {
633
+ messageElement.querySelector('.whitespace-pre-wrap').textContent = content;
634
+ }
635
+
636
+ // Scroll to bottom
637
+ elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight;
638
+ }
639
+ }
640
+
641
+ // Get current conversation
642
+ function getCurrentConversation() {
643
+ return state.conversations.find(c => c.id === state.currentConversationId);
644
+ }
645
+
646
+ // Speak text using TTS
647
+ function speak(text) {
648
+ if (synth.speaking) {
649
+ synth.cancel();
650
+ }
651
+
652
+ if (!state.currentVoice) {
653
+ showAlert('Please select a voice first', 'error');
654
+ return;
655
+ }
656
+
657
+ utterance = new SpeechSynthesisUtterance(text);
658
+ utterance.voice = state.currentVoice;
659
+ utterance.rate = 1.0;
660
+ utterance.pitch = 1.0;
661
+
662
+ utterance.onstart = () => {
663
+ state.isSpeaking = true;
664
+ elements.ttsIndicator.classList.remove('hidden');
665
+ };
666
+
667
+ utterance.onend = () => {
668
+ state.isSpeaking = false;
669
+ elements.ttsIndicator.classList.add('hidden');
670
+ };
671
+
672
+ utterance.onerror = (event) => {
673
+ console.error('Speech synthesis error:', event);
674
+ state.isSpeaking = false;
675
+ elements.ttsIndicator.classList.add('hidden');
676
+ showAlert('Error with speech synthesis', 'error');
677
+ };
678
+
679
+ synth.speak(utterance);
680
+ }
681
+
682
+ // Toggle TTS
683
+ function toggleTTS() {
684
+ state.ttsEnabled = !state.ttsEnabled;
685
+
686
+ if (state.ttsEnabled) {
687
+ elements.ttsToggle.innerHTML = '<i class="fas fa-volume-up mr-1"></i> Disable TTS';
688
+ elements.ttsToggle.className = 'bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md text-sm';
689
+ showAlert('Text-to-speech enabled', 'success');
690
+ } else {
691
+ elements.ttsToggle.innerHTML = '<i class="fas fa-volume-up mr-1"></i> Enable TTS';
692
+ elements.ttsToggle.className = 'bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md text-sm';
693
+
694
+ // Stop any ongoing speech
695
+ if (synth.speaking) {
696
+ synth.cancel();
697
+ }
698
+ }
699
+ }
700
+
701
+ // Open conversation settings modal
702
+ function openConversationSettingsModal(conversationId) {
703
+ const conversation = state.conversations.find(c => c.id === conversationId);
704
+ if (!conversation) return;
705
+
706
+ elements.conversationTitle.value = conversation.title;
707
+ elements.conversationSettingsModal.dataset.id = conversationId;
708
+ elements.conversationSettingsModal.classList.remove('hidden');
709
+ }
710
+
711
+ // Save conversation settings
712
+ function saveConversationSettings() {
713
+ const conversationId = elements.conversationSettingsModal.dataset.id;
714
+ const conversation = state.conversations.find(c => c.id === conversationId);
715
+ if (!conversation) return;
716
+
717
+ conversation.title = elements.conversationTitle.value.trim() || conversation.title;
718
+ conversation.updatedAt = new Date().toISOString();
719
+
720
+ saveConversations();
721
+ renderConversationList();
722
+ elements.conversationSettingsModal.classList.add('hidden');
723
+ }
724
+
725
+ // Delete conversation
726
+ function deleteConversation() {
727
+ const conversationId = elements.conversationSettingsModal.dataset.id;
728
+
729
+ // Confirm deletion
730
+ if (!confirm('Are you sure you want to delete this conversation?')) {
731
+ return;
732
+ }
733
+
734
+ // Remove the conversation
735
+ state.conversations = state.conversations.filter(c => c.id !== conversationId);
736
+
737
+ // If we deleted the current conversation, select another one or create a new one
738
+ if (state.currentConversationId === conversationId) {
739
+ if (state.conversations.length > 0) {
740
+ loadConversation(state.conversations[0].id);
741
+ } else {
742
+ createNewConversation();
743
+ }
744
+ }
745
+
746
+ saveConversations();
747
+ renderConversationList();
748
+ elements.conversationSettingsModal.classList.add('hidden');
749
+ }
750
+
751
+ // Show alert message
752
+ function showAlert(message, type) {
753
+ const alert = document.createElement('div');
754
+ alert.className = `fixed top-4 right-4 p-4 rounded-md shadow-md text-white ${
755
+ type === 'error' ? 'bg-red-500' :
756
+ type === 'success' ? 'bg-green-500' : 'bg-blue-500'
757
+ }`;
758
+ alert.textContent = message;
759
+
760
+ document.body.appendChild(alert);
761
+
762
+ setTimeout(() => {
763
+ alert.classList.add('opacity-0', 'transition-opacity', 'duration-300');
764
+ setTimeout(() => alert.remove(), 300);
765
+ }, 3000);
766
+ }
767
+
768
+ // Setup event listeners
769
+ function setupEventListeners() {
770
+ // Send message on Enter (Shift+Enter for new line)
771
+ elements.messageInput.addEventListener('keydown', (e) => {
772
+ if (e.key === 'Enter' && !e.shiftKey) {
773
+ e.preventDefault();
774
+ sendMessage();
775
+ }
776
+ });
777
+
778
+ // Send button
779
+ elements.sendBtn.addEventListener('click', sendMessage);
780
+
781
+ // Stop button
782
+ elements.stopBtn.addEventListener('click', stopStreaming);
783
+
784
+ // Connect button
785
+ elements.connectBtn.addEventListener('click', connectToServer);
786
+
787
+ // Model selection
788
+ elements.modelSelect.addEventListener('change', (e) => {
789
+ state.currentModel = e.target.value;
790
+ });
791
+
792
+ // Voice selection
793
+ elements.voiceSelect.addEventListener('change', (e) => {
794
+ const selectedOption = e.target.selectedOptions[0];
795
+ if (selectedOption.value === '') {
796
+ state.currentVoice = null;
797
+ } else {
798
+ const voiceName = selectedOption.dataset.name;
799
+ state.currentVoice = state.voices.find(v => v.name === voiceName);
800
+ }
801
+ });
802
+
803
+ // TTS toggle
804
+ elements.ttsToggle.addEventListener('click', toggleTTS);
805
+
806
+ // New conversation button
807
+ elements.newChatBtn.addEventListener('click', createNewConversation);
808
+
809
+ // System prompt edit
810
+ elements.editSystemPrompt.addEventListener('click', () => {
811
+ elements.systemPromptDisplay.classList.add('hidden');
812
+ elements.systemPromptEdit.classList.remove('hidden');
813
+ });
814
+
815
+ // Cancel system prompt edit
816
+ elements.cancelSystemPrompt.addEventListener('click', () => {
817
+ elements.systemPromptInput.value = state.systemPrompt;
818
+ elements.systemPromptEdit.classList.add('hidden');
819
+ elements.systemPromptDisplay.classList.remove('hidden');
820
+ });
821
+
822
+ // Save system prompt
823
+ elements.saveSystemPrompt.addEventListener('click', () => {
824
+ state.systemPrompt = elements.systemPromptInput.value.trim() || state.systemPrompt;
825
+ elements.systemPromptDisplay.textContent = state.systemPrompt;
826
+ elements.systemPromptEdit.classList.add('hidden');
827
+ elements.systemPromptDisplay.classList.remove('hidden');
828
+ showAlert('System prompt updated', 'success');
829
+ });
830
+
831
+ // Conversation settings modal
832
+ elements.closeSettingsModal.addEventListener('click', () => {
833
+ elements.conversationSettingsModal.classList.add('hidden');
834
+ });
835
+
836
+ // Save conversation settings
837
+ elements.saveConversationSettings.addEventListener('click', saveConversationSettings);
838
+
839
+ // Delete conversation
840
+ elements.deleteConversation.addEventListener('click', deleteConversation);
841
+
842
+ // Close modal when clicking outside
843
+ elements.conversationSettingsModal.addEventListener('click', (e) => {
844
+ if (e.target === elements.conversationSettingsModal) {
845
+ elements.conversationSettingsModal.classList.add('hidden');
846
+ }
847
+ });
848
+ }
849
+
850
+ // Initialize the app when DOM is loaded
851
+ document.addEventListener('DOMContentLoaded', init);
852
+ </script>
853
+ <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>
854
+ </html>
prompts.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ I need a front end chat interface that will use LMstudio server running on the backend and this beautiful frontend chat interface needs to have great sounding TTS text-to-speech. You should be able to connect to LMSTUDIO, select a model from there unless its already running then its a drop down. The repsonse from the server should be streaming so you see the text at is generates it and not one big blob at the end. You should be able to set a system prompt from the interface as well. Edit conversations, delete and create conversations and they should be stored off to the side. Like any chat interface for LLM except the main part is it has to have some kind of TTS there are plenty of options out there. lets try one of the many free options.