mihailik commited on
Commit
cb8f522
·
1 Parent(s): f761e54

Error handling improvements in chat-full.js and adding chat-gemin.html from GEmini.

Browse files
Files changed (2) hide show
  1. chat-full.js +16 -4
  2. chat-gemin.html +330 -0
chat-full.js CHANGED
@@ -23,14 +23,24 @@
23
  sendBtn.disabled = !!isBusy;
24
  modelSel.disabled = !!isBusy;
25
  }
26
- function pushMsg(role, text) {
27
  const div = document.createElement("div");
28
  div.className = `msg ${role}`;
29
- div.textContent = text;
 
 
 
 
 
30
  messagesEl.appendChild(div);
31
  messagesEl.scrollTop = messagesEl.scrollHeight;
32
  }
33
 
 
 
 
 
 
34
  // Визначення задачі для моделі
35
  function taskForModel(model) {
36
  if ((model || "").toLowerCase().includes("t5")) return "text2text-generation";
@@ -70,7 +80,8 @@
70
  return entry;
71
  } catch (err) {
72
  setStatus("Помилка завантаження моделі", false);
73
- pushMsg("sys", `Помилка при завантаженні '${model}': ${String(err && err.message || err)}`);
 
74
  throw err;
75
  }
76
  }
@@ -115,7 +126,8 @@
115
  chatHistory.push({ role: "assistant", content: reply });
116
  pushMsg("bot", reply);
117
  } catch (err) {
118
- pushMsg("sys", `Помилка генерації: ${String(err && err.message || err)}`);
 
119
  } finally {
120
  setStatus("Готово", false);
121
  }
 
23
  sendBtn.disabled = !!isBusy;
24
  modelSel.disabled = !!isBusy;
25
  }
26
+ function pushMsg(role, text, details) {
27
  const div = document.createElement("div");
28
  div.className = `msg ${role}`;
29
+ if (role === 'sys' && details) {
30
+ // Показати повідомлення з розгортанням stack trace
31
+ div.innerHTML = `<span>${text}</span><details style="margin-top:6px;"><summary style="cursor:pointer;user-select:none;">Деталі (stack trace)</summary><pre style="white-space:pre-wrap;font-size:.92em;">${escapeHtml(details)}</pre></details>`;
32
+ } else {
33
+ div.textContent = text;
34
+ }
35
  messagesEl.appendChild(div);
36
  messagesEl.scrollTop = messagesEl.scrollHeight;
37
  }
38
 
39
+ // Захист від XSS у stack trace
40
+ function escapeHtml(str) {
41
+ return String(str).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
42
+ }
43
+
44
  // Визначення задачі для моделі
45
  function taskForModel(model) {
46
  if ((model || "").toLowerCase().includes("t5")) return "text2text-generation";
 
80
  return entry;
81
  } catch (err) {
82
  setStatus("Помилка завантаження моделі", false);
83
+ pushMsg("sys", `Помилка при завантаженні '${model}': ${String(err && err.message || err)}`,
84
+ err && err.stack ? err.stack : undefined);
85
  throw err;
86
  }
87
  }
 
126
  chatHistory.push({ role: "assistant", content: reply });
127
  pushMsg("bot", reply);
128
  } catch (err) {
129
+ pushMsg("sys", `Помилка генерації: ${String(err && err.message || err)}`,
130
+ err && err.stack ? err.stack : undefined);
131
  } finally {
132
  setStatus("Готово", false);
133
  }
