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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +255 -152
index.html CHANGED
@@ -3,13 +3,15 @@
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;
@@ -19,27 +21,43 @@
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
  }
@@ -48,20 +66,30 @@
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; }
@@ -73,17 +101,29 @@
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">
81
  <div id="messages">
82
  <!-- Chat messages will appear here -->
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>
@@ -93,10 +133,10 @@
93
  <script type="module">
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;
@@ -104,191 +144,254 @@
104
  env.backends.onnx.executionProviders = ['webgpu', 'wasm'];
105
  console.log('Using Execution Providers:', env.backends.onnx.executionProviders);
106
 
107
- // --- DOM Elements ---
108
  const chatbox = document.getElementById('messages');
109
  const userInput = document.getElementById('userInput');
110
  const sendButton = document.getElementById('sendButton');
111
- const loadingIndicator = document.getElementById('loading');
112
  const chatbotNameElement = document.getElementById('chatbot-name');
113
  const speechButton = document.getElementById('speechButton');
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 ---
134
- window.addEventListener('load', async () => {
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
-
168
- text = text.replace(/</g, "<").replace(/>/g, ">");
169
- text = text.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
170
- text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\*(.*?)\*/g, '<em>$1</em>');
171
- text = text.replace(/\n/g, '<br>');
172
-
173
- messageDiv.innerHTML = text;
174
- chatbox.appendChild(messageDiv);
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;
182
- userInput.disabled = disableButtons;
183
- sendButton.disabled = disableButtons || userInput.value.trim() === '';
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;
224
- } finally {
225
- setLoading(false);
226
- userInput.disabled = !generator;
227
- sendButton.disabled = userInput.value.trim() === '' || !generator;
228
- speechButton.disabled = !generator || !recognition;
229
- toggleSpeakerButton.disabled = !generator || !synthesis;
230
- if (generator) { userInput.focus(); }
231
  }
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>
 
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 (Selectable Model)</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
+ --error-color: #dc3545; --error-bg: #f8d7da; --error-border: #f5c6cb;
14
+ --success-color: #28a745; --success-bg: #d4edda; --success-border: #c3e6cb;
15
  --border-color: #dee2e6; --input-bg: #ffffff; --input-border: #ced4da;
16
  --button-bg: var(--primary-color); --button-hover-bg: #0056b3; --button-disabled-bg: #adb5bd;
17
  --scrollbar-thumb: var(--primary-color); --scrollbar-track: #e9ecef;
 
21
  * { box-sizing: border-box; margin: 0; padding: 0; }
22
  html { height: 100%; }
23
  body {
24
+ font-family: 'Roboto', sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; /* 상단 정렬 */
25
+ min-height: 100vh; background-color: var(--bg-color); color: var(--text-color); padding: 10px; overscroll-behavior: none;
26
  }
27
+ #control-panel { /* 추가됨 */
28
+ background: var(--header-bg); padding: 15px; border-radius: 8px; margin-bottom: 10px;
29
+ box-shadow: var(--header-shadow); width: 100%; max-width: 600px; border: 1px solid var(--border-color);
30
+ }
31
+ #control-panel h2 { font-size: 1.1em; margin-bottom: 10px; color: var(--primary-color); font-weight: 500; }
32
+ .model-options label { margin-right: 15px; cursor: pointer; }
33
+ .model-options input[type="radio"] { margin-right: 5px; }
34
+ #loadModelButton {
35
+ margin-top: 10px; padding: 8px 15px; font-size: 0.95em; background-color: var(--secondary-color);
36
+ color: white; border: none; border-radius: 5px; cursor: pointer; transition: background-color 0.2s;
37
+ }
38
+ #loadModelButton:hover:not(:disabled) { background-color: #5a6268; }
39
+ #loadModelButton:disabled { background-color: var(--button-disabled-bg); cursor: not-allowed; }
40
+ #model-status { margin-top: 10px; font-size: 0.9em; padding: 8px; border-radius: 4px; text-align: center; }
41
+ #model-status.loading { background-color: #fff3cd; border: 1px solid #ffeeba; color: #856404; }
42
+ #model-status.success { background-color: var(--success-bg); border: 1px solid var(--success-border); color: #155724; }
43
+ #model-status.error { background-color: var(--error-bg); border: 1px solid var(--error-border); color: #721c24; }
44
+
45
  #chat-container {
