isaikr 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +35 -0
  2. package/cdn/api.js +19 -0
  3. package/cdn/character.js +254 -0
  4. package/cdn/chat.js +33 -0
  5. package/cdn/code-editor.js +1131 -0
  6. package/cdn/community-compose.js +270 -0
  7. package/cdn/games/2048/index.html +12 -0
  8. package/cdn/games/breakout/index.html +13 -0
  9. package/cdn/games/clicker/index.html +26 -0
  10. package/cdn/games/flappy/index.html +11 -0
  11. package/cdn/games/memory/index.html +34 -0
  12. package/cdn/games/pong/index.html +13 -0
  13. package/cdn/games/reaction/index.html +38 -0
  14. package/cdn/games/runner/index.html +11 -0
  15. package/cdn/games/snake/index.html +11 -0
  16. package/cdn/games/tetris/index.html +14 -0
  17. package/cdn/games/whack/index.html +8 -0
  18. package/cdn/go.js +126 -0
  19. package/cdn/go2.js +127 -0
  20. package/cdn/header3_behavior.js +1167 -0
  21. package/cdn/header3_layout.js +1004 -0
  22. package/cdn/header3_layout.js.bak +1004 -0
  23. package/cdn/header3_style.css +3524 -0
  24. package/cdn/header3_style.css.bak +3514 -0
  25. package/cdn/lang.js +198 -0
  26. package/cdn/loading.js +143 -0
  27. package/cdn/loading2.js +144 -0
  28. package/cdn/local-model.js +2941 -0
  29. package/cdn/main.js +4 -0
  30. package/cdn/main_asset.js +1849 -0
  31. package/cdn/main_asset.js.bak +6999 -0
  32. package/cdn/main_index.css +287 -0
  33. package/cdn/re_board3.css +733 -0
  34. package/cdn/re_board3.js +734 -0
  35. package/cdn/re_chat_tts.js +652 -0
  36. package/cdn/re_local_runtime.js +2246 -0
  37. package/cdn/re_local_runtime.js.bak +2246 -0
  38. package/cdn/re_share.js +577 -0
  39. package/cdn/re_voice.js +542 -0
  40. package/cdn/utils.js +36 -0
  41. package/cdn/view.js +321 -0
  42. package/header3_behavior.js +804 -0
  43. package/header3_layout.js +998 -0
  44. package/header3_style.css +2740 -0
  45. package/index.js +0 -0
  46. package/lang.js +179 -0
  47. package/main_asset.js +2416 -0
  48. package/main_index.css +274 -0
  49. package/package.json +14 -0
  50. package/re_chat_tts.js +1419 -0
  51. package/re_voice.js +430 -0