chat-gemin.html ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>On-Device AI Chat</title>
8
+ <style>
9
+ :root {
10
+ --background-color: #f0f2f5;
11
+ --container-bg-color: #ffffff;
12
+ --text-color: #050505;
13
+ --secondary-text-color: #65676b;
14
+ --border-color: #ced0d4;
15
+ --accent-color: #007bff;
16
+ --accent-text-color: #ffffff;
17
+ --user-msg-bg: #007bff;
18
+ --ai-msg-bg: #e4e6eb;
19
+ }
20
+
21
+ html,
22
+ body {
23
+ margin: 0;
24
+ padding: 0;
25
+ height: 100%;
26
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
27
+ background-color: var(--background-color);
28
+ color: var(--text-color);
29
+ }
30
+
31
+ #app-container {
32
+ display: flex;
33
+ flex-direction: column;
34
+ height: 100vh;
35
+ /* Full viewport height */
36
+ max-width: 800px;
37
+ margin: 0 auto;
38
+ background-color: var(--container-bg-color);
39
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
40
+ }
41
+
42
+ header {
43
+ padding: 10px 15px;
44
+ border-bottom: 1px solid var(--border-color);
45
+ background-color: #f7f7f7;
46
+ display: flex;
47
+ flex-wrap: wrap;
48
+ align-items: center;
49
+ gap: 15px;
50
+ }
51
+
52
+ header h1 {
53
+ font-size: 1.2em;
54
+ margin: 0;
55
+ flex-grow: 1;
56
+ }
57
+
58
+ #model-selector {
59
+ padding: 8px 12px;
60
+ border-radius: 8px;
61
+ border: 1px solid var(--border-color);
62
+ font-size: 0.9em;
63
+ background-color: white;
64
+ cursor: pointer;
65
+ }
66
+
67
+ #chat-history {
68
+ flex-grow: 1;
69
+ overflow-y: auto;
70
+ padding: 15px;
71
+ display: flex;
72
+ flex-direction: column;
73
+ }
74
+
75
+ .message {
76
+ max-width: 85%;
77
+ padding: 10px 14px;
78
+ border-radius: 18px;
79
+ margin-bottom: 10px;
80
+ line-height: 1.4;
81
+ word-wrap: break-word;
82
+ }
83
+
84
+ .user-message {
85
+ background-color: var(--user-msg-bg);
86
+ color: var(--accent-text-color);
87
+ align-self: flex-end;
88
+ border-bottom-right-radius: 4px;
89
+ }
90
+
91
+ .ai-message {
92
+ background-color: var(--ai-msg-bg);
93
+ color: var(--text-color);
94
+ align-self: flex-start;
95
+ border-bottom-left-radius: 4px;
96
+ }
97
+
98
+ #status {
99
+ padding: 5px 15px;
100
+ font-size: 0.85em;
101
+ color: var(--secondary-text-color);
102
+ text-align: center;
103
+ min-height: 22px;
104
+ transition: all 0.3s ease;
105
+ }
106
+
107
+ #input-area {
108
+ display: flex;
109
+ padding: 10px;
110
+ border-top: 1px solid var(--border-color);
111
+ gap: 10px;
112
+ }
113
+
114
+ #user-input {
115
+ flex-grow: 1;
116
+ border: 1px solid var(--border-color);
117
+ border-radius: 20px;
118
+ padding: 10px 15px;
119
+ font-size: 1em;
120
+ outline: none;
121
+ }
122
+
123
+ #user-input:focus {
124
+ border-color: var(--accent-color);
125
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
126
+ }
127
+
128
+ #send-button {
129
+ padding: 10px 20px;
130
+ border: none;
131
+ background-color: var(--accent-color);
132
+ color: var(--accent-text-color);
133
+ border-radius: 20px;
134
+ font-size: 1em;
135
+ cursor: pointer;
136
+ transition: background-color 0.2s ease;
137
+ }
138
+
139
+ #send-button:hover:not(:disabled) {
140
+ background-color: #0056b3;
141
+ }
142
+
143
+ #send-button:disabled {
144
+ background-color: #a0a0a0;
145
+ cursor: not-allowed;
146
+ }
147
+ </style>
148
+ </head>
149
+
150
+ <body>
151
+
152
+ <div id="app-container">
153
+ <header>
154
+ <h1>Local Chat AI 🤖</h1>
155
+ <select id="model-selector" title="Select a model"></select>
156
+ </header>
157
+
158
+ <main id="chat-history"></main>
159
+
160
+ <div id="status">Select a model and start chatting!</div>
161
+
162
+ <footer id="input-area">
163
+ <input type="text" id="user-input" placeholder="Type your message..." autocomplete="off">
164
+ <button id="send-button">Send</button>
165
+ </footer>
166
+ </div>
167
+
168
+ <script type="module">
169
+ import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.1';
170
+
171
+ // Configuration
172
+ // Prevent Transformers.js from looking for local models
173
+ env.allowLocalModels = false;
174
+
175
+ // --- DOM Elements ---
176
+ const statusDiv = document.getElementById('status');
177
+ const modelSelector = document.getElementById('model-selector');
178
+ const chatHistoryDiv = document.getElementById('chat-history');
179
+ const userInput = document.getElementById('user-input');
180
+ const sendButton = document.getElementById('send-button');
181
+
182
+ // --- State Management ---
183
+ let generator = null; // This will hold the loaded model pipeline
184
+ let isBusy = false;
185
+ let currentModel = '';
186
+ let chatHistory = []; // Array to store conversation [{role: 'user'|'assistant', content: '...'}, ...]
187
+
188
+ // --- Models ---
189
+ const MODELS = [
190
+ "Xenova/phi-3-mini-4k-instruct", // Good default, fast and capable
191
+ "Xenova/distilgpt2",
192
+ "Xenova/gemma-2b-it",
193
+ "Xenova/t5-small",
194
+ "Xenova/Mistral-7B-Instruct-v0.2",
195
+ "Xenova/llama-3-8b-instruct", // Note: Large model, may be slow/unstable on some devices
196
+ ];
197
+
198
+ // --- Core Functions ---
199
+
200
+ /**
201
+ * Populates the model selector dropdown with the available models.
202
+ */
203
+ function initializeModelSelector() {
204
+ MODELS.forEach(modelName => {
205
+ const option = document.createElement('option');
206
+ option.value = modelName;
207
+ option.textContent = modelName.split('/')[1]; // Display only the model name
208
+ modelSelector.appendChild(option);
209
+ });
210
+ currentModel = modelSelector.value;
211
+ }
212
+
213
+ /**
214
+ * Updates the UI to reflect the application's busy state.
215
+ * @param {boolean} busy - Whether the application is busy.
216
+ * @param {string} message - The message to display in the status div.
217
+ */
218
+ function setBusyState(busy, message = '') {
219
+ isBusy = busy;
220
+ userInput.disabled = busy;
221
+ sendButton.disabled = busy;
222
+ modelSelector.disabled = busy;
223
+ statusDiv.textContent = message;
224
+ if (busy) {
225
+ sendButton.textContent = '...';
226
+ } else {
227
+ sendButton.textContent = 'Send';
228
+ userInput.focus();
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Appends a message to the chat history div and scrolls to the bottom.
234
+ * @param {string} sender - 'user' or 'ai'.
235
+ * @param {string} text - The message content.
236
+ */
237
+ function appendMessage(sender, text) {
238
+ const messageDiv = document.createElement('div');
239
+ messageDiv.classList.add('message', sender === 'user' ? 'user-message' : 'ai-message');
240
+ messageDiv.textContent = text;
241
+ chatHistoryDiv.appendChild(messageDiv);
242
+ chatHistoryDiv.scrollTop = chatHistoryDiv.scrollHeight; // Auto-scroll to the latest message
243
+ }
244
+
245
+ /**
246
+ * Main function to handle sending a message and generating a response.
247
+ */
248
+ async function sendMessage() {
249
+ const userText = userInput.value.trim();
250
+ if (!userText || isBusy) return;
251
+
252
+ setBusyState(true, 'Preparing...');
253
+ const selectedModel = modelSelector.value;
254
+ userInput.value = '';
255
+
256
+ // 1. Add user message to UI and history
257
+ appendMessage('user', userText);
258
+ chatHistory.push({ role: 'user', content: userText });
259
+
260
+ try {
261
+ // 2. Load model if it has changed or hasn't been loaded yet
262
+ if (!generator || currentModel !== selectedModel) {
263
+ currentModel = selectedModel;
264
+ // Dispose of the old generator if it exists
265
+ if (generator) {
266
+ await generator.dispose();
267
+ }
268
+ setBusyState(true, `Loading ${currentModel.split('/')[1]}...`);
269
+ generator = await pipeline('text-generation', currentModel, {
270
+ progress_callback: (data) => {
271
+ if (data.status === 'progress') {
272
+ const loaded = (data.loaded / 1024 / 1024).toFixed(2);
273
+ const total = (data.total / 1024 / 1024).toFixed(2);
274
+ setBusyState(true, `Downloading: ${data.file} (${loaded} / ${total} MB)`);
275
+ } else {
276
+ setBusyState(true, `Status: ${data.status.replace(/_/g, ' ')}`);
277
+ }
278
+ }
279
+ });
280
+ }
281
+
282
+ // 3. Generate AI response
283
+ setBusyState(true, 'Generating response...');
284
+
285
+ // Format the conversation history for the model.
286
+ // `apply_chat_template` is the recommended way to format prompts for chat models.
287
+ const prompt = generator.tokenizer.apply_chat_template(chatHistory, {
288
+ tokenize: false,
289
+ add_generation_prompt: true,
290
+ });
291
+
292
+ const result = await generator(prompt, {
293
+ max_new_tokens: 512,
294
+ temperature: 0.7,
295
+ top_k: 50,
296
+ do_sample: true,
297
+ });
298
+
299
+ // 4. Process and display AI response
300
+ // The output contains the full conversation history; we extract only the newly generated part.
301
+ const rawResponse = result[0].generated_text;
302
+ const aiResponse = rawResponse.substring(prompt.length).trim();
303
+
304
+ appendMessage('ai', aiResponse);
305
+ chatHistory.push({ role: 'assistant', content: aiResponse });
306
+
307
+ } catch (error) {
308
+ console.error('An error occurred:', error);
309
+ appendMessage('ai', `Sorry, an error occurred: ${error.message}`);
310
+ } finally {
311
+ // 5. Reset UI state
312
+ setBusyState(false, 'Select a model and start chatting!');
313
+ }
314
+ }
315
+
316
+ // --- Event Listeners ---
317
+ sendButton.addEventListener('click', sendMessage);
318
+ userInput.addEventListener('keydown', (event) => {
319
+ if (event.key === 'Enter') {
320
+ event.preventDefault(); // Prevent form submission
321
+ sendMessage();
322
+ }
323
+ });
324
+
325
+ // Initialize the app
326
+ initializeModelSelector();
327
+ </script>
328
+ </body>
329
+
330
+ </html>