kimhyunwoo commited on
Commit
3ba6123
·
verified ·
1 Parent(s): fa541aa

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +109 -303
index.html CHANGED
@@ -1,158 +1,80 @@
1
  <!DOCTYPE html>
2
- <html lang="en"> {/* Language set to English */}
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
- <title>AI Assistant (Gemma 3 1B)</title> {/* English Title */}
7
  <style>
8
- /* Google Fonts (Using a common English font) */
9
  @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
10
-
11
- /* CSS Variables (Neutral theme) */
12
  :root {
13
- --primary-color: #007bff; /* Standard blue */
14
- --secondary-color: #6c757d; /* Gray */
15
- --text-color: #212529;
16
- --bg-color: #f8f9fa;
17
- --user-msg-bg: #e7f5ff; /* Light blue */
18
- --user-msg-text: #004085;
19
- --bot-msg-bg: #ffffff;
20
- --bot-msg-border: #dee2e6;
21
- --system-msg-color: #6c757d;
22
- --border-color: #dee2e6;
23
- --input-bg: #ffffff;
24
- --input-border: #ced4da;
25
- --button-bg: var(--primary-color);
26
- --button-hover-bg: #0056b3;
27
- --button-disabled-bg: #adb5bd;
28
- --scrollbar-thumb: var(--primary-color);
29
- --scrollbar-track: #e9ecef;
30
- --header-bg: #ffffff;
31
- --header-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
32
  --container-shadow: 0 4px 15px rgba(0, 0, 0, 0.07);
33
  }
34
-
35
- /* Reset and Base Styles */
36
  * { box-sizing: border-box; margin: 0; padding: 0; }
37
  html { height: 100%; }
38
  body {
39
- font-family: 'Roboto', sans-serif; /* Changed Font */
40
- display: flex; flex-direction: column; align-items: center; justify-content: center;
41
- min-height: 100vh; background-color: var(--bg-color); color: var(--text-color);
42
- padding: 5px; overscroll-behavior: none;
43
  }
44
-
45
- /* Chat Container */
46
  #chat-container {
47
  width: 100%; max-width: 600px; height: calc(100vh - 10px); max-height: 800px;
48
- background-color: #ffffff; border-radius: 12px; /* Less rounded */
49
- box-shadow: var(--container-shadow); display: flex; flex-direction: column;
50
- overflow: hidden; border: 1px solid var(--border-color);
51
  }
52
-
53
- /* Header */
54
  h1 {
55
- text-align: center; color: var(--primary-color); padding: 15px;
56
- background-color: var(--header-bg); border-bottom: 1px solid var(--border-color);
57
- font-size: 1.2em; font-weight: 500; flex-shrink: 0; box-shadow: var(--header-shadow);
58
- position: relative; z-index: 10;
59
  }
60
-
61
- /* Chatbox Area */
62
  #chatbox {
63
- flex-grow: 1; overflow-y: auto; padding: 15px; display: flex; flex-direction: column;
64
- gap: 12px; scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
65
- background-color: var(--bg-color); /* Match body background */
66
  }
67
  #chatbox::-webkit-scrollbar { width: 6px; }
68
  #chatbox::-webkit-scrollbar-track { background: var(--scrollbar-track); border-radius: 3px; }
69
  #chatbox::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 3px; }
70
-
71
- /* Message Bubbles */
72
  #messages div {
73
- padding: 10px 15px; border-radius: 16px; max-width: 85%; word-wrap: break-word;
74
- line-height: 1.5; font-size: 1em; box-shadow: 0 1px 2px rgba(0,0,0,0.05);
75
- position: relative; animation: fadeIn 0.25s ease-out;
76
  }
77
  @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
78
-
79
- .user-message {
80
- background: var(--user-msg-bg); color: var(--user-msg-text); align-self: flex-end;
81
- border-bottom-right-radius: 4px; margin-left: auto;
82
- }
83
- .bot-message {
84
- background-color: var(--bot-msg-bg); border: 1px solid var(--bot-msg-border); align-self: flex-start;
85
- border-bottom-left-radius: 4px; margin-right: auto;
86
- }
87
- .bot-message a { color: var(--primary-color); text-decoration: none; }
88
- .bot-message a:hover { text-decoration: underline; }
89
-
90
- .system-message {
91
- font-style: italic; color: var(--system-msg-color); text-align: center; font-size: 0.85em;
92
- background-color: transparent; box-shadow: none; align-self: center; max-width: 100%;
93
- padding: 5px 0; animation: none;
94
- }
95
-
96
- /* Loading & Status Indicators */
97
- .status-indicator {
98
- text-align: center; padding: 8px 0; color: var(--system-msg-color); font-size: 0.9em;
99
- height: 24px; display: flex; align-items: center; justify-content: center; gap: 8px;
100
- flex-shrink: 0; background-color: var(--bg-color);
101
- }
102
- #loading span.spinner {
103
- display: inline-block; width: 14px; height: 14px; border: 2px solid var(--primary-color);
104
- border-bottom-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; vertical-align: middle;
105
- }
106
  @keyframes spin { to { transform: rotate(360deg); } }
107
-
108
- /* Input Area */
109
- #input-area {
110
- display: flex; padding: 10px 12px; border-top: 1px solid var(--border-color);
111
- background-color: var(--header-bg); align-items: center; gap: 8px; flex-shrink: 0;
112
- }
113
-
114
- #userInput {
115
- flex-grow: 1; padding: 10px 15px; border: 1px solid var(--input-border);
116
- border-radius: 20px; outline: none; font-size: 1em; font-family: 'Roboto', sans-serif;
117
- background-color: var(--input-bg); transition: border-color 0.2s ease;
118
- min-height: 42px; resize: none; overflow-y: auto;
119
- }
120
  #userInput:focus { border-color: var(--primary-color); }
