kimhyunwoo commited on
Commit
fa541aa
ยท
verified ยท
1 Parent(s): 71d66b0

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +145 -234
index.html CHANGED
@@ -1,93 +1,68 @@
1
  <!DOCTYPE html>
2
- <html lang="ko">
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 ์ฑ—๋ด‡ (Gemma 3 1B)</title>
7
  <style>
8
- /* Google Fonts */
9
- @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
10
 
11
- /* CSS Variables for Theming (์ผ๋ฐ˜์ ์ธ ํ†ค์œผ๋กœ ์•ฝ๊ฐ„ ์ˆ˜์ •) */
12
  :root {
13
- --primary-color: #4a90e2; /* ์ฐจ๋ถ„ํ•œ ํŒŒ๋ž€์ƒ‰ */
14
- --secondary-color: #f5a623; /* ์ฃผํ™ฉ์ƒ‰ ์•ก์„ผํŠธ */
15
- --text-color: #333;
16
- --bg-color: #f8f9fa; /* ๋ฐ์€ ํšŒ์ƒ‰ ๋ฐฐ๊ฒฝ */
17
- --user-msg-bg: #e7f0ff; /* ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ ๋ฐฐ๊ฒฝ */
18
- --user-msg-text: #1c3d5a;
19
  --bot-msg-bg: #ffffff;
20
- --bot-msg-border: #e0e0e0;
21
- --system-msg-color: #999;
22
- --border-color: #e9ecef;
23
  --input-bg: #ffffff;
24
  --input-border: #ced4da;
25
  --button-bg: var(--primary-color);
26
- --button-hover-bg: #357abd;
27
  --button-disabled-bg: #adb5bd;
28
  --scrollbar-thumb: var(--primary-color);
29
- --scrollbar-track: #f1f3f5;
30
  --header-bg: #ffffff;
31
- --header-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
32
- --container-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
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: 'Noto Sans KR', sans-serif;
40
- display: flex;
41
- flex-direction: column;
42
- align-items: center;
43
- justify-content: center;
44
- min-height: 100vh;
45
- background-color: var(--bg-color); /* ๋‹จ์ƒ‰ ๋ฐฐ๊ฒฝ */
46
- color: var(--text-color);
47
- padding: 5px;
48
- overscroll-behavior: none;
49
  }
50
 
51
  /* Chat Container */
52
  #chat-container {
53
- width: 100%;
54
- max-width: 600px; /* ์•ฝ๊ฐ„ ๋„“๊ฒŒ */
55
- height: calc(100vh - 10px);
56
- max-height: 800px;
57
- background-color: #ffffff; /* ์ปจํ…Œ์ด๋„ˆ ๋ฐฐ๊ฒฝ ํฐ์ƒ‰ */
58
- border-radius: 16px; /* ์‚ด์ง ๊ฐ์ง„ ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ */
59
- box-shadow: var(--container-shadow);
60
- display: flex;
61
- flex-direction: column;
62
- overflow: hidden;
63
- border: 1px solid var(--border-color);
64
  }
65
 
66
  /* Header */
67
  h1 {
68
- text-align: center;
69
- color: var(--primary-color);
70
- padding: 16px;
71
- background-color: var(--header-bg);
72
- border-bottom: 1px solid var(--border-color);
73
- font-size: 1.25em;
74
- font-weight: 500;
75
- flex-shrink: 0;
76
- box-shadow: var(--header-shadow);
77
- position: relative; z-index: 10;
78
  }
79
 
80
  /* Chatbox Area */
81
  #chatbox {
82
- flex-grow: 1;
83
- overflow-y: auto;
84
- padding: 18px;
85
- display: flex;
86
- flex-direction: column;
87
- gap: 14px;
88
- scrollbar-width: thin;
89
- scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
90
- background-color: #f8f9fa; /* ์ฑ„ํŒ…์ฐฝ ๋‚ด๋ถ€ ๋ฐฐ๊ฒฝ */
91
  }
92
  #chatbox::-webkit-scrollbar { width: 6px; }
93
  #chatbox::-webkit-scrollbar-track { background: var(--scrollbar-track); border-radius: 3px; }
@@ -95,108 +70,62 @@
95
 
