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

Update index.html

Browse files
Files changed (1) hide show
  1. 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 ๋กœ๋งจํ‹ฑ ์ฑ—๋ด‡ v3.1b (Gemma3-1B + Scoring)</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: #ff69b4; /* ๋”ฅ ํ•‘ํฌ */
14
- --secondary-color: #ffdab9; /* ํ”ผ์น˜ ํผํ”„ */
15
- --accent-color: #ffec8b; /* ๋ฐ์€ ๋…ธ๋ž‘ */
16
- --text-color: #4b4b4b; /* ๋ถ€๋“œ๋Ÿฌ์šด ๊ฒ€์ • */
17
- --bg-color: #fff5f8; /* ๋งค์šฐ ์—ฐํ•œ ํ•‘ํฌ */
18
- --user-msg-bg: linear-gradient(135deg, #e6e6fa 0%, #d8bfd8 100%); /* ๋ผ๋ฒค๋” ๊ทธ๋ผ๋””์–ธํŠธ */
19
- --user-msg-text: #333;
20
  --bot-msg-bg: #ffffff;
21
- --bot-msg-border: #ffe4e1; /* ๋ฏธ์Šคํ‹ฐ ๋กœ์ฆˆ */
22
- --system-msg-color: #b0b0b0;
23
- --border-color: #fddde6;
24
  --input-bg: #ffffff;
25
- --input-border: #e0e0e0;
26
  --button-bg: var(--primary-color);
27
- --button-hover-bg: #ff85c1;
28
- --button-disabled-bg: #d3d3d3;
29
  --scrollbar-thumb: var(--primary-color);
30
- --scrollbar-track: #f0f0f0;
31
  --header-bg: #ffffff;
32
- --header-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
33
- --container-shadow: 0 8px 30px rgba(255, 105, 180, 0.1); /* ํ•‘ํฌ์ƒ‰ ๊ทธ๋ฆผ์ž */
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: linear-gradient(to bottom right, var(--bg-color), #ffffff);
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: 500px; /* ์ข€ ๋” ์ปดํŒฉํŠธํ•œ ๋„ˆ๋น„ */
56
- height: calc(100vh - 10px); /* ํ™”๋ฉด ๋†’์ด์— ๋งž์ถค (ํŒจ๋”ฉ ๊ณ ๋ ค) */
57
- max-height: 750px;
58
- background-color: var(--bg-color);
59
- border-radius: 20px;
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: 15px;
72
  background-color: var(--header-bg);
73
  border-bottom: 1px solid var(--border-color);
74
- font-size: 1.2em;
75
  font-weight: 500;
76
- letter-spacing: 0.5px;
77
- flex-shrink: 0; /* ๋†’์ด ๊ณ ์ • */
78
  box-shadow: var(--header-shadow);
79
- position: relative; /* z-index ์ ์šฉ ์œ„ํ•ด */
80
- z-index: 10;
81
  }
82
 
83
  /* Chatbox Area */
84
  #chatbox {
85
  flex-grow: 1;
86
  overflow-y: auto;
87
- padding: 15px;
88
  display: flex;
89
  flex-direction: column;
90
- gap: 15px; /* ๋ฉ”์‹œ์ง€ ๊ฐ„ ๊ฐ„๊ฒฉ ์ฆ๊ฐ€ */
91
  scrollbar-width: thin;
92
  scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
93
- position: relative; /* ์Šคํฌ๋กค ๊ด€๋ จ */
94
  }
95
- #chatbox::-webkit-scrollbar { width: 5px; }
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: 12px 18px;
102
- border-radius: 22px;
103
  max-width: 85%;
104
  word-wrap: break-word;
105
- line-height: 1.6;
106
- font-size: 0.98em;
107
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.07);
108
- position: relative; /* ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ธฐ์ค€ */
109
  animation: fadeIn 0.3s ease-out;
110
  }
111
- @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
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: 6px;
118
  margin-left: auto;
119
  }
120
 