121
-
122
- /* Buttons */
123
- .control-button {
124
- padding: 0; border: none; border-radius: 50%; cursor: pointer;
125
- background-color: var(--button-bg); color: white; width: 42px; height: 42px;
126
- font-size: 1.3em; display: flex; align-items: center; justify-content: center;
127
- flex-shrink: 0; transition: background-color 0.2s ease, transform 0.1s ease;
128
- box-shadow: 0 1px 2px rgba(0,0,0,0.08);
129
- }
130
  .control-button:hover:not(:disabled) { background-color: var(--button-hover-bg); transform: translateY(-1px); }
131
  .control-button:active:not(:disabled) { transform: scale(0.95); }
132
  .control-button:disabled { background-color: var(--button-disabled-bg); cursor: not-allowed; transform: none; box-shadow: none; }
133
  #toggleSpeakerButton.muted { background-color: #aaa; }
134
-
135
- /* Responsive Design */
136
  @media (max-width: 600px) {
137
- body { padding: 0; }
138
- #chat-container { width: 100%; height: 100vh; max-height: none; border-radius: 0; border: none; box-shadow: none; }
139
- h1 { font-size: 1.1em; padding: 12px; }
140
- #chatbox { padding: 12px 8px; gap: 10px; }
141
  #messages div { max-width: 90%; font-size: 0.95em; padding: 9px 14px;}
142
- #input-area { padding: 8px; gap: 5px; }
143
- #userInput { padding: 9px 14px; min-height: 40px; }
144
  .control-button { width: 40px; height: 40px; font-size: 1.2em; }
145
  }
146
  </style>
147
-
148
- <!-- Import map -->
149
  <script type="importmap">
150
  { "imports": { "@xenova/transformers": "https://cdn.jsdelivr.net/npm/@xenova/[email protected]" } }
151
  </script>
152
  </head>
153
  <body>
154
  <div id="chat-container">
155
- <h1 id="chatbot-name">AI Assistant</h1> {/* English Header */}
156
  <div id="loading" class="status-indicator" style="display: none;"></div>
157
  <div id="speech-status" class="status-indicator" style="display: none;"></div>
158
  <div id="chatbox">
@@ -161,10 +83,10 @@
161
  </div>
162
  </div>
163
  <div id="input-area">
164
- <textarea id="userInput" placeholder="How can I help you today?" rows="1" disabled></textarea> {/* English Placeholder */}
165
- <button id="speechButton" class="control-button" title="Speak message" disabled>🎤</button> {/* English Title */}
166
- <button id="toggleSpeakerButton" class="control-button" title="Toggle AI speech output" disabled>🔊</button> {/* English Title */}
167
- <button id="sendButton" class="control-button" title="Send message" disabled>➤</button> {/* English Title */}
168
  </div>
169
  </div>
170
 
@@ -172,9 +94,9 @@
172
  import { pipeline, env } from '@xenova/transformers';
173
 
174
  // --- Configuration ---
175
- const MODEL_NAME = 'onnx-community/gemma-3-1b-it-ONNX-GQA'; // Still using this model
176
  const TASK = 'text-generation';
177
- // No dtype specified to avoid previous loading error
178
 
179
  // ONNX Runtime & WebGPU config
180
  env.allowLocalModels = false;
@@ -192,21 +114,20 @@
192
  const toggleSpeakerButton = document.getElementById('toggleSpeakerButton');
193
  const speechStatus = document.getElementById('speech-status');
194
 
195
- // --- State Management (English) ---
196
  let generator = null;
197
  let conversationHistory = [];
198
  let botState = {
199
- botName: "AI Assistant", // English Name
200
- userName: "User", // English Default Name
201
  botSettings: { useSpeechOutput: true }
202
  };
203
- const stateKey = 'generalBotState_gemma3_1b_en_v1'; // New key for English version
204
- const historyKey = 'generalBotHistory_gemma3_1b_en_v1';
205
 
206
- // --- Web Speech API (English) ---
207
  let recognition = null;
208
  let synthesis = window.speechSynthesis;
209
- let targetVoice = null; // Target English voice
210
  let isListening = false;
211
 
212
  // --- Initialization ---
@@ -214,43 +135,33 @@
214
  loadState();
215
  chatbotNameElement.textContent = botState.botName;
216
  updateSpeakerButtonUI();
217
- initializeSpeechAPI(); // Initialize Speech API for English
218
- await initializeModel(); // Attempt to load the model
219
  setupInputAutosize();
220
- setTimeout(loadVoices, 500); // Load voices (will look for English)
221
  });
222
 
223
  // --- State Persistence ---
224
- function loadState() {
225
  const savedState = localStorage.getItem(stateKey);
226
- if (savedState) {
227
- try {
228
- const loadedState = JSON.parse(savedState);
229
- botState = {
230
- ...botState, ...loadedState,
231
- botSettings: { ...botState.botSettings, ...(loadedState.botSettings || {}) },
232
- };
233
- } catch (e) { console.error("Failed to parse state:", e); }
234
- }
235
  const savedHistory = localStorage.getItem(historyKey);
236
- if (savedHistory) {
237
- try { conversationHistory = JSON.parse(savedHistory); displayHistory(); }
238
- catch (e) { console.error("Failed to parse history:", e); conversationHistory = []; }
239
- }
240
  }