96
  /* Message Bubbles */
97
  #messages div {
98
- padding: 11px 16px;
99
- border-radius: 18px;
100
- max-width: 85%;
101
- word-wrap: break-word;
102
- line-height: 1.55;
103
- font-size: 1em;
104
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
105
- position: relative;
106
- animation: fadeIn 0.3s ease-out;
107
  }
108
- @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
109
 
110
  .user-message {
111
- background: var(--user-msg-bg);
112
- color: var(--user-msg-text);
113
- align-self: flex-end;
114
- border-bottom-right-radius: 5px;
115
- margin-left: auto;
116
  }
117
-
118
  .bot-message {
119
- background-color: var(--bot-msg-bg);
120
- border: 1px solid #e9ecef; /* ๋ด‡ ๋ฉ”์‹œ์ง€ ํ…Œ๋‘๋ฆฌ */
121
- align-self: flex-start;
122
- border-bottom-left-radius: 5px;
123
- margin-right: auto;
124
  }
125
- .bot-message a { color: var(--primary-color); text-decoration: none; }
126
- .bot-message a:hover { text-decoration: underline; }
127
 
128
  .system-message {
129
- font-style: italic;
130
- color: var(--system-msg-color);
131
- text-align: center;
132
- font-size: 0.85em;
133
- background-color: transparent;
134
- box-shadow: none;
135
- align-self: center;
136
- max-width: 100%;
137
- padding: 5px 0;
138
- animation: none;
139
  }
140
 
141
  /* Loading & Status Indicators */
142
  .status-indicator {
143
- text-align: center;
144
- padding: 8px 0; /* ํŒจ๋”ฉ ์•ฝ๊ฐ„ ์ค„์ž„ */
145
- color: var(--system-msg-color);
146
- font-size: 0.9em;
147
- height: 24px; /* ๋†’์ด ํ™•๋ณด */
148
- display: flex;
149
- align-items: center;
150
- justify-content: center;
151
- gap: 8px;
152
- flex-shrink: 0;
153
- background-color: #f8f9fa; /* ์ฑ„ํŒ…์ฐฝ ๋ฐฐ๊ฒฝ๊ณผ ํ†ต์ผ */
154
  }
155
  #loading span.spinner {
156
- display: inline-block; width: 14px; height: 14px;
157
- border: 2px solid var(--primary-color);
158
- border-bottom-color: transparent; border-radius: 50%;
159
- animation: spin 1s linear infinite; vertical-align: middle;
160
  }
161
  @keyframes spin { to { transform: rotate(360deg); } }
162
 
163
  /* Input Area */
164
  #input-area {
165
- display: flex;
166
- padding: 10px 15px;
167
- border-top: 1px solid var(--border-color);
168
- background-color: var(--header-bg);
169
- align-items: center;
170
- gap: 8px;
171
- flex-shrink: 0;
172
  }
173
 
174
  #userInput {
175
- flex-grow: 1;
176
- padding: 11px 16px;
177
- border: 1px solid var(--input-border);
178
- border-radius: 22px; /* ๋ฒ„ํŠผ๊ณผ ํ†ต์ผ */
179
- outline: none;
180
- font-size: 1em;
181
- font-family: 'Noto Sans KR', sans-serif;
182
- background-color: var(--input-bg);
183
- transition: border-color 0.2s ease;
184
- min-height: 44px;
185
- resize: none;
186
- overflow-y: auto;
187
  }
188
  #userInput:focus { border-color: var(--primary-color); }
189
 
190
  /* Buttons */
191
  .control-button {
192
  padding: 0; border: none; border-radius: 50%; cursor: pointer;
193
- background-color: var(--button-bg); color: white;
194
- width: 44px; height: 44px; /* ํฌ๊ธฐ ํ†ต์ผ */
195
- font-size: 1.4em; /* ์•„์ด์ฝ˜ ํฌ๊ธฐ */
196
- display: flex; align-items: center; justify-content: center;
197
- flex-shrink: 0;
198
- transition: background-color 0.2s ease, transform 0.1s ease;
199
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
200
  }
201
  .control-button:hover:not(:disabled) { background-color: var(--button-hover-bg); transform: translateY(-1px); }
