Spaces:
Running
Running
Update index.html
Browse files- index.html +145 -234
index.html
CHANGED
@@ -1,93 +1,68 @@
|
|
1 |
<!DOCTYPE html>
|
2 |
-
<html lang="
|
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
|
7 |
<style>
|
8 |
-
/* Google Fonts */
|
9 |
-
@import url('https://fonts.googleapis.com/css2?family=
|
10 |
|
11 |
-
/* CSS Variables
|
12 |
:root {
|
13 |
-
--primary-color: #
|
14 |
-
--secondary-color: #
|
15 |
-
--text-color: #
|
16 |
-
--bg-color: #f8f9fa;
|
17 |
-
--user-msg-bg: #
|
18 |
-
--user-msg-text: #
|
19 |
--bot-msg-bg: #ffffff;
|
20 |
-
--bot-msg-border: #
|
21 |
-
--system-msg-color: #
|
22 |
-
--border-color: #
|
23 |
--input-bg: #ffffff;
|
24 |
--input-border: #ced4da;
|
25 |
--button-bg: var(--primary-color);
|
26 |
-
--button-hover-bg: #
|
27 |
--button-disabled-bg: #adb5bd;
|
28 |
--scrollbar-thumb: var(--primary-color);
|
29 |
-
--scrollbar-track: #
|
30 |
--header-bg: #ffffff;
|
31 |
-
--header-shadow: 0 1px
|
32 |
-
--container-shadow: 0
|
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: '
|
40 |
-
display: flex;
|
41 |
-
|
42 |
-
|
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 |
-
|
55 |
-
|
56 |
-
|
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(--
|
70 |
-
|
71 |
-
|
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 |
-
|
84 |
-
|
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:
|
99 |
-
|
100 |
-
|
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 |
-
|
109 |
|
110 |
.user-message {
|
111 |
-
background: var(--user-msg-bg);
|
112 |
-
|
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:
|
121 |
-
align-self: flex-start;
|
122 |
-
border-bottom-left-radius: 5px;
|
123 |
-
margin-right: auto;
|
124 |
}
|
125 |
-
|
126 |
-
|
127 |
|
128 |
.system-message {
|
129 |
-
font-style: italic;
|
130 |
-
color:
|
131 |
-
|
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 |
-
|
145 |
-
color: var(--
|
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 |
-
|
157 |
-
|
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 |
-
|
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 |
-
|
177 |
-
|
178 |
-
|
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 |
-
|
195 |
-
|
196 |
-
|
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.
|
211 |
-
#chatbox { padding:
|
212 |
-
#messages div { max-width: 90%; font-size: 0.
|
213 |
-
#input-area { padding: 8px
|
214 |
-
#userInput { padding:
|
215 |
-
.control-button { width:
|
216 |
}
|
217 |
</style>
|
218 |
|
219 |
-
<!-- Import map
|
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
|
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="
|
236 |
-
<button id="speechButton" class="control-button" title="
|
237 |
-
<button id="toggleSpeakerButton" class="control-button" title="AI
|
238 |
-
<button id="sendButton" class="control-button" title="
|
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 |
-
//
|
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 (
|
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 = '
|
276 |
-
const historyKey = '
|
277 |
|
278 |
-
// --- Web Speech API ---
|
279 |
let recognition = null;
|
280 |
let synthesis = window.speechSynthesis;
|
281 |
-
let targetVoice = null; //
|
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 ? '
|
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, "
|
377 |
-
|
|
|
|
|
378 |
try {
|
379 |
-
// *** dtype ์ต์
์ ๊ฑฐ ***
|
380 |
generator = await pipeline(TASK, MODEL_NAME, {
|
381 |
-
// dtype
|
382 |
progress_callback: (progress) => {
|
383 |
-
const msg = `[
|
384 |
setLoading(true, msg);
|
385 |
},
|
386 |
});
|
387 |
-
displayMessage('system', "[
|
388 |
|
389 |
} catch (error) {
|
390 |
console.error("Model loading failed:", error);
|
391 |
-
displayMessage('system', `[
|
392 |
setLoading(false);
|
393 |
-
loadingIndicator.textContent = '
|
394 |
return;
|
395 |
} finally {
|
396 |
-
setLoading(false);
|
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 |
-
//
|
407 |
function buildPrompt() {
|
408 |
-
const historyLimit = 6;
|
409 |
const recentHistory = conversationHistory.slice(-historyLimit);
|
410 |
|
|
|
411 |
let prompt = "<start_of_turn>system\n";
|
412 |
-
prompt +=
|
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
|
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
|
481 |
|
482 |
displayMessage('bot', replyText);
|
483 |
conversationHistory.push({ sender: 'bot', text: replyText });
|
484 |
|
485 |
-
if (botState.botSettings.useSpeechOutput && synthesis && targetVoice) {
|
486 |
-
speakText(replyText);
|
487 |
}
|
488 |
|
489 |
saveState();
|
490 |
|
491 |
} catch (error) {
|
492 |
console.error("AI response generation error:", error);
|
493 |
-
displayMessage('system', `[
|
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 = '
|
510 |
-
recognition.
|
|
|
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(
|
513 |
-
recognition.onend = () => { isListening = false; speechButton.disabled = !generator; speechButton.textContent = '๐ค'; if (speechStatus.textContent === '
|
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 |
-
//
|
533 |
function findAndSetVoice(voices) {
|
534 |
-
//
|
535 |
-
targetVoice = voices.find(v => v.lang === '
|
536 |
-
//
|
537 |
-
if (!targetVoice) targetVoice = voices.find(v => v.lang
|
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
|
545 |
-
displayMessage('system', "[
|
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 = '
|
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(
|
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 |
|