241
- function saveState() {
242
  localStorage.setItem(stateKey, JSON.stringify(botState));
243
  localStorage.setItem(historyKey, JSON.stringify(conversationHistory));
244
  }
245
- function displayHistory() {
246
- chatbox.innerHTML = '';
247
- conversationHistory.forEach(msg => displayMessage(msg.sender, msg.text, false));
248
  }
249
 
250
  // --- UI Update Functions ---
251
- function displayMessage(sender, text, animate = true) {
252
  const messageDiv = document.createElement('div');
253
- const messageClass = sender === 'user' ? 'user-message' : sender === 'bot' ? 'bot-message' : 'system-message';
 
 
254
  messageDiv.classList.add(messageClass);
255
  if (!animate) messageDiv.style.animation = 'none';
256
 
@@ -264,7 +175,7 @@
264
  chatbox.scrollTo({ top: chatbox.scrollHeight, behavior: animate ? 'smooth' : 'auto' });
265
  }
266
 
267
- function setLoading(isLoading, message = "AI thinking...") { // English message
268
  loadingIndicator.style.display = isLoading ? 'flex' : 'none';
269
  loadingIndicator.innerHTML = isLoading ? `<span class="spinner"></span> ${message}` : '';
270
  const disableButtons = isLoading || !generator;
@@ -273,46 +184,40 @@
273
  speechButton.disabled = disableButtons || isListening || !recognition;
274
  toggleSpeakerButton.disabled = disableButtons || !synthesis;
275
  }
276
-
277
- function updateSpeakerButtonUI() {
278
  toggleSpeakerButton.textContent = botState.botSettings.useSpeechOutput ? '🔊' : '🔇';
279
- toggleSpeakerButton.title = botState.botSettings.useSpeechOutput ? 'Turn off AI speech' : 'Turn on AI speech'; // English title
280
  toggleSpeakerButton.classList.toggle('muted', !botState.botSettings.useSpeechOutput);
281
  }
282
-
283
- function showSpeechStatus(message) {
284
- speechStatus.textContent = message;
285
- speechStatus.style.display = message ? 'flex' : 'none';
286
  if (message) loadingIndicator.style.display = 'none';
287
  }
288
-
289
- function setupInputAutosize() {
290
- userInput.addEventListener('input', () => {
291
- userInput.style.height = 'auto';
292
- userInput.style.height = userInput.scrollHeight + 'px';
293
- sendButton.disabled = userInput.value.trim() === '' || !generator || loadingIndicator.style.display === 'flex';
294
- });
295
  }
296
 
297
  // --- Model & AI Logic ---