46
+ width: 100%; max-width: 600px; height: 75vh; /* 높이 조정 */ max-height: 700px;
47
  background-color: #ffffff; border-radius: 12px; box-shadow: var(--container-shadow);
48
  display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--border-color);
49
  }
50
+ h1 { /* Header 스타일 유지 */
51
  text-align: center; color: var(--primary-color); padding: 15px; background-color: var(--header-bg);
52
  border-bottom: 1px solid var(--border-color); font-size: 1.2em; font-weight: 500; flex-shrink: 0;
53
  box-shadow: var(--header-shadow); position: relative; z-index: 10;
54
  }
55
+ #chatbox { /* 채팅창 스타일 유지 */
56
  flex-grow: 1; overflow-y: auto; padding: 15px; display: flex; flex-direction: column; gap: 12px;
57
  scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); background-color: var(--bg-color);
58
  }
59
+ #chatbox::-webkit-scrollbar { width: 6px; } #chatbox::-webkit-scrollbar-track { background: var(--scrollbar-track); border-radius: 3px; } #chatbox::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 3px; }
60
+ #messages div { /* 메시지 버블 스타일 유지 */
 
 
61
  padding: 10px 15px; border-radius: 16px; max-width: 85%; word-wrap: break-word; line-height: 1.5;
62
  font-size: 1em; box-shadow: 0 1px 2px rgba(0,0,0,0.05); position: relative; animation: fadeIn 0.25s ease-out;
63
  }
 
66
  .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; }
67
  .bot-message a { color: var(--primary-color); text-decoration: none; } .bot-message a:hover { text-decoration: underline; }
68
  .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; }
69
+ .error-message { /* 채팅 에러 메시지 (모델 로딩 실패 사용 안함) */
70
+ color: var(--error-color); font-weight: 500; background-color: var(--error-bg); border: 1px solid var(--error-border);
71
+ padding: 10px 15px; border-radius: 8px; align-self: stretch; text-align: left;
72
+ }
73
+ .status-indicator { /* 여기서는 사용 안함, #model-status 대체 */
74
+ display: none;
75
+ }
76
+ #input-area { /* 입력 영역 스타일 유지 */
77
+ 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;
78
+ }
79
  #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; }
80
  #userInput:focus { border-color: var(--primary-color); }
81
+ .control-button { /* 버튼 스타일 유지 */
82
+ 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);
83
+ }
84
  .control-button:hover:not(:disabled) { background-color: var(--button-hover-bg); transform: translateY(-1px); }
85
  .control-button:active:not(:disabled) { transform: scale(0.95); }
86
  .control-button:disabled { background-color: var(--button-disabled-bg); cursor: not-allowed; transform: none; box-shadow: none; }
87
  #toggleSpeakerButton.muted { background-color: #aaa; }