202
  .control-button:active:not(:disabled) { transform: scale(0.95); }
@@ -207,35 +136,35 @@
207
  @media (max-width: 600px) {
208
  body { padding: 0; }
209
  #chat-container { width: 100%; height: 100vh; max-height: none; border-radius: 0; border: none; box-shadow: none; }
210
- h1 { font-size: 1.15em; padding: 14px; }
211
- #chatbox { padding: 15px 10px; gap: 12px; }
212
- #messages div { max-width: 90%; font-size: 0.98em; padding: 10px 15px;}
213
- #input-area { padding: 8px 10px; gap: 6px; }
214
- #userInput { padding: 10px 15px; min-height: 42px; }
215
- .control-button { width: 42px; height: 42px; font-size: 1.3em; }
216
  }
217
  </style>
218
 
219
- <!-- Import map for Transformers.js ES Module -->
220
  <script type="importmap">
221
  { "imports": { "@xenova/transformers": "https://cdn.jsdelivr.net/npm/@xenova/[email protected]" } }
222
  </script>
223
  </head>
224
  <body>
225
  <div id="chat-container">
226
- <h1 id="chatbot-name">AI ์–ด์‹œ์Šคํ„ดํŠธ</h1>
227
  <div id="loading" class="status-indicator" style="display: none;"></div>
228
  <div id="speech-status" class="status-indicator" style="display: none;"></div>
229
  <div id="chatbox">
230
  <div id="messages">
231
- <!-- ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ์˜์—ญ -->
232
  </div>
233
  </div>
234
  <div id="input-area">
235
- <textarea id="userInput" placeholder="๋ฌด์—‡์„ ๋„์™€๋“œ๋ฆด๊นŒ์š”?" rows="1" disabled></textarea>
236
- <button id="speechButton" class="control-button" title="์Œ์„ฑ์œผ๋กœ ๋งํ•˜๊ธฐ" disabled>๐ŸŽค</button>
237
- <button id="toggleSpeakerButton" class="control-button" title="AI ์Œ์„ฑ ๋“ฃ๊ธฐ ์ผœ๊ธฐ" disabled>๐Ÿ”Š</button>
238
- <button id="sendButton" class="control-button" title="๋ฉ”์‹œ์ง€ ์ „์†ก" disabled>โžค</button>
239
  </div>
240
  </div>
241
 
@@ -243,11 +172,11 @@
243
  import { pipeline, env } from '@xenova/transformers';
244
 
245
  // --- Configuration ---
246
- const MODEL_NAME = 'onnx-community/gemma-3-1b-it-ONNX-GQA'; // ์‚ฌ์šฉ์ž๊ฐ€ ์ง€์ •ํ•œ ๋ชจ๋ธ
247
  const TASK = 'text-generation';
248
- // const QUANTIZATION = 'q4'; // !! ์ œ๊ฑฐ๋จ: ๋กœ๋”ฉ ์˜ค๋ฅ˜ ํ•ด๊ฒฐ ์œ„ํ•ด ๊ธฐ๋ณธ ์ •๋ฐ€๋„ ์‚ฌ์šฉ !!
249
 
250
- // ONNX Runtime & WebGPU ์„ค์ •
251
  env.allowLocalModels = false;
252
  env.useBrowserCache = true;
253
  env.backends.onnx.executionProviders = ['webgpu', 'wasm'];
@@ -263,22 +192,21 @@
263
  const toggleSpeakerButton = document.getElementById('toggleSpeakerButton');
264
  const speechStatus = document.getElementById('speech-status');
265
 
266
- // --- State Management (Simplified) ---
267
  let generator = null;
268
  let conversationHistory = [];
269
  let botState = {
270
- botName: "AI ์–ด์‹œ์Šคํ„ดํŠธ", // ์ผ๋ฐ˜์ ์ธ ์ด๋ฆ„
271
- userName: "์‚ฌ์šฉ์ž", // ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ์ด๋ฆ„
272
- // scores, userPetNames ๋“ฑ ๋กœ๋งจ์Šค ๊ด€๋ จ ์ƒํƒœ ์ œ๊ฑฐ
273
  botSettings: { useSpeechOutput: true }
274
  };