@@ -0,0 +1,652 @@
1
+ (function () {
2
+ const ICON_SEED = encodeURIComponent(String(window.ISAI_CLIENT_IP || "IP"));
3
+ const ASSISTANT_ICON = `https://api.dicebear.com/7.x/identicon/svg?seed=${ICON_SEED}&backgroundColor=transparent`;
4
+ const SERVER_I18N = window.ISAI_SERVER_I18N || {};
5
+
6
+ const DEFAULT_ASSISTANT = {
7
+ name: "ISAI",
8
+ icon: ASSISTANT_ICON,
9
+ };
10
+
11
+ const LOCAL_WELCOME_CTA_MESSAGES = {
12
+ ko: "로컬로 더 안전하게 대화하세요",
13
+ en: "Chat more safely in local mode",
14
+ ja: "ローカルモードでより安全に会話しましょう",
15
+ zh: "在本地模式下更安全地聊天",
16
+ "zh-tw": "在本地模式下更安全地對話",
17
+ es: "Habla de forma mas segura en modo local",
18
+ fr: "Discutez plus surement en mode local",
19
+ de: "Sicherer im lokalen Modus chatten",
20
+ pt: "Converse com mais seguranca no modo local",
21
+ ru: "Общайтесь безопаснее в локальном режиме",
22
+ ar: "تحدث بشكل أكثر أمانا في الوضع المحلي",
23
+ hi: "लोकल मोड में और अधिक सुरक्षित तरीके से चैट करें",
24
+ id: "Ngobrol lebih aman di mode lokal",
25
+ vi: "Tro chuyen an toan hon o che do cuc bo",
26
+ th: "แชทได้อย่างปลอดภัยยิ่งขึ้นในโหมดโลคัล",
27
+ tr: "Yerel modda daha guvenli sohbet edin",
28
+ it: "Chatta in modo piu sicuro in locale",
29
+ nl: "Chat veiliger in lokale modus",
30
+ pl: "Rozmawiaj bezpieczniej w trybie lokalnym"
31
+ };
32
+
33
+ const MODE_META = {
34
+ chat: { badge: "Chat", kicker: "ISAI Assistant", title: "Ready to help.", subtitle: "Chat, search, and generated results stay inside this card.", hint: "Type here to keep the conversation and outputs inside this card.", placeholder: "" },
35
+ expert: { badge: "Expert", kicker: "Expert Mode", title: "Compare multiple takes", subtitle: "Several expert-style passes are merged into one final answer.", hint: "Ask for a decision, plan, or comparison and this mode will synthesize it.", placeholder: "Ask for a deeper synthesized answer..." },
36
+ search: { badge: "Search", kicker: "Search Mode", title: "Search the web", subtitle: "Summaries and source-backed answers stay in the same chat card.", hint: "Ask for news, links, or a quick web summary.", placeholder: "What should I search for?" },
37
+ image: { badge: "Image", kicker: "Image Mode", title: "Create an image", subtitle: "Describe a scene and keep the preview in this chat workspace.", hint: "Describe the image you want to generate.", placeholder: "Describe the image you want..." },
38
+ video: { badge: "Video", kicker: "Video Mode", title: "Create a motion clip", subtitle: "Storyboard-style generations stay in the same conversation.", hint: "Describe the motion or scene for the clip.", placeholder: "Describe the video you want..." },
39
+ community: { badge: "Community", kicker: "Community Mode", title: "Post to the board", subtitle: "Write a forum-style post and attach an image if you want.", hint: "Add a nickname, password, and optional image before posting.", placeholder: "Write your community post..." },
40
+ code: { badge: "Code", kicker: "Code Mode", title: "Build with code", subtitle: "Code responses stay paired with the workspace panel on the right.", hint: "Describe the code or file you want to generate.", placeholder: "Describe the code you want..." },
41
+ blog: { badge: "Blog", kicker: "Blog Mode", title: "Draft a blog post", subtitle: "Long-form writing and image tags stay in this same thread.", hint: "Give the topic, tone, or structure you want for the post.", placeholder: "What should the blog post be about?" },
42
+ voice: { badge: "Voice", kicker: "Voice Mode", title: "Talk naturally", subtitle: "Use the mic button to speak and keep the transcript in the card.", hint: "Tap the mic button to start or pause voice capture.", placeholder: "Voice mode listens through the mic." },
43
+ translate: { badge: "Translate", kicker: "Translate Mode", title: "Translate both ways", subtitle: "Pick left and right languages, then translate in the chat card.", hint: "Use the left button for the source side and the right button for the target side.", placeholder: "Enter the text you want to translate..." },
44
+ settings: { badge: "Settings", kicker: "Voice Settings", title: "Tune voice conversation", subtitle: "Choose language, voice, speed, and tone for spoken replies.", hint: "These settings apply to voice chat and spoken responses.", placeholder: "" },
45
+ music: { badge: "Music", kicker: "Music Mode", title: "Generate audio", subtitle: "Music clips are rendered and returned in the same conversation.", hint: "Describe the mood, instruments, or energy for the track.", placeholder: "Describe the music you want..." },
46
+ app: { badge: "Apps", kicker: "Shortcut Mode", title: "Browse shortcuts", subtitle: "Open saved shortcuts and run lightweight app flows from here.", hint: "Search the shortcut list or open the app panel from the side rail.", placeholder: "Search shortcuts..." }
47
+ };
48
+
49
+ Object.keys(SERVER_I18N.modeMeta || {}).forEach((modeKey) => {
50
+ MODE_META[modeKey] = Object.assign({}, MODE_META[modeKey] || {}, SERVER_I18N.modeMeta[modeKey]);
51
+ });
52
+
53
+ const TTS_TEXT = Object.assign({ previewLabel: "Preview Voice", previewHelper: "Use your browser voice to preview settings.", failedToast: "Voice preview failed", errorPrefix: "TTS failed:" }, SERVER_I18N.ttsText || {});
54
+ const VOICE_SETTINGS_STORAGE_KEY = "ISAI_VOICE_SETTINGS";
55
+ const ASSISTANT_SETTINGS_STORAGE_KEY = "ISAI_ASSISTANT_SETTINGS";
56
+ const RECENT_MODE_STORAGE_KEY = "ISAI_RECENT_MODES";
57
+ const TRANSLATE_LANG_STORAGE_KEY = "ISAI_TRANSLATE_LANGS";
58
+ const TRANSLATE_DEFAULTS = (() => { const defaults = SERVER_I18N.translationDefaults || {}; return { left: String(defaults.left || defaults.source || "English"), right: String(defaults.right || defaults.target || "Korean") }; })();
59
+ const RECENT_MODE_LIMIT = 3;
60
+ const TRACKED_RECENT_MODES = ["chat", "search", "image", "video", "community", "code", "blog", "voice", "translate", "music"];
61
+ const DEFAULT_TTS_ENGINE = "browser";
62
+ const DEFAULT_TTS_VOICE = "";
63
+ const DEFAULT_TTS_STEPS = 1;
64
+ const DEFAULT_TTS_SPEED = 1;
65
+ const DEFAULT_TTS_VOLUME = 1;
66
+ const DEFAULT_ASSISTANT_TONE = "friendly";
67
+ const TTS_ENGINES = [{ id: "browser", label: "Browser Voice", chip: "SYS", icon: "ri-volume-up-line", description: "Use the browser built-in voice." }];
68
+ const SUPER_TTS_LANGUAGES = [
69
+ { id: "ko", label: "Korean", chip: "KO", icon: "ri-global-line" }, { id: "en", label: "English", chip: "EN", icon: "ri-global-line" },
70
+ { id: "ja", label: "Japanese", chip: "JA", icon: "ri-global-line" }, { id: "zh", label: "Chinese", chip: "ZH", icon: "ri-global-line" },
71
+ { id: "es", label: "Spanish", chip: "ES", icon: "ri-global-line" }, { id: "pt", label: "Portuguese", chip: "PT", icon: "ri-global-line" },
72
+ { id: "fr", label: "French", chip: "FR", icon: "ri-global-line" }, { id: "de", label: "German", chip: "DE", icon: "ri-global-line" },
73
+ { id: "ru", label: "Russian", chip: "RU", icon: "ri-global-line" }
74
+ ];
75
+
76
+ function getBrowserLanguage() {
77
+ const lang = (navigator.language || navigator.userLanguage || "en").toLowerCase();
78
+ if (lang.startsWith("ko")) return "ko";
79
+ if (lang.startsWith("ja")) return "ja";
80
+ if (lang.startsWith("zh")) return lang.includes("tw") || lang.includes("hk") ? "zh-tw" : "zh";
81
+ if (lang.startsWith("es")) return "es";
82
+ if (lang.startsWith("fr")) return "fr";
83
+ if (lang.startsWith("de")) return "de";
84
+ if (lang.startsWith("ru")) return "ru";
85
+ if (lang.startsWith("pt")) return "pt";
86
+ if (lang.startsWith("tr")) return "tr";
87
+ if (lang.startsWith("th")) return "th";
88
+ if (lang.startsWith("vi")) return "vi";
89
+ if (lang.startsWith("id")) return "id";
90
+ if (lang.startsWith("it")) return "it";
91
+ if (lang.startsWith("nl")) return "nl";
92
+ if (lang.startsWith("pl")) return "pl";
93
+ if (lang.startsWith("ar")) return "ar";
94
+ if (lang.startsWith("hi")) return "hi";
95
+ return "en";
96
+ }
97
+
98
+ function escapeHtml(value) { return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;"); }
99
+ function isImageErrorMessage(value) { return /^Image Error:/i.test(String(value ?? "").trim()); }
100
+ function imageErrorIconHtml(message) { const safeMessage = escapeHtml(String(message ?? "").trim() || "Image generation failed"); return `<span class="image-error-icon" title="${safeMessage}" aria-label="${safeMessage}"><i class="ri-image-line"></i><i class="ri-close-circle-fill image-error-icon-badge"></i></span>`; }
101
+ function genericErrorIconHtml(message) { const safeMessage = escapeHtml(String(message ?? "").trim() || "Request failed"); return `<span class="image-error-icon" title="${safeMessage}" aria-label="${safeMessage}"><i class="ri-error-warning-line"></i><i class="ri-close-circle-fill image-error-icon-badge"></i></span>`; }
102
+ function loadRecentModes() { try { const parsed = JSON.parse(localStorage.getItem(RECENT_MODE_STORAGE_KEY) || "[]"); if (!Array.isArray(parsed)) return []; return parsed.filter((mode) => TRACKED_RECENT_MODES.includes(mode)); } catch (error) { return []; } }
103
+ function saveRecentModes(modes) { try { localStorage.setItem(RECENT_MODE_STORAGE_KEY, JSON.stringify(modes.slice(0, 8))); } catch (error) {} }
104
+
105
+ function currentAppRef() { if (typeof activeApp !== "undefined" && activeApp) return activeApp; if (window.activeApp) return window.activeApp; return null; }
106
+ function getCurrentMode() { if (typeof currentMode !== "undefined" && currentMode) return currentMode; if (typeof selectedMode !== "undefined" && selectedMode) return selectedMode; if (window.currentMode) return window.currentMode; if (window.selectedMode) return window.selectedMode; return "chat"; }
107
+
108
+ function getTranslateSelectPair() { return { leftSelect: document.getElementById("trans-select-left"), rightSelect: document.getElementById("trans-select-right") }; }
109
+ function getTranslateOptionValues(select) { return Array.from(select?.options || []).map((option) => option.value); }
110
+ function pickTranslateValue(select, preferred, fallback) { const values = getTranslateOptionValues(select); if (!values.length) return ""; if (values.includes(preferred)) return preferred; if (values.includes(fallback)) return fallback; return values[0]; }
111
+ function pickTranslateAlternative(select, avoidValue) { const values = getTranslateOptionValues(select); if (!values.length) return ""; if (avoidValue === "English" && values.includes("Spanish")) return "Spanish"; if (avoidValue !== "English" && values.includes("English")) return "English"; const alternative = values.find((value) => value !== avoidValue); return alternative || values[0]; }
112
+ function loadTranslateLangSelection() { try { const parsed = JSON.parse(localStorage.getItem(TRANSLATE_LANG_STORAGE_KEY) || "{}"); if (!parsed || typeof parsed !== "object") return {}; return parsed; } catch (error) { return {}; } }
113
+ function saveTranslateLangSelection(leftValue, rightValue) { try { localStorage.setItem(TRANSLATE_LANG_STORAGE_KEY, JSON.stringify({ left: leftValue, right: rightValue })); } catch (error) {} }
114
+ function syncTranslateVoiceRecognition() { if (typeof updateRecognitionLang === "function") { try { updateRecognitionLang(); } catch (error) {} } }
115
+
116
+ function syncFlagSelectTrigger(select) {
117
+ if (!select) return;
118
+ const root = select.closest(".flag-select");
119
+ if (!root) return;
120
+ const trigger = root.querySelector(".flag-select-trigger");
121
+ const img = root.querySelector(".flag-select-img");
122
+ const label = root.querySelector(".flag-select-label");
123
+ const selected = select.options[select.selectedIndex] || select.options[0];
124
+ if (!selected) return;
125
+
126
+ const code = String(selected.dataset.code || selected.textContent || selected.value || "").trim().toUpperCase();
127
+ const flag = String(selected.dataset.flag || "").trim();
128
+
129
+ if (label) { label.textContent = code || selected.value; label.style.display = "none"; }
130
+ if (img) { img.src = flag || ""; img.alt = `${selected.value} flag`; }
131
+ if (trigger) { trigger.setAttribute("title", selected.value); }
132
+ }
133
+
134
+ function closeAllTranslateMenus(exceptRoot = null) {
135
+ document.querySelectorAll(".flag-select").forEach((root) => {
136
+ if (exceptRoot && root === exceptRoot) return;
137
+ const trigger = root.querySelector(".flag-select-trigger");
138
+ const menu = root.querySelector(".flag-select-menu");
139
+ if (menu) { menu.classList.add("hidden"); menu.style.display = "none"; }
140
+ if (trigger) trigger.setAttribute("aria-expanded", "false");
141
+ });
142
+ }
143
+
144
+ function renderFlagSelectMenu(select) {
145
+ const root = select?.closest(".flag-select");
146
+ if (!root) return;
147
+ const menu = root.querySelector(".flag-select-menu");
148
+ if (!menu) return;
149
+ menu.style.display = menu.classList.contains("hidden") ? "none" : "block";
150
+
151
+ const options = Array.from(select.options || []);
152
+
153
+ menu.innerHTML = options.map((option, index) => {
154
+ const code = String(option.dataset.code || option.textContent || option.value || "").trim().toUpperCase();
155
+ const flag = String(option.dataset.flag || "").trim();
156
+ const isActive = option.value === select.value;
157
+ const label = `${code} ${option.value}`.trim();
158
+ return `
159
+ <button type="button"
160
+ class="flag-select-item${isActive ? " is-active" : ""}"
161
+ data-option-index="${index}"
162
+ role="option"
163
+ aria-selected="${isActive ? "true" : "false"}"
164
+ aria-label="${escapeHtml(label)}"
165
+ title="${escapeHtml(option.value)}">
166
+ <img class="flag-select-img" src="${escapeHtml(flag)}" alt="${escapeHtml(option.value)} flag" loading="lazy" decoding="async">
167
+ </button>
168
+ `;
169
+ }).join("");
170
+
171
+ menu.querySelectorAll("[data-option-index]").forEach((button) => {
172
+ button.addEventListener("click", (event) => {
173
+ event.preventDefault();
174
+ const index = Number(button.dataset.optionIndex);
175
+ if (!Number.isFinite(index) || !options[index]) return;
176
+ const nextValue = options[index].value;
177
+ if (select.value !== nextValue) {
178
+ select.value = nextValue;
179
+ select.dispatchEvent(new Event("change", { bubbles: true }));
180
+ } else {
181
+ syncFlagSelectTrigger(select);
182
+ }
183
+ closeAllTranslateMenus();
184
+ });
185
+ });
186
+ }
187
+
188
+ function normalizeTranslatePair(changedSide = null) {
189
+ const { leftSelect, rightSelect } = getTranslateSelectPair();
190
+ if (!leftSelect || !rightSelect) return;
191
+ if (leftSelect.value === rightSelect.value) {
192
+ if (changedSide === "left") { rightSelect.value = pickTranslateAlternative(rightSelect, leftSelect.value); }
193
+ else { leftSelect.value = pickTranslateAlternative(leftSelect, rightSelect.value); }
194
+ }
195
+ saveTranslateLangSelection(leftSelect.value, rightSelect.value);
196
+ syncFlagSelectTrigger(leftSelect); syncFlagSelectTrigger(rightSelect);
197
+ syncTranslateVoiceRecognition();
198
+ }
199
+
200
+ function applyTranslateDefaults(forceServerDefaults = false) {
201
+ const { leftSelect, rightSelect } = getTranslateSelectPair();
202
+ if (!leftSelect || !rightSelect) return;
203
+ const saved = loadTranslateLangSelection();
204
+ const nextLeft = forceServerDefaults ? TRANSLATE_DEFAULTS.left : String(saved.left || TRANSLATE_DEFAULTS.left);
205
+ const nextRight = forceServerDefaults ? TRANSLATE_DEFAULTS.right : String(saved.right || TRANSLATE_DEFAULTS.right);
206
+ leftSelect.value = pickTranslateValue(leftSelect, nextLeft, TRANSLATE_DEFAULTS.left);
207
+ rightSelect.value = pickTranslateValue(rightSelect, nextRight, TRANSLATE_DEFAULTS.right);
208
+ normalizeTranslatePair();
209
+ }
210
+
211
+ function bindTranslateSelect(select, side) {
212
+ if (!select || select.dataset.flagBound === "1") return;
213
+ select.dataset.flagBound = "1";
214
+ const root = select.closest(".flag-select");
215
+ const trigger = root ? root.querySelector(".flag-select-trigger") : null;
216
+ const menu = root ? root.querySelector(".flag-select-menu") : null;
217
+ if (trigger && menu) {
218
+ trigger.addEventListener("click", (event) => {
219
+ event.preventDefault();
220
+ const willOpen = menu.classList.contains("hidden");
221
+ closeAllTranslateMenus(willOpen ? root : null);
222
+ if (willOpen) {
223
+ renderFlagSelectMenu(select);
224
+ menu.classList.remove("hidden"); menu.style.display = "block";
225
+ trigger.setAttribute("aria-expanded", "true");
226
+ } else {
227
+ menu.classList.add("hidden"); menu.style.display = "none";
228
+ trigger.setAttribute("aria-expanded", "false");
229
+ }
230
+ });
231
+ }
232
+ select.addEventListener("change", () => { normalizeTranslatePair(side); renderFlagSelectMenu(select); });
233
+ syncFlagSelectTrigger(select);
234
+ }
235
+
236
+ function initTranslateSelectors() {
237
+ const { leftSelect, rightSelect } = getTranslateSelectPair();
238
+ if (!leftSelect || !rightSelect) return;
239
+ applyTranslateDefaults(false);
240
+ bindTranslateSelect(leftSelect, "left"); bindTranslateSelect(rightSelect, "right");
241
+ renderFlagSelectMenu(leftSelect); renderFlagSelectMenu(rightSelect);
242
+ syncFlagSelectTrigger(leftSelect); syncFlagSelectTrigger(rightSelect);
243
+
244
+ if (!window.__isaiTranslateOutsideClickBound) {
245
+ window.__isaiTranslateOutsideClickBound = true;
246
+ document.addEventListener("click", (event) => {
247
+ const target = event.target;
248
+ if (target instanceof Element && target.closest(".flag-select")) return;
249
+ closeAllTranslateMenus();
250
+ });
251
+ document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeAllTranslateMenus(); });
252
+ }
253
+ }
254
+
255
+ function getLocalizedWelcomeMessage() {
256
+ const userLang = getBrowserLanguage();
257
+ return LOCAL_WELCOME_CTA_MESSAGES[userLang] || LOCAL_WELCOME_CTA_MESSAGES.en;
258
+ }
259
+
260
+ // [강력한 감지 기능] 공백을 모조리 지우고 철자만 비교하여 서버가 어떤 형태로 보내든 잡아냅니다.
261
+ function isLocalWelcomeCtaMessage(text) {
262
+ if (!text) return false;
263
+ const normalized = String(text).replace(/\s+/g, "").toLowerCase();
264
+
265
+ // 1. 강제 키워드 매칭 (가장 확실한 방법)
266
+ if (normalized.includes("chatmoresafely")) return true;
267
+ if (normalized.includes("로컬로더안전하게")) return true;
268
+ if (normalized.includes("howcanihelp")) return true;
269
+ if (normalized.includes("무엇을도와드릴까요")) return true;
270
+
271
+ // 2. 사전에 등록된 모든 다국어 문장 검사
272
+ for (const msg of Object.values(LOCAL_WELCOME_CTA_MESSAGES)) {
273
+ if (normalized.includes(String(msg).replace(/\s+/g, "").toLowerCase())) {
274
+ return true;
275
+ }
276
+ }
277
+ return false;
278
+ }
279
+
280
+ function activateLocalModeFromWelcomeBubble() {
281
+ if (typeof isLocalActive !== "undefined" && !!isLocalActive) {
282
+ if (typeof showToast === "function") showToast("이미 로컬 모드가 활성화되어 있습니다.");
283
+ return;
284
+ }
285
+ if (typeof handleLocalToggle !== "function") return;
286
+ Promise.resolve(handleLocalToggle()).then(() => {
287
+ if (typeof showToast === "function") showToast("로컬 모드를 활성화했습니다.");
288
+ }).catch(() => {});
289
+ }
290
+
291
+ // [강제 변환 함수] 무조건 기존 내용을 부수고 번역된 내용으로 교체합니다.
292
+ function bindWelcomeCtaBubble(bubble) {
293
+ if (!bubble) return;
294
+ const localizedText = getLocalizedWelcomeMessage();
295
+
296
+ bubble.classList.add("chat-welcome-cta");
297
+ bubble.innerHTML = ""; // 기존 영어 내용 완벽하게 파괴
298
+
299
+ const inner = document.createElement("span");
300
+ inner.className = "chat-welcome-cta-inner";
301
+
302
+ const icon = document.createElement("i");
303
+ icon.className = "ri-ghost-4-line chat-welcome-cta-icon";
304
+ icon.setAttribute("aria-hidden", "true");
305
+
306
+ const text = document.createElement("span");
307
+ text.className = "chat-welcome-cta-text";
308
+ text.innerHTML = escapeHtml(localizedText).replace(/\n/g, "<br>");
309
+
310
+ inner.appendChild(icon);
311
+ inner.appendChild(text);
312
+ bubble.appendChild(inner);
313
+
314
+ bubble.setAttribute("role", "button");
315
+ bubble.setAttribute("tabindex", "0");
316
+ bubble.setAttribute("title", "클릭해서 로컬 모드를 활성화");
317
+
318
+ if (bubble.dataset.welcomeCtaBound !== "1") {
319
+ bubble.dataset.welcomeCtaBound = "1";
320
+ bubble.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); activateLocalModeFromWelcomeBubble(); });
321
+ bubble.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); activateLocalModeFromWelcomeBubble(); } });
322
+ }
323
+ }
324
+
325
+ function hasActiveCharacterChatSession() { return !!(window.ISAI_CHARACTER_CHAT_SESSION && window.ISAI_CHARACTER_CHAT_SESSION.active); }
326
+
327
+ function getAssistantInfo() {
328
+ const appRef = currentAppRef();
329
+ if (appRef) { return { name: appRef.title || DEFAULT_ASSISTANT.name, icon: ASSISTANT_ICON, subtitle: appRef.description || appRef.summary || "" }; }
330
+ return DEFAULT_ASSISTANT;
331
+ }
332
+
333
+ function buildAvatarContent(info, fallbackClass) {
334
+ const name = (info && info.name) || DEFAULT_ASSISTANT.name;
335
+ const icon = info && info.icon;
336
+ if (icon) { return `<img src="${escapeHtml(icon)}" alt="${escapeHtml(name)}" onerror="this.remove();this.nextElementSibling.style.display='flex';"><span class="${fallbackClass}" style="display:none;">${escapeHtml(name.charAt(0).toUpperCase())}</span>`; }
337
+ return `<span class="${fallbackClass}">${escapeHtml(name.charAt(0).toUpperCase())}</span>`;
338
+ }
339
+
340
+ function updateHeaderAvatar(info) { const wrapper = document.querySelector("#chat-profile-card .chat-profile-avatar"); if (wrapper) wrapper.innerHTML = buildAvatarContent(info, "chat-profile-fallback"); }
341
+
342
+ function updateComposerMeta(mode = getCurrentMode()) {
343
+ const meta = MODE_META[mode] || MODE_META.chat;
344
+ const assistant = getAssistantInfo();
345
+ const appRef = currentAppRef();
346
+
347
+ const kicker = document.getElementById("composer-profile-kicker");
348
+ const title = document.getElementById("composer-profile-title");
349
+ const subtitle = document.getElementById("composer-profile-subtitle");
350
+ const badge = document.getElementById("chat-mode-badge");
351
+ const hint = document.getElementById("composer-mode-hint");
352
+ const input = document.getElementById("prompt-input");
353
+
354
+ if (kicker) kicker.textContent = appRef ? "App Mode" : meta.kicker;
355
+ if (title) title.textContent = appRef ? assistant.name : meta.title;
356
+ if (subtitle) { const st = appRef && assistant.subtitle ? assistant.subtitle : meta.subtitle; subtitle.textContent = st.length > 140 ? `${st.slice(0, 137)}...` : st; }
357
+ if (badge) badge.textContent = meta.badge;
358
+ if (hint) hint.textContent = meta.hint;
359
+ if (input && !hasActiveCharacterChatSession()) input.placeholder = "";
360
+
361
+ updateHeaderAvatar(assistant);
362
+ }
363
+
364
+ function readRecentModes() { try { const raw = localStorage.getItem(RECENT_MODE_STORAGE_KEY); const parsed = JSON.parse(raw || "[]"); if (!Array.isArray(parsed)) return []; return parsed.filter((mode, index, list) => (typeof mode === "string" && TRACKED_RECENT_MODES.includes(mode) && list.indexOf(mode) === index)); } catch (error) { return []; } }
365
+ function writeRecentModes(modes) { try { localStorage.setItem(RECENT_MODE_STORAGE_KEY, JSON.stringify((Array.isArray(modes) ? modes : []).slice(0, RECENT_MODE_LIMIT + 1))); } catch (error) { return; } }
366
+ function rememberRecentMode(mode) { if (!TRACKED_RECENT_MODES.includes(mode)) return; const nextModes = [mode].concat(readRecentModes().filter((item) => item !== mode)); writeRecentModes(nextModes); }
367
+ function getModeButton(mode) { return document.getElementById(`btn-${mode}`); }
368
+ function getModeButtonMarkup(mode) { const btn = getModeButton(mode); return btn ? btn.innerHTML : `<span>${escapeHtml(String(mode || "?").charAt(0).toUpperCase())}</span>`; }
369
+ function getModeButtonLabel(mode) { const btn = getModeButton(mode); if (!btn) return mode; return btn.getAttribute("aria-label") || btn.getAttribute("title") || mode; }
370
+ function getRenderableRecentModes(currentModeValue) { return readRecentModes().filter((mode) => mode !== currentModeValue).filter((mode) => !!getModeButton(mode)).slice(0, getRecentModeLimit()); }
371
+ function syncRecentModePadding(count) { const field = document.getElementById("chat-input-field"); if (field) field.style.setProperty("padding-right", "0px", "important"); }
372
+
373
+ function renderRecentModeActions(currentModeValue = getCurrentMode()) {
374
+ const container = document.getElementById("recent-mode-actions");
375
+ if (!container) return;
376
+ const recentModes = getRenderableRecentModes(currentModeValue);
377
+ container.innerHTML = "";
378
+ if (!recentModes.length) { container.classList.add("hidden"); syncRecentModePadding(0); return; }
379
+
380
+ recentModes.forEach((mode) => {
381
+ const button = document.createElement("button");
382
+ const label = getModeButtonLabel(mode);
383
+ button.type = "button"; button.className = "input-action-btn recent-mode-btn";
384
+ button.dataset.mode = mode; button.setAttribute("aria-label", label); button.setAttribute("title", label);
385
+ button.innerHTML = getModeButtonMarkup(mode);
386
+ button.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); if (typeof setMode === "function") setMode(mode); });
387
+ container.appendChild(button);
388
+ });
389
+
390
+ container.classList.remove("hidden");
391
+ syncRecentModePadding(recentModes.length);
392
+ }
393
+
394
+ function decodeHtmlEntities(value) { const textarea = document.createElement("textarea"); textarea.innerHTML = String(value ?? ""); return textarea.value; }
395
+ function sanitizeAssistantHtml(value) { return String(value ?? "").replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "").replace(/<img\b[^>]*>/gi, "").replace(/\son\w+=(["']).*?\1/gi, "").replace(/\son\w+=([^\s>]+)/gi, "").replace(/\s(href|src)\s*=\s*(["'])\s*javascript:[\s\S]*?\2/gi, ' $1="#"'); }
396
+ function getAssistantEmptyFallback() { const uiLanguage = getBrowserLanguage(); const map = { ko: "응답을 불러오지 못했습니다.", en: "I could not load a response." }; return map[uiLanguage] || map.en; }
397
+
398
+ function normalizeMessageHtml(role, content) {
399
+ const text = String(content ?? "");
400
+ if (role === "user") return escapeHtml(text).replace(/\n/g, "<br>");
401
+ const decoded = decodeHtmlEntities(text);
402
+ const sanitizedDecoded = sanitizeAssistantHtml(decoded);
403
+ const sanitizedRaw = sanitizeAssistantHtml(text);
404
+ const htmlCandidate = /<\/?[a-z][^>]*>/i.test(sanitizedDecoded) ? sanitizedDecoded : sanitizedRaw;
405
+ const rendered = htmlCandidate.replace(/&lt;br\s*\/?&gt;/gi, "<br>").replace(/\n/g, "<br>");
406
+ const plainText = decodeHtmlEntities(rendered).replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
407
+ if (plainText) return rendered;
408
+ const fallbackText = decodeHtmlEntities(text).replace(/\s+/g, " ").trim() || getAssistantEmptyFallback();
409
+ return escapeHtml(fallbackText).replace(/\n/g, "<br>");
410
+ }
411
+
412
+ function ensureChatSpacer(chatBox) {
413
+ if (!chatBox) return null;
414
+ const first = chatBox.firstElementChild;
415
+ if (first && first.classList && first.classList.contains("chat-spacer")) return first;
416
+ let spacer = chatBox.querySelector(".chat-spacer");
417
+ if (!spacer) { spacer = document.createElement("div"); spacer.className = "chat-spacer"; spacer.setAttribute("aria-hidden", "true"); }
418
+ chatBox.prepend(spacer);
419
+ return spacer;
420
+ }
421
+
422
+ function overwriteAppendMsg() {
423
+ if (typeof appendMsg !== "function") return;
424
+ appendMsg = function (role, content) {
425
+ const chatBox = document.getElementById("chat-box");
426
+ if (!chatBox) return null;
427
+ ensureChatSpacer(chatBox);
428
+ const entries = chatBox.querySelectorAll(".chat-entry");
429
+ if (entries.length > 60) { const oldest = entries[0]; if (oldest && oldest.parentNode) oldest.parentNode.removeChild(oldest); }
430
+
431
+ const wrapper = document.createElement("div");
432
+ const body = document.createElement("div");
433
+ const bubble = document.createElement("div");
434
+
435
+ if (role === "user") {
436
+ wrapper.className = "chat-entry user-entry";
437
+ body.className = "chat-entry-body";
438
+ bubble.className = "chat-bubble-user";
439
+ bubble.innerHTML = normalizeMessageHtml("user", content);
440
+ body.appendChild(bubble); wrapper.appendChild(body);
441
+ } else {
442
+ wrapper.className = "chat-entry ai-entry";
443
+ body.className = "chat-entry-body";
444
+ const isError = role === "error";
445
+ const imageError = isError && isImageErrorMessage(content);
446
+
447
+ // [가로채기] AI 답변이 그려지기 전에 인사말이면 강제로 변환
448
+ let isWelcome = false;
449
+ if (!isError && isLocalWelcomeCtaMessage(content)) {
450
+ isWelcome = true;
451
+ }
452
+
453
+ bubble.className = isError ? "chat-bubble-image-error" : "chat-bubble-ai";
454
+ bubble.innerHTML = isError ? (imageError ? imageErrorIconHtml(content) : genericErrorIconHtml(content)) : normalizeMessageHtml(role, content);
455
+
456
+ // 렌더링된 이후 강제 컴포넌트 교체
457
+ if (isWelcome) bindWelcomeCtaBubble(bubble);
458
+
459
+ body.appendChild(bubble); wrapper.appendChild(body);
460
+ }
461
+ chatBox.appendChild(wrapper);
462
+ if (typeof scrollBottom === "function") scrollBottom();
463
+ return bubble;
464
+ };
465
+ window.appendMsg = appendMsg;
466
+ }
467
+
468
+ function ensureWelcomeMessage(force = false) {
469
+ if (hasActiveCharacterChatSession()) return;
470
+ if (window.ISAI_ENABLE_DEFAULT_WELCOME !== true) return;
471
+ const chatBox = document.getElementById("chat-box");
472
+ if (!chatBox) return;
473
+ if (force) chatBox.innerHTML = "";
474
+ ensureChatSpacer(chatBox);
475
+ if (chatBox.querySelector(".chat-entry")) return;
476
+
477
+ const welcomeMessage = getLocalizedWelcomeMessage();
478
+ if (typeof appendMsg === "function") {
479
+ const bubble = appendMsg("ai", welcomeMessage);
480
+ bindWelcomeCtaBubble(bubble);
481
+ return;
482
+ }
483
+ }
484
+
485
+ function wrapResetChat() {
486
+ if (typeof resetChat !== "function") return;
487
+ const originalResetChat = resetChat;
488
+ resetChat = function () {
489
+ const result = originalResetChat.apply(this, arguments);
490
+ setTimeout(() => {
491
+ if (hasActiveCharacterChatSession()) { updateComposerMeta(getCurrentMode()); return; }
492
+ ensureWelcomeMessage(true); updateComposerMeta("chat");
493
+ }, 0);
494
+ return result;
495
+ };
496
+ window.resetChat = resetChat;
497
+ }
498
+
499
+ function syncCodeWorkspace(mode) {
500
+ if (typeof window.syncInlineCodePanel === "function") {
501
+ window.syncInlineCodePanel(mode);
502
+ return;
503
+ }
504
+
505
+ mode = mode || "chat";
506
+ if (document.body) document.body.setAttribute("data-ui-mode", mode);
507
+
508
+ const rightPanel = document.getElementById("right-panel");
509
+ const rightPanelOriginAnchor = document.getElementById("right-panel-origin-anchor");
510
+ const desktopCodePanelHost = document.getElementById("desktop-code-panel-host");
511
+
512
+ const isCodeMode = mode === "code";
513
+ const vw = window.innerWidth || document.documentElement.clientWidth || 0;
514
+ const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || null;
515
+ const fullscreenActive = !!(fullscreenElement && fullscreenElement.classList && fullscreenElement.classList.contains("island-box"));
516
+ const mobileCodeMode = isCodeMode && vw <= 900;
517
+ const desktopCodeMode = isCodeMode && vw > 900;
518
+
519
+ if (!rightPanel) return;
520
+
521
+ if (rightPanelOriginAnchor && rightPanelOriginAnchor.parentElement && desktopCodePanelHost) {
522
+ if (desktopCodeMode && !fullscreenActive) {
523
+ if (rightPanel.parentElement !== desktopCodePanelHost) desktopCodePanelHost.appendChild(rightPanel);
524
+ } else {
525
+ if (rightPanel.parentElement !== rightPanelOriginAnchor.parentElement) rightPanelOriginAnchor.parentElement.insertBefore(rightPanel, rightPanelOriginAnchor.nextSibling);
526
+ }
527
+ }
528
+
529
+ document.body.classList.toggle("mode-code", isCodeMode);
530
+ document.body.classList.toggle("desktop-code-open", desktopCodeMode);
531
+ document.body.classList.toggle("desktop-code-panel-mounted", desktopCodeMode && !fullscreenActive);
532
+ document.body.classList.remove("desktop-code-stage");
533
+ rightPanel.classList.toggle("mobile-active", mobileCodeMode);
534
+
535
+ if (isCodeMode) {
536
+ const codeTabs = document.getElementById("code-tabs");
537
+ const codeEditor = document.getElementById("code-editor");
538
+ if (codeTabs && !codeTabs.querySelector(".code-tab-btn")) {
539
+ codeTabs.innerHTML = '<div class="code-panel-empty"><span class="code-panel-empty-icon"><i class="ri-ai-generate-text text-sm"></i></span><span class="code-panel-empty-dots" aria-hidden="true"><span></span><span></span><span></span></span></div>';
540
+ }
541
+ if (codeEditor && !String(codeEditor.value || "").trim()) {
542
+ codeEditor.value = "// Describe the code you want and generated files will appear here.";
543
+ }
544
+
545
+ setTimeout(() => {
546
+ try {
547
+ (desktopCodeMode && desktopCodePanelHost ? desktopCodePanelHost : rightPanel).scrollIntoView({ behavior: "smooth", block: "start" });
548
+ } catch (error) {}
549
+ }, vw <= 900 ? 90 : 20);
550
+ }
551
+
552
+ if (typeof window.__applyMobileChatRailSafety === "function") {
553
+ [0, 60, 160, 360].forEach((delay) => { setTimeout(window.__applyMobileChatRailSafety, delay); });
554
+ }
555
+ }
556
+ window.syncCodeWorkspace = syncCodeWorkspace;
557
+
558
+ function wrapModeSetters() {
559
+ if (typeof setMode !== "function") return;
560
+ const originalSetMode = setMode;
561
+ setMode = function (mode) {
562
+ if (mode === "chat" && !window.ISAI_CHAT_PAGE) {
563
+ const redirectUrl = new URL("/chat.php", window.location.origin);
564
+ try {
565
+ const cdnMode = new URL(window.location.href).searchParams.get("jsdelivr");
566
+ if (cdnMode !== null) redirectUrl.searchParams.set("jsdelivr", cdnMode);
567
+ } catch (error) {}
568
+ window.location.href = redirectUrl.toString();
569
+ return;
570
+ }
571
+ if (mode !== "voice" && typeof stopVoiceMode === "function") { try { stopVoiceMode(true); } catch (error) {} }
572
+ const result = originalSetMode.apply(this, arguments);
573
+ try { currentMode = mode; } catch (error) {} try { selectedMode = mode; } catch (error) {}
574
+ window.currentMode = mode; window.selectedMode = mode;
575
+ rememberRecentMode(mode);
576
+
577
+ const ttsControls = document.getElementById("tts-controls");
578
+ const chatBox = document.getElementById("chat-box");
579
+ const chatStack = document.getElementById("chat-main-stack");
580
+ const topZone = document.getElementById("top-zone");
581
+ const leftVoiceButton = document.getElementById("btn-submit-left");
582
+ const chatInputShell = document.getElementById("chat-input-shell");
583
+
584
+ if (ttsControls) ttsControls.classList.toggle("hidden", mode !== "settings");
585
+ if (chatBox) chatBox.classList.toggle("hidden", mode === "settings");
586
+ if (chatStack) chatStack.classList.toggle("settings-mode", mode === "settings");
587
+ if (topZone) topZone.classList.toggle("settings-mode", mode === "settings");
588
+ if (chatInputShell) chatInputShell.classList.toggle("hidden", mode === "settings");
589
+
590
+ syncCodeWorkspace(mode);
591
+
592
+ if (leftVoiceButton) { leftVoiceButton.classList.remove("hidden"); leftVoiceButton.style.display = "flex"; }
593
+ if (mode === "chat") { setTimeout(() => ensureWelcomeMessage(false), 0); }
594
+ if (mode === "translate") { initTranslateSelectors(); syncTranslateVoiceRecognition(); }
595
+ else { closeAllTranslateMenus(); }
596
+
597
+ updateComposerMeta(mode);
598
+ renderRecentModeActions(mode);
599
+ return result;
600
+ };
601
+ window.setMode = setMode;
602
+ }
603
+
604
+ function initChatTtsUi() {
605
+ overwriteAppendMsg();
606
+ wrapResetChat();
607
+ wrapModeSetters();
608
+
609
+ // [핵심] 페이지 접속 시 서버가 이미 그려둔 영어 말풍선이 있는지 스캔하고, 있다면 기기 언어로 박살냅니다!
610
+ const chatBox = document.getElementById("chat-box");
611
+ if (chatBox) {
612
+ chatBox.querySelectorAll(".chat-bubble-ai").forEach(bubble => {
613
+ if (isLocalWelcomeCtaMessage(bubble.textContent || "")) {
614
+ bindWelcomeCtaBubble(bubble);
615
+ }
616
+ });
617
+ }
618
+
619
+ ensureWelcomeMessage(false);
620
+ updateComposerMeta(getCurrentMode());
621
+ renderRecentModeActions(getCurrentMode());
622
+
623
+ if (!window.__isaiRecentModeResizeBound) {
624
+ window.addEventListener("resize", () => renderRecentModeActions(getCurrentMode()));
625
+ window.__isaiRecentModeResizeBound = true;
626
+ }
627
+
628
+ const ttsControls = document.getElementById("tts-controls");
629
+ const chatStack = document.getElementById("chat-main-stack");
630
+ const topZone = document.getElementById("top-zone");
631
+ const chatInputShell = document.getElementById("chat-input-shell");
632
+
633
+ if (ttsControls) ttsControls.classList.toggle("hidden", getCurrentMode() !== "settings");
634
+ if (chatBox) chatBox.classList.toggle("hidden", getCurrentMode() === "settings");
635
+ if (chatStack) chatStack.classList.toggle("settings-mode", getCurrentMode() === "settings");
636
+ if (topZone) topZone.classList.toggle("settings-mode", getCurrentMode() === "settings");
637
+ if (chatInputShell) chatInputShell.classList.toggle("hidden", getCurrentMode() === "settings");
638
+
639
+ syncCodeWorkspace(getCurrentMode());
640
+ if (getCurrentMode() === "translate") { syncTranslateVoiceRecognition(); }
641
+ }
642
+
643
+ if (document.readyState === "loading") {
644
+ document.addEventListener("DOMContentLoaded", initChatTtsUi);
645
+ } else {
646
+ initChatTtsUi();
647
+ }
648
+ })();
649
+
650
+ function getRecentModeLimit() {
651
+ return 1;
652
+ }