298
  async function initializeModel() {
299
- setLoading(true, "Connecting to AI model..."); // English message
300
- // **CRITICAL WARNING:** The following model loading might still fail due to potential incompatibility.
301
- displayMessage('system', `[NOTICE] Attempting to load ${MODEL_NAME}... This might take a while.`, false);
302
- displayMessage('system', `[WARNING] If loading fails with 'TypeError', the model ${MODEL_NAME} might be incompatible with this library version. Consider trying 'Xenova/gemma-2b-it'.`, false);
303
  try {
 
304
  generator = await pipeline(TASK, MODEL_NAME, {
305
- // No dtype option
306
  progress_callback: (progress) => {
307
  const msg = `[Loading: ${progress.status}] ${progress.file ? progress.file.split('/').pop() : ''} (${Math.round(progress.progress || 0)}%)`;
308
  setLoading(true, msg);
309
  },
310
  });
311
- displayMessage('system', "[NOTICE] AI Model ready! How can I assist you?", false); // English message
312
 
313
  } catch (error) {
314
  console.error("Model loading failed:", error);
315
- displayMessage('system', `[ERROR] Failed to load AI model: ${error.message}. Please refresh or try a different model.`, false); // English message
 
 
316
  setLoading(false);
317
  loadingIndicator.textContent = 'Model Load Failed';
318
  return;
@@ -327,162 +232,63 @@
327
  }
328
 
329
  // Build prompt for English conversation
330
- function buildPrompt() {
331
- const historyLimit = 6;
332
- const recentHistory = conversationHistory.slice(-historyLimit);
333
-
334
- // Using Gemma Instruct format for English
335
- let prompt = "<start_of_turn>system\n";
336
- prompt += `You are '${botState.botName}', a helpful AI assistant. Answer the user's questions clearly and concisely in English.\n<end_of_turn>\n`;
337
-
338
- recentHistory.forEach(msg => {
339
- const role = msg.sender === 'user' ? 'user' : 'model';
340
- prompt += `<start_of_turn>${role}\n${msg.text}\n<end_of_turn>\n`;
341
- });
342
-
343
- prompt += "<start_of_turn>model\n"; // Model response starts here
344
-
345
- console.log("Generated English Prompt:", prompt);
346
- return prompt;
347
  }
348
 
349
- // Cleanup response (English focus)
350
- function cleanupResponse(responseText, prompt) {
351
- let cleaned = responseText;
352
- if (cleaned.startsWith(prompt)) { cleaned = cleaned.substring(prompt.length); }
353
- else { cleaned = cleaned.replace(/^model\n?/, '').trim(); }
354
-
355
- cleaned = cleaned.replace(/<end_of_turn>/g, '').trim();
356
- cleaned = cleaned.replace(/<start_of_turn>/g, '').trim();
357
- cleaned = cleaned.replace(/^['"]/, '').replace(/['"]$/, '');
358
-
359
- if (!cleaned || cleaned.length < 2) {
360
- console.warn("Generated reply seems empty:", cleaned);
361
- const fallbacks = [ "Sorry, I didn't quite understand. Could you please rephrase?", "Hmm, I'm not sure how to respond to that. Can you try asking differently?", "Is there something else I can help you with?" ]; // English fallbacks
362
- return fallbacks[Math.floor(Math.random() * fallbacks.length)];
363
- }
364
  return cleaned;
365
  }
366
 
367
  // --- Main Interaction Logic ---
368
- async function handleUserMessage() {
369
- const userText = userInput.value.trim();
370
- if (!userText || !generator || loadingIndicator.style.display === 'flex') return;
371
-
372
- userInput.value = ''; userInput.style.height = 'auto';
373
- sendButton.disabled = true;
374
- displayMessage('user', userText);
375
- conversationHistory.push({ sender: 'user', text: userText });
376
-
377
- setLoading(true);
378
- const prompt = buildPrompt(); // Get English prompt
379
-
380
  try {
381
- const outputs = await generator(prompt, {
382
- max_new_tokens: 300,
383
- temperature: 0.7,
384
- repetition_penalty: 1.1,
385
- top_k: 50,
386
- top_p: 0.9,
387
- do_sample: true,
388
- });
389
-
390
  const rawResponse = Array.isArray(outputs) ? outputs[0].generated_text : outputs.generated_text;
391
  const replyText = cleanupResponse(rawResponse, prompt);
392
-
393
  console.log("Cleaned English Output:", replyText);
394
-
395
- displayMessage('bot', replyText);
396
- conversationHistory.push({ sender: 'bot', text: replyText });
397
-
398
- if (botState.botSettings.useSpeechOutput && synthesis && targetVoice) {
399
- speakText(replyText); // Speak English response
400
- }
401
-
402
  saveState();
403
-
404
  } catch (error) {
405
- console.error("AI response generation error:", error);
406
- displayMessage('system', `[ERROR] Failed to generate response: ${error.message}`); // English error
407
- const errorReply = "Sorry, I encountered an error while generating the response. Please try again later."; // English error reply
408
- displayMessage('bot', errorReply);
409
- conversationHistory.push({ sender: 'bot', text: errorReply });
410
- } finally {
411
- setLoading(false);
412
- userInput.focus();
413
- }
414
  }
415
 
416
  // --- Speech API Functions (English) ---
417
- function initializeSpeechAPI() {
418
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
419
- if (SpeechRecognition) {
420
- recognition = new SpeechRecognition();
421
- recognition.lang = 'en-US'; // Set to English
422
- recognition.continuous = false; recognition.interimResults = false;
423
- recognition.onstart = () => { isListening = true; speechButton.disabled = true; speechButton.textContent = '👂'; showSpeechStatus('Listening...'); }; // English status
424
- recognition.onresult = (event) => { userInput.value = event.results[0][0].transcript; userInput.dispatchEvent(new Event('input')); handleUserMessage(); };
425
- recognition.onerror = (event) => { console.error("Speech error:", event.error); showSpeechStatus(`Speech recognition error (${event.error})`); setTimeout(() => showSpeechStatus(''), 3000); }; // English status
426
- recognition.onend = () => { isListening = false; speechButton.disabled = !generator; speechButton.textContent = '🎤'; if (speechStatus.textContent === 'Listening...') showSpeechStatus(''); };
427
- speechButton.disabled = false;
428
- } else { console.warn("Speech Recognition not supported."); speechButton.style.display = 'none'; }
429
-
430
- if (!synthesis) { console.warn("Speech Synthesis not supported."); toggleSpeakerButton.style.display = 'none'; }
431
- else {
432
- toggleSpeakerButton.addEventListener('click', () => { botState.botSettings.useSpeechOutput = !botState.botSettings.useSpeechOutput; updateSpeakerButtonUI(); saveState(); if (!botState.botSettings.useSpeechOutput) synthesis.cancel(); });
433
- toggleSpeakerButton.disabled = false;
434
- }
435
  }
436
-
437
- function loadVoices() {
438
- if (!synthesis) return;
439
- let voices = synthesis.getVoices();
440
- if (voices.length === 0) {
441
- synthesis.onvoiceschanged = () => { voices = synthesis.getVoices(); findAndSetVoice(voices); };
442
- } else { findAndSetVoice(voices); }
443
  }
444
-
445
- // Find English voice
446
- function findAndSetVoice(voices) {
447
- // Prioritize US English voices
448
- targetVoice = voices.find(v => v.lang === 'en-US');
449
- // Fallback to any English voice
450
- if (!targetVoice) targetVoice = voices.find(v => v.lang.startsWith('en-'));
451
-
452
- if (targetVoice) {
453
- console.log("Using English voice:", targetVoice.name, targetVoice.lang);
454
- } else {
455
- console.warn("No suitable English voice found. Speech output might use default voice.");
456
- displayMessage('system', "[NOTICE] No English voice found. Speech output may use the default system voice.", false); // English message
457
- }
458
  }
459
-
460
- // Speak English text
461
- function speakText(text) {
462
- if (!synthesis || !botState.botSettings.useSpeechOutput) return;
463
- synthesis.cancel();
464
- const utterance = new SpeechSynthesisUtterance(text);
465
- if (targetVoice) {
466
- utterance.voice = targetVoice;
467
- utterance.lang = targetVoice.lang; // Use the found voice's language
468
- } else {
469
- utterance.lang = 'en-US'; // Default to US English if no voice found
470
- }
471
- utterance.rate = 1.0; utterance.pitch = 1.0;
472
- synthesis.speak(utterance);
473
  }
474
 
475
  // --- Event Listeners ---
476
  sendButton.addEventListener('click', handleUserMessage);
477
- userInput.addEventListener('keypress', (e) => {
478
- if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleUserMessage(); }
479
- });
480
- speechButton.addEventListener('click', () => {
481
- if (recognition && !isListening && generator) {
482
- try { recognition.start(); }
483
- catch (error) { console.error("Rec start fail:", error); showSpeechStatus(`Failed to start recognition`); setTimeout(() => showSpeechStatus(''), 2000); isListening = false; speechButton.disabled = !generator; speechButton.textContent = '🎤';}
484
- }
485
- });
486
 
487
  </script>
488
  </body>
 
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, maximum-scale=1.0, user-scalable=no">
6
+ <title>AI Assistant (Gemma 3 1B - Q4 Attempt)</title>
7
  <style>
 
8
  @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
 
 
9
  :root {
10
+ --primary-color: #007bff; --secondary-color: #6c757d; --text-color: #212529;
11
+ --bg-color: #f8f9fa; --user-msg-bg: #e7f5ff; --user-msg-text: #004085;
12
+ --bot-msg-bg: #ffffff; --bot-msg-border: #dee2e6; --system-msg-color: #6c757d;
13
+ --border-color: #dee2e6; --input-bg: #ffffff; --input-border: #ced4da;
14
+ --button-bg: var(--primary-color); --button-hover-bg: #0056b3; --button-disabled-bg: #adb5bd;
15
+ --scrollbar-thumb: var(--primary-color); --scrollbar-track: #e9ecef;
16
+ --header-bg: #ffffff; --header-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
 
 
 
 
 
 
 
 
 
 
 
 
17
  --container-shadow: 0 4px 15px rgba(0, 0, 0, 0.07);
18
  }
 
 
19
  * { box-sizing: border-box; margin: 0; padding: 0; }
20
  html { height: 100%; }
21
  body {
22
+ font-family: 'Roboto', sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center;
23
+ min-height: 100vh; background-color: var(--bg-color); color: var(--text-color); padding: 5px; overscroll-behavior: none;
 
 
24
  }
 
 
25
  #chat-container {
26
  width: 100%; max-width: 600px; height: calc(100vh - 10px); max-height: 800px;
27
+ background-color: #ffffff; border-radius: 12px; box-shadow: var(--container-shadow);
28
+ display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--border-color);
 
29
  }
 
 