275
- const stateKey = 'generalBotState_gemma3_1b_v1'; // ํ‚ค ๋ณ€๊ฒฝ
276
- const historyKey = 'generalBotHistory_gemma3_1b_v1';
277
 
278
- // --- Web Speech API ---
279
  let recognition = null;
280
  let synthesis = window.speechSynthesis;
281
- let targetVoice = null; // ํŠน์ • ์ด๋ฆ„(Jenny) ๋Œ€์‹  ์ผ๋ฐ˜ ๋ชฉ์†Œ๋ฆฌ ์ฐพ๊ธฐ
282
  let isListening = false;
283
 
284
  // --- Initialization ---
@@ -286,10 +214,10 @@
286
  loadState();
287
  chatbotNameElement.textContent = botState.botName;
288
  updateSpeakerButtonUI();
289
- initializeSpeechAPI();
290
- await initializeModel(); // ๋ชจ๋ธ ๋กœ๋”ฉ ์‹œ๋„
291
  setupInputAutosize();
292
- setTimeout(loadVoices, 500);
293
  });
294
 
295
  // --- State Persistence ---
@@ -298,11 +226,8 @@
298
  if (savedState) {
299
  try {
300
  const loadedState = JSON.parse(savedState);
301
- // ํ•„์š”ํ•œ ์ƒํƒœ๋งŒ ๋กœ๋“œ (๋กœ๋งจ์Šค ์ƒํƒœ ์ œ์™ธ)
302
  botState = {
303
- ...botState,
304
- botName: loadedState.botName || botState.botName,
305
- userName: loadedState.userName || botState.userName,
306
  botSettings: { ...botState.botSettings, ...(loadedState.botSettings || {}) },
307
  };
308
  } catch (e) { console.error("Failed to parse state:", e); }
@@ -329,7 +254,6 @@
329
  messageDiv.classList.add(messageClass);
330
  if (!animate) messageDiv.style.animation = 'none';
331
 
332
- // Basic Sanitization & Formatting
333
  text = text.replace(/</g, "<").replace(/>/g, ">");
334
  text = text.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
335
  text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\*(.*?)\*/g, '<em>$1</em>');
@@ -340,19 +264,19 @@
340
  chatbox.scrollTo({ top: chatbox.scrollHeight, behavior: animate ? 'smooth' : 'auto' });
341
  }
342
 
343
- function setLoading(isLoading, message = "AI ์‘๋‹ต ์ƒ์„ฑ ์ค‘...") {
344
  loadingIndicator.style.display = isLoading ? 'flex' : 'none';
345
  loadingIndicator.innerHTML = isLoading ? `<span class="spinner"></span> ${message}` : '';
346
  const disableButtons = isLoading || !generator;
347
  userInput.disabled = disableButtons;
348
- sendButton.disabled = disableButtons || userInput.value.trim() === ''; // ์ž…๋ ฅ ์—†์œผ๋ฉด ๋น„ํ™œ์„ฑํ™”
349
  speechButton.disabled = disableButtons || isListening || !recognition;
350
  toggleSpeakerButton.disabled = disableButtons || !synthesis;
351
  }
352
 
353
  function updateSpeakerButtonUI() {
354
  toggleSpeakerButton.textContent = botState.botSettings.useSpeechOutput ? '๐Ÿ”Š' : '๐Ÿ”‡';
355
- toggleSpeakerButton.title = botState.botSettings.useSpeechOutput ? 'AI ์Œ์„ฑ ๋“ฃ๊ธฐ ๋„๊ธฐ' : 'AI ์Œ์„ฑ ๋“ฃ๊ธฐ ์ผœ๊ธฐ';
356
  toggleSpeakerButton.classList.toggle('muted', !botState.botSettings.useSpeechOutput);
357
  }
358
 
@@ -366,35 +290,34 @@
366
  userInput.addEventListener('input', () => {
367
  userInput.style.height = 'auto';
368
  userInput.style.height = userInput.scrollHeight + 'px';
369
- // ์ž…๋ ฅ ๋‚ด์šฉ ์žˆ์œผ๋ฉด ์ „์†ก ๋ฒ„ํŠผ ํ™œ์„ฑํ™” (๋ชจ๋ธ ๋กœ๋”ฉ ์™„๋ฃŒ && ๋กœ๋”ฉ ์ค‘ ์•„๋‹ ๋•Œ)
370
  sendButton.disabled = userInput.value.trim() === '' || !generator || loadingIndicator.style.display === 'flex';
371
  });