88
+ @media (max-width: 600px) { /* 반응형 유지 */
89
+ body { padding: 5px; justify-content: flex-start; }
90
+ #control-panel { margin-bottom: 5px; padding: 12px; }
91
+ .model-options label { display: block; margin-bottom: 5px; } /* 모바일에서 세로 배치 */
92
+ #chat-container { width: 100%; height: auto; flex-grow: 1; border-radius: 12px; max-height: none; margin-bottom: 5px; }
93
  h1 { font-size: 1.1em; padding: 12px; } #chatbox { padding: 12px 8px; gap: 10px; }
94
  #messages div { max-width: 90%; font-size: 0.95em; padding: 9px 14px;}
95
  #input-area { padding: 8px; gap: 5px; } #userInput { padding: 9px 14px; min-height: 40px; }
 
101
  </script>
102
  </head>
103
  <body>
104
+ <div id="control-panel">
105
+ <h2>Select AI Model</h2>
106
+ <div class="model-options">
107
+ <label>
108
+ <input type="radio" name="modelSelection" value="Xenova/gemma-2b-it" checked> Gemma 2B (Recommended, Stable)
109
+ </label>
110
+ <label>
111
+ <input type="radio" name="modelSelection" value="onnx-community/gemma-3-1b-it-ONNX-GQA"> Gemma 3 1B (Experimental, Likely Fails)
112
+ </label>
113
+ </div>
114
+ <button id="loadModelButton">Load Selected Model</button>
115
+ <div id="model-status">No model loaded. Select a model and click load.</div>
116
+ </div>
117
+
118
  <div id="chat-container">
119
  <h1 id="chatbot-name">AI Assistant</h1>
 
 
120
  <div id="chatbox">
121
  <div id="messages">
122
  <!-- Chat messages will appear here -->
123
  </div>
124
  </div>
125
  <div id="input-area">
126
+ <textarea id="userInput" placeholder="Please load a model first..." rows="1" disabled></textarea>
127
  <button id="speechButton" class="control-button" title="Speak message" disabled>🎤</button>
128
  <button id="toggleSpeakerButton" class="control-button" title="Toggle AI speech output" disabled>🔊</button>
129
  <button id="sendButton" class="control-button" title="Send message" disabled>➤</button>
 
133
  <script type="module">
134
  import { pipeline, env } from '@xenova/transformers';
135
 
136
+ // Configuration is now more dynamic
 
137
  const TASK = 'text-generation';
138
+ const COMPATIBLE_MODEL = 'Xenova/gemma-2b-it'; // Known working model
139
+ const EXPERIMENTAL_MODEL = 'onnx-community/gemma-3-1b-it-ONNX-GQA'; // The problematic one
140
 
141
  // ONNX Runtime & WebGPU config
142
  env.allowLocalModels = false;
 
144
  env.backends.onnx.executionProviders = ['webgpu', 'wasm'];
145
  console.log('Using Execution Providers:', env.backends.onnx.executionProviders);
146
 
147
+ // DOM Elements
148
  const chatbox = document.getElementById('messages');
149
  const userInput = document.getElementById('userInput');
150
  const sendButton = document.getElementById('sendButton');
 
151
  const chatbotNameElement = document.getElementById('chatbot-name');
152
  const speechButton = document.getElementById('speechButton');
153
  const toggleSpeakerButton = document.getElementById('toggleSpeakerButton');
154
+ const modelStatus = document.getElementById('model-status'); // Status display
155
+ const loadModelButton = document.getElementById('loadModelButton'); // Load button
156
+ const modelSelectionRadios = document.querySelectorAll('input[name="modelSelection"]');
157
 
158
+ // State Management
159
+ let generator = null; // Loaded pipeline
160
+ let currentModelName = null; // Name of the currently loaded model
161
+ let isLoadingModel = false; // Flag to prevent multiple loads
162
  let conversationHistory = [];
163
+ let botState = { botName: "AI Assistant", userName: "User", botSettings: { useSpeechOutput: true } };
164
+ const stateKey = 'selectableBotState_en_v1'; // New key
165
+ const historyKey = 'selectableBotHistory_en_v1';
166
+
167
+ // Web Speech API
 
 
 
168
  let recognition = null;
169
  let synthesis = window.speechSynthesis;
170
  let targetVoice = null;
171
  let isListening = false;
172
 
173
  // --- Initialization ---
174
+ window.addEventListener('load', () => {
175
+ loadState(); // Load bot settings and potentially history
176
  chatbotNameElement.textContent = botState.botName;
177
  updateSpeakerButtonUI();
178
  initializeSpeechAPI();
 
179
  setupInputAutosize();
180
+ updateChatUIState(); // Initial UI state (disabled)
181
  setTimeout(loadVoices, 500);
182
+
183
+ // Add event listener for the load button
184
+ loadModelButton.addEventListener('click', handleLoadModelClick);
185
+
186
+ // Restore potentially saved history (display requires chatbox ready)
187
+ if (conversationHistory.length > 0) displayHistory();
188
  });
189
 
190
  // --- State Persistence ---
191
+ function loadState() {
192
+ const savedState = localStorage.getItem(stateKey);
193
+ if (savedState) { try { const loaded = JSON.parse(savedState); botState = { ...botState, ...loaded, botSettings: { ...botState.botSettings, ...(loaded.botSettings || {}) } }; } catch(e) { console.error("State parse error", e); } }
194
+ const savedHistory = localStorage.getItem(historyKey);
195
+ // Load history but don't display yet, wait for chatbox element
196
+ if (savedHistory) { try { conversationHistory = JSON.parse(savedHistory); } catch(e) { console.error("History parse error", e); conversationHistory = []; } }
197
+ }
198
+ function saveState() {
199
+ localStorage.setItem(stateKey, JSON.stringify(botState));
200
+ localStorage.setItem(historyKey, JSON.stringify(conversationHistory));
201
+ }
202
+ function displayHistory() {
203
+ chatbox.innerHTML = '';
204
+ conversationHistory.forEach(msg => displayMessage(msg.sender, msg.text, false));
205
+ }
206
 
207
  // --- UI Update Functions ---
208
+ function displayMessage(sender, text, animate = true, isError = false) {
209
+ const messageDiv = document.createElement('div');
210
+ let messageClass = sender === 'user' ? 'user-message' : sender === 'bot' ? 'bot-message' : 'system-message';
211
+ if (isError) messageClass = 'error-message'; // Use error class if needed (e.g., for response errors)
212
+ messageDiv.classList.add(messageClass);
213
+ if (!animate) messageDiv.style.animation = 'none';
214
+ text = text.replace(/</g, "<").replace(/>/g, ">");
215
+ text = text.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
216
+ text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\*(.*?)\*/g, '<em>$1</em>');
217
+ text = text.replace(/\n/g, '<br>');
218
+ messageDiv.innerHTML = text;
219
+ chatbox.appendChild(messageDiv);
220
+ chatbox.scrollTo({ top: chatbox.scrollHeight, behavior: animate ? 'smooth' : 'auto' });
221
+ }
 
 
 