30
  h1 {
31
+ text-align: center; color: var(--primary-color); padding: 15px; background-color: var(--header-bg);
32
+ border-bottom: 1px solid var(--border-color); font-size: 1.2em; font-weight: 500; flex-shrink: 0;
33
+ box-shadow: var(--header-shadow); position: relative; z-index: 10;
 
34
  }
 
 
35
  #chatbox {
36
+ flex-grow: 1; overflow-y: auto; padding: 15px; display: flex; flex-direction: column; gap: 12px;
37
+ scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); background-color: var(--bg-color);
 
38
  }
39
  #chatbox::-webkit-scrollbar { width: 6px; }
40
  #chatbox::-webkit-scrollbar-track { background: var(--scrollbar-track); border-radius: 3px; }
41
  #chatbox::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 3px; }
 
 
42
  #messages div {
43
+ padding: 10px 15px; border-radius: 16px; max-width: 85%; word-wrap: break-word; line-height: 1.5;
44
+ font-size: 1em; box-shadow: 0 1px 2px rgba(0,0,0,0.05); position: relative; animation: fadeIn 0.25s ease-out;
 
45
  }
46
  @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
47
+ .user-message { background: var(--user-msg-bg); color: var(--user-msg-text); align-self: flex-end; border-bottom-right-radius: 4px; margin-left: auto; }
48
+ .bot-message { background-color: var(--bot-msg-bg); border: 1px solid var(--bot-msg-border); align-self: flex-start; border-bottom-left-radius: 4px; margin-right: auto; }
49
+ .bot-message a { color: var(--primary-color); text-decoration: none; } .bot-message a:hover { text-decoration: underline; }
50
+ .system-message { font-style: italic; color: var(--system-msg-color); text-align: center; font-size: 0.85em; background-color: transparent; box-shadow: none; align-self: center; max-width: 100%; padding: 5px 0; animation: none; }
51
+ .error-message { color: #dc3545; font-weight: 500; background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 10px 15px; border-radius: 8px; align-self: stretch; text-align: left; } /* Error Message Style */
52
+ .status-indicator { text-align: center; padding: 8px 0; color: var(--system-msg-color); font-size: 0.9em; height: 24px; display: flex; align-items: center; justify-content: center; gap: 8px; flex-shrink: 0; background-color: var(--bg-color); }
53
+ #loading span.spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--primary-color); border-bottom-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; vertical-align: middle; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  @keyframes spin { to { transform: rotate(360deg); } }
55
+ #input-area { display: flex; padding: 10px 12px; border-top: 1px solid var(--border-color); background-color: var(--header-bg); align-items: center; gap: 8px; flex-shrink: 0; }
56
+ #userInput { flex-grow: 1; padding: 10px 15px; border: 1px solid var(--input-border); border-radius: 20px; outline: none; font-size: 1em; font-family: 'Roboto', sans-serif; background-color: var(--input-bg); transition: border-color 0.2s ease; min-height: 42px; resize: none; overflow-y: auto; }
 
 
 
 
 
 
 
 
 
 
 
57
  #userInput:focus { border-color: var(--primary-color); }
58
+ .control-button { padding: 0; border: none; border-radius: 50%; cursor: pointer; background-color: var(--button-bg); color: white; width: 42px; height: 42px; font-size: 1.3em; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: background-color 0.2s ease, transform 0.1s ease; box-shadow: 0 1px 2px rgba(0,0,0,0.08); }
 
 
 
 
 
 
 
 
59
  .control-button:hover:not(:disabled) { background-color: var(--button-hover-bg); transform: translateY(-1px); }
60
  .control-button:active:not(:disabled) { transform: scale(0.95); }
61
  .control-button:disabled { background-color: var(--button-disabled-bg); cursor: not-allowed; transform: none; box-shadow: none; }
62
  #toggleSpeakerButton.muted { background-color: #aaa; }
 
 
63
  @media (max-width: 600px) {
64
+ body { padding: 0; } #chat-container { width: 100%; height: 100vh; max-height: none; border-radius: 0; border: none; box-shadow: none; }
65
+ h1 { font-size: 1.1em; padding: 12px; } #chatbox { padding: 12px 8px; gap: 10px; }
 
 
66
  #messages div { max-width: 90%; font-size: 0.95em; padding: 9px 14px;}
67
+ #input-area { padding: 8px; gap: 5px; } #userInput { padding: 9px 14px; min-height: 40px; }
 
68
  .control-button { width: 40px; height: 40px; font-size: 1.2em; }
69
  }
70
  </style>
 
 
71
  <script type="importmap">
72
  { "imports": { "@xenova/transformers": "https://cdn.jsdelivr.net/npm/@xenova/[email protected]" } }
73
  </script>
74
  </head>
75
  <body>
76
  <div id="chat-container">
77
+ <h1 id="chatbot-name">AI Assistant</h1>
78
  <div id="loading" class="status-indicator" style="display: none;"></div>
79
  <div id="speech-status" class="status-indicator" style="display: none;"></div>
80
  <div id="chatbox">
 
83
  </div>
84
  </div>
85
  <div id="input-area">
86
+ <textarea id="userInput" placeholder="How can I help you today?" rows="1" disabled></textarea>
87
+ <button id="speechButton" class="control-button" title="Speak message" disabled>🎤</button>
88
+ <button id="toggleSpeakerButton" class="control-button" title="Toggle AI speech output" disabled>🔊</button>
89
+ <button id="sendButton" class="control-button" title="Send message" disabled>➤</button>
90
  </div>
91
  </div>
92
 
 
94
  import { pipeline, env } from '@xenova/transformers';
95
 
96
  // --- Configuration ---
97
+ const MODEL_NAME = 'onnx-community/gemma-3-1b-it-ONNX-GQA';
98
  const TASK = 'text-generation';
99
+ const QUANTIZATION = 'q4'; // Re-adding based on model card example
100
 
101
  // ONNX Runtime & WebGPU config
102
  env.allowLocalModels = false;
 
114
  const toggleSpeakerButton = document.getElementById('toggleSpeakerButton');
115
  const speechStatus = document.getElementById('speech-status');
116
 
117
+ // --- State Management ---
118
  let generator = null;
119
  let conversationHistory = [];
120
  let botState = {
121
+ botName: "AI Assistant", userName: "User",
 
122
  botSettings: { useSpeechOutput: true }
123
  };
124
+ const stateKey = 'generalBotState_gemma3_1b_en_v2'; // Updated key
125
+ const historyKey = 'generalBotHistory_gemma3_1b_en_v2';
126
 
127
+ // --- Web Speech API ---
128
  let recognition = null;
129
  let synthesis = window.speechSynthesis;
130
+ let targetVoice = null;
131
  let isListening = false;
132
 
133
  // --- Initialization ---
 
135
  loadState();
136
  chatbotNameElement.textContent = botState.botName;
137
  updateSpeakerButtonUI();
138
+ initializeSpeechAPI();
139
+ await initializeModel();
140
  setupInputAutosize();
141
+ setTimeout(loadVoices, 500);
142
  });