372
  }
373
 
374
  // --- Model & AI Logic ---
375
  async function initializeModel() {
376
- setLoading(true, "AI ๋ชจ๋ธ ์—ฐ๊ฒฐ ์ค‘...");
377
- displayMessage('system', `[์•Œ๋ฆผ] ${MODEL_NAME} ๋ชจ๋ธ ๋กœ๋”ฉ ์‹œ์ž‘... ์ž ์‹œ ๊ธฐ๋‹ค๋ ค ์ฃผ์„ธ์š”.`, false);
 
 
378
  try {
379
- // *** dtype ์˜ต์…˜ ์ œ๊ฑฐ ***
380
  generator = await pipeline(TASK, MODEL_NAME, {
381
- // dtype: QUANTIZATION, // ์ œ๊ฑฐ๋จ!
382
  progress_callback: (progress) => {
383
- const msg = `[๋กœ๋”ฉ: ${progress.status}] ${progress.file ? progress.file.split('/').pop() : ''} (${Math.round(progress.progress || 0)}%)`;
384
  setLoading(true, msg);
385
  },
386
  });
387
- displayMessage('system', "[์•Œ๋ฆผ] ์•ˆ๋…•ํ•˜์„ธ์š”! ๋ฌด์—‡์„ ๋„์™€๋“œ๋ฆด๊นŒ์š”? ๐Ÿ˜Š", false);
388
 
389
  } catch (error) {
390
  console.error("Model loading failed:", error);
391
- displayMessage('system', `[์˜ค๋ฅ˜] AI ๋ชจ๋ธ ๋กœ๋”ฉ ์‹คํŒจ: ${error.message}. ์ƒˆ๋กœ๊ณ ์นจํ•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ๋ชจ๋ธ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.`, false);
392
  setLoading(false);
393
- loadingIndicator.textContent = '๋ชจ๋ธ ๋กœ๋”ฉ ์‹คํŒจ';
394
  return;
395
  } finally {
396
- setLoading(false); // ๋กœ๋”ฉ UI ์ •๋ฆฌ
397
- // ๋ชจ๋ธ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ์—๋„ ์ž…๋ ฅ์ฐฝ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
398
  userInput.disabled = !generator;
399
  sendButton.disabled = userInput.value.trim() === '' || !generator;
400
  speechButton.disabled = !generator || !recognition;
@@ -403,70 +326,60 @@
403
  }
404
  }
405
 
406
- // ์ผ๋ฐ˜์ ์ธ ์ฑ—๋ด‡ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ (Gemma 3 ํ˜•์‹)
407
  function buildPrompt() {
408
- const historyLimit = 6; // ์ปจํ…์ŠคํŠธ ๊ธธ์ด ์กฐ์ • ๊ฐ€๋Šฅ
409
  const recentHistory = conversationHistory.slice(-historyLimit);
410
 
 
411
  let prompt = "<start_of_turn>system\n";
412
- prompt += `๋‹น์‹ ์€ '${botState.botName}'์ด๋ผ๋Š” ์ด๋ฆ„์˜ ๋„์›€์ด ๋˜๋Š” AI ์–ด์‹œ์Šคํ„ดํŠธ์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์— ์นœ์ ˆํ•˜๊ณ  ๋ช…ํ™•ํ•˜๊ฒŒ ํ•œ๊ตญ์–ด๋กœ ๋‹ต๋ณ€ํ•˜์„ธ์š”.\n<end_of_turn>\n`;
413
 
414
  recentHistory.forEach(msg => {
415
  const role = msg.sender === 'user' ? 'user' : 'model';
416
  prompt += `<start_of_turn>${role}\n${msg.text}\n<end_of_turn>\n`;
417
  });
418
 
419
- prompt += "<start_of_turn>model\n"; // ๋ชจ๋ธ ์‘๋‹ต ์‹œ์ž‘
420
 
421
- console.log("Generated Prompt:", prompt);
422
  return prompt;
423
  }
424
 