222
 
223
+ // Updates the model status display area
224
+ function updateModelStatus(message, type = 'info') { // type: 'info', 'loading', 'success', 'error'
225
+ modelStatus.textContent = message;
226
+ modelStatus.className = 'model-status'; // Reset classes
227
+ if (type) {
228
+ modelStatus.classList.add(type);
229
+ }
230
+ console.log(`Model Status (${type}): ${message}`); // Also log status
231
+ }
232
+
233
+ // Updates the enabled/disabled state of chat UI elements
234
+ function updateChatUIState() {
235
+ const modelLoaded = generator !== null;
236
+ userInput.disabled = !modelLoaded || isLoadingModel;
237
+ sendButton.disabled = !modelLoaded || isLoadingModel || userInput.value.trim() === '';
238
+ speechButton.disabled = !modelLoaded || isLoadingModel || isListening || !recognition;
239
+ toggleSpeakerButton.disabled = !modelLoaded || isLoadingModel || !synthesis;
240
+ loadModelButton.disabled = isLoadingModel || modelLoaded; // Disable load if loading or already loaded
241
+
242
+ if (modelLoaded) {
243
+ userInput.placeholder = "How can I help you today?";
244
+ } else if (isLoadingModel) {
245
+ userInput.placeholder = "Model loading...";
246
+ } else {
247
+ userInput.placeholder = "Please load a model first...";
248
+ }
249
  }
250
 
251
+
252
+ function updateSpeakerButtonUI() {
253
+ toggleSpeakerButton.textContent = botState.botSettings.useSpeechOutput ? '🔊' : '🔇';
254
+ toggleSpeakerButton.title = botState.botSettings.useSpeechOutput ? 'Turn off AI speech' : 'Turn on AI speech';
255
+ toggleSpeakerButton.classList.toggle('muted', !botState.botSettings.useSpeechOutput);
256
+ }
257
+ function showSpeechStatus(message) {
258
+ // Simplified: log to console, or could update a small icon later
259
+ console.log("Speech Status:", message);
260
+ // Optionally update a dedicated small status area if needed
261
+ }
262
+ function setupInputAutosize() {
263
+ userInput.addEventListener('input', () => {
264
+ userInput.style.height = 'auto';
265
+ userInput.style.height = userInput.scrollHeight + 'px';
266
+ updateChatUIState(); // Update button state based on input
267
+ });
268
+ }
269
+
270
  // --- Model & AI Logic ---
