Francisco2025 commited on
Commit
91e1b93
·
verified ·
1 Parent(s): da7472c

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +517 -14
index.html CHANGED
@@ -1,19 +1,522 @@
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
+ <title>OpenAI Chat Streaming - Markdown</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <!-- Bootstrap (latest) -->
8
+ <link
9
+ href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
10
+ rel="stylesheet"
11
+ />
12
+ <!-- Marked.js for Markdown support -->
13
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
14
+ <!-- Optional: Enable if you need sanitization
15
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.2/dist/purify.min.js"></script>
16
+ -->
17
+ <style>
18
+ html,
19
+ body {
20
+ height: 100%;
21
+ width: 100vw;
22
+ margin: 0;
23
+ padding: 0;
24
+ }
25
+ body {
26
+ background: #f8f9fa;
27
+ min-height: 100vh;
28
+ width: 100vw;
29
+ }
30
+ .chat-window {
31
+ width: 100vw;
32
+ height: 100vh;
33
+ display: flex;
34
+ flex-direction: column;
35
+ background: #fff;
36
+ }
37
+ .chat-messages {
38
+ flex: 1;
39
+ overflow-y: auto;
40
+ padding: 16px;
41
+ border-bottom: 1px solid #eee;
42
+ background: #fff;
43
+ }
44
+ .message.user {
45
+ text-align: right;
46
+ }
47
+ .message.openai {
48
+ text-align: left;
49
+ }
50
+ .message span {
51
+ display: inline-block;
52
+ padding: 8px 12px;
53
+ border-radius: 16px;
54
+ margin: 6px 0;
55
+ max-width: 80%;
56
+ word-break: break-word;
57
+ }
58
+ .message.user span {
59
+ background: #0d6efd;
60
+ color: #fff;
61
+ }
62
+ .message.openai span {
63
+ background: #e9ecef;
64
+ color: #333;
65
+ }
66
+ .settings-toggle {
67
+ cursor: pointer;
68
+ color: #0d6efd;
69
+ font-size: 1.1em;
70
+ float: right;
71
+ }
72
+ .settings-panel {
73
+ display: none;
74
+ border-bottom: 1px solid #eee;
75
+ padding: 16px;
76
+ background: #f7f7f7;
77
+ }
78
+ .settings-panel.show {
79
+ display: block;
80
+ }
81
+ .new-chat-btn {
82
+ margin-right: 10px;
83
+ background: #198754;
84
+ color: #fff;
85
+ border: none;
86
+ padding: 5px 16px;
87
+ border-radius: 20px;
88
+ font-size: 1em;
89
+ cursor: pointer;
90
+ }
91
+ .new-chat-btn:hover {
92
+ background: #157347;
93
+ }
94
+ .chat-id-tag {
95
+ font-size: 0.75em;
96
+ color: #888;
97
+ font-family: monospace;
98
+ background: #eee;
99
+ padding: 1px 7px;
100
+ border-radius: 12px;
101
+ margin-left: 8px;
102
+ }
103
+ .speed-indicator {
104
+ font-size: 0.85em;
105
+ color: #888;
106
+ padding-left: 1em;
107
+ }
108
+ @media (max-width: 600px) {
109
+ .chat-window {
110
+ width: 100vw;
111
+ height: 100vh;
112
+ border-radius: 0;
113
+ margin: 0;
114
+ }
115
+ .chat-messages {
116
+ padding: 8px;
117
+ }
118
+ .settings-panel,
119
+ .p-3 {
120
+ padding: 8px !important;
121
+ }
122
+ .message span {
123
+ max-width: 100%;
124
+ font-size: 1em;
125
+ }
126
+ }
127
+ code,
128
+ pre {
129
+ font-family: 'Fira Mono', 'Consolas', monospace;
130
+ background: #f1f3f5;
131
+ border-radius: 4px;
132
+ padding: 2px 6px;
133
+ }
134
+ </style>
135
  </head>
136
+
137
  <body>