425
- // ๋ชจ๋ธ ์‘๋‹ต ํ›„์ฒ˜๋ฆฌ (๊ฐ„๋‹จํ™”)
426
  function cleanupResponse(responseText, prompt) {
427
  let cleaned = responseText;
 
 
428
 
429
- // ํ”„๋กฌํ”„ํŠธ ๋ถ€๋ถ„ ์ œ๊ฑฐ (์ •ํ™•ํžˆ ์ œ๊ฑฐ)
430
- if (cleaned.startsWith(prompt)) {
431
- cleaned = cleaned.substring(prompt.length);
432
- } else {
433
- // ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ, ๋ชจ๋ธ์ด ์ถ”๊ฐ€ํ•œ ์‹œ์ž‘ ํ† ํฐ ๋“ฑ์„ ์ œ๊ฑฐ
434
- cleaned = cleaned.replace(/^model\n?/, '').trim();
435
- }
436
-
437
- // ์ข…๋ฃŒ ํ† ํฐ ๋ฐ ๋ถˆํ•„์š”ํ•œ ํ† ํฐ ์ œ๊ฑฐ
438
  cleaned = cleaned.replace(/<end_of_turn>/g, '').trim();
439
  cleaned = cleaned.replace(/<start_of_turn>/g, '').trim();
440
  cleaned = cleaned.replace(/^['"]/, '').replace(/['"]$/, '');
441
 
442
- // ๋งค์šฐ ์งง๊ฑฐ๋‚˜ ์˜๋ฏธ ์—†๋Š” ์‘๋‹ต ํ•„ํ„ฐ๋ง
443
  if (!cleaned || cleaned.length < 2) {
444
- console.warn("Generated reply seems empty or too short:", cleaned);
445
- const fallbacks = [ "์ฃ„์†กํ•ด์š”, ์ž˜ ์ดํ•ดํ•˜์ง€ ๋ชปํ–ˆ์–ด์š”. ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ์งˆ๋ฌธํ•ด์ฃผ์‹œ๊ฒ ์–ด์š”?", "์Œ... ๋‹ค์‹œ ํ•œ๋ฒˆ ๋ง์”€ํ•ด์ฃผ์‹œ๊ฒ ์–ด์š”?", "์ œ๊ฐ€ ๋„์™€๋“œ๋ฆด ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์ด ์žˆ์„๊นŒ์š”?" ];
446
  return fallbacks[Math.floor(Math.random() * fallbacks.length)];
447
  }
448
-
449
  return cleaned;
450
  }
451
 
452
  // --- Main Interaction Logic ---
453
  async function handleUserMessage() {
454
  const userText = userInput.value.trim();
455
- // ๋กœ๋”ฉ ์ค‘์ด๊ฑฐ๋‚˜, ์ž…๋ ฅ์ด ์—†๊ฑฐ๋‚˜, ๋ชจ๋ธ ์ค€๋น„ ์•ˆ๋์œผ๋ฉด ์ฒ˜๋ฆฌ ์•ˆํ•จ
456
  if (!userText || !generator || loadingIndicator.style.display === 'flex') return;
457
 
458
- userInput.value = ''; userInput.style.height = 'auto'; // ์ž…๋ ฅ์ฐฝ ์ •๋ฆฌ
459
- sendButton.disabled = true; // ์ „์†ก ํ›„ ๋น„ํ™œ์„ฑํ™”
460
  displayMessage('user', userText);
461
  conversationHistory.push({ sender: 'user', text: userText });
462
- // ์ด๋ฆ„/์• ์นญ ์„ค์ • ๋กœ์ง ์ œ๊ฑฐ
463
 
464
  setLoading(true);
465
- const prompt = buildPrompt();
466
 
467
  try {
468
  const outputs = await generator(prompt, {
469
- max_new_tokens: 300, // ์‘๋‹ต ๊ธธ์ด ์กฐ์ •
470
  temperature: 0.7,
471
  repetition_penalty: 1.1,
472
  top_k: 50,
@@ -475,49 +388,49 @@
475
  });
476
 
477
  const rawResponse = Array.isArray(outputs) ? outputs[0].generated_text : outputs.generated_text;
478
- const replyText = cleanupResponse(rawResponse, prompt); // ํ”„๋กฌํ”„ํŠธ ์ „๋‹ฌํ•˜์—ฌ ์ •ํ™•ํžˆ ์ œ๊ฑฐ
479
 
480
- console.log("Cleaned Gemma Output:", replyText);
481
 
482
  displayMessage('bot', replyText);
483
  conversationHistory.push({ sender: 'bot', text: replyText });
484
 
485
- if (botState.botSettings.useSpeechOutput && synthesis && targetVoice) { // targetVoice ์‚ฌ์šฉ
486
- speakText(replyText);
487
  }
488
 
489
  saveState();
490
 
491
  } catch (error) {
492
  console.error("AI response generation error:", error);
493
- displayMessage('system', `[์˜ค๋ฅ˜] ์‘๋‹ต ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`);
494
- const errorReply = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์‘๋‹ต์„ ์ƒ์„ฑํ•˜๋Š” ์ค‘์— ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.";
495
  displayMessage('bot', errorReply);
496
  conversationHistory.push({ sender: 'bot', text: errorReply });
497
  } finally {
498
- setLoading(false); // ๋กœ๋”ฉ ํ•ด์ œ
499
  userInput.focus();
500
- // ์ž…๋ ฅ์ฐฝ ๋‚ด์šฉ ์—†์œผ๋ฏ€๋กœ ์ „์†ก ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™” ์œ ์ง€๋จ
501
  }
502
  }
503
 
504
- // --- Speech API Functions ---
505
  function initializeSpeechAPI() {
506
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
507
  if (SpeechRecognition) {
508
  recognition = new SpeechRecognition();
509
- recognition.lang = 'ko-KR'; recognition.continuous = false; recognition.interimResults = false;
510
- recognition.onstart = () => { isListening = true; speechButton.disabled = true; speechButton.textContent = '๐Ÿ‘‚'; showSpeechStatus('๋ง์”€ํ•˜์„ธ์š”...'); };
 
511
  recognition.onresult = (event) => { userInput.value = event.results[0][0].transcript; userInput.dispatchEvent(new Event('input')); handleUserMessage(); };
512
- recognition.onerror = (event) => { console.error("Speech error:", event.error); showSpeechStatus(`์Œ์„ฑ ์ธ์‹ ์˜ค๋ฅ˜ (${event.error})`); setTimeout(() => showSpeechStatus(''), 3000); };
513
- recognition.onend = () => { isListening = false; speechButton.disabled = !generator; speechButton.textContent = '๐ŸŽค'; if (speechStatus.textContent === '๋ง์”€ํ•˜์„ธ์š”...') showSpeechStatus(''); };
514
- speechButton.disabled = false; // ์ดˆ๊ธฐ ํ™œ์„ฑํ™” (๋ชจ๋ธ ๋กœ๋”ฉ ์ „์ด๋ผ๋„)
515
  } else { console.warn("Speech Recognition not supported."); speechButton.style.display = 'none'; }
516
 
517
  if (!synthesis) { console.warn("Speech Synthesis not supported."); toggleSpeakerButton.style.display = 'none'; }
518
  else {
519
  toggleSpeakerButton.addEventListener('click', () => { botState.botSettings.useSpeechOutput = !botState.botSettings.useSpeechOutput; updateSpeakerButtonUI(); saveState(); if (!botState.botSettings.useSpeechOutput) synthesis.cancel(); });
520
- toggleSpeakerButton.disabled = false; // ์ดˆ๊ธฐ ํ™œ์„ฑํ™”
521
  }
522
  }
523
 
@@ -529,32 +442,31 @@
529
  } else { findAndSetVoice(voices); }
530
  }
