Spaces:
Running
Running
Add 3 files
Browse files- README.md +7 -5
- index.html +854 -19
- prompts.txt +1 -0
README.md
CHANGED
@@ -1,10 +1,12 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
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 |
-
<!
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
|