138
+ <div class="chat-window">
139
+ <header
140
+ class="p-3 border-bottom d-flex align-items-center justify-content-between"
141
+ >
142
+ <span class="fw-bold">
143
+ Chat OpenAI <small class="text-secondary">Streaming Markdown</small>
144
+ <span
145
+ id="chatIdTag"
146
+ class="chat-id-tag"
147
+ title="Conversation ID"
148
+ ></span>
149
+ </span>
150
+ <div>
151
+ <button class="new-chat-btn" id="newChatBtn" title="New Chat">
152
+ New chat
153
+ </button>
154
+ <button
155
+ class="new-chat-btn"
156
+ id="generateTitleBtn"
157
+ title="Generate Title"
158
+ >
159
+ Generate title
160
+ </button>
161
+ <span class="settings-toggle" id="settingsToggle" title="Settings"
162
+ >&#9881;</span
163
+ >
164
+ </div>
165
+ </header>
166
+ <section class="settings-panel" id="settingsPanel">
167
+ <form id="settingsForm" autocomplete="on">
168
+ <div class="mb-2">
169
+ <label class="form-label">Base URL (up to /v1)</label>
170
+ <input type="text" class="form-control" id="urlBase" required />
171
+ </div>
172
+ <div class="mb-2">
173
+ <label class="form-label">API Key</label>
174
+ <input type="text" class="form-control" id="apiKey" required />
175
+ </div>
176
+ <div class="mb-2">
177
+ <label class="form-label">Model</label>
178
+ <input type="text" class="form-control" id="model" required />
179
+ </div>
180
+ <div class="mb-2">
181
+ <label class="form-label">System prompt</label>
182
+ <textarea
183
+ class="form-control"
184
+ id="systemPrompt"
185
+ rows="2"
186
+ placeholder="Example: You are a helpful and concise assistant."
187
+ ></textarea>
188
+ <small class="text-secondary">Controls AI behavior.</small>
189
+ </div>
190
+ <button type="submit" class="btn btn-primary btn-sm w-100">
191
+ Save settings
192
+ </button>
193
+ </form>
194
+ </section>
195
+ <main class="chat-messages" id="chatMessages"></main>
196
+ <form class="d-flex p-3 gap-2" id="chatForm" autocomplete="off">
197
+ <input
198
+ type="text"
199
+ class="form-control flex-grow-1"
200
+ id="userInput"
201
+ placeholder="Type your message..."
202
+ required
203
+ />
204
+ <button class="btn btn-primary flex-shrink-0" type="submit">
205
+ Send
206
+ </button>
207
+ </form>
208
  </div>