271
+ async function handleLoadModelClick() {
272
+ if (isLoadingModel || generator) return; // Prevent multiple loads
273
+
274
+ const selectedModel = document.querySelector('input[name="modelSelection"]:checked').value;
275
+ if (!selectedModel) {
276
+ updateModelStatus("Please select a model first.", "error");
277
+ return;
278
+ }
279
+
280
+ isLoadingModel = true;
281
+ currentModelName = null; // Reset current model name
282
+ updateChatUIState();
283
+ await initializeModel(selectedModel); // Call initialize with selected model
284
+ isLoadingModel = false;
285
+ updateChatUIState(); // Update UI based on success/failure
286
+ }
287
+
288
+
289
+ async function initializeModel(modelId) {
290
+ updateModelStatus(`Loading ${modelId}... This may take time.`, 'loading');
291
+ let pipelineOptions = {};
292
+
293
+ // *** IMPORTANT: Only add dtype for the experimental model ***
294
+ if (modelId === EXPERIMENTAL_MODEL) {
295
+ pipelineOptions.dtype = 'q4'; // Use q4 only for Gemma 3 1B as per its (potentially faulty) example
296
+ updateModelStatus(`Loading ${modelId} with Q4 quantization... (Experimental, might fail)`, 'loading');
297
+ }
298
 
299
  try {
300
+ pipelineOptions.progress_callback = (progress) => {
301
+ const msg = `[Loading ${modelId}: ${progress.status}] ${progress.file ? progress.file.split('/').pop() : ''} (${Math.round(progress.progress || 0)}%)`;
302
+ updateModelStatus(msg, 'loading');
303
+ };
304
+
305
+ // Attempt to load the pipeline
306
+ generator = await pipeline(TASK, modelId, pipelineOptions);
307
+
308
+ currentModelName = modelId; // Store the loaded model name
309
+ updateModelStatus(`${currentModelName} loaded successfully!`, 'success');
310
+ displayMessage('system', `[NOTICE] ${currentModelName} is ready.`, false);
311
 
312
  } catch (error) {
313
+ console.error(`Model loading failed for ${modelId}:`, error);
314
+ updateModelStatus(`Failed to load ${modelId}: ${error.message}`, 'error');
315
+ displayMessage('system', `[ERROR] Could not load ${modelId}. It might be incompatible. Try the other model.`, true, true); // Show error in chat too
316
+ generator = null; // Ensure generator is null on failure
317
+ currentModelName = null;
 
 
 
 
 
 
 
 
 
318
  }
319
  }
320
 
321
+ function buildPrompt() {
322
+ const historyLimit = (currentModelName === EXPERIMENTAL_MODEL) ? 5 : 6; // Shorter context for 1B?
323
+ const recentHistory = conversationHistory.slice(-historyLimit);
324
  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";
325
  recentHistory.forEach(msg => { const role = msg.sender === 'user' ? 'user' : 'model'; prompt += `<start_of_turn>${role}\n${msg.text}\n<end_of_turn>\n`; });
326
  prompt += "<start_of_turn>model\n";
327
+ console.log("Generated Prompt:", prompt); return prompt;
328
  }
329
 
330
+ function cleanupResponse(responseText, prompt) {
 
331
  let cleaned = responseText; if (cleaned.startsWith(prompt)) { cleaned = cleaned.substring(prompt.length); } else { cleaned = cleaned.replace(/^model\n?/, '').trim(); }
332
  cleaned = cleaned.replace(/<end_of_turn>/g, '').trim(); cleaned = cleaned.replace(/<start_of_turn>/g, '').trim(); cleaned = cleaned.replace(/^['"]/, '').replace(/['"]$/, '');
333
+ if (!cleaned || cleaned.length < 2) { console.warn("Generated reply empty/short:", cleaned); const fallbacks = [ "Sorry, I didn't quite understand.", "Could you please rephrase that?", "I'm not sure how to respond." ]; return fallbacks[Math.floor(Math.random() * fallbacks.length)]; }
334
  return cleaned;
335
  }
336
 
337
  // --- Main Interaction Logic ---
338
+ async function handleUserMessage() {
339
+ const userText = userInput.value.trim();
340
+ // Check if model is loaded AND not currently loading
341
+ if (!userText || !generator || isLoadingModel) return;
342
+
343
+ userInput.value = ''; userInput.style.height = 'auto';
344
+ updateChatUIState(); // Disable send button immediately
345
+ displayMessage('user', userText);
346
+ conversationHistory.push({ sender: 'user', text: userText });
347
+
348
+ updateModelStatus("AI thinking...", "loading"); // Use model status for thinking indication
349
+
350
+ const prompt = buildPrompt();
351
  try {
352
  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 });
353
+ const rawResponse = Array.isArray(outputs) ? outputs[0].generated_text : outputs.generated_text;
354
+ const replyText = cleanupResponse(rawResponse, prompt);
355
+
356
+ displayMessage('bot', replyText);
357
+ conversationHistory.push({ sender: 'bot', text: replyText });
358
+ if (botState.botSettings.useSpeechOutput && synthesis && targetVoice) { speakText(replyText); }
359
+ saveState();
360
  } catch (error) {
361
+ console.error("AI response generation error:", error);
362
+ displayMessage('system', `[ERROR] Failed to generate response: ${error.message}`, true, true);
363
+ const errorReply = "Sorry, I encountered an error generating the response.";
364
+ displayMessage('bot', errorReply);
365
+ conversationHistory.push({ sender: 'bot', text: errorReply });
366
+ } finally {
367
+ updateModelStatus(`${currentModelName} ready.`, "success"); // Reset status after thinking
368
+ updateChatUIState(); // Re-enable/check UI state
369
+ userInput.focus();
370
+ }
371
  }
