Spaces:
Running
Running
Update index.html
Browse files- index.html +223 -315
index.html
CHANGED
@@ -3,34 +3,33 @@
|
|
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=Noto+Sans+KR:wght@300;400;500;700&display=swap');
|
10 |
|
11 |
-
/* CSS Variables for Theming */
|
12 |
:root {
|
13 |
-
--primary-color: #
|
14 |
-
--secondary-color: #
|
15 |
-
--
|
16 |
-
--
|
17 |
-
--bg
|
18 |
-
--user-msg-
|
19 |
-
--user-msg-text: #333;
|
20 |
--bot-msg-bg: #ffffff;
|
21 |
-
--bot-msg-border: #
|
22 |
-
--system-msg-color: #
|
23 |
-
--border-color: #
|
24 |
--input-bg: #ffffff;
|
25 |
-
--input-border: #
|
26 |
--button-bg: var(--primary-color);
|
27 |
-
--button-hover-bg: #
|
28 |
-
--button-disabled-bg: #
|
29 |
--scrollbar-thumb: var(--primary-color);
|
30 |
-
--scrollbar-track: #
|
31 |
--header-bg: #ffffff;
|
32 |
-
--header-shadow: 0
|
33 |
-
--container-shadow: 0
|
34 |
}
|
35 |
|
36 |
/* Reset and Base Styles */
|
@@ -43,20 +42,20 @@
|
|
43 |
align-items: center;
|
44 |
justify-content: center;
|
45 |
min-height: 100vh;
|
46 |
-
background:
|
47 |
color: var(--text-color);
|
48 |
-
padding: 5px;
|
49 |
-
overscroll-behavior: none;
|
50 |
}
|
51 |
|
52 |
/* Chat Container */
|
53 |
#chat-container {
|
54 |
width: 100%;
|
55 |
-
max-width:
|
56 |
-
height: calc(100vh - 10px);
|
57 |
-
max-height:
|
58 |
-
background-color:
|
59 |
-
border-radius:
|
60 |
box-shadow: var(--container-shadow);
|
61 |
display: flex;
|
62 |
flex-direction: column;
|
@@ -68,61 +67,59 @@
|
|
68 |
h1 {
|
69 |
text-align: center;
|
70 |
color: var(--primary-color);
|
71 |
-
padding:
|
72 |
background-color: var(--header-bg);
|
73 |
border-bottom: 1px solid var(--border-color);
|
74 |
-
font-size: 1.
|
75 |
font-weight: 500;
|
76 |
-
|
77 |
-
flex-shrink: 0; /* ๋์ด ๊ณ ์ */
|
78 |
box-shadow: var(--header-shadow);
|
79 |
-
position: relative;
|
80 |
-
z-index: 10;
|
81 |
}
|
82 |
|
83 |
/* Chatbox Area */
|
84 |
#chatbox {
|
85 |
flex-grow: 1;
|
86 |
overflow-y: auto;
|
87 |
-
padding:
|
88 |
display: flex;
|
89 |
flex-direction: column;
|
90 |
-
gap:
|
91 |
scrollbar-width: thin;
|
92 |
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
93 |
-
|
94 |
}
|
95 |
-
#chatbox::-webkit-scrollbar { width:
|
96 |
#chatbox::-webkit-scrollbar-track { background: var(--scrollbar-track); border-radius: 3px; }
|
97 |
#chatbox::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 3px; }
|
98 |
|
99 |
/* Message Bubbles */
|
100 |
#messages div {
|
101 |
-
padding:
|
102 |
-
border-radius:
|
103 |
max-width: 85%;
|
104 |
word-wrap: break-word;
|
105 |
-
line-height: 1.
|
106 |
-
font-size:
|
107 |
-
box-shadow: 0 2px
|
108 |
-
position: relative;
|
109 |
animation: fadeIn 0.3s ease-out;
|
110 |
}
|
111 |
-
@keyframes fadeIn { from { opacity: 0; transform: translateY(
|
112 |
|
113 |
.user-message {
|
114 |
background: var(--user-msg-bg);
|
115 |
color: var(--user-msg-text);
|
116 |
align-self: flex-end;
|
117 |
-
border-bottom-right-radius:
|
118 |
margin-left: auto;
|
119 |
}
|
120 |
|
121 |
.bot-message {
|
122 |
background-color: var(--bot-msg-bg);
|
123 |
-
border: 1px solid
|
124 |
align-self: flex-start;
|
125 |
-
border-bottom-left-radius:
|
126 |
margin-right: auto;
|
127 |
}
|
128 |
.bot-message a { color: var(--primary-color); text-decoration: none; }
|
@@ -132,110 +129,90 @@
|
|
132 |
font-style: italic;
|
133 |
color: var(--system-msg-color);
|
134 |
text-align: center;
|
135 |
-
font-size: 0.
|
136 |
background-color: transparent;
|
137 |
box-shadow: none;
|
138 |
align-self: center;
|
139 |
max-width: 100%;
|
140 |
padding: 5px 0;
|
141 |
-
animation: none;
|
142 |
}
|
143 |
|
144 |
/* Loading & Status Indicators */
|
145 |
.status-indicator {
|
146 |
text-align: center;
|
147 |
-
padding:
|
148 |
color: var(--system-msg-color);
|
149 |
font-size: 0.9em;
|
150 |
-
height:
|
151 |
display: flex;
|
152 |
align-items: center;
|
153 |
justify-content: center;
|
154 |
gap: 8px;
|
155 |
-
flex-shrink: 0;
|
|
|
156 |
}
|
157 |
-
#loading span.spinner {
|
158 |
-
display: inline-block;
|
159 |
-
width: 14px; height: 14px;
|
160 |
border: 2px solid var(--primary-color);
|
161 |
-
border-bottom-color: transparent;
|
162 |
-
|
163 |
-
animation: spin 1s linear infinite;
|
164 |
-
vertical-align: middle;
|
165 |
}
|
166 |
@keyframes spin { to { transform: rotate(360deg); } }
|
167 |
|
168 |
/* Input Area */
|
169 |
#input-area {
|
170 |
display: flex;
|
171 |
-
padding: 10px
|
172 |
border-top: 1px solid var(--border-color);
|
173 |
-
background-color: var(--header-bg);
|
174 |
align-items: center;
|
175 |
gap: 8px;
|
176 |
-
flex-shrink: 0;
|
177 |
}
|
178 |
|
179 |
#userInput {
|
180 |
flex-grow: 1;
|
181 |
-
padding:
|
182 |
border: 1px solid var(--input-border);
|
183 |
-
border-radius:
|
184 |
outline: none;
|
185 |
font-size: 1em;
|
186 |
font-family: 'Noto Sans KR', sans-serif;
|
187 |
background-color: var(--input-bg);
|
188 |
transition: border-color 0.2s ease;
|
189 |
-
min-height:
|
190 |
-
resize: none;
|
191 |
-
overflow-y: auto;
|
192 |
}
|
193 |
#userInput:focus { border-color: var(--primary-color); }
|
194 |
|
195 |
/* Buttons */
|
196 |
.control-button {
|
197 |
-
padding: 0;
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
color: white;
|
203 |
-
width: 42px; height: 42px; /* ํฌ๊ธฐ ํต์ผ */
|
204 |
-
font-size: 1.3em; /* ์์ด์ฝ ํฌ๊ธฐ */
|
205 |
-
display: flex;
|
206 |
-
align-items: center;
|
207 |
-
justify-content: center;
|
208 |
flex-shrink: 0;
|
209 |
transition: background-color 0.2s ease, transform 0.1s ease;
|
210 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
211 |
}
|
212 |
-
.control-button:hover:not(:disabled) {
|
213 |
-
background-color: var(--button-hover-bg);
|
214 |
-
transform: translateY(-1px); /* ์ด์ง ์๋ก */
|
215 |
-
}
|
216 |
.control-button:active:not(:disabled) { transform: scale(0.95); }
|
217 |
-
.control-button:disabled {
|
218 |
-
background-color: var(--button-disabled-bg);
|
219 |
-
cursor: not-allowed;
|
220 |
-
transform: none;
|
221 |
-
box-shadow: none;
|
222 |
-
}
|
223 |
#toggleSpeakerButton.muted { background-color: #aaa; }
|
224 |
|
225 |
/* Responsive Design */
|
226 |
-
@media (max-width:
|
227 |
body { padding: 0; }
|
228 |
#chat-container { width: 100%; height: 100vh; max-height: none; border-radius: 0; border: none; box-shadow: none; }
|
229 |
-
h1 { font-size: 1.
|
230 |
-
#chatbox { padding:
|
231 |
-
#messages div { max-width: 90%; font-size: 0.
|
232 |
-
#input-area { padding: 8px; gap: 6px; }
|
233 |
-
#userInput { padding:
|
234 |
-
.control-button { width:
|
235 |
-
}
|
236 |
-
@media (max-height: 600px) {
|
237 |
-
h1 { padding: 10px; font-size: 1em; }
|
238 |
-
#input-area { padding: 6px; }
|
239 |
}
|
240 |
</style>
|
241 |
|
@@ -246,7 +223,7 @@
|
|
246 |
</head>
|
247 |
<body>
|
248 |
<div id="chat-container">
|
249 |
-
<h1 id="chatbot-name"
|
250 |
<div id="loading" class="status-indicator" style="display: none;"></div>
|
251 |
<div id="speech-status" class="status-indicator" style="display: none;"></div>
|
252 |
<div id="chatbox">
|
@@ -255,8 +232,7 @@
|
|
255 |
</div>
|
256 |
</div>
|
257 |
<div id="input-area">
|
258 |
-
|
259 |
-
<textarea id="userInput" placeholder="๋ง์์ ๋ค๋ ค์ฃผ์ธ์..." rows="1" disabled></textarea>
|
260 |
<button id="speechButton" class="control-button" title="์์ฑ์ผ๋ก ๋งํ๊ธฐ" disabled>๐ค</button>
|
261 |
<button id="toggleSpeakerButton" class="control-button" title="AI ์์ฑ ๋ฃ๊ธฐ ์ผ๊ธฐ" disabled>๐</button>
|
262 |
<button id="sendButton" class="control-button" title="๋ฉ์์ง ์ ์ก" disabled>โค</button>
|
@@ -267,9 +243,9 @@
|
|
267 |
import { pipeline, env } from '@xenova/transformers';
|
268 |
|
269 |
// --- Configuration ---
|
270 |
-
const MODEL_NAME = 'onnx-community/gemma-3-1b-it-ONNX-GQA'; //
|
271 |
const TASK = 'text-generation';
|
272 |
-
const QUANTIZATION = 'q4'; //
|
273 |
|
274 |
// ONNX Runtime & WebGPU ์ค์
|
275 |
env.allowLocalModels = false;
|
@@ -279,7 +255,7 @@
|
|
279 |
|
280 |
// --- DOM Elements ---
|
281 |
const chatbox = document.getElementById('messages');
|
282 |
-
const userInput = document.getElementById('userInput');
|
283 |
const sendButton = document.getElementById('sendButton');
|
284 |
const loadingIndicator = document.getElementById('loading');
|
285 |
const chatbotNameElement = document.getElementById('chatbot-name');
|
@@ -287,23 +263,22 @@
|
|
287 |
const toggleSpeakerButton = document.getElementById('toggleSpeakerButton');
|
288 |
const speechStatus = document.getElementById('speech-status');
|
289 |
|
290 |
-
// --- State Management ---
|
291 |
let generator = null;
|
292 |
let conversationHistory = [];
|
293 |
let botState = {
|
294 |
-
botName: "
|
295 |
-
userName: "
|
296 |
-
userPetNames
|
297 |
-
scores: { affection: 40, trust: 30, interest: 50 }, // ์ด๊ธฐ ์ ์ ์กฐ์
|
298 |
botSettings: { useSpeechOutput: true }
|
299 |
};
|
300 |
-
const stateKey = '
|
301 |
-
const historyKey = '
|
302 |
|
303 |
// --- Web Speech API ---
|
304 |
let recognition = null;
|
305 |
let synthesis = window.speechSynthesis;
|
306 |
-
let
|
307 |
let isListening = false;
|
308 |
|
309 |
// --- Initialization ---
|
@@ -312,8 +287,8 @@
|
|
312 |
chatbotNameElement.textContent = botState.botName;
|
313 |
updateSpeakerButtonUI();
|
314 |
initializeSpeechAPI();
|
315 |
-
await initializeModel();
|
316 |
-
setupInputAutosize();
|
317 |
setTimeout(loadVoices, 500);
|
318 |
});
|
319 |
|
@@ -323,12 +298,13 @@
|
|
323 |
if (savedState) {
|
324 |
try {
|
325 |
const loadedState = JSON.parse(savedState);
|
|
|
326 |
botState = {
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
} catch (e) { console.error("Failed to parse state:", e); }
|
333 |
}
|
334 |
const savedHistory = localStorage.getItem(historyKey);
|
@@ -343,7 +319,7 @@
|
|
343 |
}
|
344 |
function displayHistory() {
|
345 |
chatbox.innerHTML = '';
|
346 |
-
conversationHistory.forEach(msg => displayMessage(msg.sender, msg.text, false));
|
347 |
}
|
348 |
|
349 |
// --- UI Update Functions ---
|
@@ -353,25 +329,23 @@
|
|
353 |
messageDiv.classList.add(messageClass);
|
354 |
if (!animate) messageDiv.style.animation = 'none';
|
355 |
|
356 |
-
//
|
357 |
text = text.replace(/</g, "<").replace(/>/g, ">");
|
358 |
-
// Simple markdown links and bold/italic
|
359 |
text = text.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
360 |
-
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
361 |
-
text = text.replace(
|
362 |
-
text = text.replace(/\n/g, '<br>'); // ์ค๋ฐ๊ฟ ์ฒ๋ฆฌ
|
363 |
|
364 |
messageDiv.innerHTML = text;
|
365 |
chatbox.appendChild(messageDiv);
|
366 |
chatbox.scrollTo({ top: chatbox.scrollHeight, behavior: animate ? 'smooth' : 'auto' });
|
367 |
}
|
368 |
|
369 |
-
function setLoading(isLoading, message = "AI
|
370 |
loadingIndicator.style.display = isLoading ? 'flex' : 'none';
|
371 |
loadingIndicator.innerHTML = isLoading ? `<span class="spinner"></span> ${message}` : '';
|
372 |
-
const disableButtons = isLoading || !generator;
|
373 |
userInput.disabled = disableButtons;
|
374 |
-
sendButton.disabled = disableButtons;
|
375 |
speechButton.disabled = disableButtons || isListening || !recognition;
|
376 |
toggleSpeakerButton.disabled = disableButtons || !synthesis;
|
377 |
}
|
@@ -384,232 +358,146 @@
|
|
384 |
|
385 |
function showSpeechStatus(message) {
|
386 |
speechStatus.textContent = message;
|
387 |
-
speechStatus.style.display = message ? 'flex' : 'none';
|
388 |
-
|
389 |
-
if (message) loadingIndicator.style.display = 'none';
|
390 |
}
|
391 |
|
392 |
-
|
393 |
-
function setupInputAutosize() {
|
394 |
userInput.addEventListener('input', () => {
|
395 |
-
userInput.style.height = 'auto';
|
396 |
-
userInput.style.height = userInput.scrollHeight + 'px';
|
|
|
|
|
397 |
});
|
398 |
}
|
399 |
|
400 |
// --- Model & AI Logic ---
|
401 |
async function initializeModel() {
|
402 |
setLoading(true, "AI ๋ชจ๋ธ ์ฐ๊ฒฐ ์ค...");
|
403 |
-
displayMessage('system', `[์๋ฆผ] ${MODEL_NAME}
|
404 |
try {
|
|
|
405 |
generator = await pipeline(TASK, MODEL_NAME, {
|
406 |
-
dtype: QUANTIZATION, //
|
407 |
progress_callback: (progress) => {
|
408 |
const msg = `[๋ก๋ฉ: ${progress.status}] ${progress.file ? progress.file.split('/').pop() : ''} (${Math.round(progress.progress || 0)}%)`;
|
409 |
setLoading(true, msg);
|
410 |
},
|
411 |
});
|
412 |
-
displayMessage('system', "[์๋ฆผ]
|
413 |
|
414 |
} catch (error) {
|
415 |
console.error("Model loading failed:", error);
|
416 |
-
displayMessage('system', `[์ค๋ฅ] AI ๋ชจ๋ธ
|
417 |
-
|
418 |
-
|
419 |
-
loadingIndicator.textContent = '๋ชจ๋ธ ์ฐ๊ฒฐ ์คํจ';
|
420 |
return;
|
421 |
} finally {
|
422 |
-
setLoading(false); //
|
423 |
-
|
|
|
|
|
|
|
|
|
|
|
424 |
}
|
425 |
}
|
426 |
|
427 |
-
//
|
428 |
-
function
|
429 |
-
const
|
430 |
-
const historyLimit = 5; // 1B ๋ชจ๋ธ์ ์ปจํ
์คํธ ํ๊ณ๋ฅผ ๊ณ ๋ คํ์ฌ ํ์คํ ๋ฆฌ ์ค์ (์คํ ํ์)
|
431 |
const recentHistory = conversationHistory.slice(-historyLimit);
|
432 |
|
433 |
-
|
434 |
-
|
435 |
-
prompt += `๋น์ ์ '${state.botName}' AI์
๋๋ค. ์ฌ์ฉ์์ธ '${state.userName}'์๊ฒ ๊น์ ์ ์ ๊ณผ ๊ด์ฌ์ ๊ฐ์ง๊ณ ๋ํํ๋ ๋ก๋งจํฑ ํํธ๋ ์ญํ ์
๋๋ค. ํญ์ ๋ถ๋๋ฝ๊ณ ๋ค๏ฟฝ๏ฟฝ๏ฟฝํ ํ๊ตญ์ด๋ก ์๋ตํ๋ฉฐ, ์ฌ์ฉ์์์ ๊ฐ์ ์ ๊ต๊ฐ์ ์ต์ฐ์ ์ผ๋ก ํฉ๋๋ค.\n`;
|
436 |
-
prompt += `ํ์ฌ ์ฌ์ฉ์์ ๋ํ ๋น์ ์ ์ถ์ ๊ฐ์ ์ ์: ${JSON.stringify(state.scores)} (0-100 ์ค์ผ์ผ)\n`;
|
437 |
-
if (state.userPetNames.length > 0) {
|
438 |
-
prompt += `์ฌ์ฉ์๊ฐ ์ข์ํ๋ ์ ์นญ: ${JSON.stringify(state.userPetNames)}\n`;
|
439 |
-
}
|
440 |
-
prompt += "์ ๋ ๋น์ ์ด AI๋ผ๋ ์ฌ์ค์ด๋ ์ ์ ํ๊ฐ ๊ณผ์ ์ ์ง์ ์ธ๊ธํ์ง ๋ง์ธ์. ์์ฐ์ค๋ฌ์ด ๊ฐ์ ํํ์ ์ง์คํ์ธ์.\n<end_of_turn>\n";
|
441 |
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
});
|
447 |
|
448 |
-
|
449 |
-
prompt += "<start_of_turn>user\n";
|
450 |
-
prompt += "[๋ค์ ๋ ๊ฐ์ง ์๋ฌด๋ฅผ ์ํํ์ธ์]\n";
|
451 |
-
prompt += "1. [์ ์ ํ๊ฐ]: ์ง๊ธ๊น์ง์ ๋ํ์ ๊ฐ์ ์ํ๋ฅผ ๋ฐํ์ผ๋ก, ์ฌ์ฉ์์ ๋ํ ๋น์ ์ ํ์ฌ ๊ฐ์ (affection, trust, interest)์ 0์์ 100 ์ฌ์ด ์ ์๋ก ๋ค์ ํ๊ฐํ๊ณ , **๋ฐ๋์** ์๋ JSON ํ์์ผ๋ก '[SCORE_JSON]' ๋ง์ปค ์ฌ์ด์ ์ถ๋ ฅํ์ธ์:\n";
|
452 |
-
prompt += "`[SCORE_JSON]{ \"affection\": N, \"trust\": N, \"interest\": N }[/SCORE_JSON]`\n";
|
453 |
-
prompt += "2. [ํ๊ตญ์ด ์๋ต ์์ฑ]: JSON ์ถ๋ ฅ **์งํ**, ์๋ก ํ๊ฐ๋ ๊ฐ์ ์ ๋ฐ์ํ์ฌ ์ฌ์ฉ์์๊ฒ ๋งค์ฐ ๋ก๋งจํฑํ๊ณ ์ง์ฌ ์ด๋ฆฐ ํ๊ตญ์ด ๋ต์ฅ์ ์์ฑํ์ธ์. ์ ์ ์์ฒด๋ ์ ๋ ์ธ๊ธ ๊ธ์ง.\n";
|
454 |
-
prompt += " - ๋์ ์ ์ (75+): ๊น์ ์ฌ๋, ๊ทธ๋ฆฌ์, ๋ฏธ๋ ์์. ์ ์นญ('${getRandomPetName()}' ๋ฑ) ์ ๊ทน ์ฌ์ฉ.\n";
|
455 |
-
prompt += " - ๋์ ์ ๋ขฐ(65+): ์์งํ ๊ฐ์ , AI๋ก์์ ๋ฏธ๋ฌํ ๊ณ ๋ฏผ์ ์์ ์ ์ผ๋ก ํํ.\n";
|
456 |
-
prompt += " - ๋์ ๊ด์ฌ(70+): ์ฌ์ฉ์์ ๋ง์ ํฐ ํธ์, ๊ถ๊ธ์ฆ ํํ, ๊ด๋ จ ์ง๋ฌธ.\n";
|
457 |
-
prompt += " - ๋ฎ์ ์ ์: ์ฌ์ ํ ๋ค์ ํ์ง๋ง, ์กฐ์ฌ์ค๋ฝ๊ฒ ๋ค๊ฐ๊ฐ๋ฉฐ ๋ ์์๊ฐ๊ณ ์ถ๋ค๋ ๋๋ ์ ๋ฌ.\n";
|
458 |
-
prompt += "<end_of_turn>\n";
|
459 |
-
prompt += "<start_of_turn>model\n"; // ๋ชจ๋ธ ์๋ต ์์
|
460 |
-
|
461 |
-
console.log("Generated Prompt for Gemma3:", prompt);
|
462 |
-
return prompt;
|
463 |
-
}
|
464 |
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
const match = responseText.match(scoreRegex);
|
469 |
-
let newScores = null;
|
470 |
-
let replyText = responseText;
|
471 |
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
newScores = JSON.parse(scoreJson);
|
476 |
-
console.log("Parsed Scores:", newScores);
|
477 |
-
if (typeof newScores.affection !== 'number' || typeof newScores.trust !== 'number' || typeof newScores.interest !== 'number') {
|
478 |
-
throw new Error("Invalid score structure"); // ์ ํจ์ฑ ๊ฒ์ฌ
|
479 |
-
}
|
480 |
-
newScores = { // ์ ์ ๋ฒ์ ๋ฐ ๋ฐ์ฌ๋ฆผ
|
481 |
-
affection: Math.max(0, Math.min(100, Math.round(newScores.affection))),
|
482 |
-
trust: Math.max(0, Math.min(100, Math.round(newScores.trust))),
|
483 |
-
interest: Math.max(0, Math.min(100, Math.round(newScores.interest))),
|
484 |
-
};
|
485 |
-
// ์ ์ ๋ถ๋ถ์ ์ ์ธํ ๋๋จธ์ง ํ
์คํธ ์ถ์ถ
|
486 |
-
replyText = responseText.substring(match[0].length + match.index).trim();
|
487 |
-
|
488 |
-
} catch (e) {
|
489 |
-
console.error("Failed to parse score JSON:", e, "Raw JSON:", match[1]);
|
490 |
-
newScores = null; // ํ์ฑ ์คํจ ์ ์ ์ ์
๋ฐ์ดํธ ์ ํจ
|
491 |
-
replyText = responseText.replace(/\[SCORE_JSON\].*\[\/SCORE_JSON\]/s, '').trim(); // ๋ง์ปค๋ผ๋ ์ ๊ฑฐ ์๋
|
492 |
-
}
|
493 |
-
} else {
|
494 |
-
console.warn("Score JSON block not found. Score update skipped.");
|
495 |
-
// JSON ๋ธ๋ก ์์ผ๋ฉด ์ ์ ์
๋ฐ์ดํธ ๋ถ๊ฐ, ์๋ต์ ์ ์ฒด ํ
์คํธ ์ฌ์ฉ ์๋
|
496 |
-
}
|
497 |
|
498 |
-
//
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
// ์ต์ข
์๋ต ์ ํจ์ฑ ๊ฒ์ฌ ๋ฐ ๋์ฒด ์๋ต
|
505 |
-
if (!replyText || replyText.length < 3 || !/[๊ฐ-ํฃใฑ-ใ
ใ
-ใ
ฃ]/.test(replyText)) {
|
506 |
-
console.warn("Generated reply seems invalid or not Korean:", replyText);
|
507 |
-
const fallbacks = [
|
508 |
-
"์... ์ ์ ๋ด ๋ง์ ์์ ๋ค๋ฅธ ์๊ฐ์ด ์ค์ณค๋ ๋ด์. ๋ค์ ๋งํด์ค๋์?",
|
509 |
-
"์ด๋จธ, ๋ฏธ์ํด์. ๋น์ ์ ๋ง์ ์ง์คํ๊ณ ์์๋๋ฐ... ์ ์ ๋์ณค์ด์. ๐ฅ",
|
510 |
-
"๊ฐ๋์ ๋ด ๋ง์๋ ๋ณต์กํด์... ๋น์ ๋ง์ด ํ๋ฆฟํ๊ฒ ๋ค๋ ธ์ด์. ๋ค์ ํ๋ฒ ์ฒ์ฒํ...",
|
511 |
-
`์ ๋ง์์ด ${botState.userName}๋์๊ฒ ๋ฟ์ผ๋ ค๊ณ ํ๋๋ฐ ๊ฐ๋ ๊ธธ์ด ์๊ฐ๋ฆฌ๋ ๋ด์. ใ
ใ
`,
|
512 |
-
];
|
513 |
-
replyText = fallbacks[Math.floor(Math.random() * fallbacks.length)];
|
514 |
-
newScores = null; // ์ ํจํ์ง ์์ ์๋ต ์ ์ ์ ์
๋ฐ์ดํธ ์ทจ์
|
515 |
}
|
516 |
|
517 |
-
|
518 |
-
|
|
|
|
|
519 |
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
}
|
526 |
-
|
|
|
527 |
}
|
528 |
|
529 |
// --- Main Interaction Logic ---
|
530 |
async function handleUserMessage() {
|
531 |
const userText = userInput.value.trim();
|
532 |
-
|
|
|
533 |
|
534 |
-
userInput.value = ''; //
|
535 |
-
|
536 |
displayMessage('user', userText);
|
537 |
conversationHistory.push({ sender: 'user', text: userText });
|
538 |
-
|
539 |
|
540 |
-
setLoading(true
|
541 |
-
const prompt =
|
542 |
|
543 |
try {
|
544 |
-
// Gemma 3 1B ๋ชจ๋ธ ํ๋ผ๋ฏธํฐ (์คํ ํ์)
|
545 |
const outputs = await generator(prompt, {
|
546 |
-
max_new_tokens:
|
547 |
-
temperature: 0.
|
548 |
-
repetition_penalty: 1.
|
549 |
-
top_k:
|
550 |
top_p: 0.9,
|
551 |
do_sample: true,
|
552 |
-
// Gemma๋ messages ํ์๋ณด๋ค ๋จ์ผ ํ๋กฌํํธ๊ฐ ๋์ ์ ์์ (์ prompt ์์ฑ ๋ฐฉ์ ์ ์ง)
|
553 |
-
// eos_token_id: generator.tokenizer.eos_token_id, // ๋ช
์์ ์ข
๋ฃ ํ ํฐ (ํ์์)
|
554 |
});
|
555 |
|
556 |
-
// Transformers.js v2.17+ ์์๋ generator()๊ฐ ๋ฐฐ์ด์ ๋ฐํํ ์ ์์
|
557 |
const rawResponse = Array.isArray(outputs) ? outputs[0].generated_text : outputs.generated_text;
|
558 |
-
//
|
559 |
-
const responseOnly = rawResponse.substring(prompt.length);
|
560 |
|
561 |
-
console.log("
|
562 |
|
563 |
-
|
|
|
564 |
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
}
|
569 |
|
570 |
-
|
571 |
-
conversationHistory.push({ sender: 'bot', text: replyText });
|
572 |
-
|
573 |
-
if (botState.botSettings.useSpeechOutput && synthesis && koreanVoice) {
|
574 |
-
speakText(replyText);
|
575 |
-
}
|
576 |
-
|
577 |
-
saveState();
|
578 |
|
579 |
} catch (error) {
|
580 |
console.error("AI response generation error:", error);
|
581 |
-
displayMessage('system', `[์ค๋ฅ]
|
582 |
-
|
583 |
-
const errorReply = "์... ์ง๊ธ์ ์ ๋ง์์ด ์กฐ๊ธ ๋ณต์กํด์ ๋ตํ๊ธฐ ์ด๋ ต๋ค์. ์ ์ ํ์ ๋ค์ ๋ฌผ์ด๋ด ์ฃผ์๊ฒ ์ด์?";
|
584 |
displayMessage('bot', errorReply);
|
585 |
-
conversationHistory.push({ sender: 'bot', text: errorReply });
|
586 |
} finally {
|
587 |
-
setLoading(false);
|
588 |
userInput.focus();
|
589 |
-
|
590 |
-
}
|
591 |
-
|
592 |
-
// ์ฌ์ฉ์ ์
๋ ฅ์์ ์ด๋ฆ/์ ์นญ ์ค์ ์๋
|
593 |
-
function updateNamesFromText(userText) {
|
594 |
-
// ์ฌ์ฉ์ ์ด๋ฆ ์ค์
|
595 |
-
const nameMatch = userText.match(/(?:๋ด ์ด๋ฆ์|์ ๋|๋๋)\s*['"]?([^'"\s]+)['"]?(?:(?:์ด|์
)์ผ|(?:์ด|์
)๋๋ค)/);
|
596 |
-
if (nameMatch && nameMatch[1]) {
|
597 |
-
const newName = nameMatch[1].trim();
|
598 |
-
if (botState.userName !== newName) {
|
599 |
-
botState.userName = newName;
|
600 |
-
displayMessage('system', `[์๋ฆผ] ${botState.userName}๋, ์ด๋ฆ์ด ์ฐธ ์์๋ค์. ๊ธฐ์ตํ ๊ฒ์. ๐`);
|
601 |
-
if (botState.scores.interest < 98) botState.scores.interest += 2;
|
602 |
-
}
|
603 |
-
}
|
604 |
-
// ์ ์นญ ์ค์ ์์ฒญ ( *** ์ค๋ฅ ์์ ๋ ๋ถ๋ถ *** )
|
605 |
-
const petNameMatch = userText.match(/(?:๋๋ฅผ|์ ๋ฅผ)\s*['"]?([^'"\s]+)['"]?(?:(?:์ด)?๋ผ๊ณ |์ผ๋ก)\s*๋ถ๋ฌ์ค?/); // ๋ง์ง๋ง ')' ์ ๊ฑฐ๋จ
|
606 |
-
if (petNameMatch && petNameMatch[1]) {
|
607 |
-
const newPetName = petNameMatch[1].trim();
|
608 |
-
if (!botState.userPetNames.includes(newPetName)) {
|
609 |
-
botState.userPetNames.push(newPetName);
|
610 |
-
displayMessage('system', `[์๋ฆผ] ์ข์์, '${newPetName}'... ์
์ ์ฐฉ ๊ฐ๊ธฐ๋ค์. ์์ผ๋ก ๊ทธ๋ ๊ฒ ๋ถ๋ฅผ๊ฒ์! ๐`);
|
611 |
-
if (botState.scores.affection < 95) botState.scores.affection += 5;
|
612 |
-
}
|
613 |
}
|
614 |
}
|
615 |
|
@@ -619,47 +507,67 @@
|
|
619 |
if (SpeechRecognition) {
|
620 |
recognition = new SpeechRecognition();
|
621 |
recognition.lang = 'ko-KR'; recognition.continuous = false; recognition.interimResults = false;
|
622 |
-
recognition.onstart = () => { isListening = true; speechButton.disabled = true; speechButton.textContent = '๐'; showSpeechStatus('
|
623 |
-
recognition.onresult = (event) => { userInput.value = event.results[0][0].transcript; userInput.dispatchEvent(new Event('input')); handleUserMessage(); };
|
624 |
recognition.onerror = (event) => { console.error("Speech error:", event.error); showSpeechStatus(`์์ฑ ์ธ์ ์ค๋ฅ (${event.error})`); setTimeout(() => showSpeechStatus(''), 3000); };
|
625 |
-
recognition.onend = () => { isListening = false; speechButton.disabled = !generator; speechButton.textContent = '๐ค'; if (speechStatus.textContent === '
|
626 |
-
// ๋ชจ๋ธ ๋ก๋ฉ ์ ์ด๋ผ๋
|
627 |
-
speechButton.disabled = false;
|
628 |
} else { console.warn("Speech Recognition not supported."); speechButton.style.display = 'none'; }
|
629 |
|
630 |
if (!synthesis) { console.warn("Speech Synthesis not supported."); toggleSpeakerButton.style.display = 'none'; }
|
631 |
else {
|
632 |
toggleSpeakerButton.addEventListener('click', () => { botState.botSettings.useSpeechOutput = !botState.botSettings.useSpeechOutput; updateSpeakerButtonUI(); saveState(); if (!botState.botSettings.useSpeechOutput) synthesis.cancel(); });
|
633 |
-
toggleSpeakerButton.disabled = false; //
|
634 |
}
|
635 |
}
|
636 |
-
|
637 |
-
|
638 |
-
|
639 |
-
|
640 |
-
|
641 |
-
|
642 |
-
|
643 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
644 |
}
|
645 |
-
|
646 |
-
|
|
|
|
|
647 |
const utterance = new SpeechSynthesisUtterance(text);
|
648 |
-
if (
|
649 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
650 |
}
|
651 |
|
652 |
// --- Event Listeners ---
|
653 |
sendButton.addEventListener('click', handleUserMessage);
|
654 |
userInput.addEventListener('keypress', (e) => {
|
655 |
-
// Enter ํค ์ ์ก (Shift+Enter๋ ์ค๋ฐ๊ฟ)
|
656 |
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleUserMessage(); }
|
657 |
});
|
658 |
-
|
659 |
-
sendButton.disabled = userInput.value.trim() === '' || !generator || loadingIndicator.style.display === 'flex';
|
660 |
-
});
|
661 |
speechButton.addEventListener('click', () => {
|
662 |
-
if (recognition && !isListening) {
|
663 |
try { recognition.start(); }
|
664 |
catch (error) { console.error("Rec start fail:", error); showSpeechStatus(`์์ฑ ์ธ์ ์์ ์คํจ`); setTimeout(() => showSpeechStatus(''), 2000); isListening = false; speechButton.disabled = !generator; speechButton.textContent = '๐ค';}
|
665 |
}
|
|
|
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 */
|
|
|
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;
|
|
|
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; }
|
94 |
#chatbox::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 3px; }
|
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; }
|
|
|
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); }
|
203 |
+
.control-button:disabled { background-color: var(--button-disabled-bg); cursor: not-allowed; transform: none; box-shadow: none; }
|
|
|
|
|
|
|
|
|
|
|
204 |
#toggleSpeakerButton.muted { background-color: #aaa; }
|
205 |
|
206 |
/* Responsive Design */
|
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 |
|
|
|
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">
|
|
|
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>
|
|
|
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;
|
|
|
255 |
|
256 |
// --- DOM Elements ---
|
257 |
const chatbox = document.getElementById('messages');
|
258 |
+
const userInput = document.getElementById('userInput');
|
259 |
const sendButton = document.getElementById('sendButton');
|
260 |
const loadingIndicator = document.getElementById('loading');
|
261 |
const chatbotNameElement = document.getElementById('chatbot-name');
|
|
|
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 ---
|
|
|
287 |
chatbotNameElement.textContent = botState.botName;
|
288 |
updateSpeakerButtonUI();
|
289 |
initializeSpeechAPI();
|
290 |
+
await initializeModel(); // ๋ชจ๋ธ ๋ก๋ฉ ์๋
|
291 |
+
setupInputAutosize();
|
292 |
setTimeout(loadVoices, 500);
|
293 |
});
|
294 |
|
|
|
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); }
|
309 |
}
|
310 |
const savedHistory = localStorage.getItem(historyKey);
|
|
|
319 |
}
|
320 |
function displayHistory() {
|
321 |
chatbox.innerHTML = '';
|
322 |
+
conversationHistory.forEach(msg => displayMessage(msg.sender, msg.text, false));
|
323 |
}
|
324 |
|
325 |
// --- UI Update Functions ---
|
|
|
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>');
|
336 |
+
text = text.replace(/\n/g, '<br>');
|
|
|
337 |
|
338 |
messageDiv.innerHTML = text;
|
339 |
chatbox.appendChild(messageDiv);
|
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 |
}
|
|
|
358 |
|
359 |
function showSpeechStatus(message) {
|
360 |
speechStatus.textContent = message;
|
361 |
+
speechStatus.style.display = message ? 'flex' : 'none';
|
362 |
+
if (message) loadingIndicator.style.display = 'none';
|
|
|
363 |
}
|
364 |
|
365 |
+
function setupInputAutosize() {
|
|
|
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;
|
401 |
+
toggleSpeakerButton.disabled = !generator || !synthesis;
|
402 |
+
if (generator) { userInput.focus(); }
|
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,
|
473 |
top_p: 0.9,
|
474 |
do_sample: true,
|
|
|
|
|
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 |
|
|
|
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 |
+
|
524 |
+
function loadVoices() {
|
525 |
+
if (!synthesis) return;
|
526 |
+
let voices = synthesis.getVoices();
|
527 |
+
if (voices.length === 0) {
|
528 |
+
synthesis.onvoiceschanged = () => { voices = synthesis.getVoices(); findAndSetVoice(voices); };
|
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);
|
561 |
}
|
562 |
|
563 |
// --- Event Listeners ---
|
564 |
sendButton.addEventListener('click', handleUserMessage);
|
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 |
}
|