531
 
532
- // ํŠน์ • ์ด๋ฆ„("Jenny") ๋Œ€์‹  ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ•œ๊ตญ์–ด ๋ชฉ์†Œ๋ฆฌ ์ฐพ๊ธฐ
533
  function findAndSetVoice(voices) {
534
- // 1์ˆœ์œ„: 'Microsoft Heami' (๋กœ๊ทธ์—์„œ ํ™•์ธ๋จ) ๋˜๋Š” 'Google' ํ•œ๊ตญ์–ด
535
- targetVoice = voices.find(v => v.lang === 'ko-KR' && /Microsoft Heami|Google/i.test(v.name));
536
- // 2์ˆœ์œ„: ๋‹ค๋ฅธ ํ•œ๊ตญ์–ด ๋ชฉ์†Œ๋ฆฌ
537
- if (!targetVoice) targetVoice = voices.find(v => v.lang === 'ko-KR');
538
- // 3์ˆœ์œ„: ์–ธ์–ด ์ฝ”๋“œ ์‹œ์ž‘์ด 'ko-' ์ธ ๋ชฉ์†Œ๋ฆฌ
539
- if (!targetVoice) targetVoice = voices.find(v => v.lang.startsWith('ko-'));
540
 
541
  if (targetVoice) {
542
- console.log("Using voice:", targetVoice.name, targetVoice.lang);
543
  } else {
544
- console.warn("No suitable Korean voice found. Speech output might use default voice.");
545
- displayMessage('system', "[์•Œ๋ฆผ] ํ•œ๊ตญ์–ด ์Œ์„ฑ์„ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์Œ์„ฑ์œผ๋กœ ์ถœ๋ ฅ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", false);
546
  }
547
  }