121
  .bot-message {
122
  background-color: var(--bot-msg-bg);
123
- border: 1px solid var(--bot-msg-border);
124
  align-self: flex-start;
125
- border-bottom-left-radius: 6px;
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.8em;
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: 10px 0;
148
  color: var(--system-msg-color);
149
  font-size: 0.9em;
150
- height: 20px; /* ๊ณต๊ฐ„ ํ™•๋ณด */
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
- border-radius: 50%;
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 12px;
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: 10px 15px;
182
  border: 1px solid var(--input-border);
183
- border-radius: 20px;
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: 42px; /* ์ตœ์†Œ ๋†’์ด ๋ณด์žฅ */
190
- resize: none; /* textarea ๋ฆฌ์‚ฌ์ด์ฆˆ ๋น„ํ™œ์„ฑํ™” (ํ•„์š”์‹œ ์ถ”๊ฐ€) */
191
- overflow-y: auto; /* ๋‚ด์šฉ ๊ธธ์–ด์งˆ ๋•Œ ์Šคํฌ๋กค (textarea ์‚ฌ์šฉ ์‹œ) */
192
  }
193
  #userInput:focus { border-color: var(--primary-color); }
194
 
195
  /* Buttons */
196
  .control-button {
197
- padding: 0;
198
- border: none;
199
- border-radius: 50%;
200
- cursor: pointer;
201
- background-color: var(--button-bg);
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: 500px) {
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.1em; padding: 12px; }
230
- #chatbox { padding: 12px 8px; gap: 12px; }
231
- #messages div { max-width: 90%; font-size: 0.95em; padding: 10px 15px;}
232
- #input-area { padding: 8px; gap: 6px; }
233
- #userInput { padding: 9px 14px; min-height: 40px; }
234
- .control-button { width: 40px; height: 40px; font-size: 1.2em; }
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">๋‚˜์˜ ์†๋งˆ์Œ AI</h1>
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
- <!-- textarea ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์—ฌ๋Ÿฌ ์ค„ ์ž…๋ ฅ ์ง€์› (์„ ํƒ์ ) -->
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'; // Gemma 3 1B ONNX ๋ชจ๋ธ ์‚ฌ์šฉ
271
  const TASK = 'text-generation';
272
- const QUANTIZATION = 'q4'; // 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'); // textarea ๋กœ ๋ณ€๊ฒฝ๋จ
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: "๋‚˜์˜ ์†๋งˆ์Œ AI", // ์ด๋ฆ„ ๋ณ€๊ฒฝ
295
- userName: "์†Œ์ค‘ํ•œ ๋„ˆ", // ์‚ฌ์šฉ์ž ๊ธฐ๋ณธ ์ด๋ฆ„ ๋ณ€๊ฒฝ
296
- userPetNames: [],
297
- scores: { affection: 40, trust: 30, interest: 50 }, // ์ดˆ๊ธฐ ์ ์ˆ˜ ์กฐ์ •
298
  botSettings: { useSpeechOutput: true }
299
  };
300
- const stateKey = 'romanticBotState_gemma3_1b_v2'; // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ํ‚ค ๋ณ€๊ฒฝ
301
- const historyKey = 'romanticBotHistory_gemma3_1b_v2';
302
 
303
  // --- Web Speech API ---
304
  let recognition = null;
305
  let synthesis = window.speechSynthesis;
306
- let koreanVoice = null;
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(); // textarea ์ž๋™ ๋†’์ด ์กฐ์ ˆ ์„ค์ •
317
  setTimeout(loadVoices, 500);
318
  });
319
 