209
+ <script type="module">
210
+ import { openDB } from 'https://cdn.jsdelivr.net/npm/idb@7/+esm';
211
+
212
+ // ----------- IndexedDB helpers -----------
213
+ const DB_NAME = 'chatdb';
214
+ const STORE = 'messages';
215
+ const dbPromise = openDB(DB_NAME, 1, {
216
+ upgrade(db) {
217
+ if (!db.objectStoreNames.contains(STORE))
218
+ db.createObjectStore(STORE, { keyPath: 'id', autoIncrement: true });
219
+ },
220
+ });
221
+ const saveMessage = async (chatId, msg) => {
222
+ const db = await dbPromise;
223
+ await db.add(STORE, { chatId, ...msg });
224
+ };
225
+ const getMessages = async chatId => {
226
+ const db = await dbPromise;
227
+ return (await db.getAll(STORE)).filter(m => m.chatId === chatId);
228
+ };
229
+ const clearMessages = async chatId => {
230
+ const db = await dbPromise;
231
+ const tx = db.transaction(STORE, 'readwrite');
232
+ const store = tx.objectStore(STORE);
233
+ const all = await store.getAll();
234
+ for (const msg of all)
235
+ if (msg.chatId === chatId) await store.delete(msg.id);
236
+ await tx.done;
237
+ };
238
+
239
+ // ----------- localStorage helpers --------
240
+ const save = (key, value) =>
241
+ localStorage.setItem(key, JSON.stringify(value));
242
+ const load = key => JSON.parse(localStorage.getItem(key));
243
+
244
+ // ----------- Chat ID management ----------
245
+ const generateChatId = () =>
246
+ globalThis.crypto?.randomUUID?.() ||
247
+ Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
248
+ const getOrCreateChatId = () => {
249
+ const params = new URLSearchParams(location.search);
250
+ let chatId = params.get('chat');
251
+ if (!chatId) {
252
+ chatId = generateChatId();
253
+ params.set('chat', chatId);
254
+ history.replaceState(null, '', '?' + params.toString());
255
+ }
256
+ return chatId;
257
+ };
258
+ const chatId = getOrCreateChatId();
259
+ document.getElementById('chatIdTag').textContent = chatId;
260
+
261
+ // ----------- DOM elements ---------------
262
+ const settingsPanel = document.getElementById('settingsPanel');
263
+ const settingsToggle = document.getElementById('settingsToggle');
264
+ const settingsForm = document.getElementById('settingsForm');
265
+ const urlBaseInput = document.getElementById('urlBase');
266
+ const apiKeyInput = document.getElementById('apiKey');
267
+ const modelInput = document.getElementById('model');
268
+ const systemPromptInput = document.getElementById('systemPrompt');
269
+ const chatMessages = document.getElementById('chatMessages');
270
+ const chatForm = document.getElementById('chatForm');
271
+ const userInput = document.getElementById('userInput');
272
+ const newChatBtn = document.getElementById('newChatBtn');
273
+ const generateTitleBtn = document.getElementById('generateTitleBtn');
274
+
275
+ // ----------- Config management ----------
276
+ const configKey = `chatConfig-${chatId}`;
277
+
278
+ // Cargar configuración del chat actual, o valores por defecto
279
+ const loadConfig = () => {
280
+ const config = load(configKey) ?? {};
281
+ urlBaseInput.value = config.urlBase ?? 'https://api.openai.com/v1';
282
+ apiKeyInput.value = config.apiKey ?? '';
283
+ modelInput.value = config.model ?? 'gpt-3.5-turbo';
284
+ systemPromptInput.value = config.systemPrompt ?? '';
285
+ };
286
+ loadConfig();
287
+
288
+ settingsToggle.addEventListener('click', () =>
289
+ settingsPanel.classList.toggle('show'),
290
+ );
291
+
292
+ settingsForm.addEventListener('submit', e => {
293
+ e.preventDefault();
294
+ save(configKey, {
295
+ urlBase: urlBaseInput.value.trim(),
296
+ apiKey: apiKeyInput.value.trim(),
297
+ model: modelInput.value.trim(),
298
+ systemPrompt: systemPromptInput.value.trim(),
299
+ });
300
+ settingsPanel.classList.remove('show');
301
+ });
302
+
303
+ // ----------- Conversation history --------
304
+ let autosaveEnabled = false;
305
+ let conversation = [];
306
+ const saveConversation = async () => {
307
+ if (autosaveEnabled) {
308
+ const msg = conversation.at(-1);
309
+ if (msg) await saveMessage(chatId, msg);
310
+ }
311
+ };
312
+ const restoreHistory = async () => {
313
+ autosaveEnabled = false;
314
+ let history = await getMessages(chatId);
315
+ chatMessages.innerHTML = '';
316
+ if (history.length && history.at(-1).role === 'user') history.pop();
317
+ history = history.filter(
318
+ msg => !(msg.role === 'assistant' && !msg.content),
319
+ );
320
+ conversation = history.map(({ role, content }) => ({ role, content }));
321
+ conversation.forEach(msg =>
322
+ addMessage(msg.content, msg.role === 'user' ? 'user' : 'openai'),
323
+ );
324
+ autosaveEnabled = true;
325
+ };
326
+
327
+ // ----------- Markdown & rendering -------
328
+ const renderMarkdown = text => marked.parse(text);
329
+ const addMessage = (content, sender) => {
330
+ const div = document.createElement('div');
331
+ div.className = `message ${sender}`;
332
+ div.innerHTML = `<span>${
333
+ sender === 'openai' ? renderMarkdown(content) : content
334
+ }</span>`;
335
+ chatMessages.appendChild(div);
336
+ // Use the same approach as original working version
337
+ div.scrollIntoView({ block: 'end' });
338
+ };
339
+
340
+ // ----------- OpenAI streaming -----------
341
+ const streamOpenAI = async ({
342
+ urlBase,
343
+ apiKey,
344
+ model,
345
+ conversation,
346
+ onChunk,
347
+ onDone,
348
+ onError,
349
+ }) => {
350
+ const headers = {
351
+ 'Content-Type': 'application/json',
352
+ Authorization: `Bearer ${apiKey}`,
353
+ };
354
+ const startTime = performance.now();
355
+ let lastText = '',
356
+ tokenCount = 0;
357
+ const response = await fetch(`${urlBase}/chat/completions`, {
358
+ method: 'POST',
359
+ headers,
360
+ body: JSON.stringify({ model, messages: conversation, stream: true }),
361
+ });
362
+ const reader = response.body.getReader();
363
+ const decoder = new TextDecoder();
364
+ let buffer = '',
365
+ done = false;
366
+ while (!done) {
367
+ const { value, done: doneReading } = await reader.read();
368
+ if (doneReading) break;
369
+ buffer += decoder.decode(value, { stream: true });
370
+ const lines = buffer.split('\n');
371
+ buffer = lines.pop();
372
+ for (let line of lines) {
373
+ const ltrim = line.trim();
374
+ if (!ltrim.startsWith('data:')) continue;
375
+ const jsonStr = ltrim.slice(5).trim();
376
+ if (jsonStr === '[DONE]') {
377
+ done = true;
378
+ break;
379
+ }
380
+ const parsed = JSON.parse(jsonStr);
381
+ const delta = parsed.choices[0]?.delta?.content ?? '';
382
+ if (delta) {
383
+ lastText += delta;
384
+ tokenCount++;
385
+ onChunk?.(
386
+ lastText,
387
+ tokenCount,
388
+ (performance.now() - startTime) / 1000,
389
+ );
390
+ }
391
+ }
392
+ }
393
+ onDone?.(lastText, tokenCount, (performance.now() - startTime) / 1000);
394
+ };
395
+
396
+ // ----------- Chat submission/stream -------
397
+ await restoreHistory();
398
+
399
+ let messageCounter = 0;
400
+ const handleChatSubmit = async e => {
401
+ e.preventDefault();
402
+ const text = userInput.value.trim();
403
+ if (!text) return;
404
+ addMessage(text, 'user');
405
+ userInput.value = '';
406
+
407
+ // Usar configuración solo del chat actual
408
+ const config = load(configKey) ?? {};
409
+ const sysPrompt = config.systemPrompt || '';
410
+ conversation.push({ role: 'user', content: text });
411
+ await saveConversation();
412
+
413
+ let convSend = [...conversation];
414
+ if (sysPrompt && !(convSend.length && convSend[0].role === 'system'))
415
+ convSend = [{ role: 'system', content: sysPrompt }, ...convSend];
416
+
417
+ messageCounter++;
418
+ const replyId = `reply-${messageCounter}`;
419
+ const speedId = `speed-${messageCounter}`;
420
+ addMessage(
421
+ `<span id="${replyId}"></span><br><small id="${speedId}" class="speed-indicator"></small>`,
422
+ 'openai',
423
+ );
424
+ const replyElem = document.getElementById(replyId);
425
+ const speedElem = document.getElementById(speedId);
426
+ const msgDiv = chatMessages.lastChild;
427
+
428
+ await streamOpenAI({
429
+ urlBase: config.urlBase ?? 'https://api.openai.com/v1',
430
+ apiKey: config.apiKey ?? '',
431
+ model: config.model ?? 'gpt-3.5-turbo',
432
+ conversation: convSend,
433
+ onChunk: (content, tokens, secs) => {
434
+ if (tokens % 10 === 0 || tokens === 1)
435
+ replyElem.innerHTML = renderMarkdown(content);
436
+ if (tokens > 0)
437
+ speedElem.innerText = `Tokens: ${tokens} • Speed: ${(
438
+ tokens / (secs || 1)
439
+ ).toFixed(2)} tks/sec`;
440
+ msgDiv.scrollIntoView({ block: 'end' });
441
+ },
442
+ onDone: async (content, tokens, secs) => {
443
+ if (content) {
444
+ conversation.push({ role: 'assistant', content });
445
+ await saveConversation();
446
+ }
447
+ replyElem.innerHTML = renderMarkdown(content);
448
+ speedElem.innerText = `Done. Tokens: ${tokens}, Max speed: ${(
449
+ tokens / (secs || 1)
450
+ ).toFixed(2)} tks/sec`;
451
+ },
452
+ onError: () => {
453
+ chatMessages.lastChild.remove();
454
+ addMessage('Connection error (streaming failed).', 'openai');
455
+ },
456
+ });
457
+ };
458
+ chatForm.addEventListener('submit', handleChatSubmit);
459
+
460
+ // ----------- Start new chat -------------
461
+ newChatBtn.addEventListener('click', () => {
462
+ const newId = generateChatId();
463
+ const thisConfig = {
464
+ urlBase: urlBaseInput.value.trim(),
465
+ apiKey: apiKeyInput.value.trim(),
466
+ model: modelInput.value.trim(),
467
+ systemPrompt: systemPromptInput.value.trim(),
468
+ };
469
+ save(`chatConfig-${newId}`, thisConfig);
470
+ window.open(`?chat=${newId}`, '_blank');
471
+ });
472
+
473
+ // ----------- Generate title -------------
474
+ const generateTitle = async () => {
475
+ if (conversation.length === 0) return;
476
+ const config = load(configKey) ?? {};
477
+ const headers = {
478
+ 'Content-Type': 'application/json',
479
+ Authorization: `Bearer ${config.apiKey}`,
480
+ };
481
+ const titleConversation = [
482
+ ...conversation,
483
+ {
484
+ role: 'user',
485
+ content:
486
+ 'Generate a very short (max 5 words) and concise title for this conversation. Respond only with the title, no other text.',
487
+ },
488
+ ];
489
+ const response = await fetch(`${config.urlBase}/chat/completions`, {
490
+ method: 'POST',
491
+ headers,
492
+ body: JSON.stringify({
493
+ model: config.model,
494
+ messages: titleConversation,
495
+ stream: false,
496
+ }),
497
+ });
498
+ const data = await response.json();
499
+ const title = data.choices?.[0]?.message?.content;
500
+ if (title) {
501
+ const trimmedTitle = title.trim().substring(0, 50);
502
+ document.title = trimmedTitle;
503
+ localStorage.setItem(`chatTitle-${chatId}`, trimmedTitle);
504
+ const headerTitle = document.querySelector('header span.fw-bold');
505
+ if (headerTitle)
506
+ headerTitle.innerHTML = `${trimmedTitle} <small class="text-secondary">Streaming Markdown</small><span id="chatIdTag" class="chat-id-tag" title="Conversation ID"></span>`;
507
+ }
508
+ };
509
+
510
+ generateTitleBtn.addEventListener('click', generateTitle);
511
+
512
+ // Load saved title if exists
513
+ const savedTitle = localStorage.getItem(`chatTitle-${chatId}`);
514
+ if (savedTitle) {
515
+ document.title = savedTitle;
516
+ const headerTitle = document.querySelector('header span.fw-bold');
517
+ if (headerTitle)
518
+ headerTitle.innerHTML = `${savedTitle} <small class="text-secondary">Streaming Markdown</small><span id="chatIdTag" class="chat-id-tag" title="Conversation ID"></span>`;
519
+ }
520
+ </script>
521
  </body>
522
+ </html>