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.
- package/README.md +35 -0
- package/cdn/api.js +19 -0
- package/cdn/character.js +254 -0
- package/cdn/chat.js +33 -0
- package/cdn/code-editor.js +1131 -0
- package/cdn/community-compose.js +270 -0
- package/cdn/games/2048/index.html +12 -0
- package/cdn/games/breakout/index.html +13 -0
- package/cdn/games/clicker/index.html +26 -0
- package/cdn/games/flappy/index.html +11 -0
- package/cdn/games/memory/index.html +34 -0
- package/cdn/games/pong/index.html +13 -0
- package/cdn/games/reaction/index.html +38 -0
- package/cdn/games/runner/index.html +11 -0
- package/cdn/games/snake/index.html +11 -0
- package/cdn/games/tetris/index.html +14 -0
- package/cdn/games/whack/index.html +8 -0
- package/cdn/go.js +126 -0
- package/cdn/go2.js +127 -0
- package/cdn/header3_behavior.js +1167 -0
- package/cdn/header3_layout.js +1004 -0
- package/cdn/header3_layout.js.bak +1004 -0
- package/cdn/header3_style.css +3524 -0
- package/cdn/header3_style.css.bak +3514 -0
- package/cdn/lang.js +198 -0
- package/cdn/loading.js +143 -0
- package/cdn/loading2.js +144 -0
- package/cdn/local-model.js +2941 -0
- package/cdn/main.js +4 -0
- package/cdn/main_asset.js +1849 -0
- package/cdn/main_asset.js.bak +6999 -0
- package/cdn/main_index.css +287 -0
- package/cdn/re_board3.css +733 -0
- package/cdn/re_board3.js +734 -0
- package/cdn/re_chat_tts.js +652 -0
- package/cdn/re_local_runtime.js +2246 -0
- package/cdn/re_local_runtime.js.bak +2246 -0
- package/cdn/re_share.js +577 -0
- package/cdn/re_voice.js +542 -0
- package/cdn/utils.js +36 -0
- package/cdn/view.js +321 -0
- package/header3_behavior.js +804 -0
- package/header3_layout.js +998 -0
- package/header3_style.css +2740 -0
- package/index.js +0 -0
- package/lang.js +179 -0
- package/main_asset.js +2416 -0
- package/main_index.css +274 -0
- package/package.json +14 -0
- package/re_chat_tts.js +1419 -0
- package/re_voice.js +430 -0
package/main_asset.js
ADDED
|
@@ -0,0 +1,2416 @@
|
|
|
1
|
+
|
|
2
|
+
let recognition = null;
|
|
3
|
+
let isVoiceProcessing = false;
|
|
4
|
+
let isVoiceListening = false;
|
|
5
|
+
let targetLangCode = "en-US";
|
|
6
|
+
let musicTokenizer = null;
|
|
7
|
+
let musicModel = null;
|
|
8
|
+
let currentMode = "chat";
|
|
9
|
+
let chatHistory =[];
|
|
10
|
+
let wllama = null;
|
|
11
|
+
let localEngine = null;
|
|
12
|
+
let localRuntime = localStorage.getItem((window.MODEL_CONFIG && window.MODEL_CONFIG.runtimeKey) || "ISAI_LOCAL_MODEL_RUNTIME_V1") || null;
|
|
13
|
+
let isModelDownloaded = false;
|
|
14
|
+
let isModelLoaded = false;
|
|
15
|
+
let isLocalActive = false;
|
|
16
|
+
let isStarted = false;
|
|
17
|
+
let isGenerating = false;
|
|
18
|
+
let abortController = null;
|
|
19
|
+
let stopSignal = false;
|
|
20
|
+
let isMenuOpen = false;
|
|
21
|
+
let activeApp = null;
|
|
22
|
+
let searchTimeout = null;
|
|
23
|
+
let currentStorePage = 1;
|
|
24
|
+
let isStoreLoading = false;
|
|
25
|
+
let hasMoreStoreApps = true;
|
|
26
|
+
let currentStoreQuery = "";
|
|
27
|
+
let currentStoreCategory = "All";
|
|
28
|
+
|
|
29
|
+
let currentAppPage = 1;
|
|
30
|
+
let isAppLoading = false;
|
|
31
|
+
let hasMoreApps = true;
|
|
32
|
+
let currentAppQuery = "";
|
|
33
|
+
|
|
34
|
+
const STORE_LIMIT = 12;
|
|
35
|
+
const APP_LIMIT = 24;
|
|
36
|
+
|
|
37
|
+
function filterStore(category, element) {
|
|
38
|
+
document.querySelectorAll(".store-filter-btn").forEach((btn) => btn.classList.remove("active"));
|
|
39
|
+
if (element) element.classList.add("active");
|
|
40
|
+
currentStoreCategory = category;
|
|
41
|
+
fetchStoreApps(currentStoreQuery, false);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderFixedApps() {
|
|
45
|
+
const container = document.getElementById("fixed-apps-list");
|
|
46
|
+
container.innerHTML = "";
|
|
47
|
+
|
|
48
|
+
if (fixedApps.length !== 0) {
|
|
49
|
+
container.style.display = "flex";
|
|
50
|
+
fixedApps.forEach((app, index) => {
|
|
51
|
+
const item = document.createElement("div");
|
|
52
|
+
item.className = "fixed-app-item relative w-[44px] h-[44px] rounded-[14px]";
|
|
53
|
+
item.title = app.title;
|
|
54
|
+
item.onclick = () => loadAppDetails(app.id);
|
|
55
|
+
item.innerHTML = getAppIconHtml(app) + `
|
|
56
|
+
<button class="absolute -top-1 -right-1 bg-red-500 text-white rounded-full w-4 h-4 flex items-center justify-center text-[10px] z-20 shadow-md transition-transform hover:scale-110" onclick="removeFixedApp(event, ${index})" title="Remove">
|
|
57
|
+
<i class="ri-close-line"></i>
|
|
58
|
+
</button>
|
|
59
|
+
`;
|
|
60
|
+
container.appendChild(item);
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
container.style.display = "none";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function saveCurrentApp() {
|
|
68
|
+
if (activeApp) {
|
|
69
|
+
if (fixedApps.some((app) => app.id === activeApp.id)) {
|
|
70
|
+
showToast("Already added to favorites.");
|
|
71
|
+
} else {
|
|
72
|
+
fixedApps.push({ id: activeApp.id, title: activeApp.title, icon_url: activeApp.icon_url });
|
|
73
|
+
localStorage.setItem("ISAI_FIXED_APPS", JSON.stringify(fixedApps));
|
|
74
|
+
renderFixedApps();
|
|
75
|
+
showToast("Saved to favorites!");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function applyStoreMenuState(open) {
|
|
81
|
+
if (typeof window.setStoreMenuState === "function") {
|
|
82
|
+
window.setStoreMenuState(open);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const shouldOpen = !!open;
|
|
87
|
+
const chatStack = document.getElementById("chat-main-stack");
|
|
88
|
+
const topZone = document.getElementById("top-zone");
|
|
89
|
+
const storePanel = document.getElementById("store-panel");
|
|
90
|
+
const appPanel = document.getElementById("app-container");
|
|
91
|
+
const btnMenu = document.getElementById("btn-menu");
|
|
92
|
+
const iconMenu = document.getElementById("icon-menu");
|
|
93
|
+
const promptInput = document.getElementById("prompt-input");
|
|
94
|
+
|
|
95
|
+
if (chatStack) chatStack.classList.toggle("menu-mode", shouldOpen);
|
|
96
|
+
if (topZone) topZone.classList.toggle("menu-mode", shouldOpen);
|
|
97
|
+
if (storePanel) storePanel.classList.toggle("open", shouldOpen);
|
|
98
|
+
if (appPanel) appPanel.classList.remove("open");
|
|
99
|
+
if (chatStack) {
|
|
100
|
+
if (shouldOpen) {
|
|
101
|
+
chatStack.style.setProperty("height", "100%", "important");
|
|
102
|
+
chatStack.style.setProperty("min-height", "0", "important");
|
|
103
|
+
chatStack.style.setProperty("max-height", "100%", "important");
|
|
104
|
+
} else {
|
|
105
|
+
chatStack.style.removeProperty("height");
|
|
106
|
+
chatStack.style.removeProperty("min-height");
|
|
107
|
+
chatStack.style.removeProperty("max-height");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (topZone) {
|
|
111
|
+
if (shouldOpen) {
|
|
112
|
+
topZone.style.setProperty("display", "none", "important");
|
|
113
|
+
topZone.style.setProperty("height", "0", "important");
|
|
114
|
+
topZone.style.setProperty("min-height", "0", "important");
|
|
115
|
+
topZone.style.setProperty("max-height", "0", "important");
|
|
116
|
+
topZone.style.setProperty("flex", "0 0 auto", "important");
|
|
117
|
+
topZone.style.setProperty("padding", "0", "important");
|
|
118
|
+
topZone.style.setProperty("margin", "0", "important");
|
|
119
|
+
topZone.style.setProperty("overflow", "hidden", "important");
|
|
120
|
+
topZone.style.setProperty("visibility", "hidden", "important");
|
|
121
|
+
topZone.style.setProperty("opacity", "0", "important");
|
|
122
|
+
} else {
|
|
123
|
+
topZone.style.removeProperty("display");
|
|
124
|
+
topZone.style.removeProperty("height");
|
|
125
|
+
topZone.style.removeProperty("min-height");
|
|
126
|
+
topZone.style.removeProperty("max-height");
|
|
127
|
+
topZone.style.removeProperty("flex");
|
|
128
|
+
topZone.style.removeProperty("padding");
|
|
129
|
+
topZone.style.removeProperty("margin");
|
|
130
|
+
topZone.style.removeProperty("overflow");
|
|
131
|
+
topZone.style.removeProperty("visibility");
|
|
132
|
+
topZone.style.removeProperty("opacity");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (btnMenu) {
|
|
136
|
+
btnMenu.classList.toggle("menu-open", shouldOpen);
|
|
137
|
+
btnMenu.classList.toggle("text-white", shouldOpen);
|
|
138
|
+
}
|
|
139
|
+
if (iconMenu) {
|
|
140
|
+
iconMenu.className = shouldOpen ? "ri-draggable text-xl" : "ri-draggable text-lg";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
isMenuOpen = shouldOpen;
|
|
144
|
+
window.isMenuOpen = shouldOpen;
|
|
145
|
+
|
|
146
|
+
if (shouldOpen && typeof fetchStoreApps === "function") {
|
|
147
|
+
const query = promptInput ? promptInput.value.trim() : "";
|
|
148
|
+
fetchStoreApps(query, false);
|
|
149
|
+
}
|
|
150
|
+
if (typeof window.__applyMobileChatRailSafety === "function") {
|
|
151
|
+
[0, 40, 120, 260].forEach((delay) => {
|
|
152
|
+
setTimeout(window.__applyMobileChatRailSafety, delay);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function toggleStoreMenu() {
|
|
158
|
+
const chatStack = document.getElementById("chat-main-stack");
|
|
159
|
+
const shouldOpen = !(chatStack && chatStack.classList.contains("menu-mode"));
|
|
160
|
+
if (shouldOpen && typeof window.currentMode !== "undefined" && window.currentMode !== "chat" && typeof window.setMode === "function") {
|
|
161
|
+
window.setMode("chat");
|
|
162
|
+
}
|
|
163
|
+
applyStoreMenuState(shouldOpen);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function toggleAppPanel() {
|
|
167
|
+
const appPanel = document.getElementById('app-container');
|
|
168
|
+
const btnApp = document.getElementById('btn-app');
|
|
169
|
+
|
|
170
|
+
if (!appPanel.classList.contains('open')) {
|
|
171
|
+
applyStoreMenuState(false);
|
|
172
|
+
appPanel.classList.add('open');
|
|
173
|
+
appPanel.style.display = "block";
|
|
174
|
+
if (btnApp) btnApp.classList.add('active', 'text-white', 'bg-[#262626]');
|
|
175
|
+
setMode('app');
|
|
176
|
+
fetchApps(document.getElementById('prompt-input').value.trim());
|
|
177
|
+
} else {
|
|
178
|
+
appPanel.classList.remove('open');
|
|
179
|
+
appPanel.style.display = "none";
|
|
180
|
+
if (btnApp) btnApp.classList.remove('active', 'text-white', 'bg-[#262626]');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function handleInput(element) {
|
|
185
|
+
element.style.height = "auto";
|
|
186
|
+
const maxHeight = window.innerWidth <= 900 ? 64 : 72;
|
|
187
|
+
const nextHeight = Math.min(element.scrollHeight, maxHeight);
|
|
188
|
+
element.style.height = nextHeight + "px";
|
|
189
|
+
element.style.overflowY = element.scrollHeight > maxHeight ? "auto" : "hidden";
|
|
190
|
+
|
|
191
|
+
if (isMenuOpen) {
|
|
192
|
+
const query = element.value.trim();
|
|
193
|
+
clearTimeout(searchTimeout);
|
|
194
|
+
searchTimeout = setTimeout(() => {
|
|
195
|
+
fetchStoreApps(query, false);
|
|
196
|
+
}, 500);
|
|
197
|
+
} else if (currentMode === "app") {
|
|
198
|
+
const query = element.value.trim();
|
|
199
|
+
clearTimeout(searchTimeout);
|
|
200
|
+
searchTimeout = setTimeout(() => {
|
|
201
|
+
fetchApps(query, false);
|
|
202
|
+
}, 500);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function fetchStoreApps(query, append = false) {
|
|
207
|
+
const loader = document.getElementById("store-loader");
|
|
208
|
+
const grid = document.getElementById("store-grid");
|
|
209
|
+
|
|
210
|
+
if (!append) {
|
|
211
|
+
currentStorePage = 1;
|
|
212
|
+
hasMoreStoreApps = true;
|
|
213
|
+
currentStoreQuery = query;
|
|
214
|
+
grid.innerHTML = "";
|
|
215
|
+
grid.scrollTop = 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!hasMoreStoreApps || isStoreLoading) return;
|
|
219
|
+
|
|
220
|
+
isStoreLoading = true;
|
|
221
|
+
loader.classList.remove("hidden");
|
|
222
|
+
|
|
223
|
+
let url = `re_store.php?action=list_apps&limit=${STORE_LIMIT}&page=${currentStorePage}`;
|
|
224
|
+
if (query) {
|
|
225
|
+
url += `&q=${encodeURIComponent(query)}`;
|
|
226
|
+
} else if (currentStoreCategory !== "All") {
|
|
227
|
+
url += `&cat=${encodeURIComponent(currentStoreCategory)}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const response = await fetch(url);
|
|
232
|
+
const json = await response.json();
|
|
233
|
+
const data = json.data ||[];
|
|
234
|
+
|
|
235
|
+
loader.classList.add("hidden");
|
|
236
|
+
|
|
237
|
+
if (data.length > 0) {
|
|
238
|
+
renderStoreItems(data, grid);
|
|
239
|
+
if (data.length < STORE_LIMIT) {
|
|
240
|
+
hasMoreStoreApps = false;
|
|
241
|
+
} else {
|
|
242
|
+
currentStorePage++;
|
|
243
|
+
}
|
|
244
|
+
} else if (!append) {
|
|
245
|
+
hasMoreStoreApps = false;
|
|
246
|
+
const noResultsMsg = typeof T !== "undefined" && T.no_results ? T.no_results : "No apps found.";
|
|
247
|
+
grid.innerHTML = `<div class="col-span-full text-center text-gray-500 text-[10px] py-4">${noResultsMsg}</div>`;
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
loader.classList.add("hidden");
|
|
251
|
+
isStoreLoading = false;
|
|
252
|
+
if (!append) {
|
|
253
|
+
grid.innerHTML = '<div class="col-span-full text-center text-gray-500 text-[10px] py-4">Failed to load.</div>';
|
|
254
|
+
}
|
|
255
|
+
} finally {
|
|
256
|
+
isStoreLoading = false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function loadAppDetails(id) {
|
|
261
|
+
showLoader(true);
|
|
262
|
+
try {
|
|
263
|
+
const response = await fetch(`re_store.php?action=detail_app&id=${id}`);
|
|
264
|
+
const data = await response.json();
|
|
265
|
+
|
|
266
|
+
if (data && !data.error) {
|
|
267
|
+
activeApp = data;
|
|
268
|
+
activateAppMode();
|
|
269
|
+
|
|
270
|
+
const category = (data.category || "").toLowerCase();
|
|
271
|
+
if (category === "code") setMode("code");
|
|
272
|
+
else if (category === "image") setMode("image");
|
|
273
|
+
else if (category === "music") setMode("music");
|
|
274
|
+
else if (category === "video") setMode("video");
|
|
275
|
+
else if (category === "blog") setMode("blog");
|
|
276
|
+
else setMode("chat");
|
|
277
|
+
|
|
278
|
+
showToast(`App Loaded: ${data.title}`);
|
|
279
|
+
} else {
|
|
280
|
+
showToast("App not found");
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
showToast("Error loading app details");
|
|
284
|
+
} finally {
|
|
285
|
+
showLoader(false);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function executeAction(side = "right") {
|
|
290
|
+
if (typeof side !== "string") side = "right";
|
|
291
|
+
|
|
292
|
+
if (side === "left" && currentMode === "voice") {
|
|
293
|
+
if (isVoiceProcessing) return;
|
|
294
|
+
|
|
295
|
+
if (isVoiceListening) {
|
|
296
|
+
if (translationSide === side) {
|
|
297
|
+
isVoiceListening = false;
|
|
298
|
+
stopVoiceMode(false);
|
|
299
|
+
showToast("Listening Paused");
|
|
300
|
+
if (recognition) {
|
|
301
|
+
recognition.abort();
|
|
302
|
+
recognition = null;
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
translationSide = side;
|
|
309
|
+
setTimeout(() => {
|
|
310
|
+
try {
|
|
311
|
+
isVoiceListening = true;
|
|
312
|
+
setVoiceState(side, "recording");
|
|
313
|
+
if (recognition) {
|
|
314
|
+
updateRecognitionLang();
|
|
315
|
+
recognition.start();
|
|
316
|
+
} else {
|
|
317
|
+
initVoiceMode();
|
|
318
|
+
}
|
|
319
|
+
} catch (error) {
|
|
320
|
+
initVoiceMode();
|
|
321
|
+
}
|
|
322
|
+
}, 100);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (side !== "left" && currentMode === "voice" && isVoiceListening) {
|
|
327
|
+
isVoiceListening = false;
|
|
328
|
+
stopVoiceMode(false);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const inputElement = document.getElementById("prompt-input");
|
|
332
|
+
const userText = inputElement.value.trim();
|
|
333
|
+
|
|
334
|
+
if (isMenuOpen && userText) {
|
|
335
|
+
fetchStoreApps(userText);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (currentMode === "app") {
|
|
340
|
+
inputElement.value = "";
|
|
341
|
+
inputElement.style.height = "auto";
|
|
342
|
+
fetchApps(userText);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (currentMode !== "community") {
|
|
347
|
+
if (!userText) return;
|
|
348
|
+
|
|
349
|
+
window.currentImagePrompt = userText;
|
|
350
|
+
window.savedPrompt = userText;
|
|
351
|
+
startExperience();
|
|
352
|
+
|
|
353
|
+
if (typeof closePreview === "function") closePreview();
|
|
354
|
+
|
|
355
|
+
inputElement.value = "";
|
|
356
|
+
inputElement.style.height = "auto";
|
|
357
|
+
showLoader(true);
|
|
358
|
+
appendMsg("user", userText);
|
|
359
|
+
|
|
360
|
+
if (abortController) abortController.abort();
|
|
361
|
+
abortController = new AbortController();
|
|
362
|
+
stopSignal = false;
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
if (activeApp) {
|
|
366
|
+
const apiUrl = activeApp.api_url && activeApp.api_url.trim() !== "";
|
|
367
|
+
const category = (activeApp.category || "").toLowerCase();
|
|
368
|
+
|
|
369
|
+
if (apiUrl && !["image", "code", "music", "video", "blog", "character"].includes(category)) {
|
|
370
|
+
await executeAppLogic(userText);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (currentMode === "translate") {
|
|
376
|
+
const langLeft = document.getElementById("trans-select-left").value;
|
|
377
|
+
const langRight = document.getElementById("trans-select-right").value;
|
|
378
|
+
|
|
379
|
+
let sourceLang = side === "left" ? langLeft : langRight;
|
|
380
|
+
let targetLang = side === "left" ? langRight : langLeft;
|
|
381
|
+
|
|
382
|
+
const response = await fetch("?action=ai_translate", {
|
|
383
|
+
method: "POST",
|
|
384
|
+
body: JSON.stringify({ text: userText, target_lang: targetLang, source_lang: sourceLang }),
|
|
385
|
+
signal: abortController.signal
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const data = await response.json();
|
|
389
|
+
|
|
390
|
+
if (data.error === "LIMIT_REACHED") {
|
|
391
|
+
showToast("Limit reached. Local Translation...");
|
|
392
|
+
if (!isModelLoaded) await startDownload();
|
|
393
|
+
|
|
394
|
+
const localPrompt =[
|
|
395
|
+
{ role: "system", content: `Translate the following text to ${targetLang}. Output ONLY the translated text.` },
|
|
396
|
+
{ role: "user", content: userText }
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
let localResult = "";
|
|
400
|
+
const bubble = appendMsg("ai", "...");
|
|
401
|
+
const speechLang = langMap[targetLang] || "en-US";
|
|
402
|
+
|
|
403
|
+
await runLocalInference(localPrompt, (token) => {
|
|
404
|
+
if (!stopSignal) {
|
|
405
|
+
localResult += token;
|
|
406
|
+
bubble.innerHTML = `<div class="text-lg font-bold text-blue-300 leading-relaxed">${localResult}</div>`;
|
|
407
|
+
scrollBottom();
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
if (!stopSignal) speakText(localResult, speechLang);
|
|
412
|
+
} else if (data.error) {
|
|
413
|
+
appendMsg("error", data.error);
|
|
414
|
+
} else {
|
|
415
|
+
let resultObj = { text: data.response };
|
|
416
|
+
try {
|
|
417
|
+
let cleanJson = data.response.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/```json/g, "").replace(/```/g, "").trim();
|
|
418
|
+
let jsonMatch = cleanJson.match(/\{[\s\S]*\}/);
|
|
419
|
+
if (jsonMatch) cleanJson = jsonMatch[0];
|
|
420
|
+
resultObj = JSON.parse(cleanJson);
|
|
421
|
+
} catch (err) {
|
|
422
|
+
resultObj.text = data.response;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
appendMsg("ai", `<div class="text-lg font-bold text-blue-300 leading-relaxed">${resultObj.text}</div>`);
|
|
426
|
+
const speechLang = langMap[targetLang] || "en-US";
|
|
427
|
+
speakText(resultObj.text, speechLang);
|
|
428
|
+
}
|
|
429
|
+
} else if (currentMode === "video") {
|
|
430
|
+
let prompt = userText;
|
|
431
|
+
if (activeApp && activeApp.system_prompt) prompt = `${activeApp.system_prompt}, ${userText}`;
|
|
432
|
+
|
|
433
|
+
const gridPrompt = "2x2 grid " + prompt;
|
|
434
|
+
const response = await fetch("?action=ai_video", {
|
|
435
|
+
method: "POST",
|
|
436
|
+
body: JSON.stringify({ prompt: gridPrompt, watermark: false }),
|
|
437
|
+
signal: abortController.signal
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const data = await response.json();
|
|
441
|
+
|
|
442
|
+
if (data.error) {
|
|
443
|
+
appendMsg("error", "Video Error: " + data.error);
|
|
444
|
+
} else {
|
|
445
|
+
generateGifFromGrid(data.b64, data.translated || prompt);["app-container", "welcome-msg", "center-app-name"].forEach((id) => {
|
|
446
|
+
const el = document.getElementById(id);
|
|
447
|
+
if (el) el.style.display = "none";
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
} else if (currentMode === "image") {
|
|
451
|
+
let prompt = userText;
|
|
452
|
+
if (activeApp && activeApp.system_prompt) prompt = `${activeApp.system_prompt}, ${userText}`;
|
|
453
|
+
|
|
454
|
+
const ratio = document.getElementById("image-ratio-value")?.value || "square";
|
|
455
|
+
const response = await fetch("?action=ai_image", {
|
|
456
|
+
method: "POST",
|
|
457
|
+
body: JSON.stringify({ prompt: prompt, ratio: ratio }),
|
|
458
|
+
signal: abortController.signal
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const data = await response.json();
|
|
462
|
+
|
|
463
|
+
if (data.error) {
|
|
464
|
+
appendMsg("error", "Image Error: " + data.error);
|
|
465
|
+
} else {
|
|
466
|
+
window.currentImagePrompt = prompt;
|
|
467
|
+
appendImg(data.b64, prompt);
|
|
468
|
+
openPreview("data:image/png;base64," + data.b64);["app-container", "welcome-msg", "center-app-name"].forEach((id) => {
|
|
469
|
+
const el = document.getElementById(id);
|
|
470
|
+
if (el) el.style.display = "none";
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
} else if (currentMode === "music") {
|
|
474
|
+
let prompt = userText;
|
|
475
|
+
if (activeApp && activeApp.system_prompt) prompt = `${activeApp.system_prompt}, ${userText}`;
|
|
476
|
+
await generateMusic(prompt);
|
|
477
|
+
} else if (currentMode === "search") {
|
|
478
|
+
const response = await fetch("?action=search_data", {
|
|
479
|
+
method: "POST",
|
|
480
|
+
body: JSON.stringify({ prompt: userText }),
|
|
481
|
+
signal: abortController.signal
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// --- 수정된 부분: 안전한 JSON 파싱 ---
|
|
485
|
+
const rawText = await response.text();
|
|
486
|
+
let data = {};
|
|
487
|
+
try {
|
|
488
|
+
data = rawText ? JSON.parse(rawText) : {};
|
|
489
|
+
} catch (e) {
|
|
490
|
+
console.error("Search Data JSON 파싱 에러:", rawText);
|
|
491
|
+
throw new Error("검색 서버로부터 올바른 데이터를 받지 못했습니다.");
|
|
492
|
+
}
|
|
493
|
+
// -------------------------------------
|
|
494
|
+
|
|
495
|
+
if (data.results && data.results.length > 0) {
|
|
496
|
+
const topResults = data.results.slice(0, 5);
|
|
497
|
+
let contextStr = topResults.map((item, idx) => `[문서 ${idx + 1}] ${item.content}`).join("\n");
|
|
498
|
+
|
|
499
|
+
const synthResponse = await fetch("?action=search_synthesis", {
|
|
500
|
+
method: "POST",
|
|
501
|
+
body: JSON.stringify({ query: userText, context: contextStr }),
|
|
502
|
+
signal: abortController.signal
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// --- 수정된 부분: 안전한 JSON 파싱 ---
|
|
506
|
+
const rawSynthText = await synthResponse.text();
|
|
507
|
+
let synthData = {};
|
|
508
|
+
try {
|
|
509
|
+
synthData = rawSynthText ? JSON.parse(rawSynthText) : {};
|
|
510
|
+
} catch (e) {
|
|
511
|
+
console.error("Search Synthesis JSON 파싱 에러:", rawSynthText);
|
|
512
|
+
throw new Error("검색 요약 서버로부터 올바른 데이터를 받지 못했습니다.");
|
|
513
|
+
}
|
|
514
|
+
// -------------------------------------
|
|
515
|
+
|
|
516
|
+
if (synthData.error === "LIMIT_REACHED") {
|
|
517
|
+
showToast("Server limit reached. Local Summarizing...");
|
|
518
|
+
if (!isModelLoaded) await startDownload();
|
|
519
|
+
|
|
520
|
+
const bubble = appendMsg("ai", "...");
|
|
521
|
+
let localResult = "";
|
|
522
|
+
const localPrompt =[
|
|
523
|
+
{ role: "system", content: "넌 천재 요약봇." },
|
|
524
|
+
{ role: "user", content: `${contextStr}\n위 내용을 기반으로 답변해줘.` }
|
|
525
|
+
];
|
|
526
|
+
|
|
527
|
+
await runLocalInference(localPrompt, (token) => {
|
|
528
|
+
if (!stopSignal) {
|
|
529
|
+
localResult += token;
|
|
530
|
+
bubble.innerHTML = parseMarkdownLocal(localResult, false);
|
|
531
|
+
scrollBottom();
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
if (!stopSignal) {
|
|
536
|
+
bubble.innerHTML = parseMarkdownLocal(localResult, true);
|
|
537
|
+
addSourcesToBubble(bubble, topResults);
|
|
538
|
+
scrollBottom();
|
|
539
|
+
}
|
|
540
|
+
} else if (synthData.error) {
|
|
541
|
+
// 서버에서 에러 메시지를 보낸 경우 처리
|
|
542
|
+
appendMsg("error", "검색 요약 오류: " + synthData.error);
|
|
543
|
+
} else {
|
|
544
|
+
addSourcesToBubble(appendMsg("ai", synthData.html || "Thinking..."), topResults);
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
appendMsg("ai", "검색 결과가 없습니다.");
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
let finalPrompt = userText;
|
|
551
|
+
let currentHistory = chatHistory;
|
|
552
|
+
let sysPrompt = typeof SYSTEM_PROMPTS !== "undefined" && SYSTEM_PROMPTS[LANG] ? SYSTEM_PROMPTS[LANG] : "You are a helpful AI.";
|
|
553
|
+
|
|
554
|
+
if (activeApp) {
|
|
555
|
+
if (activeApp.system_prompt) sysPrompt = activeApp.system_prompt;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (typeof window.buildIsaiSystemPrompt === "function") {
|
|
559
|
+
sysPrompt = window.buildIsaiSystemPrompt(sysPrompt);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (activeApp && activeApp.category === "Code") {
|
|
563
|
+
finalPrompt = `System: ${sysPrompt}\n\nUser Request: ${userText}`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (currentMode === "blog") {
|
|
567
|
+
finalPrompt = `Topic: "${userText}". Instructions: ${sysPrompt}. Lang: ${LANG === "ko" ? "Korean" : "English"}. Add [[IMG: keyword]] tags.`;
|
|
568
|
+
currentHistory =[];
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (isModelLoaded && isLocalActive) {
|
|
572
|
+
const localPromptArr =[
|
|
573
|
+
{ role: "system", content: sysPrompt },
|
|
574
|
+
...currentHistory.slice(-4),
|
|
575
|
+
{ role: "user", content: finalPrompt }
|
|
576
|
+
];
|
|
577
|
+
|
|
578
|
+
let localResult = "";
|
|
579
|
+
const bubble = appendMsg("ai", "...");
|
|
580
|
+
|
|
581
|
+
await runLocalInference(localPromptArr, (token) => {
|
|
582
|
+
if (!stopSignal) {
|
|
583
|
+
localResult += token;
|
|
584
|
+
bubble.innerHTML = parseMarkdownLocal(localResult, false);
|
|
585
|
+
scrollBottom();
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
if (!stopSignal) {
|
|
590
|
+
bubble.innerHTML = parseMarkdownLocal(localResult, true);
|
|
591
|
+
updateCodeUI(localResult);
|
|
592
|
+
let historyResult = localResult.replace(/<think>[\s\S]*?(<\/think>|$)/gi, "").trim();
|
|
593
|
+
chatHistory.push({ role: "user", content: userText }, { role: "assistant", content: historyResult });
|
|
594
|
+
if (currentMode === "blog") await processBlogImages(bubble);
|
|
595
|
+
scrollBottom();
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
const response = await fetch("?action=ai_chat", {
|
|
599
|
+
method: "POST",
|
|
600
|
+
body: JSON.stringify({ prompt: finalPrompt, history: currentHistory, system_prompt: sysPrompt }),
|
|
601
|
+
signal: abortController.signal
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const data = await response.json();
|
|
605
|
+
|
|
606
|
+
if (data.error === "LIMIT_REACHED") {
|
|
607
|
+
showToast("Limit reached. Local Model...");
|
|
608
|
+
if (!isModelLoaded) await startDownload();
|
|
609
|
+
|
|
610
|
+
const bubble = appendMsg("ai", "...");
|
|
611
|
+
let localResult = "";
|
|
612
|
+
const localPromptArr =[
|
|
613
|
+
{ role: "system", content: sysPrompt },
|
|
614
|
+
...currentHistory.slice(-4),
|
|
615
|
+
{ role: "user", content: finalPrompt }
|
|
616
|
+
];
|
|
617
|
+
|
|
618
|
+
await runLocalInference(localPromptArr, (token) => {
|
|
619
|
+
if (!stopSignal) {
|
|
620
|
+
localResult += token;
|
|
621
|
+
bubble.innerHTML = parseMarkdownLocal(localResult, false);
|
|
622
|
+
scrollBottom();
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
if (!stopSignal) {
|
|
627
|
+
bubble.innerHTML = parseMarkdownLocal(localResult, true);
|
|
628
|
+
updateCodeUI(localResult);
|
|
629
|
+
let historyResult = localResult.replace(/<think>[\s\S]*?(<\/think>|$)/gi, "").trim();
|
|
630
|
+
chatHistory.push({ role: "user", content: userText }, { role: "assistant", content: historyResult });
|
|
631
|
+
if (currentMode === "blog") await processBlogImages(bubble);
|
|
632
|
+
scrollBottom();
|
|
633
|
+
}
|
|
634
|
+
} else if (data.response) {
|
|
635
|
+
const bubble = appendMsg("ai", parseMarkdownLocal(data.response, true));
|
|
636
|
+
updateCodeUI(data.response);
|
|
637
|
+
let historyResult = data.response.replace(/<think>[\s\S]*?(<\/think>|$)/gi, "").trim();
|
|
638
|
+
chatHistory.push({ role: "user", content: userText }, { role: "assistant", content: historyResult });
|
|
639
|
+
|
|
640
|
+
if (currentMode === "blog") await processBlogImages(bubble);
|
|
641
|
+
if (currentMode === "voice") speakText(data.response);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function updateCodeUI(text) {
|
|
647
|
+
if (window.extractedCodes && window.extractedCodes.length > 0) {
|
|
648
|
+
codeFiles = [...window.extractedCodes];
|
|
649
|
+
renderCodeTabs();
|
|
650
|
+
} else if (currentMode === "code") {
|
|
651
|
+
updateCodePanel(text);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
} catch (error) {
|
|
655
|
+
if (error.name !== "AbortError") {
|
|
656
|
+
appendMsg("error", `오류: ${error.message}`);
|
|
657
|
+
}
|
|
658
|
+
} finally {
|
|
659
|
+
showLoader(false);
|
|
660
|
+
const focusInput = document.getElementById("prompt-input");
|
|
661
|
+
if (focusInput) focusInput.focus();
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
const fileInput = document.getElementById("comm-file-input");
|
|
665
|
+
const nickname = document.getElementById("comm-nickname") ? document.getElementById("comm-nickname").value : "";
|
|
666
|
+
const password = document.getElementById("comm-password").value;
|
|
667
|
+
|
|
668
|
+
if (!userText && (!fileInput.files || fileInput.files.length === 0)) {
|
|
669
|
+
showToast("내용이나 이미지를 입력해주세요.");
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
showLoader(true);
|
|
674
|
+
try {
|
|
675
|
+
const formData = new FormData();
|
|
676
|
+
formData.append("content", userText);
|
|
677
|
+
formData.append("password", password);
|
|
678
|
+
formData.append("nickname", nickname);
|
|
679
|
+
formData.append("type", "forum");
|
|
680
|
+
|
|
681
|
+
if (fileInput.files.length > 0) {
|
|
682
|
+
const keyRes = await fetch("get_key.php");
|
|
683
|
+
const keyData = await keyRes.json();
|
|
684
|
+
|
|
685
|
+
const imgurData = new FormData();
|
|
686
|
+
imgurData.append("image", fileInput.files[0]);
|
|
687
|
+
|
|
688
|
+
const uploadRes = await fetch("https://api.imgur.com/3/image", {
|
|
689
|
+
method: "POST",
|
|
690
|
+
headers: { Authorization: "Client-ID " + keyData.clientId },
|
|
691
|
+
body: imgurData
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const uploadData = await uploadRes.json();
|
|
695
|
+
if (!uploadData.success) throw new Error("Imgur Upload Failed");
|
|
696
|
+
|
|
697
|
+
formData.append("image_url", uploadData.data.link);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const postRes = await fetch("re_store.php?action=create_post", {
|
|
701
|
+
method: "POST",
|
|
702
|
+
body: formData
|
|
703
|
+
});
|
|
704
|
+
const postData = await postRes.json();
|
|
705
|
+
|
|
706
|
+
if (postData.success) {
|
|
707
|
+
showToast("Published.");
|
|
708
|
+
inputElement.value = "";
|
|
709
|
+
handleInput(inputElement);
|
|
710
|
+
document.getElementById("comm-password").value = "";
|
|
711
|
+
if (typeof clearCommFile === "function") clearCommFile();
|
|
712
|
+
|
|
713
|
+
if (postData.id) {
|
|
714
|
+
window.location.href = "https://isai.kr/view/" + postData.id;
|
|
715
|
+
} else {
|
|
716
|
+
if (typeof switchTab === "function") switchTab("forum");
|
|
717
|
+
if (typeof loadData === "function") loadData("forum", true);
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
showToast("Error: " + postData.error);
|
|
721
|
+
}
|
|
722
|
+
} catch (error) {
|
|
723
|
+
showToast("Error: " + error.message);
|
|
724
|
+
} finally {
|
|
725
|
+
showLoader(false);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function triggerVoiceButton() {
|
|
731
|
+
const voiceToolbarButton = document.getElementById("btn-voice");
|
|
732
|
+
if (currentMode === "voice") {
|
|
733
|
+
stopVoiceMode(true);
|
|
734
|
+
if (typeof setMode === "function") {
|
|
735
|
+
setMode("chat");
|
|
736
|
+
} else {
|
|
737
|
+
setVoiceState("left", "idle");
|
|
738
|
+
}
|
|
739
|
+
if (voiceToolbarButton) voiceToolbarButton.classList.remove("voice-armed", "voice-recording", "voice-processing");
|
|
740
|
+
if (typeof showToast === "function") showToast("Voice conversation off");
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
try { stopVoiceMode(true); } catch (error) {}
|
|
745
|
+
translationSide = "left";
|
|
746
|
+
if (typeof setMode === "function") {
|
|
747
|
+
setMode("voice");
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
setVoiceState("left", "processing");
|
|
751
|
+
if (typeof showToast === "function") showToast("Voice conversation on");
|
|
752
|
+
|
|
753
|
+
setTimeout(() => {
|
|
754
|
+
executeAction("left");
|
|
755
|
+
}, 120);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function renderStoreItems(apps, container) {
|
|
759
|
+
apps.forEach((app) => {
|
|
760
|
+
const item = document.createElement("div");
|
|
761
|
+
item.className = "store-item relative w-[44px] h-[44px] rounded-[14px]";
|
|
762
|
+
item.title = `${app.title} (Views: ${app.views || 0})`;
|
|
763
|
+
item.onclick = () => loadAppDetails(app.id);
|
|
764
|
+
item.innerHTML = getAppIconHtml(app);
|
|
765
|
+
container.appendChild(item);
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function activateAppMode() {
|
|
770
|
+
if (isMenuOpen) toggleStoreMenu();
|
|
771
|
+
document.getElementById("active-app-status").classList.remove("hidden");
|
|
772
|
+
document.getElementById("active-app-name").innerText = activeApp.title;
|
|
773
|
+
document.getElementById("prompt-input").value = "";
|
|
774
|
+
document.getElementById("chat-box").innerHTML = "";
|
|
775
|
+
|
|
776
|
+
if (activeApp.first_message) {
|
|
777
|
+
appendMsg("ai", activeApp.first_message);
|
|
778
|
+
} else {
|
|
779
|
+
appendMsg("ai", `App '${activeApp.title}' started.`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function exitAppMode() {
|
|
784
|
+
activeApp = null;
|
|
785
|
+
document.getElementById("active-app-status").classList.add("hidden");
|
|
786
|
+
resetChat();
|
|
787
|
+
showToast("App Mode Exited");
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async function fetchApps(query = "", append = false) {
|
|
791
|
+
const grid = document.getElementById("app-grid");
|
|
792
|
+
|
|
793
|
+
if (!append) {
|
|
794
|
+
currentAppPage = 1;
|
|
795
|
+
hasMoreApps = true;
|
|
796
|
+
currentAppQuery = query;
|
|
797
|
+
// 기존에 있던 애니메이션(투명도 조절) 제거: grid.style.opacity = 0.5;
|
|
798
|
+
grid.innerHTML = "";
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (!hasMoreApps || isAppLoading) return;
|
|
802
|
+
|
|
803
|
+
isAppLoading = true;
|
|
804
|
+
|
|
805
|
+
try {
|
|
806
|
+
const response = await fetch("?action=search_app", {
|
|
807
|
+
method: "POST",
|
|
808
|
+
body: JSON.stringify({ prompt: query, page: currentAppPage, limit: APP_LIMIT })
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
if (response.ok) {
|
|
812
|
+
const data = await response.json();
|
|
813
|
+
|
|
814
|
+
if (data && data.length > 0) {
|
|
815
|
+
// 불필요한 애니메이션 관련 클래스 토글 제거
|
|
816
|
+
data.forEach((app) => {
|
|
817
|
+
grid.appendChild(createAppItem(app));
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
if (data.length < APP_LIMIT) {
|
|
821
|
+
hasMoreApps = false;
|
|
822
|
+
} else {
|
|
823
|
+
currentAppPage++;
|
|
824
|
+
}
|
|
825
|
+
} else {
|
|
826
|
+
if (!append) {
|
|
827
|
+
const noAppsMsg = typeof T !== "undefined" && T.no_apps ? T.no_apps : "No shortcuts.";
|
|
828
|
+
grid.innerHTML = `<p class="text-gray-500 text-xs col-span-full py-2 text-center">${noAppsMsg}</p>`;
|
|
829
|
+
}
|
|
830
|
+
hasMoreApps = false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
} catch (error) {
|
|
834
|
+
console.error("Error fetching apps:", error);
|
|
835
|
+
} finally {
|
|
836
|
+
// 기존에 있던 투명도 원상복구 제거: grid.style.opacity = 1;
|
|
837
|
+
isAppLoading = false;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function createAppItem(app, isClone = false) {
|
|
842
|
+
const item = document.createElement("a");
|
|
843
|
+
item.href = app.url || "#";
|
|
844
|
+
if(app.url) item.target = "_blank";
|
|
845
|
+
|
|
846
|
+
item.className = "app-item relative w-[44px] h-[44px] rounded-[14px] transition-transform hover:scale-105";
|
|
847
|
+
if (isClone) item.classList.add("clone-item");
|
|
848
|
+
item.title = app.name || "";
|
|
849
|
+
|
|
850
|
+
item.innerHTML = getAppIconHtml(app);
|
|
851
|
+
return item;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
window.addEventListener("DOMContentLoaded", () => {
|
|
855
|
+
applyLocalModelProfileToConfig();
|
|
856
|
+
renderLocalModelTierSelector();
|
|
857
|
+
observeLocalModelTierVisibility();
|
|
858
|
+
syncLocalModelTierVisibility(currentMode);
|
|
859
|
+
initCheckModel();
|
|
860
|
+
fetchApps("");
|
|
861
|
+
renderFixedApps();
|
|
862
|
+
|
|
863
|
+
const chatBox = document.getElementById("chat-box");
|
|
864
|
+
chatBox.innerHTML = "";
|
|
865
|
+
chatBox.style.display = "flex";
|
|
866
|
+
chatBox.style.flexDirection = "column";
|
|
867
|
+
chatBox.style.height = "100%";
|
|
868
|
+
|
|
869
|
+
const btnChat = document.getElementById("btn-chat");
|
|
870
|
+
if (btnChat) btnChat.classList.add("active");
|
|
871
|
+
|
|
872
|
+
const handleStoreScroll = (target) => {
|
|
873
|
+
if (!target) return;
|
|
874
|
+
target.addEventListener("scroll", () => {
|
|
875
|
+
if (target.scrollTop + target.clientHeight >= target.scrollHeight - 50 && hasMoreStoreApps && !isStoreLoading && isMenuOpen) {
|
|
876
|
+
fetchStoreApps(currentStoreQuery, true);
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
};
|
|
880
|
+
const storePanel = document.getElementById("store-panel");
|
|
881
|
+
const storeGrid = document.getElementById("store-grid");
|
|
882
|
+
handleStoreScroll(storePanel);
|
|
883
|
+
handleStoreScroll(storeGrid);
|
|
884
|
+
|
|
885
|
+
const appContainer = document.getElementById("app-container");
|
|
886
|
+
|
|
887
|
+
if (appContainer) {
|
|
888
|
+
appContainer.addEventListener("scroll", () => {
|
|
889
|
+
const scrollLocation = appContainer.scrollTop + appContainer.clientHeight;
|
|
890
|
+
const totalHeight = appContainer.scrollHeight;
|
|
891
|
+
|
|
892
|
+
if (scrollLocation >= totalHeight - 30) {
|
|
893
|
+
if (hasMoreApps && !isAppLoading && currentMode === "app") {
|
|
894
|
+
console.log("Loading more apps... Page:", currentAppPage);
|
|
895
|
+
fetchApps(currentAppQuery, true);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const params = new URLSearchParams(window.location.search);
|
|
902
|
+
const modes =["chat", "search", "image", "code", "video", "music", "blog"];
|
|
903
|
+
for (const mode of modes) {
|
|
904
|
+
const queryVal = params.get(mode);
|
|
905
|
+
if (queryVal) {
|
|
906
|
+
setMode(mode);
|
|
907
|
+
const promptInput = document.getElementById("prompt-input");
|
|
908
|
+
if (promptInput) {
|
|
909
|
+
promptInput.value = queryVal;
|
|
910
|
+
handleInput(promptInput);
|
|
911
|
+
}
|
|
912
|
+
setTimeout(() => {
|
|
913
|
+
executeAction();
|
|
914
|
+
}, 800);
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
window.removeFixedApp = function (event, index) {
|
|
921
|
+
event.stopPropagation();
|
|
922
|
+
fixedApps.splice(index, 1);
|
|
923
|
+
localStorage.setItem("ISAI_FIXED_APPS", JSON.stringify(fixedApps));
|
|
924
|
+
renderFixedApps();
|
|
925
|
+
showToast("Removed from favorites.");
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
let fixedApps = JSON.parse(localStorage.getItem("ISAI_FIXED_APPS") || "[]");
|
|
929
|
+
|
|
930
|
+
function getAppIconHtml(app) {
|
|
931
|
+
const title = app.title || app.name || "App";
|
|
932
|
+
const initial = title.charAt(0).toUpperCase();
|
|
933
|
+
|
|
934
|
+
const gradients =[
|
|
935
|
+
"bg-gradient-to-br from-[#22c55e] to-[#16a34a]",
|
|
936
|
+
"bg-gradient-to-br from-[#d946ef] to-[#c026d3]",
|
|
937
|
+
"bg-gradient-to-br from-[#0ea5e9] to-[#0284c7]",
|
|
938
|
+
"bg-gradient-to-br from-[#f97316] to-[#ea580c]",
|
|
939
|
+
"bg-gradient-to-br from-[#a855f7] to-[#9333ea]",
|
|
940
|
+
"bg-gradient-to-br from-[#ec4899] to-[#db2777]"
|
|
941
|
+
];
|
|
942
|
+
|
|
943
|
+
let hash = 0;
|
|
944
|
+
for (let i = 0; i < title.length; i++) { hash = title.charCodeAt(i) + ((hash << 5) - hash); }
|
|
945
|
+
const gradient = gradients[Math.abs(hash) % gradients.length];
|
|
946
|
+
|
|
947
|
+
let iconUrl = app.icon_url || "";
|
|
948
|
+
if (!iconUrl && app.url) {
|
|
949
|
+
iconUrl = `https://www.google.com/s2/favicons?sz=64&domain_url=${encodeURIComponent(app.url)}`;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const imgHtml = iconUrl ? `
|
|
953
|
+
<img src="${iconUrl}" style="display:none;"
|
|
954
|
+
onload="if(this.naturalWidth <= 32) { this.style.display='none'; } else { this.nextElementSibling.querySelector('.icon-bg').style.backgroundImage = 'url(\\'' + this.src + '\\')'; this.nextElementSibling.classList.remove('opacity-0'); this.previousElementSibling.style.display='none'; }"
|
|
955
|
+
onerror="this.style.display='none'">
|
|
956
|
+
<div class="absolute inset-0 w-full h-full rounded-[14px] z-10 bg-white opacity-0 transition-opacity duration-300 p-[5px]">
|
|
957
|
+
<div class="icon-bg w-full h-full rounded-[8px]"
|
|
958
|
+
style="background-size: contain; background-position: center; background-repeat: no-repeat;"></div>
|
|
959
|
+
</div>
|
|
960
|
+
` : "";
|
|
961
|
+
|
|
962
|
+
return `
|
|
963
|
+
<div class="absolute inset-0 w-full h-full flex items-center justify-center text-white font-bold text-[18px] rounded-[14px] shadow-sm ${gradient}">
|
|
964
|
+
${initial}
|
|
965
|
+
</div>
|
|
966
|
+
${imgHtml}
|
|
967
|
+
`;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function startExperience() {
|
|
971
|
+
if (!isStarted) {
|
|
972
|
+
document.body.classList.add("started");
|
|
973
|
+
document.getElementById("custom-scrollbar").style.display = "block";
|
|
974
|
+
isStarted = true;
|
|
975
|
+
setTimeout(scrollBottom, 600);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
let translationSide = "right";
|
|
980
|
+
const langMap = {
|
|
981
|
+
English: "en-US",
|
|
982
|
+
Korean: "ko-KR",
|
|
983
|
+
Japanese: "ja-JP",
|
|
984
|
+
Chinese: "zh-CN",
|
|
985
|
+
Spanish: "es-ES",
|
|
986
|
+
French: "fr-FR",
|
|
987
|
+
German: "de-DE",
|
|
988
|
+
Russian: "ru-RU"
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
function setVoiceState(side, state) {
|
|
992
|
+
const btnLeft = document.getElementById("btn-submit-left");
|
|
993
|
+
const btnRight = document.getElementById("btn-submit");
|
|
994
|
+
const iconLeft = document.getElementById("icon-submit-left");
|
|
995
|
+
const iconRight = document.getElementById("icon-submit");
|
|
996
|
+
const btnVoice = document.getElementById("btn-voice");
|
|
997
|
+
const btnVoiceIcon = btnVoice ? btnVoice.querySelector("i") : null;
|
|
998
|
+
|
|
999
|
+
if (!btnLeft || !iconLeft) return;
|
|
1000
|
+
|
|
1001
|
+
btnLeft.className = "btn-icon voice-toggle-btn transition-all duration-300";
|
|
1002
|
+
if (btnRight && btnRight.closest("#chat-input-actions")) {
|
|
1003
|
+
btnRight.className = "input-action-btn";
|
|
1004
|
+
} else if (btnRight) {
|
|
1005
|
+
btnRight.className = "btn-submit w-9 h-9 flex items-center justify-center flex-shrink-0 transition-all duration-300";
|
|
1006
|
+
}
|
|
1007
|
+
btnLeft.style.display = "flex";
|
|
1008
|
+
btnLeft.style.backgroundColor = "rgba(255,255,255,0.10)";
|
|
1009
|
+
btnLeft.style.color = "#ffffff";
|
|
1010
|
+
btnLeft.style.boxShadow = "none";
|
|
1011
|
+
btnLeft.style.opacity = "1";
|
|
1012
|
+
btnLeft.style.pointerEvents = "auto";
|
|
1013
|
+
btnLeft.setAttribute("aria-pressed", state === "idle" ? "false" : "true");
|
|
1014
|
+
btnLeft.setAttribute("title", state === "idle" ? "Voice Conversation" : "Voice Conversation Active");
|
|
1015
|
+
if (btnRight) {
|
|
1016
|
+
btnRight.style.backgroundColor = "";
|
|
1017
|
+
btnRight.style.color = "";
|
|
1018
|
+
btnRight.style.opacity = "1";
|
|
1019
|
+
btnRight.style.pointerEvents = "auto";
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
iconLeft.className = "ri-mic-line text-lg text-white";
|
|
1023
|
+
if (iconRight) iconRight.className = "ri-arrow-up-s-line text-[14px]";
|
|
1024
|
+
if (btnVoice) {
|
|
1025
|
+
btnVoice.classList.remove("voice-armed", "voice-recording", "voice-processing");
|
|
1026
|
+
btnVoice.style.backgroundColor = "";
|
|
1027
|
+
btnVoice.style.color = "";
|
|
1028
|
+
btnVoice.style.boxShadow = "";
|
|
1029
|
+
btnVoice.style.transform = "";
|
|
1030
|
+
btnVoice.setAttribute("aria-pressed", state === "idle" ? "false" : "true");
|
|
1031
|
+
}
|
|
1032
|
+
if (btnVoiceIcon) {
|
|
1033
|
+
btnVoiceIcon.className = "ri-mic-2-line text-lg";
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (state === "idle" && currentMode === "voice") {
|
|
1037
|
+
btnLeft.style.backgroundColor = "rgba(255,255,255,0.14)";
|
|
1038
|
+
btnLeft.style.boxShadow = "0 0 0 1px rgba(255,255,255,0.10), 0 10px 24px rgba(0,0,0,0.22)";
|
|
1039
|
+
if (btnVoice) {
|
|
1040
|
+
btnVoice.classList.add("voice-armed");
|
|
1041
|
+
btnVoice.style.backgroundColor = "rgba(255,255,255,0.12)";
|
|
1042
|
+
btnVoice.style.color = "#ffffff";
|
|
1043
|
+
btnVoice.style.boxShadow = "0 0 0 1px rgba(255,255,255,0.10), 0 10px 24px rgba(0,0,0,0.22)";
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (state === "idle") return;
|
|
1048
|
+
|
|
1049
|
+
if (state === "recording") {
|
|
1050
|
+
iconLeft.className = "ri-voiceprint-line text-lg text-white animate-mic-breath";
|
|
1051
|
+
btnLeft.style.backgroundColor = "#ef4444";
|
|
1052
|
+
btnLeft.style.color = "white";
|
|
1053
|
+
btnLeft.style.boxShadow = "0 0 0 4px rgba(239,68,68,0.18), 0 10px 24px rgba(239,68,68,0.30)";
|
|
1054
|
+
if (btnVoice) {
|
|
1055
|
+
btnVoice.classList.add("voice-recording");
|
|
1056
|
+
btnVoice.style.backgroundColor = "#ef4444";
|
|
1057
|
+
btnVoice.style.color = "#ffffff";
|
|
1058
|
+
btnVoice.style.boxShadow = "0 0 0 4px rgba(239,68,68,0.18), 0 10px 24px rgba(239,68,68,0.30)";
|
|
1059
|
+
btnVoice.style.transform = "scale(1.03)";
|
|
1060
|
+
}
|
|
1061
|
+
if (btnVoiceIcon) {
|
|
1062
|
+
btnVoiceIcon.className = "ri-voiceprint-line text-lg text-white animate-mic-breath";
|
|
1063
|
+
}
|
|
1064
|
+
} else if (state === "processing") {
|
|
1065
|
+
iconLeft.className = "ri-voiceprint-line text-lg text-white animate-spin";
|
|
1066
|
+
btnLeft.style.backgroundColor = "rgba(255,255,255,0.16)";
|
|
1067
|
+
btnLeft.style.color = "#ffffff";
|
|
1068
|
+
btnLeft.style.boxShadow = "0 0 0 1px rgba(255,255,255,0.14), 0 10px 24px rgba(0,0,0,0.26)";
|
|
1069
|
+
if (btnVoice) {
|
|
1070
|
+
btnVoice.classList.add("voice-processing");
|
|
1071
|
+
btnVoice.style.backgroundColor = "rgba(255,255,255,0.16)";
|
|
1072
|
+
btnVoice.style.color = "#ffffff";
|
|
1073
|
+
btnVoice.style.boxShadow = "0 0 0 1px rgba(255,255,255,0.14), 0 10px 24px rgba(0,0,0,0.26)";
|
|
1074
|
+
}
|
|
1075
|
+
if (btnVoiceIcon) {
|
|
1076
|
+
btnVoiceIcon.className = "ri-voiceprint-line text-lg text-white animate-spin";
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function updateSubmitIcon(state, side = "right") {
|
|
1082
|
+
setVoiceState("left", state === "mic" || state === "default" ? "idle" : state);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function showPopup(title, msg, confirmCallback) {
|
|
1086
|
+
const layer = document.getElementById("modal-layer");
|
|
1087
|
+
document.getElementById("modal-title").innerText = title;
|
|
1088
|
+
document.getElementById("modal-msg").innerText = msg;
|
|
1089
|
+
document.getElementById("modal-cancel").innerText = T.btn_cancel;
|
|
1090
|
+
document.getElementById("modal-confirm").innerText = T.btn_confirm;
|
|
1091
|
+
|
|
1092
|
+
const btnConfirm = document.getElementById("modal-confirm");
|
|
1093
|
+
const btnCancel = document.getElementById("modal-cancel");
|
|
1094
|
+
|
|
1095
|
+
const newConfirm = btnConfirm.cloneNode(true);
|
|
1096
|
+
const newCancel = btnCancel.cloneNode(true);
|
|
1097
|
+
|
|
1098
|
+
btnConfirm.parentNode.replaceChild(newConfirm, btnConfirm);
|
|
1099
|
+
btnCancel.parentNode.replaceChild(newCancel, btnCancel);
|
|
1100
|
+
|
|
1101
|
+
newConfirm.onclick = () => {
|
|
1102
|
+
closePopup();
|
|
1103
|
+
confirmCallback();
|
|
1104
|
+
};
|
|
1105
|
+
newCancel.onclick = () => {
|
|
1106
|
+
closePopup();
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
layer.classList.add("show");
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function closePopup() {
|
|
1113
|
+
document.getElementById("modal-layer").classList.remove("show");
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function updateLocalBtnState() {
|
|
1117
|
+
const btn = document.getElementById("btn-download");
|
|
1118
|
+
if (!btn) return;
|
|
1119
|
+
const icon = btn.querySelector("i");
|
|
1120
|
+
if (!icon) return;
|
|
1121
|
+
|
|
1122
|
+
if (isLocalActive) {
|
|
1123
|
+
btn.classList.add("text-green-400", "active");
|
|
1124
|
+
icon.className = "ri-check-line text-xl";
|
|
1125
|
+
btn.style.color = "";
|
|
1126
|
+
} else if (isModelDownloaded) {
|
|
1127
|
+
btn.classList.remove("text-green-400", "active");
|
|
1128
|
+
icon.className = "ri-check-line text-xl";
|
|
1129
|
+
btn.style.color = "rgba(255,255,255,0.6)";
|
|
1130
|
+
} else {
|
|
1131
|
+
btn.classList.remove("text-green-400", "active");
|
|
1132
|
+
icon.className = "ri-download-line text-xl";
|
|
1133
|
+
btn.style.color = "rgba(255,255,255,0.3)";
|
|
1134
|
+
}
|
|
1135
|
+
if (typeof syncLocalModelTierVisibility === "function") {
|
|
1136
|
+
syncLocalModelTierVisibility();
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function getLocalModelTierStorageKey() {
|
|
1141
|
+
return (window.MODEL_CONFIG && window.MODEL_CONFIG.modelTierKey) || "ISAI_LOCAL_MODEL_TIER_V1";
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function getLocalModelProfiles() {
|
|
1145
|
+
const coreProfiles = window.__ISAI_MODEL_CORE_PROFILES || {};
|
|
1146
|
+
if (coreProfiles && Object.keys(coreProfiles).length > 0) {
|
|
1147
|
+
return coreProfiles;
|
|
1148
|
+
}
|
|
1149
|
+
const configuredProfiles = (window.MODEL_CONFIG && window.MODEL_CONFIG.modelProfiles) || {};
|
|
1150
|
+
if (configuredProfiles && Object.keys(configuredProfiles).length > 0) {
|
|
1151
|
+
return configuredProfiles;
|
|
1152
|
+
}
|
|
1153
|
+
return {
|
|
1154
|
+
light: {
|
|
1155
|
+
key: "light",
|
|
1156
|
+
label: "라이트",
|
|
1157
|
+
fallbackUrl: "https://huggingface.co/LiquidAI/LFM2.5-350M-GGUF/resolve/main/LFM2.5-350M-Q4_0.gguf",
|
|
1158
|
+
popupSizeText: "257MB",
|
|
1159
|
+
preferredRuntime: "wllama"
|
|
1160
|
+
},
|
|
1161
|
+
middle: {
|
|
1162
|
+
key: "middle",
|
|
1163
|
+
label: "중간",
|
|
1164
|
+
fallbackUrl: "https://huggingface.co/LiquidAI/LFM2.5-350M-GGUF/resolve/main/LFM2.5-350M-Q6_K.gguf",
|
|
1165
|
+
preferredRuntime: "wllama"
|
|
1166
|
+
},
|
|
1167
|
+
hard: {
|
|
1168
|
+
key: "hard",
|
|
1169
|
+
label: "하드",
|
|
1170
|
+
fallbackUrl: "https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-IQ4_XS.gguf",
|
|
1171
|
+
preferredRuntime: "wllama"
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function getDefaultLocalModelTier() {
|
|
1177
|
+
const profiles = getLocalModelProfiles();
|
|
1178
|
+
const configuredDefault = String((window.MODEL_CONFIG && window.MODEL_CONFIG.defaultModelTier) || "").trim().toLowerCase();
|
|
1179
|
+
if (configuredDefault && profiles[configuredDefault]) return configuredDefault;
|
|
1180
|
+
if (profiles.light) return "light";
|
|
1181
|
+
const keys = Object.keys(profiles);
|
|
1182
|
+
return keys.length > 0 ? keys[0] : "light";
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function getActiveLocalModelTier() {
|
|
1186
|
+
const profiles = getLocalModelProfiles();
|
|
1187
|
+
const storageKey = getLocalModelTierStorageKey();
|
|
1188
|
+
const storedTier = String(localStorage.getItem(storageKey) || "").trim().toLowerCase();
|
|
1189
|
+
if (storedTier && profiles[storedTier]) return storedTier;
|
|
1190
|
+
const fallbackTier = getDefaultLocalModelTier();
|
|
1191
|
+
localStorage.setItem(storageKey, fallbackTier);
|
|
1192
|
+
return fallbackTier;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function getActiveLocalModelProfile() {
|
|
1196
|
+
const profiles = getLocalModelProfiles();
|
|
1197
|
+
const tier = getActiveLocalModelTier();
|
|
1198
|
+
return profiles[tier] || null;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function applyLocalModelProfileToConfig() {
|
|
1202
|
+
const modelConfig = window.MODEL_CONFIG || {};
|
|
1203
|
+
const profile = getActiveLocalModelProfile();
|
|
1204
|
+
if (!profile) return null;
|
|
1205
|
+
modelConfig.fallback = modelConfig.fallback || {};
|
|
1206
|
+
modelConfig.fallback.url = profile.fallbackUrl || modelConfig.fallback.url;
|
|
1207
|
+
if (profile.preferredRuntime) {
|
|
1208
|
+
modelConfig.preferredRuntime = profile.preferredRuntime;
|
|
1209
|
+
}
|
|
1210
|
+
window.MODEL_CONFIG = modelConfig;
|
|
1211
|
+
return profile;
|
|
1212
|
+
}
|
|
1213
|
+
window.applyLocalModelProfileToConfig = applyLocalModelProfileToConfig;
|
|
1214
|
+
|
|
1215
|
+
function getLocalDownloadStorageKey() {
|
|
1216
|
+
const baseKey = (window.MODEL_CONFIG && window.MODEL_CONFIG.storageKey) || "ISAI_MODEL_DOWNLOADED";
|
|
1217
|
+
const tier = getActiveLocalModelTier();
|
|
1218
|
+
return `${baseKey}__${tier}`;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function getLocalRuntimeStorageKey() {
|
|
1222
|
+
return (window.MODEL_CONFIG && window.MODEL_CONFIG.runtimeKey) || "ISAI_LOCAL_MODEL_RUNTIME_V1";
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function getLocalModelPopupTitle() {
|
|
1226
|
+
const locale = String(document.documentElement.lang || navigator.language || "ko").toLowerCase();
|
|
1227
|
+
if (locale.startsWith("ko")) return "로컬 모델 다운로드";
|
|
1228
|
+
return "Download Local Model";
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function getLocalModelPopupMessage() {
|
|
1232
|
+
const locale = String(document.documentElement.lang || navigator.language || "ko").toLowerCase();
|
|
1233
|
+
const profile = getActiveLocalModelProfile();
|
|
1234
|
+
if (locale.startsWith("ko")) {
|
|
1235
|
+
if (profile && profile.key === "light") {
|
|
1236
|
+
const sizeText = profile.popupSizeText || "257MB";
|
|
1237
|
+
return `LFM2.5 350M 모델(${sizeText})을 다운로드해 오프라인 모드를 사용합니다. 계속할까요?`;
|
|
1238
|
+
}
|
|
1239
|
+
if (profile && profile.key === "middle") {
|
|
1240
|
+
return "LFM2.5 350M Q6_K 모델을 다운로드해 오프라인 모드를 사용합니다. 계속할까요?";
|
|
1241
|
+
}
|
|
1242
|
+
if (profile && profile.key === "hard") {
|
|
1243
|
+
return "Qwen3 0.6B IQ4_XS 모델을 다운로드해 오프라인 모드를 사용합니다. 계속할까요?";
|
|
1244
|
+
}
|
|
1245
|
+
return "로컬 모델을 다운로드해 오프라인 모드를 사용합니다. 계속할까요?";
|
|
1246
|
+
}
|
|
1247
|
+
if (profile && profile.key === "light") {
|
|
1248
|
+
const sizeText = profile.popupSizeText || "257MB";
|
|
1249
|
+
return `Download LFM2.5 350M (${sizeText}) for local mode. Continue?`;
|
|
1250
|
+
}
|
|
1251
|
+
if (profile && profile.key === "middle") {
|
|
1252
|
+
return "Download LFM2.5 350M Q6_K for local mode. Continue?";
|
|
1253
|
+
}
|
|
1254
|
+
if (profile && profile.key === "hard") {
|
|
1255
|
+
return "Download Qwen3 0.6B IQ4_XS for local mode. Continue?";
|
|
1256
|
+
}
|
|
1257
|
+
return "Download the local model for offline mode. Continue?";
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function syncLocalModelTierVisibility(mode) {
|
|
1261
|
+
const wrapper = document.getElementById("local-model-tier-wrapper");
|
|
1262
|
+
if (!wrapper) return;
|
|
1263
|
+
const effectiveMode = String(mode || (document.body && document.body.getAttribute("data-ui-mode")) || currentMode || "chat").toLowerCase();
|
|
1264
|
+
const shouldShow = (effectiveMode === "chat" || effectiveMode === "code") && !!isLocalActive;
|
|
1265
|
+
wrapper.style.display = shouldShow ? "block" : "none";
|
|
1266
|
+
}
|
|
1267
|
+
window.syncLocalModelTierVisibility = syncLocalModelTierVisibility;
|
|
1268
|
+
|
|
1269
|
+
function renderLocalModelTierSelector() {
|
|
1270
|
+
const wrapper = document.getElementById("local-model-tier-wrapper");
|
|
1271
|
+
const list = document.getElementById("local-model-tier-list");
|
|
1272
|
+
if (!wrapper || !list) return;
|
|
1273
|
+
|
|
1274
|
+
const profiles = getLocalModelProfiles();
|
|
1275
|
+
const activeTier = getActiveLocalModelTier();
|
|
1276
|
+
const orderedTiers = getOrderedLocalModelTierKeys().filter((tier) => !!profiles[tier]);
|
|
1277
|
+
|
|
1278
|
+
list.innerHTML = "";
|
|
1279
|
+
orderedTiers.forEach((tier) => {
|
|
1280
|
+
const profile = profiles[tier] || {};
|
|
1281
|
+
const button = document.createElement("button");
|
|
1282
|
+
button.type = "button";
|
|
1283
|
+
button.className = `local-model-tier-btn${tier === activeTier ? " active" : ""}`;
|
|
1284
|
+
button.textContent = profile.label || tier;
|
|
1285
|
+
button.dataset.tier = tier;
|
|
1286
|
+
button.setAttribute("aria-pressed", tier === activeTier ? "true" : "false");
|
|
1287
|
+
button.addEventListener("click", (event) => {
|
|
1288
|
+
event.preventDefault();
|
|
1289
|
+
event.stopPropagation();
|
|
1290
|
+
setLocalModelTier(tier);
|
|
1291
|
+
});
|
|
1292
|
+
list.appendChild(button);
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
syncLocalModelTierVisibility();
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function observeLocalModelTierVisibility() {
|
|
1299
|
+
if (window.__localModelTierModeObserverBound) return;
|
|
1300
|
+
const body = document.body;
|
|
1301
|
+
if (!body) return;
|
|
1302
|
+
const observer = new MutationObserver(() => {
|
|
1303
|
+
syncLocalModelTierVisibility();
|
|
1304
|
+
});
|
|
1305
|
+
observer.observe(body, { attributes: true, attributeFilter: ["data-ui-mode"] });
|
|
1306
|
+
window.__localModelTierModeObserverBound = true;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function setLocalModelTier(tierKey) {
|
|
1310
|
+
const profiles = getLocalModelProfiles();
|
|
1311
|
+
const nextTier = String(tierKey || "").trim().toLowerCase();
|
|
1312
|
+
if (!profiles[nextTier]) return;
|
|
1313
|
+
|
|
1314
|
+
const prevTier = getActiveLocalModelTier();
|
|
1315
|
+
if (prevTier === nextTier) {
|
|
1316
|
+
renderLocalModelTierSelector();
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const wasLocalActive = !!isLocalActive;
|
|
1321
|
+
localStorage.setItem(getLocalModelTierStorageKey(), nextTier);
|
|
1322
|
+
localStorage.setItem(getLocalModelTierUserSetKey(), "true");
|
|
1323
|
+
applyLocalModelProfileToConfig();
|
|
1324
|
+
|
|
1325
|
+
isLocalActive = wasLocalActive;
|
|
1326
|
+
isModelLoaded = false;
|
|
1327
|
+
localEngine = null;
|
|
1328
|
+
wllama = null;
|
|
1329
|
+
setLocalRuntimeState(null);
|
|
1330
|
+
isModelDownloaded = localStorage.getItem(getLocalDownloadStorageKey()) === "true";
|
|
1331
|
+
|
|
1332
|
+
updateLocalBtnState();
|
|
1333
|
+
renderLocalModelTierSelector();
|
|
1334
|
+
|
|
1335
|
+
const activeProfile = getActiveLocalModelProfile();
|
|
1336
|
+
if (activeProfile && typeof showToast === "function") {
|
|
1337
|
+
const readySuffix = isModelDownloaded ? " (다운로드됨)" : "";
|
|
1338
|
+
showToast(`${activeProfile.label || nextTier} 모델 선택${readySuffix}`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
window.setLocalModelTier = setLocalModelTier;
|
|
1342
|
+
|
|
1343
|
+
// Encoding-safe overrides for local model tier labels/messages.
|
|
1344
|
+
function getLocalModelProfiles() {
|
|
1345
|
+
const coreProfiles = window.__ISAI_MODEL_CORE_PROFILES || {};
|
|
1346
|
+
if (coreProfiles && Object.keys(coreProfiles).length > 0) {
|
|
1347
|
+
return coreProfiles;
|
|
1348
|
+
}
|
|
1349
|
+
const configuredProfiles = (window.MODEL_CONFIG && window.MODEL_CONFIG.modelProfiles) || {};
|
|
1350
|
+
if (configuredProfiles && Object.keys(configuredProfiles).length > 0) {
|
|
1351
|
+
return configuredProfiles;
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
code: {
|
|
1355
|
+
key: "code",
|
|
1356
|
+
label: "\ucf54\ub4dc",
|
|
1357
|
+
fallbackUrl: "https://huggingface.co/lmstudio-community/Qwen2.5-Coder-0.5B-Instruct-GGUF/resolve/main/Qwen2.5-Coder-0.5B-Instruct-Q3_K_L.gguf",
|
|
1358
|
+
popupSizeText: "369MB",
|
|
1359
|
+
preferredRuntime: "wllama"
|
|
1360
|
+
},
|
|
1361
|
+
light: {
|
|
1362
|
+
key: "light",
|
|
1363
|
+
label: "\ub77c\uc774\ud2b8",
|
|
1364
|
+
fallbackUrl: "https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q4_0.gguf",
|
|
1365
|
+
popupSizeText: "429MB",
|
|
1366
|
+
preferredRuntime: "wllama"
|
|
1367
|
+
},
|
|
1368
|
+
middle: {
|
|
1369
|
+
key: "middle",
|
|
1370
|
+
label: "\uc911\uac04",
|
|
1371
|
+
fallbackUrl: "https://huggingface.co/unsloth/gemma-3-1b-it-GGUF/resolve/main/gemma-3-1b-it-UD-IQ3_XXS.gguf",
|
|
1372
|
+
popupSizeText: "592MB",
|
|
1373
|
+
preferredRuntime: "wllama"
|
|
1374
|
+
},
|
|
1375
|
+
hard: {
|
|
1376
|
+
key: "hard",
|
|
1377
|
+
label: "\ud558\ub4dc",
|
|
1378
|
+
fallbackUrl: "https://huggingface.co/unsloth/LFM2.5-1.2B-Instruct-GGUF/resolve/main/LFM2.5-1.2B-Instruct-Q3_K_S.gguf",
|
|
1379
|
+
popupSizeText: "558MB",
|
|
1380
|
+
preferredRuntime: "wllama"
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function getLocalModelPopupTitle() {
|
|
1386
|
+
const locale = String(document.documentElement.lang || navigator.language || "ko").toLowerCase();
|
|
1387
|
+
if (locale.startsWith("ko")) return "\ub85c\uceec \ubaa8\ub378 \ub2e4\uc6b4\ub85c\ub4dc";
|
|
1388
|
+
return "Download Local Model";
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function getLocalModelPopupMessage() {
|
|
1392
|
+
const locale = String(document.documentElement.lang || navigator.language || "ko").toLowerCase();
|
|
1393
|
+
const profile = getActiveLocalModelProfile();
|
|
1394
|
+
if (locale.startsWith("ko")) {
|
|
1395
|
+
if (profile && profile.key === "light") {
|
|
1396
|
+
const sizeText = profile.popupSizeText || "257MB";
|
|
1397
|
+
return `LFM2.5 350M (${sizeText}) \ubaa8\ub378\uc744 \ub2e4\uc6b4\ub85c\ub4dc\ud55c \ub4a4 \ub85c\uceec \ubaa8\ub4dc\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc5b4\uc694. \uacc4\uc18d\ud560\uae4c\uc694?`;
|
|
1398
|
+
}
|
|
1399
|
+
if (profile && profile.key === "middle") {
|
|
1400
|
+
return "LFM2.5 350M Q6_K \ubaa8\ub378\uc744 \ub2e4\uc6b4\ub85c\ub4dc\ud55c \ub4a4 \ub85c\uceec \ubaa8\ub4dc\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc5b4\uc694. \uacc4\uc18d\ud560\uae4c\uc694?";
|
|
1401
|
+
}
|
|
1402
|
+
if (profile && profile.key === "hard") {
|
|
1403
|
+
return "Qwen3 0.6B IQ4_XS \ubaa8\ub378\uc744 \ub2e4\uc6b4\ub85c\ub4dc\ud55c \ub4a4 \ub85c\uceec \ubaa8\ub4dc\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc5b4\uc694. \uacc4\uc18d\ud560\uae4c\uc694?";
|
|
1404
|
+
}
|
|
1405
|
+
return "\ub85c\uceec \ubaa8\ub378\uc744 \ub2e4\uc6b4\ub85c\ub4dc\ud55c \ub4a4 \uc624\ud504\ub77c\uc778 \ubaa8\ub4dc\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc5b4\uc694. \uacc4\uc18d\ud560\uae4c\uc694?";
|
|
1406
|
+
}
|
|
1407
|
+
if (profile && profile.key === "light") {
|
|
1408
|
+
const sizeText = profile.popupSizeText || "257MB";
|
|
1409
|
+
return `Download LFM2.5 350M (${sizeText}) for local mode. Continue?`;
|
|
1410
|
+
}
|
|
1411
|
+
if (profile && profile.key === "middle") {
|
|
1412
|
+
return "Download LFM2.5 350M Q6_K for local mode. Continue?";
|
|
1413
|
+
}
|
|
1414
|
+
if (profile && profile.key === "hard") {
|
|
1415
|
+
return "Download Qwen3 0.6B IQ4_XS for local mode. Continue?";
|
|
1416
|
+
}
|
|
1417
|
+
return "Download the local model for offline mode. Continue?";
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function getOrderedLocalModelTierKeys() {
|
|
1421
|
+
const profiles = getLocalModelProfiles();
|
|
1422
|
+
const preferredOrder = ["code", "light", "middle", "hard"];
|
|
1423
|
+
const ordered = preferredOrder.filter((tier) => !!profiles[tier]);
|
|
1424
|
+
return ordered.length > 0 ? ordered : ["light"];
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function getNextLocalModelTier(currentTier) {
|
|
1428
|
+
const ordered = getOrderedLocalModelTierKeys();
|
|
1429
|
+
const current = String(currentTier || "").trim().toLowerCase();
|
|
1430
|
+
const currentIndex = ordered.indexOf(current);
|
|
1431
|
+
if (currentIndex < 0) return ordered[0];
|
|
1432
|
+
return ordered[(currentIndex + 1) % ordered.length];
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function getLocalModelTierUserSetKey() {
|
|
1436
|
+
return "ISAI_LOCAL_MODEL_TIER_USER_SET_V1";
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function ensureDefaultLocalModelTierForActivation() {
|
|
1440
|
+
const profiles = getLocalModelProfiles();
|
|
1441
|
+
const tierKey = getLocalModelTierStorageKey();
|
|
1442
|
+
const userSetKey = getLocalModelTierUserSetKey();
|
|
1443
|
+
const hasUserSelection = localStorage.getItem(userSetKey) === "true";
|
|
1444
|
+
const storedTier = String(localStorage.getItem(tierKey) || "").trim().toLowerCase();
|
|
1445
|
+
const fallbackTier = profiles.light ? "light" : getDefaultLocalModelTier();
|
|
1446
|
+
if (!hasUserSelection || !profiles[storedTier]) {
|
|
1447
|
+
localStorage.setItem(tierKey, fallbackTier);
|
|
1448
|
+
}
|
|
1449
|
+
return getActiveLocalModelTier();
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function setLocalModelTier(tierKey) {
|
|
1453
|
+
const profiles = getLocalModelProfiles();
|
|
1454
|
+
const nextTier = String(tierKey || "").trim().toLowerCase();
|
|
1455
|
+
if (!profiles[nextTier]) return;
|
|
1456
|
+
|
|
1457
|
+
const prevTier = getActiveLocalModelTier();
|
|
1458
|
+
if (prevTier === nextTier) {
|
|
1459
|
+
renderLocalModelTierSelector();
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const wasLocalActive = !!isLocalActive;
|
|
1464
|
+
localStorage.setItem(getLocalModelTierStorageKey(), nextTier);
|
|
1465
|
+
localStorage.setItem(getLocalModelTierUserSetKey(), "true");
|
|
1466
|
+
applyLocalModelProfileToConfig();
|
|
1467
|
+
|
|
1468
|
+
isLocalActive = wasLocalActive;
|
|
1469
|
+
isModelLoaded = false;
|
|
1470
|
+
localEngine = null;
|
|
1471
|
+
wllama = null;
|
|
1472
|
+
setLocalRuntimeState(null);
|
|
1473
|
+
isModelDownloaded = localStorage.getItem(getLocalDownloadStorageKey()) === "true";
|
|
1474
|
+
|
|
1475
|
+
updateLocalBtnState();
|
|
1476
|
+
renderLocalModelTierSelector();
|
|
1477
|
+
|
|
1478
|
+
const activeProfile = getActiveLocalModelProfile();
|
|
1479
|
+
if (activeProfile && typeof showToast === "function") {
|
|
1480
|
+
const readySuffix = isModelDownloaded ? " (\ub2e4\uc6b4\ub85c\ub4dc\ub428)" : "";
|
|
1481
|
+
showToast(`${activeProfile.label || nextTier} \ubaa8\ub378 \uc120\ud0dd${readySuffix}`);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
if (isModelDownloaded) {
|
|
1485
|
+
if (wasLocalActive) {
|
|
1486
|
+
startDownload();
|
|
1487
|
+
}
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// On tier change, start downloading the newly selected model immediately.
|
|
1492
|
+
isLocalActive = true;
|
|
1493
|
+
updateLocalBtnState();
|
|
1494
|
+
renderLocalModelTierSelector();
|
|
1495
|
+
syncLocalModelTierVisibility();
|
|
1496
|
+
startDownload();
|
|
1497
|
+
}
|
|
1498
|
+
window.setLocalModelTier = setLocalModelTier;
|
|
1499
|
+
|
|
1500
|
+
function getLocalModelPopupSizeText(profile) {
|
|
1501
|
+
if (!profile) return "429MB";
|
|
1502
|
+
if (profile.popupSizeText) return String(profile.popupSizeText);
|
|
1503
|
+
if (profile.key === "code") return "369MB";
|
|
1504
|
+
if (profile.key === "middle") return "592MB";
|
|
1505
|
+
if (profile.key === "hard") return "558MB";
|
|
1506
|
+
return "429MB";
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function getLocalModelPopupTitle() {
|
|
1510
|
+
const locale = String(document.documentElement.lang || navigator.language || "ko").toLowerCase();
|
|
1511
|
+
if (locale.startsWith("ko")) return "\ub85c\uceec \ubaa8\ub378 \ub2e4\uc6b4\ub85c\ub4dc";
|
|
1512
|
+
return "Download Local Model";
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function getLocalModelPopupMessage() {
|
|
1516
|
+
const locale = String(document.documentElement.lang || navigator.language || "ko").toLowerCase();
|
|
1517
|
+
const profile = getActiveLocalModelProfile();
|
|
1518
|
+
const sizeText = getLocalModelPopupSizeText(profile);
|
|
1519
|
+
if (locale.startsWith("ko")) {
|
|
1520
|
+
return `\ub85c\uceec \ubaa8\ub378(${sizeText})\uc744 \ub2e4\uc6b4\ub85c\ub4dc\ud55c \ub4a4 \ub85c\uceec \ubaa8\ub4dc\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc5b4\uc694. \uacc4\uc18d\ud560\uae4c\uc694?`;
|
|
1521
|
+
}
|
|
1522
|
+
return `Download the local model (${sizeText}) to use local mode. Continue?`;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function handleLocalToggle() {
|
|
1526
|
+
ensureDefaultLocalModelTierForActivation();
|
|
1527
|
+
applyLocalModelProfileToConfig();
|
|
1528
|
+
isModelDownloaded = localStorage.getItem(getLocalDownloadStorageKey()) === "true";
|
|
1529
|
+
|
|
1530
|
+
if (isModelDownloaded) {
|
|
1531
|
+
isLocalActive = !isLocalActive;
|
|
1532
|
+
if (!isLocalActive || isModelLoaded) {
|
|
1533
|
+
updateLocalBtnState();
|
|
1534
|
+
renderLocalModelTierSelector();
|
|
1535
|
+
syncLocalModelTierVisibility();
|
|
1536
|
+
} else {
|
|
1537
|
+
startDownload();
|
|
1538
|
+
}
|
|
1539
|
+
} else {
|
|
1540
|
+
showPopup(getLocalModelPopupTitle(), getLocalModelPopupMessage(), () => {
|
|
1541
|
+
isLocalActive = true;
|
|
1542
|
+
syncLocalModelTierVisibility();
|
|
1543
|
+
startDownload();
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function setLocalRuntimeState(runtimeName) {
|
|
1549
|
+
localRuntime = runtimeName || null;
|
|
1550
|
+
window.ISAI_LOCAL_RUNTIME = localRuntime;
|
|
1551
|
+
if (localRuntime) {
|
|
1552
|
+
localStorage.setItem(getLocalRuntimeStorageKey(), localRuntime);
|
|
1553
|
+
} else {
|
|
1554
|
+
localStorage.removeItem(getLocalRuntimeStorageKey());
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function updateLocalProgress(bar, progress) {
|
|
1559
|
+
if (!bar) return;
|
|
1560
|
+
const numeric = Number(progress);
|
|
1561
|
+
const safeProgress = Number.isFinite(numeric) ? numeric : 0;
|
|
1562
|
+
const normalized = safeProgress > 1 ? safeProgress : safeProgress * 100;
|
|
1563
|
+
const percent = Math.max(0, Math.min(100, Math.round(normalized)));
|
|
1564
|
+
bar.style.width = percent + "%";
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function mapPromptMessagesForLocalEngine(promptArr) {
|
|
1568
|
+
if (!Array.isArray(promptArr)) return [];
|
|
1569
|
+
return promptArr
|
|
1570
|
+
.map((message) => ({
|
|
1571
|
+
role: message && typeof message.role === "string" ? message.role : "user",
|
|
1572
|
+
content: String(message && message.content != null ? message.content : "")
|
|
1573
|
+
}))
|
|
1574
|
+
.filter((message) => message.content.trim().length > 0);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
async function loadWebLLMLocalEngine(container, bar) {
|
|
1578
|
+
const modelConfig = window.MODEL_CONFIG || {};
|
|
1579
|
+
const webllm = window.WebLLMObj;
|
|
1580
|
+
if (!webllm || typeof webllm.CreateMLCEngine !== "function") {
|
|
1581
|
+
throw new Error("WebLLM module is not ready.");
|
|
1582
|
+
}
|
|
1583
|
+
if (!navigator.gpu) {
|
|
1584
|
+
throw new Error("WebGPU is not available in this browser.");
|
|
1585
|
+
}
|
|
1586
|
+
if (localEngine && localRuntime === "webllm") {
|
|
1587
|
+
return localEngine;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
const modelId = modelConfig.webllm ? modelConfig.webllm.modelId : "Qwen2-0.5B-Instruct-q4f16_1-MLC";
|
|
1591
|
+
localEngine = await webllm.CreateMLCEngine(modelId, {
|
|
1592
|
+
logLevel: "WARN",
|
|
1593
|
+
initProgressCallback: (report) => {
|
|
1594
|
+
if (container) container.classList.remove("hidden");
|
|
1595
|
+
updateLocalProgress(bar, report && report.progress);
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
setLocalRuntimeState("webllm");
|
|
1600
|
+
return localEngine;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
async function loadWllamaFallbackEngine(container, bar) {
|
|
1604
|
+
const modelConfig = window.MODEL_CONFIG || {};
|
|
1605
|
+
const fallbackConfig = modelConfig.fallback || {};
|
|
1606
|
+
const { Wllama, LoggerWithoutDebug } = window.WllamaObj || {};
|
|
1607
|
+
if (!Wllama) {
|
|
1608
|
+
throw new Error("Fallback runtime is not available.");
|
|
1609
|
+
}
|
|
1610
|
+
if (!fallbackConfig.url || !fallbackConfig.wasmPaths) {
|
|
1611
|
+
throw new Error("Fallback model configuration is missing.");
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
if (!wllama) {
|
|
1615
|
+
wllama = new Wllama(fallbackConfig.wasmPaths, { logger: LoggerWithoutDebug });
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
await wllama.loadModelFromUrl(fallbackConfig.url, {
|
|
1619
|
+
n_ctx: 4096,
|
|
1620
|
+
progressCallback: ({ loaded, total }) => {
|
|
1621
|
+
if (container) container.classList.remove("hidden");
|
|
1622
|
+
if (total) {
|
|
1623
|
+
updateLocalProgress(bar, loaded / total);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
localEngine = wllama;
|
|
1629
|
+
setLocalRuntimeState("wllama");
|
|
1630
|
+
return localEngine;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
async function startDownload() {
|
|
1634
|
+
const container = document.getElementById("progress-container");
|
|
1635
|
+
const bar = document.getElementById("progress-bar");
|
|
1636
|
+
applyLocalModelProfileToConfig();
|
|
1637
|
+
|
|
1638
|
+
try {
|
|
1639
|
+
container.classList.remove("hidden");
|
|
1640
|
+
updateLocalProgress(bar, 0);
|
|
1641
|
+
const modelConfig = window.MODEL_CONFIG || {};
|
|
1642
|
+
const preferredRuntime = String(modelConfig.preferredRuntime || "").toLowerCase();
|
|
1643
|
+
let webllmError = null;
|
|
1644
|
+
|
|
1645
|
+
if (preferredRuntime === "wllama") {
|
|
1646
|
+
await loadWllamaFallbackEngine(container, bar);
|
|
1647
|
+
} else {
|
|
1648
|
+
try {
|
|
1649
|
+
await loadWebLLMLocalEngine(container, bar);
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
webllmError = error;
|
|
1652
|
+
await loadWllamaFallbackEngine(container, bar);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
isModelDownloaded = true;
|
|
1657
|
+
isModelLoaded = true;
|
|
1658
|
+
isLocalActive = true;
|
|
1659
|
+
localStorage.setItem(getLocalDownloadStorageKey(), "true");
|
|
1660
|
+
container.classList.add("hidden");
|
|
1661
|
+
updateLocalBtnState();
|
|
1662
|
+
|
|
1663
|
+
if (webllmError && localRuntime === "wllama") {
|
|
1664
|
+
console.warn("WebLLM initialization failed, using fallback runtime instead.", webllmError);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
if (error.message && error.message.includes("initialized")) {
|
|
1669
|
+
isModelDownloaded = true;
|
|
1670
|
+
isModelLoaded = true;
|
|
1671
|
+
isLocalActive = true;
|
|
1672
|
+
localStorage.setItem(getLocalDownloadStorageKey(), "true");
|
|
1673
|
+
container.classList.add("hidden");
|
|
1674
|
+
updateLocalBtnState();
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
container.classList.add("hidden");
|
|
1679
|
+
alert(error.message);
|
|
1680
|
+
isModelDownloaded = false;
|
|
1681
|
+
isModelLoaded = false;
|
|
1682
|
+
localEngine = null;
|
|
1683
|
+
setLocalRuntimeState(null);
|
|
1684
|
+
localStorage.removeItem(getLocalDownloadStorageKey());
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
async function runLocalInference(promptArr, callback) {
|
|
1689
|
+
const modelConfig = window.MODEL_CONFIG || {};
|
|
1690
|
+
const webllmConfig = modelConfig.webllm || {};
|
|
1691
|
+
if (isLocalActive && !isModelLoaded) {
|
|
1692
|
+
await startDownload();
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
if (localRuntime === "webllm" && localEngine) {
|
|
1696
|
+
const messages = mapPromptMessagesForLocalEngine(promptArr);
|
|
1697
|
+
if (messages.length === 0) return;
|
|
1698
|
+
|
|
1699
|
+
if (typeof localEngine.resetChat === "function") {
|
|
1700
|
+
try {
|
|
1701
|
+
await localEngine.resetChat();
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
console.warn("Failed to reset local WebLLM chat state.", error);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
const stream = await localEngine.chat.completions.create({
|
|
1708
|
+
messages,
|
|
1709
|
+
temperature: webllmConfig.temperature ?? 0.65,
|
|
1710
|
+
top_p: webllmConfig.topP ?? 0.9,
|
|
1711
|
+
max_tokens: webllmConfig.maxTokens ?? 900,
|
|
1712
|
+
stream: true
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
for await (const chunk of stream) {
|
|
1716
|
+
if (stopSignal) {
|
|
1717
|
+
if (typeof localEngine.interruptGenerate === "function") {
|
|
1718
|
+
localEngine.interruptGenerate();
|
|
1719
|
+
}
|
|
1720
|
+
break;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
const delta = chunk && chunk.choices && chunk.choices[0] && chunk.choices[0].delta
|
|
1724
|
+
? chunk.choices[0].delta.content
|
|
1725
|
+
: "";
|
|
1726
|
+
|
|
1727
|
+
if (delta) {
|
|
1728
|
+
callback(delta);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const formatted = await wllama.formatChat(promptArr, true);
|
|
1735
|
+
|
|
1736
|
+
const decoder = new TextDecoder("utf-8");
|
|
1737
|
+
|
|
1738
|
+
await wllama.createCompletion(formatted, {
|
|
1739
|
+
nPredict: 1000,
|
|
1740
|
+
sampling: { temp: 0.7, top_k: 40, top_p: 0.9 },
|
|
1741
|
+
onNewToken: (tokenIndex, tokenBytes) => {
|
|
1742
|
+
if (stopSignal) return false;
|
|
1743
|
+
callback(decoder.decode(tokenBytes, { stream: true }));
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function showLoader(show) {
|
|
1749
|
+
const overlay = document.getElementById("loader-overlay");
|
|
1750
|
+
if (show) {
|
|
1751
|
+
overlay.classList.add("active");
|
|
1752
|
+
} else {
|
|
1753
|
+
overlay.classList.remove("active");
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function stopGeneration() {
|
|
1758
|
+
if (isGenerating) {
|
|
1759
|
+
if (abortController) {
|
|
1760
|
+
abortController.abort();
|
|
1761
|
+
abortController = null;
|
|
1762
|
+
}
|
|
1763
|
+
if (localRuntime === "webllm" && localEngine && typeof localEngine.interruptGenerate === "function") {
|
|
1764
|
+
localEngine.interruptGenerate();
|
|
1765
|
+
}
|
|
1766
|
+
stopSignal = true;
|
|
1767
|
+
showLoader(false);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function decodeHtmlEntitiesLocal(value) {
|
|
1772
|
+
const textarea = document.createElement("textarea");
|
|
1773
|
+
textarea.innerHTML = String(value ?? "");
|
|
1774
|
+
return textarea.value;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function sanitizeAiHtmlLocal(value) {
|
|
1778
|
+
return String(value ?? "")
|
|
1779
|
+
.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "")
|
|
1780
|
+
.replace(/<img\b[^>]*>/gi, "")
|
|
1781
|
+
.replace(/\son\w+=(["']).*?\1/gi, "")
|
|
1782
|
+
.replace(/\son\w+=([^\s>]+)/gi, "")
|
|
1783
|
+
.replace(/\s(href|src)\s*=\s*(["'])\s*javascript:[\s\S]*?\2/gi, ' $1="#"');
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function formatAiBubbleContent(content) {
|
|
1787
|
+
const raw = String(content ?? "");
|
|
1788
|
+
const decoded = decodeHtmlEntitiesLocal(raw);
|
|
1789
|
+
const sanitizedDecoded = sanitizeAiHtmlLocal(decoded);
|
|
1790
|
+
const sanitizedRaw = sanitizeAiHtmlLocal(raw);
|
|
1791
|
+
const htmlCandidate = /<\/?[a-z][^>]*>/i.test(sanitizedDecoded) ? sanitizedDecoded : sanitizedRaw;
|
|
1792
|
+
|
|
1793
|
+
if (/<\/?[a-z][^>]*>/i.test(htmlCandidate)) {
|
|
1794
|
+
return htmlCandidate;
|
|
1795
|
+
}
|
|
1796
|
+
return htmlCandidate.replace(/\n/g, "<br>");
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function isImageErrorMessageLocal(value) {
|
|
1800
|
+
return /^Image Error:/i.test(String(value ?? "").trim());
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function imageErrorIconHtmlLocal(message) {
|
|
1804
|
+
const safeMessage = String(message ?? "")
|
|
1805
|
+
.replace(/&/g, "&")
|
|
1806
|
+
.replace(/</g, "<")
|
|
1807
|
+
.replace(/>/g, ">")
|
|
1808
|
+
.replace(/"/g, """)
|
|
1809
|
+
.trim() || "Image generation failed";
|
|
1810
|
+
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>`;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function appendMsg(role, content) {
|
|
1814
|
+
const chatBox = document.getElementById("chat-box");
|
|
1815
|
+
|
|
1816
|
+
if (chatBox.childElementCount > 50) chatBox.removeChild(chatBox.firstElementChild);
|
|
1817
|
+
if (chatBox.childElementCount === 0) {
|
|
1818
|
+
const spacer = document.createElement("div");
|
|
1819
|
+
spacer.style.marginTop = "auto";
|
|
1820
|
+
chatBox.appendChild(spacer);
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
const bubble = document.createElement("div");
|
|
1824
|
+
if (role === "user") {
|
|
1825
|
+
bubble.className = "self-end px-4 py-2.5 rounded-[18px] rounded-tr-none max-w-[85%] shadow mb-3 text-sm break-words ml-auto";
|
|
1826
|
+
bubble.style.backgroundColor = "var(--accent, #3b82f6)";
|
|
1827
|
+
bubble.style.color = "var(--accent-text, #ffffff)";
|
|
1828
|
+
} else {
|
|
1829
|
+
bubble.className = "self-start px-4 py-2.5 rounded-[18px] rounded-tl-none max-w-[85%] mb-3 text-sm break-words leading-relaxed shadow-sm";
|
|
1830
|
+
bubble.style.backgroundColor = "var(--bg-island, #ffffff)";
|
|
1831
|
+
bubble.style.color = "var(--text-main, #333333)";
|
|
1832
|
+
if (currentMode === "search") {
|
|
1833
|
+
bubble.classList.add("search-result-bubble");
|
|
1834
|
+
bubble.style.backgroundColor = "#ffffff";
|
|
1835
|
+
bubble.style.color = "#111827";
|
|
1836
|
+
bubble.style.border = "1px solid rgba(15, 23, 42, 0.12)";
|
|
1837
|
+
bubble.style.boxShadow = "0 14px 30px rgba(15, 23, 42, 0.10)";
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
if (role === "user") {
|
|
1842
|
+
const safeContent = String(content ?? "")
|
|
1843
|
+
.replace(/&/g, "&")
|
|
1844
|
+
.replace(/</g, "<")
|
|
1845
|
+
.replace(/>/g, ">");
|
|
1846
|
+
bubble.innerHTML = safeContent.replace(/\n/g, "<br>");
|
|
1847
|
+
} else {
|
|
1848
|
+
const imageError = role === "error" && isImageErrorMessageLocal(content);
|
|
1849
|
+
if (imageError) {
|
|
1850
|
+
bubble.className = "chat-bubble-image-error";
|
|
1851
|
+
bubble.style.backgroundColor = "";
|
|
1852
|
+
bubble.style.color = "";
|
|
1853
|
+
bubble.style.border = "";
|
|
1854
|
+
bubble.style.boxShadow = "";
|
|
1855
|
+
bubble.innerHTML = imageErrorIconHtmlLocal(content);
|
|
1856
|
+
} else {
|
|
1857
|
+
bubble.innerHTML = formatAiBubbleContent(content);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
chatBox.appendChild(bubble);
|
|
1862
|
+
scrollBottom();
|
|
1863
|
+
return bubble;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function appendImg(base64Data, promptText) {
|
|
1867
|
+
const chatBox = document.getElementById("chat-box");
|
|
1868
|
+
const container = document.createElement("div");
|
|
1869
|
+
|
|
1870
|
+
container.className = "bg-[#1a1a1a] border border-white/5 rounded-[26px] p-2 mb-6 shadow-xl self-start max-w-sm";
|
|
1871
|
+
|
|
1872
|
+
container.innerHTML = `
|
|
1873
|
+
<img src="data:image/jpeg;base64,${base64Data}"
|
|
1874
|
+
class="w-full rounded-[20px] block cursor-zoom-in hover:opacity-90 transition-opacity"
|
|
1875
|
+
onclick="openImageModal('data:image/jpeg;base64,${base64Data}', '${promptText.replace(/'/g, "\\'")}')">
|
|
1876
|
+
|
|
1877
|
+
<div class="flex justify-between items-center pt-3 px-2 pb-1">
|
|
1878
|
+
<div class="flex flex-col overflow-hidden mr-3">
|
|
1879
|
+
<span class="text-[11px] text-gray-400 font-medium truncate tracking-tight uppercase opacity-50 mb-0.5">Prompt</span>
|
|
1880
|
+
<span class="text-[13px] text-gray-200 font-bold truncate leading-tight">${promptText}</span>
|
|
1881
|
+
</div>
|
|
1882
|
+
|
|
1883
|
+
<a href="data:image/jpeg;base64,${base64Data}" download="isai-art.jpg"
|
|
1884
|
+
class="flex-shrink-0 w-9 h-9 flex items-center justify-center rounded-full bg-white/5 border border-white/5 text-gray-300 hover:bg-white hover:text-black transition-all"
|
|
1885
|
+
onclick="event.stopPropagation()">
|
|
1886
|
+
<i class="ri-download-2-line text-lg"></i>
|
|
1887
|
+
</a>
|
|
1888
|
+
</div>
|
|
1889
|
+
`;
|
|
1890
|
+
|
|
1891
|
+
chatBox.appendChild(container);
|
|
1892
|
+
scrollBottom();
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
function appendGif(blob, promptText) {
|
|
1896
|
+
const url = URL.createObjectURL(blob);
|
|
1897
|
+
const chatBox = document.getElementById("chat-box");
|
|
1898
|
+
const container = document.createElement("div");
|
|
1899
|
+
|
|
1900
|
+
container.className = "self-start bg-white/30 p-2 rounded-2xl rounded-tl-sm max-w-sm mb-2 cursor-pointer hover:opacity-90 transition";
|
|
1901
|
+
container.onclick = () => window.openPreview(url);
|
|
1902
|
+
|
|
1903
|
+
container.innerHTML = `
|
|
1904
|
+
<div class="relative rounded-lg overflow-hidden mb-2">
|
|
1905
|
+
<img src="${url}" class="w-full h-auto object-cover">
|
|
1906
|
+
<div class="absolute top-2 right-2 bg-black/50 text-white text-[10px] px-2 py-0.5 rounded backdrop-blur-sm">GIF</div>
|
|
1907
|
+
</div>
|
|
1908
|
+
<div class="flex justify-end text-[10px] text-gray-400 px-1">
|
|
1909
|
+
<a href="${url}" download="ai_video.gif" class="text-white transition" onclick="event.stopPropagation()">
|
|
1910
|
+
<i class="ri-download-line text-xl"></i>
|
|
1911
|
+
</a>
|
|
1912
|
+
</div>`;
|
|
1913
|
+
|
|
1914
|
+
chatBox.appendChild(container);
|
|
1915
|
+
scrollBottom();
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
function addSourcesToBubble(bubbleElement, sources) {
|
|
1919
|
+
if (!bubbleElement || !Array.isArray(sources) || sources.length === 0) return;
|
|
1920
|
+
|
|
1921
|
+
const uniqueSources = sources.filter((src, index, arr) => {
|
|
1922
|
+
if (!src || !src.url) return false;
|
|
1923
|
+
return arr.findIndex((item) => item && item.url === src.url) === index;
|
|
1924
|
+
}).slice(0, 5);
|
|
1925
|
+
|
|
1926
|
+
const escapeHtml = (value) => String(value ?? "")
|
|
1927
|
+
.replace(/&/g, "&")
|
|
1928
|
+
.replace(/</g, "<")
|
|
1929
|
+
.replace(/>/g, ">")
|
|
1930
|
+
.replace(/"/g, """);
|
|
1931
|
+
|
|
1932
|
+
const domainLabel = (url) => {
|
|
1933
|
+
try {
|
|
1934
|
+
return new URL(url).hostname.replace(/^www\./i, "");
|
|
1935
|
+
} catch (error) {
|
|
1936
|
+
return url;
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
|
|
1940
|
+
let html = '<div class="mt-4 pt-3 border-t border-black/10 flex flex-wrap gap-2 items-center">';
|
|
1941
|
+
|
|
1942
|
+
uniqueSources.forEach((src) => {
|
|
1943
|
+
const favicon = "https://www.google.com/s2/favicons?sz=64&domain_url=" + encodeURIComponent(src.url);
|
|
1944
|
+
const label = src.title || src.url;
|
|
1945
|
+
const host = domainLabel(src.url);
|
|
1946
|
+
html += `<a href="${escapeHtml(src.url)}" target="_blank" rel="noopener noreferrer" class="search-source-link inline-flex items-center gap-1.5 max-w-[150px] rounded-full bg-black/[0.04] border border-black/[0.08] px-2.5 py-1 text-[11px] font-medium text-[#111827] hover:bg-black/[0.08] transition" title="${escapeHtml(label)}"><img src="${favicon}" class="w-3.5 h-3.5 opacity-85" alt=""><span class="truncate">${escapeHtml(host)}</span></a>`;
|
|
1947
|
+
});
|
|
1948
|
+
|
|
1949
|
+
html += "</div>";
|
|
1950
|
+
bubbleElement.innerHTML += html;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function scrollBottom() {
|
|
1954
|
+
const chatBox = document.getElementById("chat-box");
|
|
1955
|
+
requestAnimationFrame(() => {
|
|
1956
|
+
chatBox.scrollTop = chatBox.scrollHeight;
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
function parseMarkdownLocal(text, isFinished = false) {
|
|
1961
|
+
window.extractedCodes =[];
|
|
1962
|
+
|
|
1963
|
+
// 1. 생각 과정(<think>...</think>) 분리
|
|
1964
|
+
let thinkContent = "";
|
|
1965
|
+
let mainContent = text;
|
|
1966
|
+
|
|
1967
|
+
const thinkStart = mainContent.indexOf("<think>");
|
|
1968
|
+
if (thinkStart !== -1) {
|
|
1969
|
+
const thinkEnd = mainContent.indexOf("</think>", thinkStart);
|
|
1970
|
+
if (thinkEnd !== -1) {
|
|
1971
|
+
thinkContent = mainContent.substring(thinkStart + 7, thinkEnd).trim();
|
|
1972
|
+
mainContent = mainContent.substring(0, thinkStart) + mainContent.substring(thinkEnd + 9);
|
|
1973
|
+
} else {
|
|
1974
|
+
thinkContent = mainContent.substring(thinkStart + 7).trim();
|
|
1975
|
+
mainContent = mainContent.substring(0, thinkStart);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
let html = "";
|
|
1980
|
+
if (thinkContent) {
|
|
1981
|
+
const isThinking = (!isFinished) && (text.indexOf("</think>") === -1);
|
|
1982
|
+
const formattedThink = thinkContent.replace(/</g, "<").replace(/>/g, ">").replace(/\n/g, "<br>");
|
|
1983
|
+
|
|
1984
|
+
const displayStyle = isThinking ? "block" : "none";
|
|
1985
|
+
const iconRotate = isThinking ? "rotate(180deg)" : "rotate(0deg)";
|
|
1986
|
+
|
|
1987
|
+
html += `
|
|
1988
|
+
<div class="my-3 rounded-xl border border-white/10 bg-[#1e1e1e] shadow-sm overflow-hidden w-full max-w-full transition-all">
|
|
1989
|
+
<div class="flex items-center justify-between px-4 py-2 bg-white/5 cursor-pointer hover:bg-white/10 transition select-none" onclick="window.toggleThink(this)">
|
|
1990
|
+
<div class="text-gray-400 font-bold text-[12px] uppercase tracking-wider">
|
|
1991
|
+
Thinking Process
|
|
1992
|
+
</div>
|
|
1993
|
+
<i class="toggle-icon ri-arrow-down-s-line text-gray-500 transition-transform duration-200 text-lg" style="transform: ${iconRotate};"></i>
|
|
1994
|
+
</div>
|
|
1995
|
+
<div class="p-4 text-gray-500 bg-black/20 border-t border-white/5 text-[13px] leading-relaxed italic break-words font-light" style="display: ${displayStyle};">
|
|
1996
|
+
${formattedThink}
|
|
1997
|
+
</div>
|
|
1998
|
+
</div>`;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// 2. 전체 HTML 이스케이프 (안전장치: 쌩 HTML이 페이지 UI를 망가뜨리는 것 원천 차단)
|
|
2002
|
+
let processedMain = mainContent.replace(/</g, "<").replace(/>/g, ">");
|
|
2003
|
+
|
|
2004
|
+
const blocks =[];
|
|
2005
|
+
let counter = 0;
|
|
2006
|
+
|
|
2007
|
+
function createBlockUI(lang, code, isStreaming) {
|
|
2008
|
+
const blockId = "local-code-" + counter++;
|
|
2009
|
+
lang = lang || "text";
|
|
2010
|
+
|
|
2011
|
+
// 우측 코드 에디터 및 복사 기능을 위해 원본 코드로 복구하여 배열에 저장
|
|
2012
|
+
let cleanCode = code.replace(/</g, "<").replace(/>/g, ">");
|
|
2013
|
+
window.extractedCodes.push({
|
|
2014
|
+
lang: lang,
|
|
2015
|
+
content: cleanCode,
|
|
2016
|
+
name: `File ${window.extractedCodes.length + 1} (${lang})`
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
const blink = isStreaming ? ' <span class="animate-pulse">...</span>' : '';
|
|
2020
|
+
const uiBlock = `<div class="code-wrapper my-4 rounded-lg overflow-hidden border border-white/10 bg-[#1e1e1e] shadow-lg"><div class="flex justify-between items-center px-4 py-2 bg-white/5 border-b border-white/5"><span class="text-xs text-gray-200 font-mono uppercase">${lang}${blink}</span><div class="flex items-center gap-3"><button onclick="openFullEditor('${blockId}')" class="flex md:hidden items-center gap-1 text-xs text-blue-300 hover:text-blue-200 transition"><i class="ri-code-box-line"></i> Edit</button><button onclick="copyCode('${blockId}')" class="flex items-center gap-1 text-xs text-gray-200 hover:text-white transition group"><i class="ri-file-copy-line"></i> <span class="group-hover:underline">Copy</span></button></div></div><div class="relative overflow-x-auto code-scroll"><pre id="${blockId}" class="p-4 text-sm font-mono leading-relaxed w-max min-w-full text-gray-50"><code>${code}</code></pre></div></div>`;
|
|
2021
|
+
|
|
2022
|
+
blocks.push(uiBlock);
|
|
2023
|
+
return "###CODE_BLOCK_" + (blocks.length - 1) + "###";
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// 3. 완전히 닫힌 코드 블록 처리
|
|
2027
|
+
processedMain = processedMain.replace(/```([a-zA-Z0-9_\-\+]*)[ \t]*\n?([\s\S]*?)```/g, (match, lang, code) => {
|
|
2028
|
+
return createBlockUI(lang, code, false);
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
// 4. 작성 중이라 아직 닫히지 않은 코드 블록 처리 (스트리밍 대응)
|
|
2032
|
+
processedMain = processedMain.replace(/```([a-zA-Z0-9_\-\+]*)[ \t]*\n?([\s\S]*)$/g, (match, lang, code) => {
|
|
2033
|
+
return createBlockUI(lang, code, !isFinished);
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
// 5. 인라인 코드 및 기본 마크다운 변환
|
|
2037
|
+
processedMain = processedMain.replace(/`([^`]+)`/g, (match, inlineCode) => {
|
|
2038
|
+
return `<code class="bg-white/20 px-1.5 py-0.5 rounded text-white font-bold font-mono text-sm border border-white/10">${inlineCode}</code>`;
|
|
2039
|
+
})
|
|
2040
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong class="font-semibold">$1</strong>')
|
|
2041
|
+
.replace(/^###\s+(.*)$/gm, '<h3 class="text-lg font-bold mb-2 mt-4">$1</h3>')
|
|
2042
|
+
.replace(/^##\s+(.*)$/gm, '<b class="font-bold text-base mt-3 mb-1 block">$1</b>')
|
|
2043
|
+
.replace(/^[\-\*]\s+(.*)$/gm, '<li class="ml-4 list-disc text-sm">$1</li>')
|
|
2044
|
+
.replace(/\n/g, "<br>");
|
|
2045
|
+
|
|
2046
|
+
// 6. 생성해둔 UI 블록을 원래 위치에 삽입
|
|
2047
|
+
blocks.forEach((blockHtml, index) => {
|
|
2048
|
+
processedMain = processedMain.replace("###CODE_BLOCK_" + index + "###", blockHtml);
|
|
2049
|
+
});
|
|
2050
|
+
|
|
2051
|
+
return html + processedMain;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
window.currentImagePrompt = "";
|
|
2055
|
+
|
|
2056
|
+
window.openPreview = function(url) {
|
|
2057
|
+
const container = document.getElementById("img-preview-container");
|
|
2058
|
+
const img = document.getElementById("preview-img");
|
|
2059
|
+
const appContainer = document.getElementById("app-container");
|
|
2060
|
+
const centerApp = document.getElementById("center-app-name");
|
|
2061
|
+
|
|
2062
|
+
if (container && img) {
|
|
2063
|
+
if (url.startsWith("blob:") || url.startsWith("data:") || url.startsWith("http")) {
|
|
2064
|
+
img.src = url;
|
|
2065
|
+
} else {
|
|
2066
|
+
img.src = `data:image/jpeg;base64,${url}`;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
container.style.setProperty("display", "block", "important");
|
|
2070
|
+
container.classList.add("active");
|
|
2071
|
+
|
|
2072
|
+
if (appContainer) appContainer.style.setProperty("display", "none", "important");
|
|
2073
|
+
if (centerApp) centerApp.style.setProperty("display", "none", "important");
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
|
|
2077
|
+
window.closePreview = function() {
|
|
2078
|
+
const container = document.getElementById("img-preview-container");
|
|
2079
|
+
const appContainer = document.getElementById("app-container");
|
|
2080
|
+
const centerApp = document.getElementById("center-app-name");
|
|
2081
|
+
|
|
2082
|
+
if (container) {
|
|
2083
|
+
container.classList.remove("active");
|
|
2084
|
+
container.style.setProperty("display", "none", "important");
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
if (appContainer) {
|
|
2088
|
+
appContainer.style.opacity = "1";
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
if (centerApp) {
|
|
2092
|
+
centerApp.style.setProperty("display", "block", "important");
|
|
2093
|
+
centerApp.style.opacity = "1";
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
const currentUrl = new URL(window.location.href);
|
|
2097
|
+
if (currentUrl.searchParams.get("v")) {
|
|
2098
|
+
currentUrl.searchParams.delete("v");
|
|
2099
|
+
window.history.pushState({}, "", currentUrl.pathname);
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
|
|
2103
|
+
window.extractedCodes =[];
|
|
2104
|
+
|
|
2105
|
+
window.openFullEditor = function(blockId) {
|
|
2106
|
+
const el = document.getElementById(blockId);
|
|
2107
|
+
if (el) {
|
|
2108
|
+
const text = el.innerText;
|
|
2109
|
+
const codeEditor = document.getElementById("code-editor");
|
|
2110
|
+
const rightPanel = document.getElementById("right-panel");
|
|
2111
|
+
const codeTabs = document.getElementById("code-tabs");
|
|
2112
|
+
const rightPanelOriginAnchor = document.getElementById("right-panel-origin-anchor");
|
|
2113
|
+
const desktopCodePanelHost = document.getElementById("desktop-code-panel-host");
|
|
2114
|
+
const vw = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
2115
|
+
const mobileCodeMode = vw <= 900;
|
|
2116
|
+
const desktopCodeMode = vw > 900;
|
|
2117
|
+
const codePanelGridColumn = vw <= 1180 ? "1 / span 2" : "1 / span 4";
|
|
2118
|
+
const codePanelGridRow = vw <= 900 ? "2" : (vw <= 1180 ? "3" : "2");
|
|
2119
|
+
const codePanelMinHeight = vw <= 900 ? "240px" : "320px";
|
|
2120
|
+
const codePanelMaxHeight = vw <= 900 ? "360px" : "520px";
|
|
2121
|
+
if (codeEditor) codeEditor.value = text;
|
|
2122
|
+
if (codeTabs) {
|
|
2123
|
+
codeTabs.innerHTML = '<button class="px-3 py-1.5 text-xs rounded-md transition font-mono font-bold">Current Code</button>';
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
if (typeof window.syncInlineCodePanel === "function") {
|
|
2127
|
+
window.syncInlineCodePanel("code");
|
|
2128
|
+
} else {
|
|
2129
|
+
if (rightPanel) {
|
|
2130
|
+
if (document.body) document.body.setAttribute("data-ui-mode", "code");
|
|
2131
|
+
document.body.classList.remove("desktop-code-stage");
|
|
2132
|
+
document.body.classList.toggle("mode-code", mobileCodeMode);
|
|
2133
|
+
document.body.classList.toggle("desktop-code-open", desktopCodeMode);
|
|
2134
|
+
document.body.classList.toggle("desktop-code-panel-mounted", desktopCodeMode);
|
|
2135
|
+
rightPanel.classList.toggle("mobile-active", mobileCodeMode);
|
|
2136
|
+
rightPanel.classList.remove("hidden");
|
|
2137
|
+
if (rightPanelOriginAnchor && rightPanelOriginAnchor.parentElement && desktopCodePanelHost) {
|
|
2138
|
+
if (desktopCodeMode) {
|
|
2139
|
+
desktopCodePanelHost.style.display = "block";
|
|
2140
|
+
desktopCodePanelHost.style.width = "100%";
|
|
2141
|
+
desktopCodePanelHost.style.minWidth = "0";
|
|
2142
|
+
desktopCodePanelHost.style.minHeight = codePanelMinHeight;
|
|
2143
|
+
if (rightPanel.parentElement !== desktopCodePanelHost) {
|
|
2144
|
+
desktopCodePanelHost.appendChild(rightPanel);
|
|
2145
|
+
}
|
|
2146
|
+
} else if (rightPanel.parentElement !== rightPanelOriginAnchor.parentElement) {
|
|
2147
|
+
rightPanelOriginAnchor.parentElement.insertBefore(rightPanel, rightPanelOriginAnchor.nextSibling);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
rightPanel.style.display = "flex";
|
|
2151
|
+
rightPanel.style.visibility = "visible";
|
|
2152
|
+
rightPanel.style.opacity = "1";
|
|
2153
|
+
rightPanel.style.width = "100%";
|
|
2154
|
+
rightPanel.style.minWidth = "0";
|
|
2155
|
+
rightPanel.style.maxWidth = "100%";
|
|
2156
|
+
if (mobileCodeMode) {
|
|
2157
|
+
rightPanel.style.gridRow = codePanelGridRow;
|
|
2158
|
+
rightPanel.style.gridColumn = codePanelGridColumn;
|
|
2159
|
+
} else {
|
|
2160
|
+
rightPanel.style.removeProperty("grid-row");
|
|
2161
|
+
rightPanel.style.removeProperty("grid-column");
|
|
2162
|
+
}
|
|
2163
|
+
rightPanel.style.height = "auto";
|
|
2164
|
+
rightPanel.style.minHeight = codePanelMinHeight;
|
|
2165
|
+
rightPanel.style.maxHeight = codePanelMaxHeight;
|
|
2166
|
+
rightPanel.style.overflow = "hidden";
|
|
2167
|
+
rightPanel.style.borderTop = "1px solid rgba(255, 255, 255, 0.06)";
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
if (rightPanel) {
|
|
2172
|
+
setTimeout(() => {
|
|
2173
|
+
try {
|
|
2174
|
+
(desktopCodeMode && desktopCodePanelHost ? desktopCodePanelHost : rightPanel).scrollIntoView({ behavior: "smooth", block: "start" });
|
|
2175
|
+
} catch (error) {}
|
|
2176
|
+
}, vw <= 900 ? 90 : 20);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
|
|
2181
|
+
window.closeFullEditor = function() {
|
|
2182
|
+
const rightPanel = document.getElementById("right-panel");
|
|
2183
|
+
if (rightPanel) rightPanel.classList.remove("mobile-active");
|
|
2184
|
+
if (typeof currentMode !== "undefined" && currentMode === "code" && typeof setMode === "function") {
|
|
2185
|
+
setMode("chat");
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
document.body.classList.remove("mode-code");
|
|
2189
|
+
document.body.classList.remove("desktop-code-stage");
|
|
2190
|
+
if (document.body) document.body.setAttribute("data-ui-mode", "chat");
|
|
2191
|
+
document.body.classList.remove("desktop-code-open");
|
|
2192
|
+
document.body.classList.remove("desktop-code-panel-mounted");
|
|
2193
|
+
if (rightPanel) {
|
|
2194
|
+
rightPanel.classList.remove("mobile-active");
|
|
2195
|
+
rightPanel.style.removeProperty("display");
|
|
2196
|
+
rightPanel.style.removeProperty("visibility");
|
|
2197
|
+
rightPanel.style.removeProperty("opacity");
|
|
2198
|
+
rightPanel.style.removeProperty("width");
|
|
2199
|
+
rightPanel.style.removeProperty("min-width");
|
|
2200
|
+
rightPanel.style.removeProperty("max-width");
|
|
2201
|
+
rightPanel.style.removeProperty("grid-row");
|
|
2202
|
+
rightPanel.style.removeProperty("grid-column");
|
|
2203
|
+
rightPanel.style.removeProperty("height");
|
|
2204
|
+
rightPanel.style.removeProperty("min-height");
|
|
2205
|
+
rightPanel.style.removeProperty("max-height");
|
|
2206
|
+
rightPanel.style.removeProperty("overflow");
|
|
2207
|
+
rightPanel.style.removeProperty("border-top");
|
|
2208
|
+
rightPanel.classList.add("hidden");
|
|
2209
|
+
}
|
|
2210
|
+
const rightPanelOriginAnchor = document.getElementById("right-panel-origin-anchor");
|
|
2211
|
+
const desktopCodePanelHost = document.getElementById("desktop-code-panel-host");
|
|
2212
|
+
if (rightPanel && desktopCodePanelHost) {
|
|
2213
|
+
desktopCodePanelHost.style.display = "none";
|
|
2214
|
+
desktopCodePanelHost.style.removeProperty("min-height");
|
|
2215
|
+
desktopCodePanelHost.style.removeProperty("visibility");
|
|
2216
|
+
desktopCodePanelHost.style.removeProperty("opacity");
|
|
2217
|
+
desktopCodePanelHost.style.removeProperty("overflow");
|
|
2218
|
+
}
|
|
2219
|
+
if (rightPanel && rightPanelOriginAnchor && rightPanelOriginAnchor.parentElement && rightPanel.parentElement !== rightPanelOriginAnchor.parentElement) {
|
|
2220
|
+
rightPanelOriginAnchor.parentElement.insertBefore(rightPanel, rightPanelOriginAnchor.nextSibling);
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
|
|
2224
|
+
let codeFiles =[];
|
|
2225
|
+
let activeFileIndex = 0;
|
|
2226
|
+
|
|
2227
|
+
function updateCodePanel(text) {
|
|
2228
|
+
const matches =[...text.matchAll(/```(\w+)?\s*([\s\S]*?)(?:```|$)/g)];
|
|
2229
|
+
|
|
2230
|
+
if (matches.length !== 0) {
|
|
2231
|
+
codeFiles = matches.map((m, idx) => ({
|
|
2232
|
+
lang: m[1] || "Text",
|
|
2233
|
+
content: m[2],
|
|
2234
|
+
name: `File ${idx + 1} (${m[1] || "txt"})`
|
|
2235
|
+
}));
|
|
2236
|
+
renderCodeTabs();
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
function getCodeTabIcon(lang = "", name = "") {
|
|
2241
|
+
const key = String(lang || name || "").toLowerCase();
|
|
2242
|
+
if (key.includes("html")) return "ri-html5-line";
|
|
2243
|
+
if (key.includes("css") || key.includes("scss")) return "ri-css3-line";
|
|
2244
|
+
if (key.includes("js") || key.includes("ts") || key.includes("json")) return "ri-braces-line";
|
|
2245
|
+
if (key.includes("php")) return "ri-ai-generate-text";
|
|
2246
|
+
if (key.includes("py")) return "ri-terminal-box-line";
|
|
2247
|
+
if (key.includes("sql")) return "ri-database-2-line";
|
|
2248
|
+
if (key.includes("md")) return "ri-markdown-line";
|
|
2249
|
+
if (key.includes("sh") || key.includes("bash")) return "ri-terminal-line";
|
|
2250
|
+
return "ri-ai-generate-text";
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
function getCodeTabShortLabel(file = {}) {
|
|
2254
|
+
const lang = String(file.lang || "").trim();
|
|
2255
|
+
if (lang) return lang.slice(0, 4).toUpperCase();
|
|
2256
|
+
const name = String(file.name || "").trim();
|
|
2257
|
+
const ext = name.includes(".") ? name.split(".").pop() : name;
|
|
2258
|
+
return String(ext || "TXT").slice(0, 4).toUpperCase();
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
function getCodePanelEmptyMarkup() {
|
|
2262
|
+
return '<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>';
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
function renderCodeTabs() {
|
|
2266
|
+
const tabsContainer = document.getElementById("code-tabs");
|
|
2267
|
+
const editor = document.getElementById("code-editor");
|
|
2268
|
+
|
|
2269
|
+
if (tabsContainer) {
|
|
2270
|
+
tabsContainer.innerHTML = "";
|
|
2271
|
+
|
|
2272
|
+
if (codeFiles && codeFiles.length !== 0) {
|
|
2273
|
+
codeFiles.forEach((file, index) => {
|
|
2274
|
+
const btn = document.createElement("button");
|
|
2275
|
+
btn.type = "button";
|
|
2276
|
+
|
|
2277
|
+
btn.className = "code-tab-btn " + (index === activeFileIndex ? "active" : "");
|
|
2278
|
+
const iconClass = getCodeTabIcon(file.lang, file.name);
|
|
2279
|
+
const shortLabel = getCodeTabShortLabel(file);
|
|
2280
|
+
btn.title = file.name || file.lang || "Code";
|
|
2281
|
+
btn.innerHTML = `<span class="code-tab-icon"><i class="${iconClass}"></i></span><span class="code-tab-label">${shortLabel}</span>`;
|
|
2282
|
+
btn.onclick = (e) => {
|
|
2283
|
+
e.preventDefault();
|
|
2284
|
+
window.switchCodeFile(index);
|
|
2285
|
+
};
|
|
2286
|
+
|
|
2287
|
+
tabsContainer.appendChild(btn);
|
|
2288
|
+
});
|
|
2289
|
+
|
|
2290
|
+
if (editor && codeFiles[activeFileIndex]) {
|
|
2291
|
+
editor.value = codeFiles[activeFileIndex].content;
|
|
2292
|
+
}
|
|
2293
|
+
} else {
|
|
2294
|
+
tabsContainer.innerHTML = getCodePanelEmptyMarkup();
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
function switchTab(index) {
|
|
2300
|
+
activeFileIndex = index;
|
|
2301
|
+
renderCodeTabs();
|
|
2302
|
+
const editor = document.getElementById("code-editor");
|
|
2303
|
+
if (editor && codeFiles[index]) {
|
|
2304
|
+
editor.value = codeFiles[index].content;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function resetChat() {
|
|
2309
|
+
if (isGenerating) stopGeneration();
|
|
2310
|
+
|
|
2311
|
+
chatHistory =[];
|
|
2312
|
+
codeFiles =[];
|
|
2313
|
+
activeFileIndex = 0;
|
|
2314
|
+
|
|
2315
|
+
const chatBox = document.getElementById("chat-box");
|
|
2316
|
+
chatBox.style.display = "flex";
|
|
2317
|
+
chatBox.style.flexDirection = "column";
|
|
2318
|
+
chatBox.style.justifyContent = "normal";
|
|
2319
|
+
chatBox.innerHTML = "";
|
|
2320
|
+
|
|
2321
|
+
const viewId = new URLSearchParams(window.location.search).get("v");
|
|
2322
|
+
|
|
2323
|
+
document.getElementById("code-tabs").innerHTML = getCodePanelEmptyMarkup();
|
|
2324
|
+
document.getElementById("code-editor").value = "";
|
|
2325
|
+
|
|
2326
|
+
document.body.classList.remove("started");
|
|
2327
|
+
document.body.classList.remove("mode-code");
|
|
2328
|
+
isStarted = false;
|
|
2329
|
+
document.getElementById("custom-scrollbar").style.display = "none";
|
|
2330
|
+
|
|
2331
|
+
if (activeApp) exitAppMode();
|
|
2332
|
+
if (isMenuOpen) toggleStoreMenu();
|
|
2333
|
+
|
|
2334
|
+
document.getElementById("prompt-input").value = "";
|
|
2335
|
+
setMode("chat");
|
|
2336
|
+
showToast("Chat Reset Completed");
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
async function processBlogImages(element) {
|
|
2340
|
+
if (!element) return;
|
|
2341
|
+
|
|
2342
|
+
let html = element.innerHTML;
|
|
2343
|
+
const matches =[...html.matchAll(/\[\[IMG:\s*(.*?)\]\]/g)];
|
|
2344
|
+
|
|
2345
|
+
if (matches.length === 0) return;
|
|
2346
|
+
|
|
2347
|
+
const loaderId = "img-loader-" + Date.now();
|
|
2348
|
+
const loaderBlock = document.createElement("div");
|
|
2349
|
+
loaderBlock.id = loaderId;
|
|
2350
|
+
loaderBlock.className = "text-xs text-gray-500 mt-2 flex items-center gap-2 animate-pulse";
|
|
2351
|
+
loaderBlock.innerHTML = '<i class="ri-image-line"></i> Searching related images...';
|
|
2352
|
+
element.appendChild(loaderBlock);
|
|
2353
|
+
|
|
2354
|
+
for (const match of matches) {
|
|
2355
|
+
const fullMatch = match[0];
|
|
2356
|
+
const keyword = match[1].trim();
|
|
2357
|
+
let imgUrl = "";
|
|
2358
|
+
let creditHtml = "";
|
|
2359
|
+
|
|
2360
|
+
try {
|
|
2361
|
+
const res = await fetch(`?action=pixabay_search&q=${encodeURIComponent(keyword)}`);
|
|
2362
|
+
const data = await res.json();
|
|
2363
|
+
|
|
2364
|
+
if (data.hits && data.hits.length > 0) {
|
|
2365
|
+
const hit = data.hits[0];
|
|
2366
|
+
imgUrl = hit.webformatURL;
|
|
2367
|
+
creditHtml = `<div class="text-[10px] text-gray-500 mt-1 text-center">Image by <a href="${hit.pageURL}" target="_blank" class="underline">${hit.user}</a> from Pixabay</div>`;
|
|
2368
|
+
}
|
|
2369
|
+
} catch (error) {
|
|
2370
|
+
// Ignored
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
const uiHtml = `
|
|
2374
|
+
<div class="my-6 rounded-xl overflow-hidden shadow-lg border border-white/10 group relative bg-black/20">
|
|
2375
|
+
<img src="${imgUrl}" alt="${keyword}" class="w-full h-auto object-cover transition transform group-hover:scale-105 duration-700 min-h-[200px]" loading="lazy" onload="scrollBottom()">
|
|
2376
|
+
${creditHtml}
|
|
2377
|
+
<button onclick="openPreview('${imgUrl}')" class="absolute top-2 right-2 bg-black/50 text-white p-1.5 rounded-full opacity-0 group-hover:opacity-100 transition backdrop-blur-sm"><i class="ri-fullscreen-line"></i></button>
|
|
2378
|
+
</div>
|
|
2379
|
+
`;
|
|
2380
|
+
|
|
2381
|
+
html = html.replace(fullMatch, uiHtml);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
element.innerHTML = html;
|
|
2385
|
+
const loaderEl = document.getElementById(loaderId);
|
|
2386
|
+
if (loaderEl) loaderEl.remove();
|
|
2387
|
+
scrollBottom();
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
window.switchCodeFile = function(index) {
|
|
2391
|
+
activeFileIndex = index;
|
|
2392
|
+
renderCodeTabs();
|
|
2393
|
+
const editor = document.getElementById("code-editor");
|
|
2394
|
+
if (editor && codeFiles[index]) {
|
|
2395
|
+
editor.value = codeFiles[index].content;
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
|
|
2399
|
+
document.getElementById("code-editor").addEventListener("input", function(e) {
|
|
2400
|
+
if (!isGenerating && codeFiles[activeFileIndex]) {
|
|
2401
|
+
codeFiles[activeFileIndex].content = e.target.value;
|
|
2402
|
+
}
|
|
2403
|
+
});
|
|
2404
|
+
|
|
2405
|
+
window.toggleThink = function(headerEl) {
|
|
2406
|
+
const contentEl = headerEl.nextElementSibling;
|
|
2407
|
+
const iconEl = headerEl.querySelector('.toggle-icon');
|
|
2408
|
+
|
|
2409
|
+
if (contentEl.style.display === 'none') {
|
|
2410
|
+
contentEl.style.display = 'block';
|
|
2411
|
+
iconEl.style.transform = 'rotate(180deg)';
|
|
2412
|
+
} else {
|
|
2413
|
+
contentEl.style.display = 'none';
|
|
2414
|
+
iconEl.style.transform = 'rotate(0deg)';
|
|
2415
|
+
}
|
|
2416
|
+
};
|