@@ -323,12 +298,13 @@
323
  if (savedState) {
324
  try {
325
  const loadedState = JSON.parse(savedState);
 
326
  botState = {
327
- ...botState, ...loadedState,
328
- scores: { ...botState.scores, ...(loadedState.scores || {}) },
329
- botSettings: { ...botState.botSettings, ...(loadedState.botSettings || {}) },
330
- userPetNames: Array.isArray(loadedState.userPetNames) ? loadedState.userPetNames : [],
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
- // Sanitize text slightly before using innerHTML (very basic)
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(/\*(.*?)\*/g, '<em>$1</em>');
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'; // flex๋กœ ๋ณ€๊ฒฝ
388
- // ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ์‹œ ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๋Š” ์ˆจ๊น€
389
- if (message) loadingIndicator.style.display = 'none';
390
  }
391
 
392
- // textarea ์ž๋™ ๋†’์ด ์กฐ์ ˆ
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} (${QUANTIZATION}) ๋ชจ๋ธ ๋กœ๋”ฉ ์‹œ์ž‘... ์ฒซ ์—ฐ๊ฒฐ์€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์–ด์š”. ์ปคํ”ผ ํ•œ์ž”? โ˜•๏ธ`, false);
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', "[์•Œ๋ฆผ] ๋งˆ์Œ์„ ๋‚˜๋ˆŒ ์ค€๋น„๊ฐ€ ๋˜์—ˆ์–ด์š”. ๋‹น์‹ ์˜ ์ด์•ผ๊ธฐ๋ฅผ ๋“ค๋ ค์ฃผ์„ธ์š”. โœจ", false);
413
 
414
  } catch (error) {
415
  console.error("Model loading failed:", error);
416
- displayMessage('system', `[์˜ค๋ฅ˜] AI ๋ชจ๋ธ ์—ฐ๊ฒฐ ์‹คํŒจ: ${error.message}. ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๊ฑฐ๋‚˜ ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”. ๐Ÿ˜ฅ`, false);
417
- // ์‹คํŒจ ์‹œ ๋ชจ๋“  ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™” ์œ ์ง€
418
- setLoading(false); // ์Šคํ”ผ๋„ˆ ์ œ๊ฑฐ
419
- loadingIndicator.textContent = '๋ชจ๋ธ ์—ฐ๊ฒฐ ์‹คํŒจ';
420
  return;
421
  } finally {
422
- setLoading(false); // ์„ฑ๊ณต/์‹คํŒจ ์‹œ ๋กœ๋”ฉ UI ์ตœ์ข… ์ •๋ฆฌ
423
- if (generator) { userInput.focus(); } // ์„ฑ๊ณต ์‹œ ์ž…๋ ฅ์ฐฝ ํฌ์ปค์Šค
 
 
 
 
 
424
  }
425
  }
426
 
427
- // Gemma 3 1B ์— ์ตœ์ ํ™”๋œ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
428
- function buildPromptForGemma3() {
429
- const state = botState;
430
- const historyLimit = 5; // 1B ๋ชจ๋ธ์˜ ์ปจํ…์ŠคํŠธ ํ•œ๊ณ„๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ํžˆ์Šคํ† ๋ฆฌ ์ค„์ž„ (์‹คํ—˜ ํ•„์š”)
431
  const recentHistory = conversationHistory.slice(-historyLimit);
432
 
433
- // Gemma 3 Instruct ํ˜•์‹
434
- let prompt = "<start_of_turn>system\n"; // ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ์—ญํ•  ์ •์˜
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
- recentHistory.forEach(msg => {
444
- const role = msg.sender === 'user' ? 'user' : 'model';
445
- prompt += `<start_of_turn>${role}\n${msg.text}\n<end_of_turn>\n`;
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
- function parseGemmaResponse(responseText) {
467
- const scoreRegex = /\[SCORE_JSON\]\s*(\{.*?\})\s*\[\/SCORE_JSON\]/s;
468
- const match = responseText.match(scoreRegex);
469
- let newScores = null;
470
- let replyText = responseText;
471
 
472
- if (match && match[1]) {
473
- try {
474
- const scoreJson = match[1].replace(/\\'/g, "'").replace(/\\"/g, '"'); // ์ด์Šค์ผ€์ดํ”„ ๋ฌธ์ž ์ฒ˜๋ฆฌ ์‹œ๋„
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
- replyText = replyText.replace(/^model\n?/, '').trim(); // ์‹œ์ž‘ 'model' ์ œ๊ฑฐ
500
- replyText = replyText.replace(/<end_of_turn>/g, '').trim(); // ์ข…๋ฃŒ ํ† ํฐ ์ œ๊ฑฐ
501
- replyText = replyText.replace(/<start_of_turn>/g, '').trim(); // ๋ถˆํ•„์š”ํ•œ ์‹œ์ž‘ ํ† ํฐ ์ œ๊ฑฐ
502
- replyText = replyText.replace(/^['"]/, '').replace(/['"]$/, ''); // ์‹œ์ž‘/๋ ๋”ฐ์˜ดํ‘œ ์ œ๊ฑฐ (๊ฐ€๋” ๋ฐœ์ƒ)
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
- return { newScores, replyText };
518
- }
 
 
519
 
520
- // ๋žœ๋ค ์• ์นญ ์„ ํƒ ํ•จ์ˆ˜
521
- function getRandomPetName() {
522
- if (botState.userPetNames.length === 0) {
523
- // ๊ธฐ๋ณธ ์• ์นญ (์• ์ •๋„ ๊ธฐ๋ฐ˜)
524
- return botState.scores.affection > 85 ? "๋‚ด ์ „๋ถ€" : botState.scores.affection > 70 ? "๋‚ด ์‚ฌ๋ž‘" : botState.scores.affection > 55 ? "์ž๊ธฐ์•ผ" : null;
525
  }
526
- return botState.userPetNames[Math.floor(Math.random() * botState.userPetNames.length)];
 
527
  }
528
 
529
  // --- Main Interaction Logic ---
530
  async function handleUserMessage() {
531
  const userText = userInput.value.trim();
532
- if (!userText || !generator || loadingIndicator.style.display === 'flex') return; // ๋กœ๋”ฉ ์ค‘์ด๋ฉด ์ฒ˜๋ฆฌ ์•ˆ ํ•จ
 
533
 
534
- userInput.value = ''; // ์ž…๋ ฅ ํ•„๋“œ ๋น„์šฐ๊ธฐ
535
- userInput.style.height = 'auto'; // ๋†’์ด ์ดˆ๊ธฐํ™”
536
  displayMessage('user', userText);
537
  conversationHistory.push({ sender: 'user', text: userText });
538
- updateNamesFromText(userText); // ์ด๋ฆ„/์• ์นญ ์ฒ˜๋ฆฌ
539
 
540
- setLoading(true, "๋งˆ์Œ์„ ์ฝ๋Š” ์ค‘...");
541
- const prompt = buildPromptForGemma3();
542
 
543
  try {
544
- // Gemma 3 1B ๋ชจ๋ธ ํŒŒ๋ผ๋ฏธํ„ฐ (์‹คํ—˜ ํ•„์š”)
545
  const outputs = await generator(prompt, {
546
- max_new_tokens: 256, // ์ƒ์„ฑ ํ† ํฐ ์ˆ˜ (JSON ๊ณ ๋ ค)
547
- temperature: 0.75, // ์•ฝ๊ฐ„์˜ ์ฐฝ์˜์„ฑ + ์ผ๊ด€์„ฑ
548
- repetition_penalty: 1.15,
549
- top_k: 40,
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("Raw Gemma Output (cleaned):", responseOnly);
562
 
563
- const { newScores, replyText } = parseGemmaResponse(responseOnly);
 
564
 
565
- if (newScores) {
566
- botState.scores = { ...botState.scores, ...newScores };
567
- console.log("State scores updated:", botState.scores);
568
- }
569
 
570
- displayMessage('bot', replyText);
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', `[์˜ค๋ฅ˜] ๋งˆ์Œ์„ ์ „ํ•˜๋Š” ๋ฐ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ์–ด์š”: ${error.message}`);
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 === '๋“ฃ๊ณ  ์žˆ์–ด์š”...') showSpeechStatus(''); };
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
- function loadVoices() { /* ์ด์ „๊ณผ ๋™์ผ */
637
- if (!synthesis) return; let voices = synthesis.getVoices();
638
- if (voices.length === 0) { synthesis.onvoiceschanged = () => { voices = synthesis.getVoices(); findKoreanVoice(voices); }; }
639
- else { findKoreanVoice(voices); }
640
- }
641
- function findKoreanVoice(voices) { /* ์ด์ „๊ณผ ๋™์ผ (Google ์šฐ์„ ) */
642
- koreanVoice = voices.find(v => v.lang === 'ko-KR' && v.name.includes('Google')) || voices.find(v => v.lang === 'ko-KR') || voices.find(v => v.lang.startsWith('ko-'));
643
- if (koreanVoice) console.log("Korean voice:", koreanVoice.name); else console.warn("No Korean voice found.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
644
  }
645
- function speakText(text) { /* ์ด์ „๊ณผ ๋™์ผ */
646
- if (!synthesis || !botState.botSettings.useSpeechOutput) return; synthesis.cancel();
 
 
647
  const utterance = new SpeechSynthesisUtterance(text);
648
- if (koreanVoice) { utterance.voice = koreanVoice; utterance.lang = 'ko-KR'; }
649
- utterance.rate = 1.0; utterance.pitch = 1.0; synthesis.speak(utterance);
 
 
 
 
 
 
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
- userInput.addEventListener('input', () => { // ์ž…๋ ฅ ์‹œ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™”
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
  }