HirCoir commited on
Commit
a762856
1 Parent(s): 374e2c4

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +745 -179
index.html CHANGED
@@ -1,196 +1,762 @@
1
  <!DOCTYPE html>
2
- <html lang="es">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Convertidor de Texto a Audio</title>
 
 
7
  <style>
8
- body {
9
- background-color: #222;
10
- color: #eee;
11
- font-family: Arial, sans-serif;
12
- text-align: center;
13
- margin: 0;
14
- padding: 0;
15
- }
16
- h1 {
17
- margin-top: 50px;
18
- }
19
- form {
20
- margin-top: 20px;
21
- display: flex;
22
- flex-direction: column;
23
- align-items: center;
24
- }
25
- label, select, textarea {
26
- margin: 5px;
27
- font-size: 16px;
28
- color: #eee;
29
- }
30
- textarea {
31
- background-color: #333;
32
- color: #eee;
33
- border: 1px solid #666;
34
- border-radius: 5px;
35
- padding: 10px;
36
- width: 90%;
37
- height: 100px;
38
- }
39
- select {
40
- background-color: #333;
41
- color: #eee;
42
- border: 1px solid #666;
43
- border-radius: 5px;
44
- padding: 5px;
45
- width: 80%;
46
- height: 30px;
47
- }
48
- button {
49
- background-color: #007bff;
50
- color: #eee;
51
- border: none;
52
- border-radius: 5px;
53
- padding: 10px 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  cursor: pointer;
55
- transition: background-color 0.3s ease;
56
- }
57
- button:hover {
58
- background-color: #0056b3;
59
- }
60
- #audio-container {
61
- margin-top: 20px;
62
- }
63
- .animated-button {
64
- position: relative;
65
- display: flex;
66
- align-items: center;
67
- gap: 4px;
68
- padding: 16px 36px;
69
- border: 4px solid;
70
- border-color: transparent;
71
- font-size: 16px;
72
- background-color: inherit;
73
- border-radius: 100px;
74
- font-weight: 600;
75
- color: greenyellow;
76
- box-shadow: 0 0 0 2px greenyellow;
77
- cursor: pointer;
78
- overflow: hidden;
79
- transition: all 0.6s cubic-bezier(0.23, 1, 0.32, 1);
80
- }
81
- .animated-button svg {
82
- position: absolute;
83
- width: 24px;
84
- fill: greenyellow;
85
- z-index: 9;
86
- transition: all 0.8s cubic-bezier(0.23, 1, 0.32, 1);
87
- }
88
- .animated-button .arr-1 {
89
- right: 16px;
90
- }
91
- .animated-button .arr-2 {
92
- left: -25%;
93
- }
94
- .animated-button .circle {
95
- position: absolute;
96
- top: 50%;
97
- left: 50%;
98
- transform: translate(-50%, -50%);
99
- width: 20px;
100
- height: 20px;
101
- background-color: rgb(208, 162, 246);
102
- border-radius: 50%;
103
- opacity: 0;
104
- transition: all 0.8s cubic-bezier(0.23, 1, 0.32, 1);
105
- }
106
- .animated-button .text {
107
- position: relative;
108
- z-index: 1;
109
- transform: translateX(-12px);
110
- transition: all 0.8s cubic-bezier(0.23, 1, 0.32, 1);
111
- }
112
- .animated-button:hover {
113
- box-shadow: 0 0 0 12px transparent;
114
- color: #212121;
115
- border-radius: 12px;
116
- }
117
- .animated-button:hover .arr-1 {
118
- right: -25%;
119
- }
120
- .animated-button:hover .arr-2 {
121
- left: 16px;
122
- }
123
- .animated-button:hover .text {
124
- transform: translateX(12px);
125
- }
126
- .animated-button:hover svg {
127
- fill: #212121;
128
- }
129
- .animated-button:active {
130
- scale: 0.95;
131
- box-shadow: 0 0 0 4px rgb(47, 196, 255);
132
- }
133
- .animated-button:hover .circle {
134
- width: 220px;
135
- height: 220px;
136
- opacity: 1;
137
- }
138
- #audio-container {
139
- margin-top: 20px;
140
- }
141
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  </head>
143
- <body>
144
- <h1>Convertidor de Texto a Audio</h1>
145
- <form action="/convert" method="post">
146
- <label for="model">Selecciona el modelo ONNX:</label><br>
147
- <div> <p>Prueba nuevos modelos avanzados en HirLab:</p> <a href="https://tts.hircoir.eu.org" style="color: #ADD8E6;" target="_blank"> Modelos disponibles: Sora Español México, Voz HirCoir, Kamora Español México </a> <p>Try new advanced models at HirLab:</p> <a href="https://tts.hircoir.eu.org" style="color: #ADD8E6;" target="_blank"> Available models: Sora Spanish Mexico, Voz HirCoir, Kamora Spanish Mexico </a> </div>
148
- <select id="model" name="model">
149
- {% for model in model_options %}
150
- <option value="{{ model }}">{{ model }}</option>
151
- {% endfor %}
152
- </select><br>
153
- <label for="text">Texto:</label><br>
154
- <textarea placeholder="Escribe tu texto aquí, solo se tomará los primeros 500 carácteres." id="text" name="text" rows="4"></textarea>
155
- <button class="animated-button">
156
- <svg viewBox="0 0 24 24" class="arr-2" xmlns="http://www.w3.org/2000/svg">
157
- <path
158
- d="M16.1716 10.9999L10.8076 5.63589L12.2218 4.22168L20 11.9999L12.2218 19.778L10.8076 18.3638L16.1716 12.9999H4V10.9999H16.1716Z"
159
- ></path>
160
- </svg>
161
- <span class="text">Generar audio</span>
162
- <span class="circle"></span>
163
- <svg viewBox="0 0 24 24" class="arr-1" xmlns="http://www.w3.org/2000/svg">
164
- <path
165
- d="M16.1716 10.9999L10.8076 5.63589L12.2218 4.22168L20 11.9999L12.2218 19.778L10.8076 18.3638L16.1716 12.9999H4V10.9999H16.1716Z"
166
- ></path>
167
- </svg>
168
- </button>
169
- </form>
170
- <div id="audio-container"></div>
171
-
172
- <!-- Video de YouTube -->
173
- <div style="margin-top: 20px;">
174
- <iframe width="560" height="315" src="https://www.youtube.com/embed/pLJTvJ9nJEY" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  </div>
176
 