143
 
144
  // --- State Persistence ---
145
+ function loadState() { /* No changes needed */
146
  const savedState = localStorage.getItem(stateKey);
147
+ if (savedState) { try { const loadedState = JSON.parse(savedState); botState = { ...botState, ...loadedState, botSettings: { ...botState.botSettings, ...(loadedState.botSettings || {}) }, }; } catch (e) { console.error("Failed to parse state:", e); } }
 
 
 
 
 
 
 
 
148
  const savedHistory = localStorage.getItem(historyKey);
149
+ if (savedHistory) { try { conversationHistory = JSON.parse(savedHistory); displayHistory(); } catch (e) { console.error("Failed to parse history:", e); conversationHistory = []; } }
 
 
 
150
  }
151
+ function saveState() { /* No changes needed */
152
  localStorage.setItem(stateKey, JSON.stringify(botState));
153
  localStorage.setItem(historyKey, JSON.stringify(conversationHistory));
154
  }
155
+ function displayHistory() { /* No changes needed */
156
+ chatbox.innerHTML = ''; conversationHistory.forEach(msg => displayMessage(msg.sender, msg.text, false));
 
157
  }
158
 
159
  // --- UI Update Functions ---
160
+ function displayMessage(sender, text, animate = true, isError = false) { // Added isError flag
161
  const messageDiv = document.createElement('div');
162
+ let messageClass = sender === 'user' ? 'user-message' : sender === 'bot' ? 'bot-message' : 'system-message';
163
+ if (isError) { messageClass = 'error-message'; } // Apply error style
164
+
165
  messageDiv.classList.add(messageClass);
166
  if (!animate) messageDiv.style.animation = 'none';
167
 
 
175
  chatbox.scrollTo({ top: chatbox.scrollHeight, behavior: animate ? 'smooth' : 'auto' });
176
  }