372
 
373
  // --- Speech API Functions (English) ---
374
+ function initializeSpeechAPI() {
375
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
376
+ if (SpeechRecognition) { recognition = new SpeechRecognition(); recognition.lang = 'en-US'; recognition.continuous = false; recognition.interimResults = false; recognition.onstart = () => { isListening = true; updateChatUIState(); console.log('Listening...'); /* Update UI via state */ }; 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); updateModelStatus(`Speech recognition error (${event.error})`, 'error'); setTimeout(() => updateModelStatus(generator ? `${currentModelName} ready.` : 'No model loaded.', generator ? 'success' : 'info'), 3000); }; recognition.onend = () => { isListening = false; updateChatUIState(); console.log('Stopped listening.'); }; } else { console.warn("Speech Recognition not supported."); }
377
+ if (!synthesis) { console.warn("Speech Synthesis not supported."); } else { toggleSpeakerButton.addEventListener('click', () => { botState.botSettings.useSpeechOutput = !botState.botSettings.useSpeechOutput; updateSpeakerButtonUI(); saveState(); if (!botState.botSettings.useSpeechOutput) synthesis.cancel(); }); }
378
+ updateChatUIState(); // Initial state update
379
+ }
380
+ function loadVoices() {
381
+ if (!synthesis) return; let voices = synthesis.getVoices(); if (voices.length === 0) { synthesis.onvoiceschanged = () => { voices = synthesis.getVoices(); findAndSetVoice(voices); }; } else { findAndSetVoice(voices); }
382
+ }
383
+ function findAndSetVoice(voices) {
384
+ targetVoice = voices.find(v => v.lang === 'en-US') || voices.find(v => v.lang.startsWith('en-'));
385
+ if (targetVoice) { console.log("Using English voice:", targetVoice.name, targetVoice.lang); } else { console.warn("No suitable English voice found."); }
386
+ }
387
+ function speakText(text) {
388
+ if (!synthesis || !botState.botSettings.useSpeechOutput || !targetVoice) return; synthesis.cancel(); const utterance = new SpeechSynthesisUtterance(text); utterance.voice = targetVoice; utterance.lang = targetVoice.lang; utterance.rate = 1.0; utterance.pitch = 1.0; synthesis.speak(utterance);
389
  }
 
 
 
390
 
391
  // --- Event Listeners ---
392
  sendButton.addEventListener('click', handleUserMessage);
393
  userInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleUserMessage(); } });
394
+ speechButton.addEventListener('click', () => { if (recognition && !isListening && generator && !isLoadingModel) { try { recognition.start(); } catch (error) { console.error("Rec start fail:", error); updateModelStatus(`Failed to start recognition`, 'error'); setTimeout(() => updateModelStatus(generator ? `${currentModelName} ready.` : 'No model loaded.', generator ? 'success' : 'info'), 2000); isListening = false; updateChatUIState(); } } });
395
 
396
  </script>
397
  </body>