177
  <script>
178
- document.querySelector('form').addEventListener('submit', async function (e) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  e.preventDefault();
180
- const formData = new FormData(e.target);
181
- const response = await fetch('/convert', {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  method: 'POST',
183
- body: formData
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  });
185
- const data = await response.json();
186
- const audioContent = data.audio_base64;
187
- const audioElement = document.createElement('audio');
188
- audioElement.src = 'data:audio/wav;base64,' + audioContent;
189
- audioElement.controls = true;
190
- audioElement.autoplay = true; // Autoreproducción del audio
191
- document.getElementById('audio-container').innerHTML = '';
192
- document.getElementById('audio-container').appendChild(audioElement);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  </script>
195
  </body>
196
  </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>AI Chat Interface</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
9
  <style>
10
+ :root {
11
+ --primary-color: #2563eb;
12
+ --secondary-color: #3b82f6;
13
+ --background-dark: #111827;
14
+ --sidebar-dark: #1f2937;
15
+ --chat-area-dark: #1a1f2b;
16
+ }
17
+
18
+ /* Custom scrollbar */
19
+ ::-webkit-scrollbar {
20
+ width: 6px;
21
+ }
22
+
23
+ ::-webkit-scrollbar-track {
24
+ background: var(--background-dark);
25
+ }
26
+
27
+ ::-webkit-scrollbar-thumb {
28
+ background-color: #4b5563;
29
+ border-radius: 3px;
30
+ }
31
+
32
+ ::-webkit-scrollbar-thumb:hover {
33
+ background-color: #6b7280;
34
+ }
35
+
36
+ /* Chat styles */
37
+ .chat-message {
38
+ max-width: 85%;
39
+ word-wrap: break-word;
40
+ animation: fadeIn 0.3s ease-in-out;
41
+ }
42
+
43
+ .user-message {
44
+ background-color: var(--primary-color);
45
+ color: white;
46
+ margin-left: auto;
47
+ }
48
+
49
+ .assistant-message {
50
+ background-color: var(--chat-area-dark);
51
+ color: #e5e7eb;
52
+ margin-right: auto;
53
+ border: 1px solid #374151;
54
+ }
55
+
56
+ /* Code blocks */
57
+ .markdown-content pre {
58
+ background-color: #1a1f2b;
59
+ padding: 1rem;
60
+ border-radius: 0.5rem;
61
+ overflow-x: auto;
62
+ margin: 1rem 0;
63
+ border: 1px solid #374151;
64
+ }
65
+
66
+ .markdown-content code {
67
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
68
+ font-size: 0.9em;
69
+ padding: 0.2rem 0.4rem;
70
+ border-radius: 0.25rem;
71
+ background-color: rgba(45, 55, 72, 0.5);
72
+ }
73
+
74
+ /* Context Menu Styles */
75
+ .context-menu {
76
+ position: fixed;
77
+ background: var(--sidebar-dark);
78
+ border: 1px solid #374151;
79
+ border-radius: 0.5rem;
80
+ padding: 0.5rem 0;
81
+ min-width: 160px;
82
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
83
+ z-index: 1000;
84
+ display: none;
85
+ }
86
+
87
+ .context-menu-item {
88
+ padding: 0.5rem 1rem;
89
  cursor: pointer;
90
+ display: flex;
91
+ align-items: center;
92
+ transition: background-color 0.2s;
93
+ }
94
+
95
+ .context-menu-item:hover {
96
+ background-color: var(--primary-color);
97
+ }
98
+
99
+ .context-menu-item i {
100
+ margin-right: 0.5rem;
101
+ width: 20px;
102
+ }
103
+
104
+ /* Edit Modal Styles */
105
+ .modal {
106
+ display: none;
107
+ position: fixed;
108
+ top: 0;
109
+ left: 0;
110
+ width: 100%;
111
+ height: 100%;
112
+ background-color: rgba(0, 0, 0, 0.5);
113
+ z-index: 1001;
114
+ }
115
+
116
+ .modal-content {
117
+ position: relative;
118
+ background-color: var(--sidebar-dark);
119
+ margin: 0;
120
+ padding: 1.5rem;
121
+ border-radius: 0;
122
+ width: 100%;
123
+ height: 100%;
124
+ display: flex;
125
+ flex-direction: column;
126
+ }
127
+
128
+ .close-modal {
129
+ position: absolute;
130
+ right: 1rem;
131
+ top: 1rem;
132
+ cursor: pointer;
133
+ font-size: 1.5rem;
134
+ color: #9ca3af;
135
+ transition: color 0.2s;
136
+ }
137
+
138
+ .close-modal:hover {
139
+ color: #f3f4f6;
140
+ }
141
+
142
+ .modal-textarea {
143
+ flex-grow: 1;
144
+ margin-bottom: 1rem;
145
+ }
146
+
147
+ /* Animations */
148
+ @keyframes fadeIn {
149
+ from { opacity: 0; transform: translateY(10px); }
150
+ to { opacity: 1; transform: translateY(0); }
151
+ }
152
+
153
+ @keyframes pulse {
154
+ 0% { transform: scale(1); }
155
+ 50% { transform: scale(1.1); }
156
+ 100% { transform: scale(1); }
157
+ }
158
+
159
+ .sidebar-collapsed {
160
+ width: 4rem !important;
161
+ }
162
+
163
+ .sidebar-expanded {
164
+ width: 18rem;
165
+ }
166
+
167
+ .transition-width {
168
+ transition: width 0.3s ease-in-out;
169
+ }
170
+
171
+ /* Glass effect */
172
+ .glass-effect {
173
+ background: rgba(31, 41, 55, 0.7);
174
+ backdrop-filter: blur(10px);
175
+ border: 1px solid rgba(255, 255, 255, 0.1);
176
+ }
177
+
178
+ /* Button effects */
179
+ .hover-shadow {
180
+ transition: all 0.3s ease;
181
+ }
182
+
183
+ .hover-shadow:hover {
184
+ transform: translateY(-1px);
185
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
186
+ }
187
+
188
+ /* Input focus effects */
189
+ .input-focus {
190
+ transition: all 0.3s ease;
191
+ border: 1px solid rgba(255, 255, 255, 0.1);
192
+ }
193
+
194
+ .input-focus:focus {
195
+ border-color: var(--primary-color);
196
+ box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
197
+ outline: none;
198
+ }
199
+
200
+ /* Responsive design */
201
+ @media (max-width: 768px) {
202
+ .chat-message {
203
+ max-width: 95%;
204
+ }
205
+ }
206
+
207
+ /* Hide sidebar content when collapsed */
208
+ .sidebar-collapsed .sidebar-content {
209
+ display: none;
210
+ }
211
+
212
+ .sidebar-collapsed .sidebar-icon {
213
+ display: block;
214
+ }
215
+
216
+ /* Disabled UI styles */
217
+ .disabled {
218
+ pointer-events: none;
219
+ opacity: 0.6;
220
+ }
221
+
222
+ /* Stop button styles */
223
+ .stop-button {
224
+ display: none;
225
+ animation: pulse 1s infinite;
226
+ }
227
+ </style>
228
  </head>
229
+ <body class="bg-gradient-to-br from-gray-900 to-gray-800 text-gray-100 min-h-screen">
230
+ <!-- Context Menu -->
231
+ <div id="contextMenu" class="context-menu">
232
+ <div class="context-menu-item" id="editMenuItem">
233
+ <i class="fas fa-edit"></i>
234
+ <span>Edit</span>
235
+ </div>
236
+ </div>
237
+
238
+ <!-- Edit Modal -->
239
+ <div id="editModal" class="modal">
240
+ <div class="modal-content">
241
+ <span class="close-modal">&times;</span>
242
+ <h2 class="text-xl font-bold mb-4">Edit Message</h2>
243
+ <textarea id="editMessageInput" class="modal-textarea w-full bg-gray-700 input-focus rounded-lg px-3 py-2 text-sm"></textarea>
244
+ <div class="flex justify-end space-x-2">
245
+ <button id="cancelEditBtn" class="bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg transition">Cancel</button>
246
+ <button id="saveEditBtn" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg transition">Save</button>
247
+ </div>
248
+ </div>
249
+ </div>
250
+
251
+ <div class="flex h-screen">
252
+ <!-- Collapsible Sidebar -->
253
+ <div id="sidebar" class="transition-width sidebar-expanded bg-gray-800 glass-effect flex flex-col h-full">
254
+ <div class="p-4 flex items-center justify-between sidebar-icon">
255
+ <button id="toggleSidebar" class="text-gray-400 hover:text-white">
256
+ <i class="fas fa-bars text-xl"></i>
257
+ </button>
258
+ <span id="sidebarTitle" class="font-semibold text-lg ml-2 sidebar-content">AI Chat</span>
259
+ </div>
260
+
261
+ <button id="newChatBtn" class="flex items-center m-4 bg-gradient-to-r from-blue-600 to-blue-500 text-white px-4 py-2 rounded-lg hover-shadow sidebar-content">
262
+ <i class="fas fa-plus mr-2"></i>
263
+ <span>New Chat</span>
264
+ </button>
265
+
266
+ <div class="p-4 sidebar-content">
267
+ <div class="space-y-3">
268
+ <input type="text" id="baseHost" placeholder="Ollama Base Host" value="http://localhost:11434"
269
+ class="w-full bg-gray-700 input-focus rounded-lg px-3 py-2 text-sm">
270
+ <select id="ollamaModel" class="w-full bg-gray-700 input-focus rounded-lg px-3 py-2 text-sm">
271
+ <option value="">Select Model</option>
272
+ </select>
273
+ <button id="listModelsBtn" class="w-full bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg text-sm transition">
274
+ <i class="fas fa-sync-alt mr-2"></i>List Models
275
+ </button>
276
+ </div>
277
+ </div>
278
+
279
+ <div class="flex-grow overflow-y-auto px-4 sidebar-content">
280
+ <h3 class="text-sm font-semibold text-gray-400 mb-2">Saved Chats</h3>
281
+ <div id="chatsList" class="space-y-2">
282
+ {% for chat in chat_files %}
283
+ <div class="chat-file hover:bg-gray-700 p-2 rounded-lg cursor-pointer transition" data-file="{{ chat }}">
284
+ <i class="fas fa-comment-alt mr-2"></i>{{ chat }}
285
+ </div>
286
+ {% endfor %}
287
+ </div>
288
+ </div>
289
+ </div>
290
+
291
+ <!-- Main Content -->
292
+ <div class="flex-grow flex flex-col bg-gray-900">
293
+ <!-- Chat Area -->
294
+ <div id="chatArea" class="flex-grow overflow-y-auto p-4 space-y-4">
295
+ <!-- Messages will be inserted here -->
296
+ </div>
297
+
298
+ <!-- Input Area -->
299
+ <div class="border-t border-gray-700 p-4 space-y-4">
300
+ <div class="flex space-x-2">
301
+ <textarea id="messageInput" rows="3" placeholder="Type your message here..."
302
+ class="flex-grow bg-gray-800 input-focus rounded-lg px-4 py-2 resize-none"></textarea>
303
+ <div class="flex flex-col space-y-2">
304
+ <button id="sendBtn" class="bg-gradient-to-r from-blue-600 to-blue-500 px-6 py-2 rounded-lg hover-shadow">
305
+ <i class="fas fa-paper-plane mr-2"></i>Send
306
+ </button>
307
+ <button id="stopBtn" class="stop-button bg-red-600 hover:bg-red-700 px-6 py-2 rounded-lg transition">
308
+ <i class="fas fa-stop mr-2"></i>Stop
309
+ </button>
310
+ </div>
311
+ </div>
312
+
313
+ <!-- TTS Controls -->
314
+ <div class="flex items-center space-x-4 bg-gray-800 p-4 rounded-lg glass-effect">
315
+ <select id="ttsModel" class="bg-gray-700 input-focus rounded-lg px-3 py-2 text-sm">
316
+ <option value="">Select TTS Model</option>
317
+ {% for model in tts_models %}
318
+ <option value="{{ model }}">{{ model }}</option>
319
+ {% endfor %}
320
+ </select>
321
+
322
+ <button id="playBtn" class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg transition">
323
+ <i class="fas fa-play mr-2"></i>Play
324
+ </button>
325
+
326
+ <button id="pauseBtn" class="bg-yellow-600 hover:bg-yellow-700 px-4 py-2 rounded-lg transition">
327
+ <i class="fas fa-pause mr-2"></i>Pause
328
+ </button>
329
+
330
+ <div class="flex items-center space-x-2">
331
+ <i class="fas fa-volume-up text-gray-400"></i>
332
+ <input type="range" id="volumeSlider" min="0" max="100" value="100"
333
+ class="w-24 accent-blue-500">
334
+ </div>
335
+
336
+ <label class="flex items-center space-x-2 text-sm">
337
+ <input type="checkbox" id="removeMarkdown" class="form-checkbox text-blue-500">
338
+ <span>Remove Markdown</span>
339
+ </label>
340
+ </div>
341
+ </div>
342
+ </div>
343
  </div>
344
 
345
  <script>
346
+ let currentChatFile = null;
347
+ let conversationHistory = [];
348
+ let currentAudio = null;
349
+ let currentEventSource = null;
350
+ let lastAssistantMessage = null;
351
+ let abortController = null;
352
+ let selectedOllamaModel = '';
353
+ let selectedTtsModel = '';
354
+ let editingMessageIndex = null;
355
+
356
+ function escapeHtml(html) {
357
+ const div = document.createElement('div');
358
+ div.textContent = html;
359
+ return div.innerHTML;
360
+ }
361
+
362
+ function renderMarkdown(text) {
363
+ let html = text
364
+ .replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
365
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
366
+ .replace(/^### (.*$)/gm, '<h3>$1</h3>')
367
+ .replace(/^## (.*$)/gm, '<h2>$1</h2>')
368
+ .replace(/^# (.*$)/gm, '<h1>$1</h1>')
369
+ .replace(/^\* (.*$)/gm, '<li>$1</li>')
370
+ .replace(/^\d\. (.*$)/gm, '<li>$1</li>')
371
+ .replace(/\n\n/g, '</p><p>')
372
+ .replace(/\n/g, '<br>');
373
+
374
+ return `<p>${html}</p>`;
375
+ }
376
+
377
+ function addMessage(text, isUser = true, messageIndex = null) {
378
+ const messageDiv = document.createElement('div');
379
+ messageDiv.className = `chat-message ${isUser ? 'user-message' : 'assistant-message'} p-4 rounded-lg`;
380
+ messageDiv.dataset.index = messageIndex !== null ? messageIndex : conversationHistory.length;
381
+
382
+ const contentDiv = document.createElement('div');
383
+ contentDiv.className = 'markdown-content';
384
+ contentDiv.innerHTML = renderMarkdown(text);
385
+
386
+ messageDiv.appendChild(contentDiv);
387
+ document.getElementById('chatArea').appendChild(messageDiv);
388
+ scrollToBottom();
389
+
390
+ // Add context menu event listener
391
+ messageDiv.addEventListener('contextmenu', showContextMenu);
392
+
393
+ return messageDiv;
394
+ }
395
+
396
+ function scrollToBottom() {
397
+ const chatArea = document.getElementById('chatArea');
398
+ chatArea.scrollTop = chatArea.scrollHeight;
399
+ }
400
+
401
+ function loadChat(chatFile) {
402
+ fetch(`/api/load_chat/${chatFile}`)
403
+ .then(response => response.json())
404
+ .then(data => {
405
+ currentChatFile = chatFile;
406
+ conversationHistory = data.messages;
407
+ document.getElementById('chatArea').innerHTML = '';
408
+ conversationHistory.forEach((message, index) => {
409
+ addMessage(message.content, message.role === 'user', index);
410
+ });
411
+ });
412
+ }
413
+
414
+ function showContextMenu(e) {
415
  e.preventDefault();
416
+ const contextMenu = document.getElementById('contextMenu');
417
+ const messageDiv = e.currentTarget;
418
+
419
+ // Store the message index for editing
420
+ editingMessageIndex = parseInt(messageDiv.dataset.index);
421
+
422
+ // Position the context menu
423
+ contextMenu.style.left = `${e.pageX}px`;
424
+ contextMenu.style.top = `${e.pageY}px`;
425
+ contextMenu.style.display = 'block';
426
+ }
427
+
428
+ function hideContextMenu() {
429
+ document.getElementById('contextMenu').style.display = 'none';
430
+ }
431
+
432
+ function showEditModal(content) {
433
+ const modal = document.getElementById('editModal');
434
+ const input = document.getElementById('editMessageInput');
435
+ input.value = content;
436
+ modal.style.display = 'block';
437
+ }
438
+
439
+ function hideEditModal() {
440
+ document.getElementById('editModal').style.display = 'none';
441
+ editingMessageIndex = null;
442
+ }
443
+
444
+ async function saveEditedMessage() {
445
+ const newContent = document.getElementById('editMessageInput').value;
446
+ const isUserMessage = conversationHistory[editingMessageIndex].role === 'user';
447
+
448
+ try {
449
+ const response = await fetch('/api/update_message', {
450
+ method: 'POST',
451
+ headers: {
452
+ 'Content-Type': 'application/json'
453
+ },
454
+ body: JSON.stringify({
455
+ chat_file: currentChatFile,
456
+ message_index: editingMessageIndex,
457
+ content: newContent,
458
+ is_user: isUserMessage
459
+ })
460
+ });
461
+
462
+ const data = await response.json();
463
+ if (data.success) {
464
+ // Update conversation history and UI
465
+ conversationHistory = data.messages;
466
+ document.getElementById('chatArea').innerHTML = '';
467
+ conversationHistory.forEach((message, index) => {
468
+ addMessage(message.content, message.role === 'user', index);
469
+ });
470
+
471
+ // If it was a user message, regenerate the response
472
+ if (isUserMessage) {
473
+ await sendMessage(false);
474
+ }
475
+ }
476
+ } catch (error) {
477
+ console.error('Error saving edited message:', error);
478
+ alert('Failed to save the edited message');
479
+ }
480
+
481
+ hideEditModal();
482
+ }
483
+
484
+ function disableUI() {
485
+ document.getElementById('sendBtn').disabled = true;
486
+ document.getElementById('newChatBtn').disabled = true;
487
+ document.getElementById('listModelsBtn').disabled = true;
488
+ document.getElementById('messageInput').disabled = true;
489
+ document.getElementById('sidebar').classList.add('disabled');
490
+ document.getElementById('chatsList').classList.add('disabled');
491
+ document.getElementById('stopBtn').style.display = 'block';
492
+ }
493
+
494
+ function enableUI() {
495
+ document.getElementById('sendBtn').disabled = false;
496
+ document.getElementById('newChatBtn').disabled = false;
497
+ document.getElementById('listModelsBtn').disabled = false;
498
+ document.getElementById('messageInput').disabled = false;
499
+ document.getElementById('sidebar').classList.remove('disabled');
500
+ document.getElementById('chatsList').classList.remove('disabled');
501
+ document.getElementById('stopBtn').style.display = 'none';
502
+ }
503
+
504
+ async function sendMessage(addUserMessage = true) {
505
+ const messageInput = document.getElementById('messageInput');
506
+ const text = messageInput.value.trim();
507
+ if (!text && addUserMessage) return;
508
+
509
+ const model = document.getElementById('ollamaModel').value;
510
+ if (!model) {
511
+ alert('Please select a model first');
512
+ return;
513
+ }
514
+
515
+ if (addUserMessage) {
516
+ // Add user message
517
+ addMessage(text, true);
518
+ messageInput.value = '';
519
+
520
+ // Add to conversation history
521
+ conversationHistory.push({ role: 'user', content: text });
522
+ }
523
+
524
+ // Stop any existing EventSource
525
+ if (currentEventSource) {
526
+ currentEventSource.close();
527
+ }
528
+
529
+ const baseHost = document.getElementById('baseHost').value;
530
+
531
+ // Create a new AbortController
532
+ abortController = new AbortController();
533
+
534
+ // Disable UI
535
+ disableUI();
536
+
537
+ try {
538
+ const response = await fetch('/api/chat', {
539
+ method: 'POST',
540
+ headers: {
541
+ 'Content-Type': 'application/json'
542
+ },
543
+ body: JSON.stringify({
544
+ base_host: baseHost,
545
+ model: model,
546
+ messages: conversationHistory,
547
+ chat_file: currentChatFile || ''
548
+ }),
549
+ signal: abortController.signal
550
+ });
551
+
552
+ if (!response.ok) {
553
+ throw new Error(`HTTP error! status: ${response.status}`);
554
+ }
555
+
556
+ const reader = response.body.getReader();
557
+ const decoder = new TextDecoder();
558
+ let assistantMessage = addMessage('', false);
559
+ let completeResponse = '';
560
+
561
+ while (true) {
562
+ const { value, done } = await reader.read();
563
+ if (done) break;
564
+
565
+ const chunk = decoder.decode(value);
566
+ const lines = chunk.split('\n');
567
+
568
+ for (const line of lines) {
569
+ if (line.startsWith('data: ')) {
570
+ try {
571
+ const data = JSON.parse(line.slice(6));
572
+
573
+ if (data.error) {
574
+ alert(data.error);
575
+ break;
576
+ } else if (data.chunk) {
577
+ completeResponse = data.chunk;
578
+ assistantMessage.querySelector('.markdown-content').innerHTML = renderMarkdown(completeResponse);
579
+ scrollToBottom();
580
+ } else if (data.done) {
581
+ lastAssistantMessage = completeResponse;
582
+ conversationHistory.push({ role: 'assistant', content: completeResponse });
583
+
584
+ // Convert to speech
585
+ const ttsModel = document.getElementById('ttsModel').value;
586
+ if (ttsModel) {
587
+ convertToSpeech(completeResponse, ttsModel);
588
+ }
589
+ }
590
+ } catch (e) {
591
+ console.error('Error parsing SSE data:', e);
592
+ }
593
+ }
594
+ }
595
+ }
596
+ } catch (error) {
597
+ if (error.name === 'AbortError') {
598
+ console.log('Fetch aborted');
599
+ } else {
600
+ console.error('Error:', error);
601
+ alert('Error sending message: ' + error.message);
602
+ }
603
+ } finally {
604
+ // Enable UI
605
+ enableUI();
606
+ }
607
+ }
608
+
609
+ function convertToSpeech(text, model) {
610
+ const removeMarkdown = document.getElementById('removeMarkdown').checked;
611
+
612
+ fetch('/api/tts', {
613
  method: 'POST',
614
+ headers: { 'Content-Type': 'application/json' },
615
+ body: JSON.stringify({ text, model, remove_markdown: removeMarkdown })
616
+ })
617
+ .then(response => response.json())
618
+ .then(data => {
619
+ if (data.error) {
620
+ alert(data.error);
621
+ } else {
622
+ if (currentAudio) {
623
+ currentAudio.pause();
624
+ }
625
+ currentAudio = new Audio(`/audio/${data.audio_file}`);
626
+ currentAudio.volume = document.getElementById('volumeSlider').value / 100;
627
+ currentAudio.play();
628
+ }
629
  });
630
+ }
631
+
632
+ // Event Listeners
633
+ document.getElementById('sendBtn').addEventListener('click', () => sendMessage(true));
634
+ document.getElementById('messageInput').addEventListener('keypress', (e) => {
635
+ if (e.key === 'Enter' && !e.shiftKey) {
636
+ e.preventDefault();
637
+ sendMessage(true);
638
+ }
639
+ });
640
+
641
+ document.getElementById('stopBtn').addEventListener('click', () => {
642
+ if (abortController) {
643
+ abortController.abort();
644
+ }
645
+ });
646
+
647
+ document.getElementById('newChatBtn').addEventListener('click', () => {
648
+ currentChatFile = null;
649
+ conversationHistory = [];
650
+ document.getElementById('chatArea').innerHTML = '';
651
+ // Restore selected models
652
+ document.getElementById('ollamaModel').value = selectedOllamaModel;
653
+ document.getElementById('ttsModel').value = selectedTtsModel;
654
+ });
655
+
656
+ document.getElementById('listModelsBtn').addEventListener('click', () => {
657
+ const baseHost = document.getElementById('baseHost').value;
658
+ fetch(`/api/list_ollama_models?base_host=${encodeURIComponent(baseHost)}`)
659
+ .then(response => response.json())
660
+ .then(data => {
661
+ const select = document.getElementById('ollamaModel');
662
+ select.innerHTML = '<option value="">Select Ollama Model</option>';
663
+ data.models.forEach(model => {
664
+ const option = document.createElement('option');
665
+ option.value = model;
666
+ option.textContent = model;
667
+ select.appendChild(option);
668
+ });
669
+ });
670
+ });
671
+
672
+ document.getElementById('playBtn').addEventListener('click', () => {
673
+ if (currentAudio) {
674
+ currentAudio.play();
675
+ } else if (lastAssistantMessage) {
676
+ const ttsModel = document.getElementById('ttsModel').value;
677
+ if (ttsModel) {
678
+ convertToSpeech(lastAssistantMessage, ttsModel);
679
+ }
680
+ }
681
  });
682
+
683
+ document.getElementById('pauseBtn').addEventListener('click', () => {
684
+ if (currentAudio) {
685
+ currentAudio.pause();
686
+ }
687
+ });
688
+
689
+ document.getElementById('volumeSlider').addEventListener('input', (e) => {
690
+ if (currentAudio) {
691
+ currentAudio.volume = e.target.value / 100;
692
+ }
693
+ });
694
+
695
+ document.getElementById('chatsList').addEventListener('click', (e) => {
696
+ const chatFile = e.target.dataset.file;
697
+ if (chatFile) {
698
+ loadChat(chatFile);
699
+ }
700
+ });
701
+
702
+ // Store selected models
703
+ document.getElementById('ollamaModel').addEventListener('change', (e) => {
704
+ selectedOllamaModel = e.target.value;
705
+ });
706
+
707
+ document.getElementById('ttsModel').addEventListener('change', (e) => {
708
+ selectedTtsModel = e.target.value;
709
+ });
710
+
711
+ // Context Menu Event Listeners
712
+ document.addEventListener('click', hideContextMenu);
713
+ document.getElementById('editMenuItem').addEventListener('click', () => {
714
+ if (editingMessageIndex !== null) {
715
+ const content = conversationHistory[editingMessageIndex].content;
716
+ showEditModal(content);
717
+ }
718
+ hideContextMenu();
719
+ });
720
+
721
+ // Edit Modal Event Listeners
722
+ document.querySelector('.close-modal').addEventListener('click', hideEditModal);
723
+ document.getElementById('cancelEditBtn').addEventListener('click', hideEditModal);
724
+ document.getElementById('saveEditBtn').addEventListener('click', saveEditedMessage);
725
+
726
+ // Initial load of Ollama models
727
+ document.getElementById('listModelsBtn').click();
728
+
729
+ // Add sidebar toggle functionality
730
+ document.getElementById('toggleSidebar').addEventListener('click', () => {
731
+ const sidebar = document.getElementById('sidebar');
732
+ const sidebarTitle = document.getElementById('sidebarTitle');
733
+
734
+ if (sidebar.classList.contains('sidebar-expanded')) {
735
+ sidebar.classList.remove('sidebar-expanded');
736
+ sidebar.classList.add('sidebar-collapsed');
737
+ sidebarTitle.style.display = 'none';
738
+ document.querySelectorAll('.chat-file span').forEach(span => span.style.display = 'none');
739
+ } else {
740
+ sidebar.classList.remove('sidebar-collapsed');
741
+ sidebar.classList.add('sidebar-expanded');
742
+ sidebarTitle.style.display = 'block';
743
+ document.querySelectorAll('.chat-file span').forEach(span => span.style.display = 'inline');
744
+ }
745
+ });
746
+
747
+ // Add responsive sidebar behavior
748
+ function handleResize() {
749
+ const sidebar = document.getElementById('sidebar');
750
+ if (window.innerWidth < 768 && !sidebar.classList.contains('sidebar-collapsed')) {
751
+ sidebar.classList.remove('sidebar-expanded');
752
+ sidebar.classList.add('sidebar-collapsed');
753
+ document.getElementById('sidebarTitle').style.display = 'none';
754
+ document.querySelectorAll('.chat-file span').forEach(span => span.style.display = 'none');
755
+ }
756
+ }
757
+
758
+ window.addEventListener('resize', handleResize);
759
+ handleResize(); // Initial check
760
  </script>
761
  </body>
762
  </html>