548
 
 
549
  function speakText(text) {
550
  if (!synthesis || !botState.botSettings.useSpeechOutput) return;
551
  synthesis.cancel();
552
  const utterance = new SpeechSynthesisUtterance(text);
553
- if (targetVoice) { // ์ฐพ์€ ๋ชฉ์†Œ๋ฆฌ ์‚ฌ์šฉ
554
  utterance.voice = targetVoice;
555
- utterance.lang = targetVoice.lang; // ํ•ด๋‹น ๋ชฉ์†Œ๋ฆฌ์˜ ์–ธ์–ด ์ฝ”๋“œ ์‚ฌ์šฉ
556
  } else {
557
- utterance.lang = 'ko-KR'; // ํ•œ๊ตญ์–ด ๋ชฉ์†Œ๋ฆฌ ๋ชป ์ฐพ์•˜์œผ๋ฉด ํ•œ๊ตญ์–ด ์ง€์ • ์‹œ๋„
558
  }
559
  utterance.rate = 1.0; utterance.pitch = 1.0;
560
  synthesis.speak(utterance);
@@ -565,11 +477,10 @@
565
  userInput.addEventListener('keypress', (e) => {
566
  if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleUserMessage(); }
567
  });
568
- // input ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋Š” setupInputAutosize ์•ˆ์— ํ†ตํ•ฉ๋จ
569
  speechButton.addEventListener('click', () => {
570
- if (recognition && !isListening && generator) { // ๋ชจ๋ธ ์ค€๋น„ ์™„๋ฃŒ ์‹œ์—๋งŒ ์ž‘๋™
571
  try { recognition.start(); }
572
- catch (error) { console.error("Rec start fail:", error); showSpeechStatus(`์Œ์„ฑ ์ธ์‹ ์‹œ์ž‘ ์‹คํŒจ`); setTimeout(() => showSpeechStatus(''), 2000); isListening = false; speechButton.disabled = !generator; speechButton.textContent = '๐ŸŽค';}
573
  }
574
  });
575
 
 
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; }
 
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); }
 
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">
159
  <div id="messages">
160
+ <!-- Chat messages will appear here -->
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
  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;
181
  env.useBrowserCache = true;
182
  env.backends.onnx.executionProviders = ['webgpu', 'wasm'];
 
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
  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 ---
 
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); }
 
254
  messageDiv.classList.add(messageClass);
255
  if (!animate) messageDiv.style.animation = 'none';
256
 
 
257
  text = text.replace(/</g, "<").replace(/>/g, ">");
258
  text = text.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
259
  text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\*(.*?)\*/g, '<em>$1</em>');
 
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;
271
  userInput.disabled = disableButtons;
272
+ sendButton.disabled = disableButtons || userInput.value.trim() === '';
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
 
 
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;
319
  } finally {
320
+ setLoading(false);
 
321
  userInput.disabled = !generator;
322
  sendButton.disabled = userInput.value.trim() === '' || !generator;
323
  speechButton.disabled = !generator || !recognition;
 
326
  }
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,
 
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
 
 
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);
 
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