177
 
178
+ function setLoading(isLoading, message = "AI thinking...") { /* No changes needed */
179
  loadingIndicator.style.display = isLoading ? 'flex' : 'none';
180
  loadingIndicator.innerHTML = isLoading ? `<span class="spinner"></span> ${message}` : '';
181
  const disableButtons = isLoading || !generator;
 
184
  speechButton.disabled = disableButtons || isListening || !recognition;
185
  toggleSpeakerButton.disabled = disableButtons || !synthesis;
186
  }
187
+ function updateSpeakerButtonUI() { /* No changes needed */
 
188
  toggleSpeakerButton.textContent = botState.botSettings.useSpeechOutput ? '🔊' : '🔇';
189
+ toggleSpeakerButton.title = botState.botSettings.useSpeechOutput ? 'Turn off AI speech' : 'Turn on AI speech';
190
  toggleSpeakerButton.classList.toggle('muted', !botState.botSettings.useSpeechOutput);
191
  }
192
+ function showSpeechStatus(message) { /* No changes needed */
193
+ speechStatus.textContent = message; speechStatus.style.display = message ? 'flex' : 'none';
 
 
194
  if (message) loadingIndicator.style.display = 'none';
195
  }
196
+ function setupInputAutosize() { /* No changes needed */
197
+ userInput.addEventListener('input', () => { userInput.style.height = 'auto'; userInput.style.height = userInput.scrollHeight + 'px'; sendButton.disabled = userInput.value.trim() === '' || !generator || loadingIndicator.style.display === 'flex'; });
 
 
 
 
 
198
  }
199
 
200
  // --- Model & AI Logic ---
201
  async function initializeModel() {
202
+ setLoading(true, "Connecting to AI model (Q4)..."); // Indicate Q4 attempt
203
+ displayMessage('system', `[NOTICE] Attempting to load ${MODEL_NAME} with { dtype: "${QUANTIZATION}" }...`, false);
204
+
 
205
  try {
206
+ // *** Re-adding dtype: QUANTIZATION based on model card example ***
207
  generator = await pipeline(TASK, MODEL_NAME, {
208
+ dtype: QUANTIZATION,
209
  progress_callback: (progress) => {
210
  const msg = `[Loading: ${progress.status}] ${progress.file ? progress.file.split('/').pop() : ''} (${Math.round(progress.progress || 0)}%)`;
211
  setLoading(true, msg);
212
  },
213
  });
214
+ displayMessage('system', "[NOTICE] AI Model ready! How can I assist you?", false);
215
 
216
  } catch (error) {
217
  console.error("Model loading failed:", error);
218
+ // Display a more prominent error message in the chat
219
+ displayMessage('system', `[ERROR] Failed to load AI model (${MODEL_NAME} with ${QUANTIZATION}).`, true, true); // Use isError flag
220
+ displayMessage('system', `Error details: ${error.message}. This model might be incompatible. Please try refreshing or consider using a different model like 'Xenova/gemma-2b-it'.`, true, true);
221
  setLoading(false);
222
  loadingIndicator.textContent = 'Model Load Failed';
223
  return;
 
232
  }
233
 
234
  // Build prompt for English conversation
235
+ function buildPrompt() { /* No changes needed */
236
+ const historyLimit = 6; const recentHistory = conversationHistory.slice(-historyLimit);
237
+ let prompt = "<start_of_turn>system\nYou are 'AI Assistant', a helpful AI assistant. Answer the user's questions clearly and concisely in English.\n<end_of_turn>\n";
238
+ recentHistory.forEach(msg => { const role = msg.sender === 'user' ? 'user' : 'model'; prompt += `<start_of_turn>${role}\n${msg.text}\n<end_of_turn>\n`; });
239
+ prompt += "<start_of_turn>model\n";
240
+ console.log("Generated English Prompt:", prompt); return prompt;
 
 
 
 
 
 
 
 
 
 
 
241
  }
242
 
243
+ // Cleanup response
244
+ function cleanupResponse(responseText, prompt) { /* No changes needed */
245
+ let cleaned = responseText; if (cleaned.startsWith(prompt)) { cleaned = cleaned.substring(prompt.length); } else { cleaned = cleaned.replace(/^model\n?/, '').trim(); }
246
+ cleaned = cleaned.replace(/<end_of_turn>/g, '').trim(); cleaned = cleaned.replace(/<start_of_turn>/g, '').trim(); cleaned = cleaned.replace(/^['"]/, '').replace(/['"]$/, '');
247
+ if (!cleaned || cleaned.length < 2) { console.warn("Generated reply seems empty:", cleaned); const fallbacks = [ "Sorry, I didn't quite understand. Could you please rephrase?", "Hmm, I'm not sure how to respond to that. Can you try asking differently?", "Is there something else I can help you with?" ]; return fallbacks[Math.floor(Math.random() * fallbacks.length)]; }
 
 
 
 
 
 
 
 
 
 
248
  return cleaned;
249
  }
250
 
251
  // --- Main Interaction Logic ---
252
+ async function handleUserMessage() { /* No changes needed */
253
+ const userText = userInput.value.trim(); if (!userText || !generator || loadingIndicator.style.display === 'flex') return;
254
+ userInput.value = ''; userInput.style.height = 'auto'; sendButton.disabled = true;
255
+ displayMessage('user', userText); conversationHistory.push({ sender: 'user', text: userText });
256
+ setLoading(true); const prompt = buildPrompt();
 
 
 
 
 
 
 
257
  try {
258
+ const outputs = await generator(prompt, { max_new_tokens: 300, temperature: 0.7, repetition_penalty: 1.1, top_k: 50, top_p: 0.9, do_sample: true });
 
 
 
 
 
 
 
 
259
  const rawResponse = Array.isArray(outputs) ? outputs[0].generated_text : outputs.generated_text;
260
  const replyText = cleanupResponse(rawResponse, prompt);
 
261
  console.log("Cleaned English Output:", replyText);
262
+ displayMessage('bot', replyText); conversationHistory.push({ sender: 'bot', text: replyText });
263
+ if (botState.botSettings.useSpeechOutput && synthesis && targetVoice) { speakText(replyText); }
 
 
 
 
 
 
264
  saveState();
 
265
  } catch (error) {
266
+ console.error("AI response generation error:", error); displayMessage('system', `[ERROR] Failed to generate response: ${error.message}`, true, true); // Show error in chat
267
+ const errorReply = "Sorry, I encountered an error generating the response."; displayMessage('bot', errorReply); conversationHistory.push({ sender: 'bot', text: errorReply });
268
+ } finally { setLoading(false); userInput.focus(); }
 
 
 
 
 
 
269
  }
270
 
271
  // --- Speech API Functions (English) ---
272
+ function initializeSpeechAPI() { /* No changes needed */
273
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
274
+ if (SpeechRecognition) { recognition = new SpeechRecognition(); recognition.lang = 'en-US'; recognition.continuous = false; recognition.interimResults = false; recognition.onstart = () => { isListening = true; speechButton.disabled = true; speechButton.textContent = '👂'; showSpeechStatus('Listening...'); }; recognition.onresult = (event) => { userInput.value = event.results[0][0].transcript; userInput.dispatchEvent(new Event('input')); handleUserMessage(); }; recognition.onerror = (event) => { console.error("Speech error:", event.error); showSpeechStatus(`Speech recognition error (${event.error})`); setTimeout(() => showSpeechStatus(''), 3000); }; recognition.onend = () => { isListening = false; speechButton.disabled = !generator; speechButton.textContent = '🎤'; if (speechStatus.textContent === 'Listening...') showSpeechStatus(''); }; speechButton.disabled = false; } else { console.warn("Speech Recognition not supported."); speechButton.style.display = 'none'; }
275
+ if (!synthesis) { console.warn("Speech Synthesis not supported."); toggleSpeakerButton.style.display = 'none'; } else { toggleSpeakerButton.addEventListener('click', () => { botState.botSettings.useSpeechOutput = !botState.botSettings.useSpeechOutput; updateSpeakerButtonUI(); saveState(); if (!botState.botSettings.useSpeechOutput) synthesis.cancel(); }); toggleSpeakerButton.disabled = false; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  }
277
+ function loadVoices() { /* No changes needed */
278
+ if (!synthesis) return; let voices = synthesis.getVoices(); if (voices.length === 0) { synthesis.onvoiceschanged = () => { voices = synthesis.getVoices(); findAndSetVoice(voices); }; } else { findAndSetVoice(voices); }
 
 
 
 
 
279
  }
280
+ function findAndSetVoice(voices) { /* No changes needed */
281
+ targetVoice = voices.find(v => v.lang === 'en-US') || voices.find(v => v.lang.startsWith('en-'));
282
+ if (targetVoice) { console.log("Using English voice:", targetVoice.name, targetVoice.lang); } else { console.warn("No suitable English voice found."); displayMessage('system', "[NOTICE] No English voice found.", false); }
 
 
 
 
 
 
 
 
 
 
 
283
  }
284
+ function speakText(text) { /* No changes needed */
285
+ if (!synthesis || !botState.botSettings.useSpeechOutput) return; synthesis.cancel(); const utterance = new SpeechSynthesisUtterance(text); if (targetVoice) { utterance.voice = targetVoice; utterance.lang = targetVoice.lang; } else { utterance.lang = 'en-US'; } utterance.rate = 1.0; utterance.pitch = 1.0; synthesis.speak(utterance);
 
 
 
 
 
 
 
 
 
 
 
 
286
  }
287
 
288
  // --- Event Listeners ---
289
  sendButton.addEventListener('click', handleUserMessage);
290
+ userInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleUserMessage(); } });
291
+ speechButton.addEventListener('click', () => { if (recognition && !isListening && generator) { try { recognition.start(); } catch (error) { console.error("Rec start fail:", error); showSpeechStatus(`Failed to start recognition`); setTimeout(() => showSpeechStatus(''), 2000); isListening = false; speechButton.disabled = !generator; speechButton.textContent = '🎤';} } });
 
 
 
 
 
 
 
292
 
293
  </script>
294
  </body>