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
|
@@ -0,0 +1,2246 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const CHAT_MODEL_STORAGE_KEY = "ISAI_LOCAL_CHAT_MODEL_ID_V1";
|
|
3
|
+
const LOCAL_ACTIVE_STORAGE_KEY = "ISAI_LOCAL_ACTIVE_V1";
|
|
4
|
+
const WEBGPU_ACTIVE_STORAGE_KEY = "ISAI_LOCAL_WEBGPU_ACTIVE_V1";
|
|
5
|
+
const MODEL_MENU_ID = "local-model-shortcut-panel";
|
|
6
|
+
const MODEL_MENU_STYLE_ID = "isai-local-model-shortcut-style-v2";
|
|
7
|
+
const SPEED_TIER_ORDER = ["superlight", "light", "middle", "hard"];
|
|
8
|
+
const MODEL_TYPE_ORDER = ["all", "gemma", "qwen", "huggingface", "llama_blue", "granite", "ernie"];
|
|
9
|
+
const SLOT_ICON_MAP = {
|
|
10
|
+
code: "ri-code-block",
|
|
11
|
+
superlight: "ri-windy-line",
|
|
12
|
+
light: "ri-flashlight-line",
|
|
13
|
+
middle: "ri-cpu-line",
|
|
14
|
+
hard: "ri-focus-3-line"
|
|
15
|
+
};
|
|
16
|
+
const SPEED_TIER_ICON_MAP = {
|
|
17
|
+
code: "ri-code-block",
|
|
18
|
+
superlight: "ri-windy-line",
|
|
19
|
+
light: "ri-flashlight-line",
|
|
20
|
+
middle: "ri-command-line",
|
|
21
|
+
hard: "ri-fire-line"
|
|
22
|
+
};
|
|
23
|
+
let startDownloadPromise = null;
|
|
24
|
+
|
|
25
|
+
function getLocale() {
|
|
26
|
+
const locale = String(
|
|
27
|
+
(window.ISAI_SERVER_I18N && window.ISAI_SERVER_I18N.locale)
|
|
28
|
+
|| window.LANG
|
|
29
|
+
|| document.documentElement.lang
|
|
30
|
+
|| navigator.language
|
|
31
|
+
|| "en"
|
|
32
|
+
).toLowerCase();
|
|
33
|
+
if (locale.startsWith("ko")) return "ko";
|
|
34
|
+
if (locale.startsWith("ja")) return "ja";
|
|
35
|
+
if (locale.startsWith("zh")) return "zh";
|
|
36
|
+
if (locale.startsWith("es")) return "es";
|
|
37
|
+
return "en";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function t(key) {
|
|
41
|
+
const dict = {
|
|
42
|
+
ko: {
|
|
43
|
+
submit: "보내기",
|
|
44
|
+
stop: "중지",
|
|
45
|
+
modelChanged: "채팅 모델이 변경되었습니다",
|
|
46
|
+
presetChanged: "로컬 속도 설정이 변경되었습니다",
|
|
47
|
+
downloading: "로컬 모델을 준비 중입니다",
|
|
48
|
+
codeFixed: "코드 모델은 고정입니다",
|
|
49
|
+
chatModel: "채팅 모델",
|
|
50
|
+
code: "코드",
|
|
51
|
+
superlight: "슈퍼라이트",
|
|
52
|
+
light: "라이트",
|
|
53
|
+
middle: "중간",
|
|
54
|
+
hard: "하드",
|
|
55
|
+
all: "전체"
|
|
56
|
+
},
|
|
57
|
+
en: {
|
|
58
|
+
submit: "Send",
|
|
59
|
+
stop: "Stop",
|
|
60
|
+
modelChanged: "Chat model updated",
|
|
61
|
+
presetChanged: "Local speed preset updated",
|
|
62
|
+
downloading: "Preparing local model",
|
|
63
|
+
codeFixed: "Code model is fixed",
|
|
64
|
+
chatModel: "Chat model",
|
|
65
|
+
code: "Code",
|
|
66
|
+
superlight: "Superlight",
|
|
67
|
+
light: "Light",
|
|
68
|
+
middle: "Middle",
|
|
69
|
+
hard: "Hard",
|
|
70
|
+
all: "All"
|
|
71
|
+
},
|
|
72
|
+
ja: {
|
|
73
|
+
submit: "送信",
|
|
74
|
+
stop: "停止",
|
|
75
|
+
modelChanged: "チャットモデルが更新されました",
|
|
76
|
+
presetChanged: "ローカル速度設定が更新されました",
|
|
77
|
+
downloading: "ローカルモデルを準備中です",
|
|
78
|
+
codeFixed: "コードモデルは固定です",
|
|
79
|
+
chatModel: "チャットモデル",
|
|
80
|
+
code: "コード",
|
|
81
|
+
superlight: "スーパーライト",
|
|
82
|
+
light: "ライト",
|
|
83
|
+
middle: "ミドル",
|
|
84
|
+
hard: "ハード",
|
|
85
|
+
all: "すべて"
|
|
86
|
+
},
|
|
87
|
+
zh: {
|
|
88
|
+
submit: "发送",
|
|
89
|
+
stop: "停止",
|
|
90
|
+
modelChanged: "聊天模型已更新",
|
|
91
|
+
presetChanged: "本地速度预设已更新",
|
|
92
|
+
downloading: "正在准备本地模型",
|
|
93
|
+
codeFixed: "代码模型已固定",
|
|
94
|
+
chatModel: "聊天模型",
|
|
95
|
+
code: "代码",
|
|
96
|
+
superlight: "超轻量",
|
|
97
|
+
light: "轻量",
|
|
98
|
+
middle: "中等",
|
|
99
|
+
hard: "高精度",
|
|
100
|
+
all: "全部"
|
|
101
|
+
},
|
|
102
|
+
es: {
|
|
103
|
+
submit: "Enviar",
|
|
104
|
+
stop: "Detener",
|
|
105
|
+
modelChanged: "Modelo de chat actualizado",
|
|
106
|
+
presetChanged: "Ajuste local actualizado",
|
|
107
|
+
downloading: "Preparando modelo local",
|
|
108
|
+
codeFixed: "El modelo de codigo es fijo",
|
|
109
|
+
chatModel: "Modelo de chat",
|
|
110
|
+
code: "Codigo",
|
|
111
|
+
superlight: "Superligero",
|
|
112
|
+
light: "Ligero",
|
|
113
|
+
middle: "Medio",
|
|
114
|
+
hard: "Preciso",
|
|
115
|
+
all: "Todo"
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const pack = dict[getLocale()] || dict.en;
|
|
119
|
+
return pack[key] || dict.en[key] || key;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getEmptyLocalResponseText(locale) {
|
|
123
|
+
const value = String(locale || getLocale()).toLowerCase();
|
|
124
|
+
if (value.startsWith("ko")) return "응답을 불러오지 못했습니다.";
|
|
125
|
+
if (value.startsWith("ja")) return "応答を読み込めませんでした。";
|
|
126
|
+
if (value.startsWith("zh")) return "无法加载回复。";
|
|
127
|
+
if (value.startsWith("es")) return "No pude cargar una respuesta.";
|
|
128
|
+
return "I could not load a response.";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getCharacterRecoveryText(locale) {
|
|
132
|
+
const value = String(locale || getLocale()).toLowerCase();
|
|
133
|
+
if (value.startsWith("ko")) return "잠깐 끊겼네. 다시 이어서 이야기해보자. 지금 네 마음에 가장 먼저 떠오르는 건 뭐야?";
|
|
134
|
+
if (value.startsWith("ja")) return "少し途切れたみたい。もう一度ゆっくり続けよう。今いちばん浮かぶ気持ちは何?";
|
|
135
|
+
if (value.startsWith("zh")) return "刚刚有点中断了。我们慢慢继续聊吧。你现在最先想到的感受是什么?";
|
|
136
|
+
if (value.startsWith("es")) return "Se corto un momento. Sigamos con calma. Que es lo primero que te viene a la mente ahora?";
|
|
137
|
+
return "Looks like we got interrupted for a moment. Let's continue slowly. What's the first thing on your mind right now?";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getSpeedPresets() {
|
|
141
|
+
return window.__ISAI_LOCAL_SPEED_PRESETS || {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getLocalActivePreference() {
|
|
145
|
+
return localStorage.getItem(LOCAL_ACTIVE_STORAGE_KEY) === "true";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function setLocalActivePreference(value) {
|
|
149
|
+
localStorage.setItem(LOCAL_ACTIVE_STORAGE_KEY, value ? "true" : "false");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getWebGPUPreference() {
|
|
153
|
+
const stored = localStorage.getItem(WEBGPU_ACTIVE_STORAGE_KEY);
|
|
154
|
+
if (stored == null) return true;
|
|
155
|
+
return stored === "true";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function setWebGPUPreference(value) {
|
|
159
|
+
localStorage.setItem(WEBGPU_ACTIVE_STORAGE_KEY, value ? "true" : "false");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function canUseWebLLM(profile) {
|
|
163
|
+
return !!(
|
|
164
|
+
getWebGPUPreference()
|
|
165
|
+
&& profile
|
|
166
|
+
&& profile.webllmModelId
|
|
167
|
+
&& navigator.gpu
|
|
168
|
+
&& window.WebLLMObj
|
|
169
|
+
&& typeof window.WebLLMObj.CreateMLCEngine === "function"
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getWebGPUSupportState() {
|
|
174
|
+
const profile = typeof getActiveLocalModelProfile === "function"
|
|
175
|
+
? getActiveLocalModelProfile()
|
|
176
|
+
: null;
|
|
177
|
+
const engineAvailable = !!(
|
|
178
|
+
navigator.gpu
|
|
179
|
+
&& window.WebLLMObj
|
|
180
|
+
&& typeof window.WebLLMObj.CreateMLCEngine === "function"
|
|
181
|
+
);
|
|
182
|
+
const modelSupported = !!(profile && profile.webllmModelId);
|
|
183
|
+
const enabled = getWebGPUPreference();
|
|
184
|
+
return {
|
|
185
|
+
profile,
|
|
186
|
+
enabled,
|
|
187
|
+
engineAvailable,
|
|
188
|
+
modelSupported,
|
|
189
|
+
effective: !!(enabled && engineAvailable && modelSupported)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getCatalog() {
|
|
194
|
+
return window.__ISAI_LOCAL_MODEL_CATALOG || {};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isRecoverableGgufLoadError(error) {
|
|
198
|
+
const message = String(error && error.message ? error.message : error || "").toLowerCase();
|
|
199
|
+
return /invalid gguf|invalid typed array length|gguf magic|native gguf load failed|selected model file|out of bounds|model file is not a valid gguf/.test(message);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function syncDownloadedStateFromCache(profile) {
|
|
203
|
+
const targetProfile = profile || (typeof getActiveLocalModelProfile === "function" ? getActiveLocalModelProfile() : null);
|
|
204
|
+
const hasCachedModel = window.WWAIObj && typeof window.WWAIObj.hasCachedModel === "function"
|
|
205
|
+
? window.WWAIObj.hasCachedModel
|
|
206
|
+
: null;
|
|
207
|
+
let cacheReady = false;
|
|
208
|
+
if (targetProfile && hasCachedModel) {
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
const candidates = [];
|
|
211
|
+
const push = function (value) {
|
|
212
|
+
const url = String(value || "").trim();
|
|
213
|
+
if (!url || seen.has(url)) return;
|
|
214
|
+
seen.add(url);
|
|
215
|
+
candidates.push(url);
|
|
216
|
+
};
|
|
217
|
+
push(targetProfile.fallbackUrl);
|
|
218
|
+
if (Array.isArray(targetProfile.fallbackUrls)) {
|
|
219
|
+
targetProfile.fallbackUrls.forEach(push);
|
|
220
|
+
}
|
|
221
|
+
for (const candidate of candidates) {
|
|
222
|
+
try {
|
|
223
|
+
if (await hasCachedModel(candidate)) {
|
|
224
|
+
cacheReady = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
isModelDownloaded = cacheReady || localStorage.getItem(getLocalDownloadStorageKey()) === "true";
|
|
231
|
+
if (isModelDownloaded) {
|
|
232
|
+
localStorage.setItem(getLocalDownloadStorageKey(), "true");
|
|
233
|
+
} else {
|
|
234
|
+
localStorage.removeItem(getLocalDownloadStorageKey());
|
|
235
|
+
}
|
|
236
|
+
return isModelDownloaded;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getStoredChatModelId() {
|
|
240
|
+
const raw = String(localStorage.getItem(CHAT_MODEL_STORAGE_KEY) || "").trim();
|
|
241
|
+
const catalog = getCatalog();
|
|
242
|
+
if (raw && catalog[raw]) return raw;
|
|
243
|
+
return String((window.MODEL_CONFIG && window.MODEL_CONFIG.defaultChatModelId) || "qwen_05b_q3km");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function getModelType(item) {
|
|
247
|
+
const explicit = String(item && item.modelType ? item.modelType : "").toLowerCase();
|
|
248
|
+
if (explicit) return explicit;
|
|
249
|
+
const hay = `${item && item.id ? item.id : ""} ${item && item.name ? item.name : ""}`.toLowerCase();
|
|
250
|
+
if (hay.includes("smollm") || hay.includes("huggingface")) return "huggingface";
|
|
251
|
+
if (hay.includes("gemma")) return "gemma";
|
|
252
|
+
if (hay.includes("granite")) return "granite";
|
|
253
|
+
if (hay.includes("bonsai")) return "ernie";
|
|
254
|
+
if (hay.includes("ernie")) return "ernie";
|
|
255
|
+
if (hay.includes("qwen")) return "qwen";
|
|
256
|
+
return "gemma";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function getChatCatalogItems(activeTier) {
|
|
260
|
+
const catalog = Object.values(getCatalog());
|
|
261
|
+
const typeRank = { gemma: 0, qwen: 1, huggingface: 2, granite: 3, ernie: 4 };
|
|
262
|
+
const byTypeThenName = (a, b) => {
|
|
263
|
+
const aType = getModelType(a);
|
|
264
|
+
const bType = getModelType(b);
|
|
265
|
+
const aRank = Object.prototype.hasOwnProperty.call(typeRank, aType) ? typeRank[aType] : 99;
|
|
266
|
+
const bRank = Object.prototype.hasOwnProperty.call(typeRank, bType) ? typeRank[bType] : 99;
|
|
267
|
+
if (aRank !== bRank) return aRank - bRank;
|
|
268
|
+
return String(a && a.name ? a.name : "").localeCompare(String(b && b.name ? b.name : ""));
|
|
269
|
+
};
|
|
270
|
+
return catalog.sort(byTypeThenName);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getActiveChatModelItem() {
|
|
274
|
+
const catalog = getCatalog();
|
|
275
|
+
return catalog[getStoredChatModelId()] || null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function clone(obj) {
|
|
279
|
+
return JSON.parse(JSON.stringify(obj));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getResolvedProfiles() {
|
|
283
|
+
const baseProfiles = clone(window.__ISAI_LOCAL_MODEL_PROFILES || {});
|
|
284
|
+
const chatItem = getActiveChatModelItem();
|
|
285
|
+
["superlight", "light", "middle", "hard"].forEach((tierKey) => {
|
|
286
|
+
if (!baseProfiles[tierKey] || !chatItem) return;
|
|
287
|
+
baseProfiles[tierKey].modelId = chatItem.id;
|
|
288
|
+
baseProfiles[tierKey].modelName = chatItem.name;
|
|
289
|
+
baseProfiles[tierKey].fallbackUrl = chatItem.fallbackUrl;
|
|
290
|
+
baseProfiles[tierKey].fallbackUrls = Array.isArray(chatItem.fallbackUrls) ? chatItem.fallbackUrls.slice() : [];
|
|
291
|
+
baseProfiles[tierKey].popupSizeText = chatItem.popupSizeText;
|
|
292
|
+
baseProfiles[tierKey].preferredRuntime = chatItem.preferredRuntime || "wllama";
|
|
293
|
+
baseProfiles[tierKey].modelType = getModelType(chatItem);
|
|
294
|
+
baseProfiles[tierKey].webllmModelId = chatItem.webllmModelId || "";
|
|
295
|
+
});
|
|
296
|
+
if (baseProfiles.code) {
|
|
297
|
+
baseProfiles.code.preferredRuntime = "wllama";
|
|
298
|
+
baseProfiles.code.fixedModel = true;
|
|
299
|
+
baseProfiles.code.modelType = "qwen";
|
|
300
|
+
baseProfiles.code.webllmModelId = "";
|
|
301
|
+
}
|
|
302
|
+
return baseProfiles;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function getSpeedPresetForTier(tierKey) {
|
|
306
|
+
const presets = getSpeedPresets();
|
|
307
|
+
return presets[String(tierKey || "").toLowerCase()] || presets.middle || null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getLocalModelProfilesOverride() {
|
|
311
|
+
return getResolvedProfiles();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
getLocalModelProfiles = window.getLocalModelProfiles = getLocalModelProfilesOverride;
|
|
315
|
+
window.getIsaiLocalModelProfiles = getLocalModelProfilesOverride;
|
|
316
|
+
|
|
317
|
+
getOrderedLocalModelTierKeys = window.getOrderedLocalModelTierKeys = function () {
|
|
318
|
+
return SPEED_TIER_ORDER.filter((key) => !!getResolvedProfiles()[key]);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
getLocalDownloadStorageKey = window.getLocalDownloadStorageKey = function () {
|
|
322
|
+
const baseKey = (window.MODEL_CONFIG && window.MODEL_CONFIG.storageKey) || "ISAI_LOCAL_MODEL_CACHE_V7";
|
|
323
|
+
const profile = typeof getActiveLocalModelProfile === "function" ? getActiveLocalModelProfile() : null;
|
|
324
|
+
const modelId = profile && profile.modelId ? profile.modelId : getStoredChatModelId();
|
|
325
|
+
return `${baseKey}__${modelId}`;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
applyLocalModelProfileToConfig = window.applyLocalModelProfileToConfig = function () {
|
|
329
|
+
const profile = typeof getActiveLocalModelProfile === "function" ? getActiveLocalModelProfile() : null;
|
|
330
|
+
if (!profile) return null;
|
|
331
|
+
const modelConfig = window.MODEL_CONFIG || {};
|
|
332
|
+
modelConfig.preferredRuntime = "wllama";
|
|
333
|
+
modelConfig.fallback = modelConfig.fallback || {};
|
|
334
|
+
modelConfig.fallback.url = profile.fallbackUrl || "";
|
|
335
|
+
modelConfig.webllm = null;
|
|
336
|
+
modelConfig.activeProfile = profile;
|
|
337
|
+
modelConfig.activeSpeedPreset = getSpeedPresetForTier(profile.key);
|
|
338
|
+
modelConfig.modelProfiles = getResolvedProfiles();
|
|
339
|
+
window.MODEL_CONFIG = modelConfig;
|
|
340
|
+
return profile;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
function getLocalizedTierLabel(tierKey) {
|
|
344
|
+
const key = String(tierKey || "").toLowerCase();
|
|
345
|
+
return t(key);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function getButtonBadge(type, compact) {
|
|
349
|
+
const sizeClass = compact ? " is-compact" : "";
|
|
350
|
+
if (type === "huggingface") return `<span class="local-model-provider-badge huggingface${sizeClass}">H</span>`;
|
|
351
|
+
if (type === "granite") return `<span class="local-model-provider-badge granite${sizeClass}"><span class="model-provider-glyph">G</span></span>`;
|
|
352
|
+
if (type === "gemma") return `<span class="local-model-provider-badge${sizeClass}"><i class="ri-gemini-fill"></i></span>`;
|
|
353
|
+
if (type === "ernie") return `<span class="local-model-provider-badge ernie${sizeClass}"><span class="model-provider-glyph">B</span></span>`;
|
|
354
|
+
if (type === "llama_blue") return `<span class="local-model-provider-badge llama-blue${sizeClass}"><span class="model-provider-glyph">L</span></span>`;
|
|
355
|
+
return `<span class="local-model-provider-badge qwen${sizeClass}"><i class="ri-qwen-ai-fill"></i></span>`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function getBadgeTypeForItem(item) {
|
|
359
|
+
const hay = `${item && item.id ? item.id : ""} ${item && item.name ? item.name : ""}`.toLowerCase();
|
|
360
|
+
if (hay.includes("tinyllama")) return "llama_blue";
|
|
361
|
+
return getModelType(item);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function getFilterTypeForItem(item) {
|
|
365
|
+
const badgeType = getBadgeTypeForItem(item);
|
|
366
|
+
if (badgeType === "llama_blue") return badgeType;
|
|
367
|
+
return getModelType(item);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function ensureLocalModelMenuStyle() {
|
|
371
|
+
if (document.getElementById(MODEL_MENU_STYLE_ID)) return;
|
|
372
|
+
const style = document.createElement("style");
|
|
373
|
+
style.id = MODEL_MENU_STYLE_ID;
|
|
374
|
+
style.textContent = `
|
|
375
|
+
#local-model-tier-wrapper{position:absolute;top:12px;right:16px;width:fit-content!important;max-width:min(76vw,304px);border-radius:9999px;overflow:visible}
|
|
376
|
+
#local-model-tier-list{position:relative;display:flex;align-items:center;gap:5px;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;width:max-content;max-width:min(calc(100vw - 16px),312px);padding:4px 4px;-webkit-mask-image:linear-gradient(90deg,transparent 0,#000 12px,#000 calc(100% - 12px),transparent 100%);mask-image:linear-gradient(90deg,transparent 0,#000 12px,#000 calc(100% - 12px),transparent 100%)}
|
|
377
|
+
#local-model-tier-list #local-model-tier-menu-btn{position:sticky;right:0;z-index:6;background:rgba(255,255,255,.035)}
|
|
378
|
+
@media (max-width: 768px){#local-model-tier-wrapper{position:fixed;top:12px;right:74px;left:auto;width:fit-content!important;max-width:min(calc(100vw - 88px),264px);z-index:90;border-radius:9999px;overflow:visible}#local-model-tier-list{max-width:min(calc(100vw - 88px),214px);gap:4px;-webkit-mask-image:linear-gradient(90deg,transparent 0,#000 10px,#000 calc(100% - 10px),transparent 100%);mask-image:linear-gradient(90deg,transparent 0,#000 10px,#000 calc(100% - 10px),transparent 100%)}}
|
|
379
|
+
.local-model-tier-btn,.local-model-tier-menu-btn,.local-model-tier-icon-btn{height:28px;padding:0 10px;border-radius:999px;border:1px solid rgba(255,255,255,.10);background:rgba(0,0,0,.64);color:rgba(255,255,255,.84);font-size:11px;font-weight:800;display:inline-flex;align-items:center;justify-content:center;gap:5px;cursor:pointer;transition:all .16s ease}
|
|
380
|
+
.local-model-tier-menu-btn,.local-model-tier-icon-btn{width:28px;min-width:28px;min-height:28px;max-width:28px;max-height:28px;aspect-ratio:1/1;flex:0 0 28px;padding:0;overflow:hidden;box-sizing:border-box}
|
|
381
|
+
.local-model-tier-btn>i,.local-model-tier-menu-btn>i,.local-model-tier-icon-btn>i{width:100%;height:100%;display:flex;align-items:center;justify-content:center;line-height:1;margin:0;padding:0;text-align:center}
|
|
382
|
+
#local-model-tier-menu-btn>i{font-size:12px!important}
|
|
383
|
+
#btn-webgpu>i{font-size:12px!important}
|
|
384
|
+
.local-model-tier-btn:hover,.local-model-tier-btn.active,.local-model-tier-icon-btn:hover,.local-model-tier-icon-btn.active{background:#fff;color:#111;border-color:#fff}
|
|
385
|
+
.local-model-tier-menu-btn:hover,.local-model-tier-menu-btn.is-open{background:rgba(255,255,255,.12);color:rgba(255,255,255,.94);border-color:rgba(255,255,255,.16)}
|
|
386
|
+
.local-model-tier-menu-btn:hover>i,.local-model-tier-menu-btn.is-open>i{color:inherit!important}
|
|
387
|
+
.local-model-shortcut-panel{position:absolute;top:calc(100% + 8px);left:0;right:0;width:100%;min-width:100%;max-width:100%;height:min(194px,calc(100vh - 240px));max-height:min(194px,calc(100vh - 240px));padding:5px;border-radius:11px;border:1px solid rgba(255,255,255,.12);background:rgba(10,10,11,.96);backdrop-filter:blur(14px);display:none;flex-direction:column;z-index:50;overflow:hidden!important;box-sizing:border-box}
|
|
388
|
+
.local-model-shortcut-panel.is-open{display:flex}
|
|
389
|
+
.local-model-shortcut-head{display:flex;align-items:center;gap:5px;margin-bottom:5px;flex:0 0 auto}
|
|
390
|
+
.local-model-shortcut-current{display:none!important}
|
|
391
|
+
.local-model-filter-row{display:flex;align-items:center;gap:3px;flex:1 1 auto;overflow-x:auto}
|
|
392
|
+
.local-model-filter-row::-webkit-scrollbar{display:none}
|
|
393
|
+
.local-model-filter-row > button:first-child{display:none!important}
|
|
394
|
+
.local-model-filter-btn{width:28px;height:28px;min-width:28px;min-height:28px;max-width:28px;max-height:28px;aspect-ratio:1/1;border:none;border-radius:999px;background:transparent;color:rgba(255,255,255,.76);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:all .16s ease;flex:0 0 28px;padding:0;overflow:hidden;box-sizing:border-box}
|
|
395
|
+
.local-model-filter-btn:hover,.local-model-filter-btn.is-active{background:rgba(255,255,255,.14);color:#fff}
|
|
396
|
+
.local-model-filter-btn.provider-huggingface{background:#facc15;border:1px solid #fde047;color:#111}
|
|
397
|
+
.local-model-filter-btn.provider-huggingface:hover,.local-model-filter-btn.provider-huggingface.is-active{background:#facc15;color:#111;border-color:#fde68a}
|
|
398
|
+
.local-model-filter-btn.provider-qwen{background:linear-gradient(180deg,#7c3aed,#5b21b6);border:1px solid rgba(196,181,253,.42);color:#fff}
|
|
399
|
+
.local-model-filter-btn.provider-qwen:hover,.local-model-filter-btn.provider-qwen.is-active{background:linear-gradient(180deg,#7c3aed,#5b21b6);color:#fff;border-color:rgba(216,180,254,.62)}
|
|
400
|
+
.local-model-filter-btn.provider-llama_blue{background:#2563eb;border:1px solid #60a5fa;color:#fff}
|
|
401
|
+
.local-model-filter-btn.provider-llama_blue:hover,.local-model-filter-btn.provider-llama_blue.is-active{background:#2563eb;color:#fff;border-color:#93c5fd}
|
|
402
|
+
.local-model-filter-btn.provider-granite{background:#2563eb;border:1px solid #3b82f6;color:#fff}
|
|
403
|
+
.local-model-filter-btn.provider-granite:hover,.local-model-filter-btn.provider-granite.is-active{background:#2563eb;color:#fff;border-color:#60a5fa}
|
|
404
|
+
.local-model-catalog-scroll{display:block!important;flex:1 1 auto;min-height:0;height:0;max-height:100%;overflow-y:auto!important;overflow-x:hidden;overscroll-behavior:contain;touch-action:pan-y;-webkit-overflow-scrolling:touch;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.22) transparent;padding-right:1px}
|
|
405
|
+
.local-model-catalog-scroll::-webkit-scrollbar{width:4px}
|
|
406
|
+
.local-model-catalog-scroll::-webkit-scrollbar-thumb{background:rgba(255,255,255,.22);border-radius:999px}
|
|
407
|
+
.local-model-catalog-grid{display:flex;flex-direction:column;gap:5px}
|
|
408
|
+
.local-model-catalog-card{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:6px 8px;border-radius:10px;border:1px solid rgba(255,255,255,.10);background:#151515;cursor:pointer;transition:all .16s ease}
|
|
409
|
+
.local-model-catalog-card:hover,.local-model-catalog-card.is-selected{border-color:rgba(255,255,255,.3);background:#1b1b1b}
|
|
410
|
+
.local-model-catalog-card.is-fixed{cursor:default;opacity:.92}
|
|
411
|
+
.local-model-catalog-card-main{min-width:0;display:flex;align-items:center;gap:7px;flex:1 1 auto}
|
|
412
|
+
.local-model-provider-button{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;min-width:18px;min-height:18px;border-radius:999px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);box-shadow:inset 0 1px 0 rgba(255,255,255,.03);padding:0;flex:0 0 auto}
|
|
413
|
+
.local-model-provider-badge{display:grid;place-items:center;width:18px;height:18px;border-radius:999px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.16);color:#fff;flex:0 0 auto;position:relative;overflow:hidden}
|
|
414
|
+
.local-model-provider-badge i,.local-model-provider-badge .hf-glyph,.local-model-provider-badge .model-provider-glyph{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;line-height:1;margin:0;padding:0}
|
|
415
|
+
.local-model-provider-badge i::before{display:block;line-height:1;margin:0}
|
|
416
|
+
.local-model-provider-badge.huggingface{background:#facc15!important;border-color:#fde047!important;color:#fff!important;font-weight:900;font-size:10px;line-height:1}
|
|
417
|
+
.local-model-provider-badge.granite{background:#2563eb!important;border-color:#3b82f6!important;color:#fff!important;font-weight:800;font-size:10px;line-height:1}
|
|
418
|
+
.local-model-provider-badge.qwen{background:linear-gradient(180deg,#7c3aed,#5b21b6);border-color:rgba(196,181,253,.42);color:#fff;font-weight:800}
|
|
419
|
+
.local-model-provider-badge.qwen i{font-size:11px;line-height:1;color:#fff}
|
|
420
|
+
.local-model-provider-badge.ernie{position:relative;background:#0b0b0b;border-color:#3a3a3a;color:#fff;font-weight:800}
|
|
421
|
+
.local-model-provider-badge.llama-blue{position:relative;background:#2563eb;border-color:#60a5fa;color:#fff;font-weight:900}
|
|
422
|
+
.local-model-provider-badge.ernie .model-provider-glyph,.local-model-provider-badge.granite .model-provider-glyph{font-size:10px;font-weight:800}
|
|
423
|
+
.local-model-provider-badge.llama-blue .model-provider-glyph{font-size:10px;font-weight:900}
|
|
424
|
+
.local-model-provider-badge.is-compact{width:14px;height:14px;border-width:0;background:transparent}
|
|
425
|
+
.local-model-provider-button .local-model-provider-badge.is-compact{width:13px;height:13px;min-width:13px;min-height:13px;border-width:0;background:transparent!important;box-shadow:none!important;display:flex;align-items:center;justify-content:center}
|
|
426
|
+
.local-model-provider-button .local-model-provider-badge.huggingface.is-compact{background:#facc15!important;border-color:#fde047!important;color:#fff!important;box-shadow:inset 0 1px 0 rgba(255,255,255,.08)!important}
|
|
427
|
+
.local-model-provider-button .local-model-provider-badge.qwen.is-compact{background:linear-gradient(180deg,#7c3aed,#5b21b6)!important;border-color:rgba(196,181,253,.42)!important;color:#fff!important;box-shadow:inset 0 1px 0 rgba(255,255,255,.06)!important}
|
|
428
|
+
.local-model-provider-button .local-model-provider-badge.gemma.is-compact{background:#2563eb!important;border-color:#60a5fa!important;color:#fff!important;box-shadow:inset 0 1px 0 rgba(255,255,255,.06)!important}
|
|
429
|
+
.local-model-provider-button .local-model-provider-badge.granite.is-compact{background:#2563eb!important;border-color:#3b82f6!important;color:#fff!important;box-shadow:inset 0 1px 0 rgba(255,255,255,.06)!important}
|
|
430
|
+
.local-model-provider-button .local-model-provider-badge.ernie.is-compact{background:#0b0b0b!important;border-color:#3a3a3a!important;color:#fff!important;box-shadow:inset 0 1px 0 rgba(255,255,255,.04)!important}
|
|
431
|
+
.local-model-provider-button .local-model-provider-badge.llama-blue.is-compact{background:#2563eb!important;border-color:#60a5fa!important;color:#fff!important;box-shadow:inset 0 1px 0 rgba(255,255,255,.06)!important}
|
|
432
|
+
.local-model-provider-badge.qwen.is-compact{background:linear-gradient(180deg,#7c3aed,#5b21b6)!important;border-color:rgba(196,181,253,.42)!important;color:#fff!important}
|
|
433
|
+
.local-model-provider-badge.llama-blue.is-compact{background:#2563eb!important;border-color:#60a5fa!important;color:#fff!important}
|
|
434
|
+
.local-model-provider-button .local-model-provider-badge.qwen.is-compact i{font-size:11px}
|
|
435
|
+
.local-model-provider-badge.qwen.is-compact i{font-size:11px}
|
|
436
|
+
.local-model-provider-badge i{font-size:11px}
|
|
437
|
+
.local-model-catalog-name{min-width:0;font-size:9px;font-weight:800;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
438
|
+
.local-model-size-pill{height:20px;padding:0 6px;border-radius:999px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.05);font-size:9px;font-weight:800;color:rgba(255,255,255,.86);display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto}
|
|
439
|
+
`;
|
|
440
|
+
document.head.appendChild(style);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function closeLocalModelShortcutMenu() {
|
|
444
|
+
const panel = document.getElementById(MODEL_MENU_ID);
|
|
445
|
+
const trigger = document.getElementById("local-model-tier-menu-btn");
|
|
446
|
+
if (panel) panel.classList.remove("is-open");
|
|
447
|
+
if (trigger) trigger.classList.remove("is-open");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function buildFilterRow(activeType) {
|
|
451
|
+
const row = document.createElement("div");
|
|
452
|
+
row.className = "local-model-filter-row";
|
|
453
|
+
MODEL_TYPE_ORDER.forEach((typeKey) => {
|
|
454
|
+
const button = document.createElement("button");
|
|
455
|
+
button.type = "button";
|
|
456
|
+
button.className = `local-model-filter-btn provider-${typeKey}${activeType === typeKey ? " is-active" : ""}`;
|
|
457
|
+
if (typeKey === "all") {
|
|
458
|
+
button.innerHTML = '<i class="ri-apps-2-line text-[13px]"></i>';
|
|
459
|
+
} else {
|
|
460
|
+
button.innerHTML = getButtonBadge(typeKey, true);
|
|
461
|
+
}
|
|
462
|
+
button.addEventListener("click", function (event) {
|
|
463
|
+
event.preventDefault();
|
|
464
|
+
event.stopPropagation();
|
|
465
|
+
renderLocalModelTierSelector(typeKey);
|
|
466
|
+
const panel = document.getElementById(MODEL_MENU_ID);
|
|
467
|
+
if (panel) panel.classList.add("is-open");
|
|
468
|
+
const trigger = document.getElementById("local-model-tier-menu-btn");
|
|
469
|
+
if (trigger) trigger.classList.add("is-open");
|
|
470
|
+
});
|
|
471
|
+
row.appendChild(button);
|
|
472
|
+
});
|
|
473
|
+
return row;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function selectChatModel(modelId) {
|
|
477
|
+
const catalog = getCatalog();
|
|
478
|
+
if (!catalog[modelId]) return;
|
|
479
|
+
localStorage.setItem(CHAT_MODEL_STORAGE_KEY, modelId);
|
|
480
|
+
applyLocalModelProfileToConfig();
|
|
481
|
+
isModelDownloaded = localStorage.getItem(getLocalDownloadStorageKey()) === "true";
|
|
482
|
+
isModelLoaded = false;
|
|
483
|
+
if (localEngine && typeof localEngine.unload === "function") {
|
|
484
|
+
Promise.resolve(localEngine.unload()).catch(function () {});
|
|
485
|
+
}
|
|
486
|
+
localEngine = null;
|
|
487
|
+
wllama = null;
|
|
488
|
+
if (typeof setLocalRuntimeState === "function") setLocalRuntimeState(null);
|
|
489
|
+
renderLocalModelTierSelector();
|
|
490
|
+
syncLocalModelTierVisibility();
|
|
491
|
+
if (String(getActiveLocalModelTier()) !== "code") {
|
|
492
|
+
isLocalActive = true;
|
|
493
|
+
setLocalActivePreference(true);
|
|
494
|
+
startDownload();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
setLocalModelTier = window.setLocalModelTier = function (tierKey) {
|
|
499
|
+
const profiles = getResolvedProfiles();
|
|
500
|
+
const nextTier = String(tierKey || "").trim().toLowerCase();
|
|
501
|
+
if (!profiles[nextTier]) return;
|
|
502
|
+
const currentTier = String(getActiveLocalModelTier() || "").toLowerCase();
|
|
503
|
+
if (currentTier === nextTier && isLocalActive && isModelLoaded) {
|
|
504
|
+
renderLocalModelTierSelector();
|
|
505
|
+
syncLocalModelTierVisibility();
|
|
506
|
+
updateLocalBtnState();
|
|
507
|
+
updateWebGPUBtnState();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
localStorage.setItem(getLocalModelTierStorageKey(), nextTier);
|
|
511
|
+
localStorage.setItem(getLocalModelTierUserSetKey(), "true");
|
|
512
|
+
applyLocalModelProfileToConfig();
|
|
513
|
+
isLocalActive = true;
|
|
514
|
+
isModelLoaded = false;
|
|
515
|
+
if (localEngine && typeof localEngine.unload === "function") {
|
|
516
|
+
Promise.resolve(localEngine.unload()).catch(() => {});
|
|
517
|
+
}
|
|
518
|
+
localEngine = null;
|
|
519
|
+
wllama = null;
|
|
520
|
+
if (typeof setLocalRuntimeState === "function") setLocalRuntimeState(null);
|
|
521
|
+
isModelDownloaded = localStorage.getItem(getLocalDownloadStorageKey()) === "true";
|
|
522
|
+
updateLocalBtnState();
|
|
523
|
+
renderLocalModelTierSelector();
|
|
524
|
+
syncLocalModelTierVisibility();
|
|
525
|
+
if (isLocalActive) {
|
|
526
|
+
setLocalActivePreference(true);
|
|
527
|
+
}
|
|
528
|
+
startDownload();
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
handleLocalToggle = window.handleLocalToggle = async function () {
|
|
532
|
+
applyLocalModelProfileToConfig();
|
|
533
|
+
await syncDownloadedStateFromCache();
|
|
534
|
+
if (isLocalActive && isModelLoaded) {
|
|
535
|
+
isLocalActive = false;
|
|
536
|
+
setLocalActivePreference(false);
|
|
537
|
+
isModelLoaded = false;
|
|
538
|
+
if (localEngine && typeof localEngine.unload === "function") {
|
|
539
|
+
Promise.resolve(localEngine.unload()).catch(function () {});
|
|
540
|
+
}
|
|
541
|
+
localEngine = null;
|
|
542
|
+
wllama = null;
|
|
543
|
+
if (typeof setLocalRuntimeState === "function") setLocalRuntimeState(null);
|
|
544
|
+
updateLocalBtnState();
|
|
545
|
+
syncLocalModelTierVisibility();
|
|
546
|
+
if (typeof window.updateMainSubmitButtonState === "function") {
|
|
547
|
+
window.updateMainSubmitButtonState();
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Always start local mode from Light tier by default.
|
|
553
|
+
const profiles = typeof getLocalModelProfiles === "function" ? getLocalModelProfiles() : {};
|
|
554
|
+
if (profiles && profiles.light) {
|
|
555
|
+
localStorage.setItem(getLocalModelTierStorageKey(), "light");
|
|
556
|
+
}
|
|
557
|
+
applyLocalModelProfileToConfig();
|
|
558
|
+
|
|
559
|
+
isLocalActive = true;
|
|
560
|
+
setLocalActivePreference(true);
|
|
561
|
+
updateLocalBtnState();
|
|
562
|
+
renderLocalModelTierSelector();
|
|
563
|
+
syncLocalModelTierVisibility();
|
|
564
|
+
await startDownload();
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
syncLocalModelTierVisibility = window.syncLocalModelTierVisibility = function (mode) {
|
|
568
|
+
const wrapper = document.getElementById("local-model-tier-wrapper");
|
|
569
|
+
if (!wrapper) return;
|
|
570
|
+
const shouldShow = true;
|
|
571
|
+
wrapper.style.display = shouldShow ? "block" : "none";
|
|
572
|
+
wrapper.classList.toggle("hidden", !shouldShow);
|
|
573
|
+
if (!shouldShow) closeLocalModelShortcutMenu();
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
updateLocalBtnState = window.updateLocalBtnState = function () {
|
|
577
|
+
const btn = document.getElementById("btn-download");
|
|
578
|
+
if (!btn) return;
|
|
579
|
+
const icon = btn.querySelector("i");
|
|
580
|
+
btn.classList.toggle("active", !!isLocalActive);
|
|
581
|
+
btn.style.color = isLocalActive
|
|
582
|
+
? "#ffffff"
|
|
583
|
+
: (isModelDownloaded ? "rgba(255,255,255,0.76)" : "rgba(255,255,255,0.44)");
|
|
584
|
+
btn.style.background = isLocalActive ? "rgba(255,255,255,0.16)" : "rgba(0,0,0,0.64)";
|
|
585
|
+
btn.style.borderColor = isLocalActive ? "rgba(255,255,255,0.28)" : "rgba(255,255,255,0.10)";
|
|
586
|
+
btn.title = isLocalActive ? "Local Mode On" : "Local Mode";
|
|
587
|
+
btn.setAttribute("aria-pressed", isLocalActive ? "true" : "false");
|
|
588
|
+
if (icon) {
|
|
589
|
+
icon.className = "ri-ghost-4-line text-[15px]";
|
|
590
|
+
icon.style.color = isLocalActive
|
|
591
|
+
? "#ffffff"
|
|
592
|
+
: (isModelDownloaded ? "rgba(255,255,255,0.76)" : "rgba(255,255,255,0.44)");
|
|
593
|
+
}
|
|
594
|
+
if (typeof syncLocalModelTierVisibility === "function") {
|
|
595
|
+
syncLocalModelTierVisibility();
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
updateWebGPUBtnState = window.updateWebGPUBtnState = function () {
|
|
600
|
+
const btn = document.getElementById("btn-webgpu");
|
|
601
|
+
if (!btn) return;
|
|
602
|
+
const icon = btn.querySelector("i");
|
|
603
|
+
const state = getWebGPUSupportState();
|
|
604
|
+
btn.classList.toggle("active", state.effective);
|
|
605
|
+
btn.style.opacity = state.engineAvailable ? "1" : "0.45";
|
|
606
|
+
btn.style.background = state.effective ? "rgba(255,255,255,0.16)" : "rgba(255,255,255,0.04)";
|
|
607
|
+
btn.style.borderColor = state.effective ? "rgba(255,255,255,0.28)" : "rgba(255,255,255,0.08)";
|
|
608
|
+
btn.style.color = state.effective ? "#ffffff" : "rgba(255,255,255,0.62)";
|
|
609
|
+
btn.title = !state.engineAvailable
|
|
610
|
+
? "WebGPU Unavailable"
|
|
611
|
+
: (!state.modelSupported ? "Current model uses Wllama only" : (state.enabled ? "WebGPU Enabled" : "WebGPU Disabled"));
|
|
612
|
+
btn.setAttribute("aria-pressed", state.effective ? "true" : "false");
|
|
613
|
+
if (icon) {
|
|
614
|
+
icon.className = "ri-cpu-line text-lg";
|
|
615
|
+
icon.style.color = state.effective ? "#ffffff" : "rgba(255,255,255,0.62)";
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
handleWebGPUToggle = window.handleWebGPUToggle = function () {
|
|
620
|
+
const state = getWebGPUSupportState();
|
|
621
|
+
if (!state.engineAvailable) {
|
|
622
|
+
setWebGPUPreference(false);
|
|
623
|
+
updateWebGPUBtnState();
|
|
624
|
+
if (typeof showToast === "function") {
|
|
625
|
+
showToast("WebGPU is unavailable on this browser");
|
|
626
|
+
}
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (!state.modelSupported) {
|
|
630
|
+
setWebGPUPreference(false);
|
|
631
|
+
updateWebGPUBtnState();
|
|
632
|
+
if (typeof showToast === "function") {
|
|
633
|
+
showToast("This model uses Wllama only");
|
|
634
|
+
}
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const next = !getWebGPUPreference();
|
|
638
|
+
setWebGPUPreference(next);
|
|
639
|
+
if (localEngine && typeof localEngine.unload === "function") {
|
|
640
|
+
Promise.resolve(localEngine.unload()).catch(function () {});
|
|
641
|
+
}
|
|
642
|
+
localEngine = null;
|
|
643
|
+
wllama = null;
|
|
644
|
+
isModelLoaded = false;
|
|
645
|
+
if (typeof setLocalRuntimeState === "function") setLocalRuntimeState(null);
|
|
646
|
+
applyLocalModelProfileToConfig();
|
|
647
|
+
updateWebGPUBtnState();
|
|
648
|
+
updateLocalBtnState();
|
|
649
|
+
if (typeof showToast === "function") {
|
|
650
|
+
showToast(next ? "WebGPU enabled" : "WebGPU disabled");
|
|
651
|
+
}
|
|
652
|
+
if (isLocalActive) {
|
|
653
|
+
startDownload();
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
renderLocalModelTierSelector = window.renderLocalModelTierSelector = function (forcedFilter) {
|
|
658
|
+
ensureLocalModelMenuStyle();
|
|
659
|
+
const wrapper = document.getElementById("local-model-tier-wrapper");
|
|
660
|
+
const list = document.getElementById("local-model-tier-list");
|
|
661
|
+
if (!wrapper || !list) return;
|
|
662
|
+
wrapper.classList.remove("hidden");
|
|
663
|
+
|
|
664
|
+
const activeTier = String(getActiveLocalModelTier() || "middle").toLowerCase();
|
|
665
|
+
const profiles = getResolvedProfiles();
|
|
666
|
+
const selectedChatModelId = getStoredChatModelId();
|
|
667
|
+
const activeFilter = String(forcedFilter || wrapper.dataset.modelFilter || "all").toLowerCase();
|
|
668
|
+
wrapper.dataset.modelFilter = activeFilter;
|
|
669
|
+
|
|
670
|
+
list.innerHTML = "";
|
|
671
|
+
SPEED_TIER_ORDER.forEach((tier) => {
|
|
672
|
+
const profile = profiles[tier];
|
|
673
|
+
if (!profile) return;
|
|
674
|
+
const button = document.createElement("button");
|
|
675
|
+
button.type = "button";
|
|
676
|
+
button.className = `local-model-tier-btn is-icon${tier === activeTier ? " active" : ""}`;
|
|
677
|
+
button.dataset.tier = tier;
|
|
678
|
+
button.innerHTML = `<i class="${SPEED_TIER_ICON_MAP[tier] || "ri-circle-line"}"></i>`;
|
|
679
|
+
button.title = profile.modelName || getLocalizedTierLabel(tier);
|
|
680
|
+
button.setAttribute("aria-label", profile.modelName || getLocalizedTierLabel(tier));
|
|
681
|
+
button.addEventListener("click", function (event) {
|
|
682
|
+
event.preventDefault();
|
|
683
|
+
event.stopPropagation();
|
|
684
|
+
closeLocalModelShortcutMenu();
|
|
685
|
+
setLocalModelTier(tier);
|
|
686
|
+
});
|
|
687
|
+
list.appendChild(button);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const localButton = document.createElement("button");
|
|
691
|
+
localButton.type = "button";
|
|
692
|
+
localButton.id = "btn-download";
|
|
693
|
+
localButton.className = "local-model-tier-icon-btn";
|
|
694
|
+
localButton.innerHTML = '<i class="ri-ghost-4-line text-[15px]"></i>';
|
|
695
|
+
localButton.addEventListener("click", function (event) {
|
|
696
|
+
event.preventDefault();
|
|
697
|
+
event.stopPropagation();
|
|
698
|
+
handleLocalToggle();
|
|
699
|
+
});
|
|
700
|
+
list.appendChild(localButton);
|
|
701
|
+
|
|
702
|
+
const gpuButton = document.createElement("button");
|
|
703
|
+
gpuButton.type = "button";
|
|
704
|
+
gpuButton.id = "btn-webgpu";
|
|
705
|
+
gpuButton.className = "local-model-tier-icon-btn";
|
|
706
|
+
gpuButton.innerHTML = '<i class="ri-cpu-line text-[15px]"></i>';
|
|
707
|
+
gpuButton.addEventListener("click", function (event) {
|
|
708
|
+
event.preventDefault();
|
|
709
|
+
event.stopPropagation();
|
|
710
|
+
handleWebGPUToggle();
|
|
711
|
+
});
|
|
712
|
+
list.appendChild(gpuButton);
|
|
713
|
+
|
|
714
|
+
const menuButton = document.createElement("button");
|
|
715
|
+
menuButton.type = "button";
|
|
716
|
+
menuButton.id = "local-model-tier-menu-btn";
|
|
717
|
+
menuButton.className = "local-model-tier-menu-btn";
|
|
718
|
+
menuButton.innerHTML = '<i class="ri-menu-4-line text-[14px]"></i>';
|
|
719
|
+
menuButton.addEventListener("click", function (event) {
|
|
720
|
+
event.preventDefault();
|
|
721
|
+
event.stopPropagation();
|
|
722
|
+
const panel = document.getElementById(MODEL_MENU_ID);
|
|
723
|
+
const willOpen = !(panel && panel.classList.contains("is-open"));
|
|
724
|
+
closeLocalModelShortcutMenu();
|
|
725
|
+
if (willOpen && panel) {
|
|
726
|
+
panel.classList.add("is-open");
|
|
727
|
+
menuButton.classList.add("is-open");
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
list.appendChild(menuButton);
|
|
731
|
+
|
|
732
|
+
let panel = document.getElementById(MODEL_MENU_ID);
|
|
733
|
+
if (!panel) {
|
|
734
|
+
panel = document.createElement("div");
|
|
735
|
+
panel.id = MODEL_MENU_ID;
|
|
736
|
+
panel.className = "local-model-shortcut-panel";
|
|
737
|
+
wrapper.appendChild(panel);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const wasOpen = panel.classList.contains("is-open");
|
|
741
|
+
panel.innerHTML = "";
|
|
742
|
+
|
|
743
|
+
const head = document.createElement("div");
|
|
744
|
+
head.className = "local-model-shortcut-head";
|
|
745
|
+
head.appendChild(buildFilterRow(activeFilter));
|
|
746
|
+
panel.appendChild(head);
|
|
747
|
+
|
|
748
|
+
const scrollBox = document.createElement("div");
|
|
749
|
+
scrollBox.className = "local-model-catalog-scroll";
|
|
750
|
+
const grid = document.createElement("div");
|
|
751
|
+
grid.className = "local-model-catalog-grid";
|
|
752
|
+
const items = getChatCatalogItems(activeTier).filter((item) => activeFilter === "all" ? true : getFilterTypeForItem(item) === activeFilter);
|
|
753
|
+
|
|
754
|
+
items.forEach((item) => {
|
|
755
|
+
const modelType = getModelType(item);
|
|
756
|
+
const badgeType = getBadgeTypeForItem(item);
|
|
757
|
+
const isSelected = item.id === selectedChatModelId;
|
|
758
|
+
const card = document.createElement("div");
|
|
759
|
+
card.className = `local-model-catalog-card${isSelected ? " is-selected" : ""}`;
|
|
760
|
+
card.innerHTML = `
|
|
761
|
+
<div class="local-model-catalog-card-main">
|
|
762
|
+
<span class="local-model-provider-button" aria-hidden="true">${getButtonBadge(badgeType, true)}</span>
|
|
763
|
+
<div class="local-model-catalog-name" title="${item.name}">${item.name}</div>
|
|
764
|
+
</div>
|
|
765
|
+
<span class="local-model-size-pill">${item.popupSizeText || ""}</span>
|
|
766
|
+
`;
|
|
767
|
+
card.addEventListener("click", function (event) {
|
|
768
|
+
event.preventDefault();
|
|
769
|
+
event.stopPropagation();
|
|
770
|
+
closeLocalModelShortcutMenu();
|
|
771
|
+
selectChatModel(item.id);
|
|
772
|
+
});
|
|
773
|
+
grid.appendChild(card);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
scrollBox.appendChild(grid);
|
|
777
|
+
panel.appendChild(scrollBox);
|
|
778
|
+
|
|
779
|
+
if (wasOpen) {
|
|
780
|
+
panel.classList.add("is-open");
|
|
781
|
+
menuButton.classList.add("is-open");
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
updateLocalBtnState();
|
|
785
|
+
updateWebGPUBtnState();
|
|
786
|
+
syncLocalModelTierVisibility();
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
function getRuntimeCreateEngine() {
|
|
790
|
+
return window.WWAIObj && typeof window.WWAIObj.createEngine === "function"
|
|
791
|
+
? window.WWAIObj.createEngine
|
|
792
|
+
: null;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function getSimpleLocalChunkText(chunk) {
|
|
796
|
+
if (chunk == null) return "";
|
|
797
|
+
if (typeof chunk === "string" || typeof chunk === "number") return String(chunk);
|
|
798
|
+
|
|
799
|
+
const decoder = typeof TextDecoder !== "undefined" ? new TextDecoder("utf-8") : null;
|
|
800
|
+
const decodeBytes = (value) => {
|
|
801
|
+
if (!decoder || typeof Uint8Array === "undefined" || !(value instanceof Uint8Array)) return "";
|
|
802
|
+
return decoder.decode(value, { stream: true });
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
if (typeof Uint8Array !== "undefined" && chunk instanceof Uint8Array) {
|
|
806
|
+
return decodeBytes(chunk);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const directValues = [
|
|
810
|
+
chunk.currentText,
|
|
811
|
+
chunk.text,
|
|
812
|
+
chunk.content,
|
|
813
|
+
chunk.tokenText,
|
|
814
|
+
chunk.value,
|
|
815
|
+
chunk.delta && chunk.delta.content
|
|
816
|
+
];
|
|
817
|
+
for (const value of directValues) {
|
|
818
|
+
if (value == null) continue;
|
|
819
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
820
|
+
const decoded = decodeBytes(value);
|
|
821
|
+
if (decoded) return decoded;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const firstChoice = Array.isArray(chunk.choices) ? chunk.choices[0] : null;
|
|
825
|
+
if (firstChoice && typeof firstChoice === "object") {
|
|
826
|
+
const choiceValues = [
|
|
827
|
+
firstChoice.text,
|
|
828
|
+
firstChoice.content,
|
|
829
|
+
firstChoice.delta && firstChoice.delta.content,
|
|
830
|
+
firstChoice.message && firstChoice.message.content
|
|
831
|
+
];
|
|
832
|
+
for (const value of choiceValues) {
|
|
833
|
+
if (value == null) continue;
|
|
834
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
835
|
+
const decoded = decodeBytes(value);
|
|
836
|
+
if (decoded) return decoded;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (Array.isArray(chunk)) {
|
|
841
|
+
for (const item of chunk) {
|
|
842
|
+
const value = getSimpleLocalChunkText(item);
|
|
843
|
+
if (value) return value;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return "";
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function trimSimpleLocalOverlap(baseText, incomingText) {
|
|
851
|
+
const base = String(baseText || "");
|
|
852
|
+
const incoming = String(incomingText || "");
|
|
853
|
+
const maxOverlap = Math.min(base.length, incoming.length, 256);
|
|
854
|
+
for (let size = maxOverlap; size > 0; size -= 1) {
|
|
855
|
+
if (base.slice(-size) === incoming.slice(0, size)) {
|
|
856
|
+
return incoming.slice(size);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return incoming;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async function runSimpleLocalInference(promptArr, callback) {
|
|
863
|
+
if (isLocalActive && !isModelLoaded) {
|
|
864
|
+
await startDownload();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const profile = applyLocalModelProfileToConfig();
|
|
868
|
+
const speedConfig = getSpeedPresetForTier(profile && profile.key) || {};
|
|
869
|
+
const currentUiMode = String(
|
|
870
|
+
window.currentMode
|
|
871
|
+
|| window.selectedMode
|
|
872
|
+
|| (document.body && document.body.getAttribute("data-ui-mode"))
|
|
873
|
+
|| "chat"
|
|
874
|
+
).toLowerCase();
|
|
875
|
+
const isCodeMode = currentUiMode === "code";
|
|
876
|
+
const isCharacterChat = !!(window.ISAI_CHARACTER_CHAT_SESSION && window.ISAI_CHARACTER_CHAT_SESSION.active);
|
|
877
|
+
const isCharacterLiteMode = isCharacterChat && !isCodeMode;
|
|
878
|
+
const isLightTier = !!(profile && profile.key === "light");
|
|
879
|
+
const isSuperlightTier = !!(profile && profile.key === "superlight");
|
|
880
|
+
const isLightTierEffective = (isLightTier || isSuperlightTier) && !isCodeMode;
|
|
881
|
+
const outputLocale = String(
|
|
882
|
+
(document && document.documentElement && document.documentElement.lang)
|
|
883
|
+
|| (navigator && navigator.language)
|
|
884
|
+
|| "en"
|
|
885
|
+
).toLowerCase();
|
|
886
|
+
const emptyResponseText = getEmptyLocalResponseText(outputLocale);
|
|
887
|
+
|
|
888
|
+
let messages = typeof mapPromptMessagesForLocalEngine === "function"
|
|
889
|
+
? mapPromptMessagesForLocalEngine(promptArr)
|
|
890
|
+
: Array.isArray(promptArr) ? promptArr.slice() : [];
|
|
891
|
+
messages = messages.filter((msg) => msg && String(msg.role || "").trim() && String(msg.content || "").trim());
|
|
892
|
+
if (!messages.length) return "";
|
|
893
|
+
|
|
894
|
+
if (!localEngine || typeof localEngine.generateChat !== "function") {
|
|
895
|
+
throw new Error("Wllama local engine is not ready.");
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (typeof localEngine.clearChatState === "function") {
|
|
899
|
+
try {
|
|
900
|
+
await localEngine.clearChatState();
|
|
901
|
+
} catch (error) {}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const tokenFloor = isCharacterLiteMode ? 192 : (isCodeMode ? 2048 : (isLightTierEffective ? 1536 : 4096));
|
|
905
|
+
const maxTokens = Math.max(
|
|
906
|
+
tokenFloor,
|
|
907
|
+
Number(window.ISAI_LOCAL_MAX_OUTPUT_TOKENS || 0),
|
|
908
|
+
Number(speedConfig.maxOutputTokens || speedConfig.maxTokens || speedConfig.nPredict || 0)
|
|
909
|
+
);
|
|
910
|
+
const sampling = speedConfig.sampling || (
|
|
911
|
+
isCharacterLiteMode
|
|
912
|
+
? { temp: 0.16, top_k: 10, top_p: 0.74, penalty_repeat: 1.12 }
|
|
913
|
+
: (isSuperlightTier
|
|
914
|
+
? { temp: 0.1, top_k: 1, top_p: 0.5, penalty_repeat: 1.0 }
|
|
915
|
+
: (isLightTierEffective
|
|
916
|
+
? { temp: 0.18, top_k: 12, top_p: 0.78, penalty_repeat: 1.12 }
|
|
917
|
+
: { temp: 0.42, top_k: 32, top_p: 0.88, penalty_repeat: 1.06 }))
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
let renderedText = "";
|
|
921
|
+
const stream = await localEngine.generateChat(messages, {
|
|
922
|
+
useCache: !(isLightTierEffective || isCharacterLiteMode),
|
|
923
|
+
nPredict: maxTokens,
|
|
924
|
+
sampling
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
for await (const chunk of stream) {
|
|
928
|
+
if (stopSignal) {
|
|
929
|
+
if (typeof localEngine.interruptGenerate === "function") {
|
|
930
|
+
try {
|
|
931
|
+
localEngine.interruptGenerate();
|
|
932
|
+
} catch (error) {}
|
|
933
|
+
}
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const rawText = String(getSimpleLocalChunkText(chunk) || "").replace(/\uFFFD+/g, "").replace(/\r\n/g, "\n");
|
|
938
|
+
if (!rawText) continue;
|
|
939
|
+
|
|
940
|
+
const nextText = renderedText
|
|
941
|
+
? (rawText.startsWith(renderedText)
|
|
942
|
+
? rawText
|
|
943
|
+
: renderedText + trimSimpleLocalOverlap(renderedText, rawText))
|
|
944
|
+
: rawText;
|
|
945
|
+
|
|
946
|
+
if (!nextText || nextText === renderedText) continue;
|
|
947
|
+
renderedText = nextText;
|
|
948
|
+
callback(renderedText, { replace: true, isStreaming: true });
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
renderedText = String(renderedText || "").trim();
|
|
952
|
+
if (renderedText) return renderedText;
|
|
953
|
+
|
|
954
|
+
const fallbackText = isCharacterChat ? getCharacterRecoveryText(outputLocale) : emptyResponseText;
|
|
955
|
+
callback(fallbackText, { replace: true, isStreaming: false });
|
|
956
|
+
return fallbackText;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
startDownload = window.startDownload = async function () {
|
|
960
|
+
if (startDownloadPromise) {
|
|
961
|
+
return startDownloadPromise;
|
|
962
|
+
}
|
|
963
|
+
startDownloadPromise = (async () => {
|
|
964
|
+
const container = document.getElementById("progress-container");
|
|
965
|
+
const bar = document.getElementById("progress-bar");
|
|
966
|
+
const profile = applyLocalModelProfileToConfig();
|
|
967
|
+
const speedConfig = getSpeedPresetForTier(profile && profile.key);
|
|
968
|
+
const modelUrlCandidates = (function () {
|
|
969
|
+
const seen = new Set();
|
|
970
|
+
const out = [];
|
|
971
|
+
const push = function (value) {
|
|
972
|
+
const url = String(value || "").trim();
|
|
973
|
+
if (!url || seen.has(url)) return;
|
|
974
|
+
seen.add(url);
|
|
975
|
+
out.push(url);
|
|
976
|
+
};
|
|
977
|
+
push(profile && profile.fallbackUrl);
|
|
978
|
+
if (profile && Array.isArray(profile.fallbackUrls)) {
|
|
979
|
+
profile.fallbackUrls.forEach(push);
|
|
980
|
+
}
|
|
981
|
+
return out;
|
|
982
|
+
})();
|
|
983
|
+
|
|
984
|
+
const normalizeForCompare = function (value) {
|
|
985
|
+
const raw = String(value || "").trim();
|
|
986
|
+
if (!raw) return "";
|
|
987
|
+
try {
|
|
988
|
+
const u = new URL(raw, window.location.origin);
|
|
989
|
+
u.searchParams.delete("download");
|
|
990
|
+
return `${u.origin}${u.pathname}?${u.searchParams.toString()}`.replace(/\?$/, "").toLowerCase();
|
|
991
|
+
} catch (error) {
|
|
992
|
+
return raw.toLowerCase().replace(/[?&]download=true/gi, "").replace(/[?&]$/, "");
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
const loadedModelUrl = localEngine && localEngine.modelInfo && localEngine.modelInfo.url
|
|
997
|
+
? normalizeForCompare(localEngine.modelInfo.url)
|
|
998
|
+
: "";
|
|
999
|
+
const candidateUrlSet = new Set(modelUrlCandidates.map(normalizeForCompare).filter(Boolean));
|
|
1000
|
+
if (isModelLoaded && loadedModelUrl && candidateUrlSet.has(loadedModelUrl)) {
|
|
1001
|
+
if (container) container.classList.add("hidden");
|
|
1002
|
+
if (typeof updateLocalProgress === "function") updateLocalProgress(bar, 100);
|
|
1003
|
+
isModelDownloaded = true;
|
|
1004
|
+
localStorage.setItem(getLocalDownloadStorageKey(), "true");
|
|
1005
|
+
return localEngine;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (!profile || !modelUrlCandidates.length) {
|
|
1009
|
+
throw new Error("Local model configuration is missing.");
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
try {
|
|
1013
|
+
if (container) container.classList.remove("hidden");
|
|
1014
|
+
if (typeof updateLocalProgress === "function") updateLocalProgress(bar, 0);
|
|
1015
|
+
|
|
1016
|
+
if (localEngine && typeof localEngine.unload === "function") {
|
|
1017
|
+
await Promise.resolve(localEngine.unload()).catch(function () {});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
let engine = null;
|
|
1021
|
+
let runtimeName = "wllama";
|
|
1022
|
+
const shouldTryWebLLM = false;
|
|
1023
|
+
|
|
1024
|
+
if (shouldTryWebLLM) {
|
|
1025
|
+
try {
|
|
1026
|
+
const webllm = window.WebLLMObj;
|
|
1027
|
+
engine = await webllm.CreateMLCEngine(profile.webllmModelId, {
|
|
1028
|
+
logLevel: "WARN",
|
|
1029
|
+
initProgressCallback: (report) => {
|
|
1030
|
+
if (container) container.classList.remove("hidden");
|
|
1031
|
+
if (typeof updateLocalProgress === "function") {
|
|
1032
|
+
updateLocalProgress(bar, report && report.progress);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
runtimeName = "webllm";
|
|
1037
|
+
} catch (webllmError) {
|
|
1038
|
+
engine = null;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (!engine) {
|
|
1043
|
+
const createEngine = getRuntimeCreateEngine();
|
|
1044
|
+
if (!createEngine) {
|
|
1045
|
+
throw new Error("Wllama runtime is not ready.");
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
engine = await createEngine("compat");
|
|
1049
|
+
const requestedSeqLen = Math.max(
|
|
1050
|
+
Number(window.ISAI_LOCAL_MAX_CONTEXT || 0),
|
|
1051
|
+
Number(speedConfig && speedConfig.maxSeqLen ? speedConfig.maxSeqLen : 0),
|
|
1052
|
+
4096
|
|
1053
|
+
);
|
|
1054
|
+
const targetSeqLen = Math.min(Math.max(requestedSeqLen, 512), 8192);
|
|
1055
|
+
const baseLoadOptions = {
|
|
1056
|
+
speedPreset: speedConfig && speedConfig.enginePreset ? speedConfig.enginePreset : "turbo",
|
|
1057
|
+
maxSeqLen: targetSeqLen,
|
|
1058
|
+
nBatch: 96,
|
|
1059
|
+
nCtx: targetSeqLen,
|
|
1060
|
+
onStage(stage) {
|
|
1061
|
+
if (container) container.classList.remove("hidden");
|
|
1062
|
+
if (stage && stage.text === "compat-ready" && typeof updateLocalProgress === "function") {
|
|
1063
|
+
updateLocalProgress(bar, 100);
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
onProgress(progress) {
|
|
1067
|
+
if (container) container.classList.remove("hidden");
|
|
1068
|
+
if (typeof updateLocalProgress === "function") {
|
|
1069
|
+
updateLocalProgress(bar, progress && progress.ratio ? progress.ratio : 0);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
const loadWithCandidates = async function (overrideLoadOptions) {
|
|
1074
|
+
let lastError = null;
|
|
1075
|
+
const purgeModelCache = window.WWAIObj && typeof window.WWAIObj.purgeModelCache === "function"
|
|
1076
|
+
? window.WWAIObj.purgeModelCache
|
|
1077
|
+
: null;
|
|
1078
|
+
for (const candidateUrl of modelUrlCandidates) {
|
|
1079
|
+
try {
|
|
1080
|
+
await engine.loadModel(candidateUrl, overrideLoadOptions || baseLoadOptions);
|
|
1081
|
+
profile.fallbackUrl = candidateUrl;
|
|
1082
|
+
return;
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
if (purgeModelCache && isRecoverableGgufLoadError(error)) {
|
|
1085
|
+
try { await purgeModelCache(candidateUrl); } catch (cacheError) {}
|
|
1086
|
+
try {
|
|
1087
|
+
// Retry once after cache purge to recover from stale/corrupted GGUF cache.
|
|
1088
|
+
await engine.loadModel(candidateUrl, overrideLoadOptions || baseLoadOptions);
|
|
1089
|
+
profile.fallbackUrl = candidateUrl;
|
|
1090
|
+
return;
|
|
1091
|
+
} catch (retryError) {
|
|
1092
|
+
error = retryError;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
lastError = error;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
throw lastError || new Error("Model loading failed.");
|
|
1099
|
+
};
|
|
1100
|
+
try {
|
|
1101
|
+
await loadWithCandidates(baseLoadOptions);
|
|
1102
|
+
} catch (compatLoadError) {
|
|
1103
|
+
throw compatLoadError;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
localEngine = engine;
|
|
1108
|
+
isModelDownloaded = true;
|
|
1109
|
+
isModelLoaded = true;
|
|
1110
|
+
isLocalActive = true;
|
|
1111
|
+
wllama = null;
|
|
1112
|
+
if (typeof setLocalRuntimeState === "function") setLocalRuntimeState(runtimeName);
|
|
1113
|
+
localStorage.setItem(getLocalDownloadStorageKey(), "true");
|
|
1114
|
+
setLocalActivePreference(true);
|
|
1115
|
+
if (container) container.classList.add("hidden");
|
|
1116
|
+
updateLocalBtnState();
|
|
1117
|
+
updateWebGPUBtnState();
|
|
1118
|
+
renderLocalModelTierSelector();
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
if (container) container.classList.add("hidden");
|
|
1121
|
+
isModelDownloaded = false;
|
|
1122
|
+
isModelLoaded = false;
|
|
1123
|
+
if (localEngine && typeof localEngine.unload === "function") {
|
|
1124
|
+
await Promise.resolve(localEngine.unload()).catch(function () {});
|
|
1125
|
+
}
|
|
1126
|
+
localEngine = null;
|
|
1127
|
+
wllama = null;
|
|
1128
|
+
if (typeof setLocalRuntimeState === "function") setLocalRuntimeState(null);
|
|
1129
|
+
localStorage.removeItem(getLocalDownloadStorageKey());
|
|
1130
|
+
throw error;
|
|
1131
|
+
}
|
|
1132
|
+
})();
|
|
1133
|
+
try {
|
|
1134
|
+
return await startDownloadPromise;
|
|
1135
|
+
} finally {
|
|
1136
|
+
startDownloadPromise = null;
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
runLocalInference = window.runLocalInference = async function (promptArr, callback) {
|
|
1141
|
+
return runSimpleLocalInference(promptArr, callback);
|
|
1142
|
+
|
|
1143
|
+
if (isLocalActive && !isModelLoaded) {
|
|
1144
|
+
await startDownload();
|
|
1145
|
+
}
|
|
1146
|
+
const profile = applyLocalModelProfileToConfig();
|
|
1147
|
+
const speedConfig = getSpeedPresetForTier(profile && profile.key) || {};
|
|
1148
|
+
const isLightTier = !!(profile && profile.key === "light");
|
|
1149
|
+
const isSuperlightTier = !!(profile && profile.key === "superlight");
|
|
1150
|
+
const currentUiMode = String(
|
|
1151
|
+
window.currentMode
|
|
1152
|
+
|| window.selectedMode
|
|
1153
|
+
|| (document.body && document.body.getAttribute("data-ui-mode"))
|
|
1154
|
+
|| "chat"
|
|
1155
|
+
).toLowerCase();
|
|
1156
|
+
const isCharacterChat = !!(
|
|
1157
|
+
window.ISAI_CHARACTER_CHAT_SESSION
|
|
1158
|
+
&& window.ISAI_CHARACTER_CHAT_SESSION.active
|
|
1159
|
+
);
|
|
1160
|
+
const isCodeMode = currentUiMode === "code";
|
|
1161
|
+
const isLightTierEffective = (isLightTier || isSuperlightTier) && !isCodeMode;
|
|
1162
|
+
const isCharacterLiteMode = isCharacterChat && !isCodeMode;
|
|
1163
|
+
const outputLocale = String(
|
|
1164
|
+
(document && document.documentElement && document.documentElement.lang)
|
|
1165
|
+
|| (navigator && navigator.language)
|
|
1166
|
+
|| "en"
|
|
1167
|
+
).toLowerCase();
|
|
1168
|
+
const emptyResponseText = getEmptyLocalResponseText(outputLocale);
|
|
1169
|
+
let messages = typeof mapPromptMessagesForLocalEngine === "function"
|
|
1170
|
+
? mapPromptMessagesForLocalEngine(promptArr)
|
|
1171
|
+
: [];
|
|
1172
|
+
messages = messages.filter((msg) => {
|
|
1173
|
+
if (!msg || msg.role !== "assistant") return true;
|
|
1174
|
+
const text = String(msg.content || "").trim();
|
|
1175
|
+
if (!text) return false;
|
|
1176
|
+
const normalized = text
|
|
1177
|
+
.toLowerCase()
|
|
1178
|
+
.replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/gu, "")
|
|
1179
|
+
.replace(/[.!?~"'\u2019\u201D\u3002\uFF01\uFF1F]+/g, "")
|
|
1180
|
+
.replace(/\s+/g, " ")
|
|
1181
|
+
.trim();
|
|
1182
|
+
const blocked = new Set([
|
|
1183
|
+
"안녕하세요 무엇을 도와드릴까요",
|
|
1184
|
+
"로컬로 더 안전하게 대화하세요",
|
|
1185
|
+
"hello how can i help you",
|
|
1186
|
+
"chat more safely in local mode",
|
|
1187
|
+
"opening chat"
|
|
1188
|
+
]);
|
|
1189
|
+
return !blocked.has(normalized);
|
|
1190
|
+
});
|
|
1191
|
+
if (isCharacterLiteMode) {
|
|
1192
|
+
messages = messages.map((msg) => {
|
|
1193
|
+
if (!msg || typeof msg !== "object") return msg;
|
|
1194
|
+
const role = String(msg.role || "user").toLowerCase();
|
|
1195
|
+
const content = String(msg.content || "");
|
|
1196
|
+
if (role !== "system") {
|
|
1197
|
+
return { role, content: content.slice(0, 220) };
|
|
1198
|
+
}
|
|
1199
|
+
const lines = content
|
|
1200
|
+
.split(/\r?\n/)
|
|
1201
|
+
.map((line) => String(line || "").trim())
|
|
1202
|
+
.filter(Boolean);
|
|
1203
|
+
const picked = lines.filter((line) => (
|
|
1204
|
+
/^you are roleplaying/i.test(line) ||
|
|
1205
|
+
/^scene setup:/i.test(line) ||
|
|
1206
|
+
/^character:/i.test(line) ||
|
|
1207
|
+
/^personality:/i.test(line) ||
|
|
1208
|
+
/^speech style:/i.test(line) ||
|
|
1209
|
+
/^start with a realistic/i.test(line) ||
|
|
1210
|
+
/^stay in character/i.test(line) ||
|
|
1211
|
+
/^reply in the user/i.test(line) ||
|
|
1212
|
+
/^keep responses concise/i.test(line)
|
|
1213
|
+
));
|
|
1214
|
+
const compactSystem = (picked.length ? picked : lines).join("\n").slice(0, 420);
|
|
1215
|
+
return { role: "system", content: compactSystem };
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
if (!messages.length) return;
|
|
1219
|
+
async function ensureCompatEngineForLocalFallback() {
|
|
1220
|
+
const createEngine = getRuntimeCreateEngine();
|
|
1221
|
+
if (!createEngine) {
|
|
1222
|
+
throw new Error("Wllama runtime is not ready.");
|
|
1223
|
+
}
|
|
1224
|
+
if (localEngine && typeof localEngine.unload === "function") {
|
|
1225
|
+
await Promise.resolve(localEngine.unload()).catch(function () {});
|
|
1226
|
+
}
|
|
1227
|
+
const compatEngine = await createEngine("compat");
|
|
1228
|
+
const candidates = [];
|
|
1229
|
+
const seen = new Set();
|
|
1230
|
+
const push = function (value) {
|
|
1231
|
+
const url = String(value || "").trim();
|
|
1232
|
+
if (!url || seen.has(url)) return;
|
|
1233
|
+
seen.add(url);
|
|
1234
|
+
candidates.push(url);
|
|
1235
|
+
};
|
|
1236
|
+
push(profile && profile.fallbackUrl);
|
|
1237
|
+
if (profile && Array.isArray(profile.fallbackUrls)) profile.fallbackUrls.forEach(push);
|
|
1238
|
+
const purgeModelCache = window.WWAIObj && typeof window.WWAIObj.purgeModelCache === "function"
|
|
1239
|
+
? window.WWAIObj.purgeModelCache
|
|
1240
|
+
: null;
|
|
1241
|
+
let lastError = null;
|
|
1242
|
+
for (const candidateUrl of candidates) {
|
|
1243
|
+
try {
|
|
1244
|
+
await compatEngine.loadModel(candidateUrl, {
|
|
1245
|
+
speedPreset: speedConfig.enginePreset || "balanced",
|
|
1246
|
+
maxSeqLen: Math.max(speedConfig.maxSeqLen || 256, isCharacterLiteMode ? 192 : (isSuperlightTier ? 64 : (isLightTierEffective ? 128 : 640)))
|
|
1247
|
+
});
|
|
1248
|
+
profile.fallbackUrl = candidateUrl;
|
|
1249
|
+
lastError = null;
|
|
1250
|
+
break;
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
if (purgeModelCache && isRecoverableGgufLoadError(error)) {
|
|
1253
|
+
try { await purgeModelCache(candidateUrl); } catch (cacheError) {}
|
|
1254
|
+
}
|
|
1255
|
+
lastError = error;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (lastError) throw lastError;
|
|
1259
|
+
localEngine = compatEngine;
|
|
1260
|
+
localRuntime = "wllama";
|
|
1261
|
+
if (typeof setLocalRuntimeState === "function") setLocalRuntimeState("wllama");
|
|
1262
|
+
return compatEngine;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (localRuntime === "webllm" && localEngine && localEngine.chat && localEngine.chat.completions) {
|
|
1266
|
+
if (typeof localEngine.resetChat === "function") {
|
|
1267
|
+
try {
|
|
1268
|
+
await localEngine.resetChat();
|
|
1269
|
+
} catch (error) {}
|
|
1270
|
+
}
|
|
1271
|
+
let emittedWebllmToken = false;
|
|
1272
|
+
const webllmRequest = {
|
|
1273
|
+
messages,
|
|
1274
|
+
temperature: isCharacterLiteMode ? 0.16 : (isSuperlightTier ? 0.1 : (isLightTierEffective ? 0.18 : 0.45)),
|
|
1275
|
+
top_p: isCharacterLiteMode ? 0.74 : (isSuperlightTier ? 0.5 : (isLightTierEffective ? 0.78 : 0.82)),
|
|
1276
|
+
stream: true
|
|
1277
|
+
};
|
|
1278
|
+
if (isCharacterLiteMode) {
|
|
1279
|
+
webllmRequest.max_tokens = 48;
|
|
1280
|
+
} else if (isSuperlightTier) {
|
|
1281
|
+
webllmRequest.max_tokens = 512;
|
|
1282
|
+
}
|
|
1283
|
+
const stream = await localEngine.chat.completions.create(webllmRequest);
|
|
1284
|
+
for await (const chunk of stream) {
|
|
1285
|
+
if (stopSignal) {
|
|
1286
|
+
if (typeof localEngine.interruptGenerate === "function") {
|
|
1287
|
+
localEngine.interruptGenerate();
|
|
1288
|
+
}
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
const delta = chunk && chunk.choices && chunk.choices[0] && chunk.choices[0].delta
|
|
1292
|
+
? chunk.choices[0].delta.content
|
|
1293
|
+
: "";
|
|
1294
|
+
if (delta) {
|
|
1295
|
+
emittedWebllmToken = true;
|
|
1296
|
+
callback(delta);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (emittedWebllmToken) {
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
await ensureCompatEngineForLocalFallback();
|
|
1303
|
+
}
|
|
1304
|
+
if (!localEngine || typeof localEngine.generateChat !== "function") {
|
|
1305
|
+
throw new Error("Wllama local engine is not ready.");
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (typeof localEngine.clearChatState === "function") {
|
|
1309
|
+
try {
|
|
1310
|
+
await localEngine.clearChatState();
|
|
1311
|
+
} catch (error) {}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function normalizeRepeatText(value) {
|
|
1315
|
+
return String(value || "")
|
|
1316
|
+
.toLowerCase()
|
|
1317
|
+
.replace(/\s+/g, " ")
|
|
1318
|
+
.replace(/[?쒋?']/g, "")
|
|
1319
|
+
.trim();
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function trimIncomingOverlap(baseText, incomingText) {
|
|
1323
|
+
const base = String(baseText || "");
|
|
1324
|
+
const incoming = String(incomingText || "");
|
|
1325
|
+
const maxOverlap = Math.min(base.length, incoming.length, 180);
|
|
1326
|
+
for (let size = maxOverlap; size > 0; size -= 1) {
|
|
1327
|
+
if (base.slice(-size) === incoming.slice(0, size)) {
|
|
1328
|
+
return incoming.slice(size);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return incoming;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function getTrailingSentenceUnits(text) {
|
|
1335
|
+
return normalizeRepeatText(text)
|
|
1336
|
+
.split(/(?<=[.!?])\s+|\n+/)
|
|
1337
|
+
.map((part) => part.trim())
|
|
1338
|
+
.filter(Boolean);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function hasRepeatedSentence(text) {
|
|
1342
|
+
const units = getTrailingSentenceUnits(text);
|
|
1343
|
+
if (units.length < 2) return false;
|
|
1344
|
+
const last = units[units.length - 1];
|
|
1345
|
+
const prev = units[units.length - 2];
|
|
1346
|
+
if (last.length >= 8 && last === prev) return true;
|
|
1347
|
+
if (units.length >= 3) {
|
|
1348
|
+
const prev2 = units[units.length - 3];
|
|
1349
|
+
if (last.length >= 8 && last === prev2) return true;
|
|
1350
|
+
}
|
|
1351
|
+
return false;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function hasRepeatedTailBlock(text) {
|
|
1355
|
+
const normalized = normalizeRepeatText(text);
|
|
1356
|
+
const maxUnit = Math.min(96, Math.floor(normalized.length / 2));
|
|
1357
|
+
for (let unitLen = maxUnit; unitLen >= 10; unitLen -= 1) {
|
|
1358
|
+
const tail = normalized.slice(-unitLen * 2);
|
|
1359
|
+
if (!tail || tail.length < unitLen * 2) continue;
|
|
1360
|
+
const block = tail.slice(0, unitLen);
|
|
1361
|
+
if (block && tail === block + block) {
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return false;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function hasRepeatedListPattern(text) {
|
|
1369
|
+
const lines = String(text || "")
|
|
1370
|
+
.split("\n")
|
|
1371
|
+
.map((line) => line.trim())
|
|
1372
|
+
.filter(Boolean);
|
|
1373
|
+
if (lines.length < 4) return false;
|
|
1374
|
+
const recent = lines
|
|
1375
|
+
.slice(-6)
|
|
1376
|
+
.map((line) => normalizeRepeatKey(
|
|
1377
|
+
line
|
|
1378
|
+
.replace(/^\d+\.\s*/g, "")
|
|
1379
|
+
.replace(/^[-*]\s*/g, "")
|
|
1380
|
+
))
|
|
1381
|
+
.filter(Boolean);
|
|
1382
|
+
if (recent.length < 4) return false;
|
|
1383
|
+
return new Set(recent).size <= Math.ceil(recent.length / 2);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function getComparableRepeatLines(text) {
|
|
1387
|
+
return String(text || "")
|
|
1388
|
+
.split("\n")
|
|
1389
|
+
.map((line) => line.trim())
|
|
1390
|
+
.filter(Boolean)
|
|
1391
|
+
.map((line) => normalizeRepeatKey(
|
|
1392
|
+
line
|
|
1393
|
+
.replace(/^\d+\.\s*/g, "")
|
|
1394
|
+
.replace(/^[-*]\s*/g, "")
|
|
1395
|
+
))
|
|
1396
|
+
.filter(Boolean);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function getComparableRepeatSentences(text) {
|
|
1400
|
+
return String(text || "")
|
|
1401
|
+
.split(/(?<=[.!?])\s+|\n+/)
|
|
1402
|
+
.map((part) => normalizeRepeatKey(
|
|
1403
|
+
String(part || "")
|
|
1404
|
+
.replace(/^\d+\.\s*/g, "")
|
|
1405
|
+
.replace(/^[-*]\s*/g, "")
|
|
1406
|
+
))
|
|
1407
|
+
.filter(Boolean);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function findRepeatedSuffixSequence(items, minWindow = 2, maxWindow = 4) {
|
|
1411
|
+
if (!Array.isArray(items) || items.length < minWindow * 2) return null;
|
|
1412
|
+
const limit = Math.min(maxWindow, Math.floor(items.length / 2));
|
|
1413
|
+
for (let size = limit; size >= minWindow; size -= 1) {
|
|
1414
|
+
const suffix = items.slice(-size);
|
|
1415
|
+
if (!suffix.every(Boolean)) continue;
|
|
1416
|
+
for (let start = items.length - size * 2; start >= 0; start -= 1) {
|
|
1417
|
+
let matched = true;
|
|
1418
|
+
for (let index = 0; index < size; index += 1) {
|
|
1419
|
+
if (items[start + index] !== suffix[index]) {
|
|
1420
|
+
matched = false;
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (matched) {
|
|
1425
|
+
return { start, size };
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return null;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function hasRepeatedBlockSequence(text) {
|
|
1433
|
+
return Boolean(
|
|
1434
|
+
findRepeatedSuffixSequence(getComparableRepeatLines(text), 2, 4) ||
|
|
1435
|
+
findRepeatedSuffixSequence(getComparableRepeatSentences(text), 2, 3)
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function shouldSuppressRepeatedDelta(baseText, incomingText, previousDelta) {
|
|
1440
|
+
const trimmedIncoming = trimIncomingOverlap(baseText, incomingText);
|
|
1441
|
+
const normalizedDelta = normalizeRepeatText(trimmedIncoming);
|
|
1442
|
+
const normalizedPrevDelta = normalizeRepeatText(previousDelta);
|
|
1443
|
+
const candidateText = baseText + trimmedIncoming;
|
|
1444
|
+
if (!trimmedIncoming) {
|
|
1445
|
+
return { blocked: true, trimmed: "" };
|
|
1446
|
+
}
|
|
1447
|
+
if (normalizedDelta.length >= 12 && normalizedDelta === normalizedPrevDelta) {
|
|
1448
|
+
return { blocked: true, trimmed: "" };
|
|
1449
|
+
}
|
|
1450
|
+
if (hasRepeatedSentence(candidateText) || hasRepeatedTailBlock(candidateText) || hasRepeatedListPattern(candidateText) || hasRepeatedBlockSequence(candidateText)) {
|
|
1451
|
+
return { blocked: true, trimmed: "" };
|
|
1452
|
+
}
|
|
1453
|
+
return { blocked: false, trimmed: trimmedIncoming };
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function normalizeRepeatKey(text) {
|
|
1457
|
+
return String(text || "")
|
|
1458
|
+
.replace(/\uFFFD/g, "")
|
|
1459
|
+
.replace(/\s+/g, " ")
|
|
1460
|
+
.trim()
|
|
1461
|
+
.toLowerCase();
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function sanitizeStreamText(text) {
|
|
1465
|
+
return String(text || "")
|
|
1466
|
+
.replace(/\uFFFD+/g, "")
|
|
1467
|
+
.replace(/\r\n/g, "\n");
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
const localChunkDecoder = typeof TextDecoder !== "undefined"
|
|
1471
|
+
? new TextDecoder("utf-8")
|
|
1472
|
+
: null;
|
|
1473
|
+
|
|
1474
|
+
function hasRenderableLocalText(value) {
|
|
1475
|
+
const raw = String(value || "");
|
|
1476
|
+
if (!raw) return false;
|
|
1477
|
+
let extracted = "";
|
|
1478
|
+
if (typeof extractPlainTextLocal === "function") {
|
|
1479
|
+
try {
|
|
1480
|
+
extracted = String(extractPlainTextLocal(raw) || "").trim();
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
extracted = "";
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (extracted) return true;
|
|
1486
|
+
const fallback = raw
|
|
1487
|
+
.replace(/<think>[\s\S]*?(<\/think>|$)/gi, " ")
|
|
1488
|
+
.replace(/```json/gi, "")
|
|
1489
|
+
.replace(/```/g, "")
|
|
1490
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
1491
|
+
.replace(/<[^>]+>/g, " ")
|
|
1492
|
+
.replace(/ /gi, " ")
|
|
1493
|
+
.replace(/\s+/g, " ")
|
|
1494
|
+
.trim();
|
|
1495
|
+
return fallback.length > 0;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function readLocalChunkText(chunk) {
|
|
1499
|
+
if (chunk == null) return "";
|
|
1500
|
+
if (typeof chunk === "string") return chunk;
|
|
1501
|
+
if (typeof chunk === "number") return String(chunk);
|
|
1502
|
+
if (typeof Uint8Array !== "undefined" && chunk instanceof Uint8Array && localChunkDecoder) {
|
|
1503
|
+
return localChunkDecoder.decode(chunk, { stream: true });
|
|
1504
|
+
}
|
|
1505
|
+
if (Array.isArray(chunk)) {
|
|
1506
|
+
for (const item of chunk) {
|
|
1507
|
+
if (item == null) continue;
|
|
1508
|
+
if (typeof item === "string" || typeof item === "number") {
|
|
1509
|
+
return String(item);
|
|
1510
|
+
}
|
|
1511
|
+
if (typeof Uint8Array !== "undefined" && item instanceof Uint8Array && localChunkDecoder) {
|
|
1512
|
+
return localChunkDecoder.decode(item, { stream: true });
|
|
1513
|
+
}
|
|
1514
|
+
if (typeof item === "object" && item) {
|
|
1515
|
+
const nested = item.currentText || item.text || item.content || (item.delta && item.delta.content) || "";
|
|
1516
|
+
if (nested != null && String(nested).trim() !== "") {
|
|
1517
|
+
return String(nested);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const directCandidates = [
|
|
1524
|
+
chunk.currentText,
|
|
1525
|
+
chunk.text,
|
|
1526
|
+
chunk.content,
|
|
1527
|
+
chunk.delta,
|
|
1528
|
+
chunk.token,
|
|
1529
|
+
chunk.tokenText,
|
|
1530
|
+
chunk.tokenBytes,
|
|
1531
|
+
chunk.bytes
|
|
1532
|
+
];
|
|
1533
|
+
for (const candidate of directCandidates) {
|
|
1534
|
+
if (candidate == null) continue;
|
|
1535
|
+
if (typeof candidate === "string" || typeof candidate === "number") {
|
|
1536
|
+
return String(candidate);
|
|
1537
|
+
}
|
|
1538
|
+
if (typeof Uint8Array !== "undefined" && candidate instanceof Uint8Array && localChunkDecoder) {
|
|
1539
|
+
return localChunkDecoder.decode(candidate, { stream: true });
|
|
1540
|
+
}
|
|
1541
|
+
if (typeof candidate === "object" && candidate && typeof candidate.content === "string") {
|
|
1542
|
+
return candidate.content;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const firstChoice = Array.isArray(chunk.choices) ? chunk.choices[0] : null;
|
|
1547
|
+
if (firstChoice && typeof firstChoice === "object") {
|
|
1548
|
+
const nestedCandidates = [
|
|
1549
|
+
firstChoice.text,
|
|
1550
|
+
firstChoice.content,
|
|
1551
|
+
chunk.delta && chunk.delta.content,
|
|
1552
|
+
firstChoice.delta && firstChoice.delta.content,
|
|
1553
|
+
firstChoice.message && firstChoice.message.content
|
|
1554
|
+
];
|
|
1555
|
+
for (const candidate of nestedCandidates) {
|
|
1556
|
+
if (candidate == null) continue;
|
|
1557
|
+
if (typeof candidate === "string" || typeof candidate === "number") {
|
|
1558
|
+
return String(candidate);
|
|
1559
|
+
}
|
|
1560
|
+
if (typeof Uint8Array !== "undefined" && candidate instanceof Uint8Array && localChunkDecoder) {
|
|
1561
|
+
return localChunkDecoder.decode(candidate, { stream: true });
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
return "";
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function cleanupAssistantBoilerplate(text) {
|
|
1570
|
+
let value = String(text || "");
|
|
1571
|
+
value = value
|
|
1572
|
+
.replace(/\n?\(hello!\)\s*how are you doing\?\)?/gi, "")
|
|
1573
|
+
.replace(/\n?i['??m here to help[\s\S]*$/i, "")
|
|
1574
|
+
.replace(/\n?don['??t hesitate[\s\S]*$/i, "")
|
|
1575
|
+
.replace(/\n?please let me know what['??s on your mind[\s\S]*$/i, "")
|
|
1576
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1577
|
+
.trim();
|
|
1578
|
+
|
|
1579
|
+
if (outputLocale.startsWith("ko")) {
|
|
1580
|
+
const lines = value
|
|
1581
|
+
.split("\n")
|
|
1582
|
+
.map((line) => line.trim())
|
|
1583
|
+
.filter(Boolean);
|
|
1584
|
+
const cleaned = lines.filter((line) => {
|
|
1585
|
+
const hasHangul = /[가-힣]/.test(line);
|
|
1586
|
+
const helperLike = /hello|how are you|i'?m here to help|don'?t hesitate|translate|write stories|chat about/i.test(line);
|
|
1587
|
+
if (!hasHangul && helperLike) return false;
|
|
1588
|
+
return true;
|
|
1589
|
+
});
|
|
1590
|
+
value = cleaned.join("\n").trim();
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
return value;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function escapeRegExp(text) {
|
|
1597
|
+
return String(text || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function cleanupAssistantBoilerplateSafe(text) {
|
|
1601
|
+
let value = String(text || "");
|
|
1602
|
+
value = value
|
|
1603
|
+
.replace(/\n?\(hello!\)\s*how are you doing\?\)?/gi, "")
|
|
1604
|
+
.replace(/\n?i['??m here to help[\s\S]*$/i, "")
|
|
1605
|
+
.replace(/\n?don['??t hesitate[\s\S]*$/i, "")
|
|
1606
|
+
.replace(/\n?please let me know what['??s on your mind[\s\S]*$/i, "")
|
|
1607
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1608
|
+
.trim();
|
|
1609
|
+
|
|
1610
|
+
if (!outputLocale.startsWith("ko")) {
|
|
1611
|
+
return value;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const lines = value
|
|
1615
|
+
.split("\n")
|
|
1616
|
+
.map((line) => line.trim())
|
|
1617
|
+
.filter(Boolean);
|
|
1618
|
+
|
|
1619
|
+
const cleaned = lines.filter((line) => {
|
|
1620
|
+
const hasHangul = /[\uAC00-\uD7A3]/.test(line);
|
|
1621
|
+
const helperLike = /hello|how are you|i'?m here to help|don'?t hesitate|translate|write stories|chat about/i.test(line);
|
|
1622
|
+
if (!hasHangul && helperLike) return false;
|
|
1623
|
+
return true;
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
return cleaned.join("\n").trim();
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function cleanupAssistantBoilerplateAscii(text) {
|
|
1630
|
+
let value = String(text || "");
|
|
1631
|
+
value = value
|
|
1632
|
+
.replace(/\n?\(hello!\)\s*how are you doing\?\)?/gi, "")
|
|
1633
|
+
.replace(/\n?i(?:'|\u2019)m here to help[\s\S]*$/i, "")
|
|
1634
|
+
.replace(/\n?don(?:'|\u2019)t hesitate[\s\S]*$/i, "")
|
|
1635
|
+
.replace(/\n?please let me know what(?:'|\u2019)s on your mind[\s\S]*$/i, "")
|
|
1636
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1637
|
+
.trim();
|
|
1638
|
+
|
|
1639
|
+
if (!outputLocale.startsWith("ko")) return value;
|
|
1640
|
+
|
|
1641
|
+
const lines = value
|
|
1642
|
+
.split("\n")
|
|
1643
|
+
.map((line) => line.trim())
|
|
1644
|
+
.filter(Boolean);
|
|
1645
|
+
|
|
1646
|
+
const cleaned = lines.filter((line) => {
|
|
1647
|
+
const hasHangul = /[\uAC00-\uD7A3]/.test(line);
|
|
1648
|
+
const helperLike = /hello|how are you|i(?:'|\u2019)m here to help|don(?:'|\u2019)t hesitate|translate|write stories|chat about/i.test(line);
|
|
1649
|
+
if (!hasHangul && helperLike) return false;
|
|
1650
|
+
return true;
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
return cleaned.join("\n").trim();
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function trimRepeatedTail(text) {
|
|
1657
|
+
if (!text) return text;
|
|
1658
|
+
|
|
1659
|
+
const lines = String(text)
|
|
1660
|
+
.split("\n")
|
|
1661
|
+
.map((line) => line.trim())
|
|
1662
|
+
.filter(Boolean);
|
|
1663
|
+
|
|
1664
|
+
if (lines.length >= 3) {
|
|
1665
|
+
const recent = lines.slice(-3).map(normalizeRepeatKey);
|
|
1666
|
+
if (recent[0] && recent[0] === recent[1] && recent[1] === recent[2]) {
|
|
1667
|
+
return String(text)
|
|
1668
|
+
.replace(new RegExp("(?:\\n)?" + escapeRegExp(lines[lines.length - 1]) + "\\s*$"), "")
|
|
1669
|
+
.trimEnd();
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
const windowSize = Math.min(80, Math.floor(String(text).length / 3));
|
|
1674
|
+
if (windowSize >= 24) {
|
|
1675
|
+
const a = normalizeRepeatKey(String(text).slice(-windowSize));
|
|
1676
|
+
const b = normalizeRepeatKey(String(text).slice(-windowSize * 2, -windowSize));
|
|
1677
|
+
if (a && a === b) {
|
|
1678
|
+
return String(text).slice(0, -windowSize).trimEnd();
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
if (lines.length >= 4) {
|
|
1683
|
+
const keys = lines.map((line) => normalizeRepeatKey(
|
|
1684
|
+
line
|
|
1685
|
+
.replace(/^\d+\.\s*/g, "")
|
|
1686
|
+
.replace(/^[-*]\s*/g, "")
|
|
1687
|
+
));
|
|
1688
|
+
const lastKey = keys[keys.length - 1];
|
|
1689
|
+
const prevKey = keys[keys.length - 2];
|
|
1690
|
+
const earlierLastIndex = keys.slice(0, -1).lastIndexOf(lastKey);
|
|
1691
|
+
const earlierPrevIndex = keys.slice(0, -2).lastIndexOf(prevKey);
|
|
1692
|
+
if (lastKey && prevKey && earlierLastIndex >= 0 && earlierPrevIndex >= 0) {
|
|
1693
|
+
if (keys.length - 1 - earlierLastIndex <= 4 || keys.length - 2 - earlierPrevIndex <= 4) {
|
|
1694
|
+
return lines.slice(0, Math.min(earlierLastIndex, earlierPrevIndex) + 1).join("\n").trimEnd();
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
const repeatedLineSuffix = findRepeatedSuffixSequence(getComparableRepeatLines(text), 2, 4);
|
|
1700
|
+
if (repeatedLineSuffix && repeatedLineSuffix.size < lines.length) {
|
|
1701
|
+
return lines.slice(0, lines.length - repeatedLineSuffix.size).join("\n").trimEnd();
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
return text;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function trimDuplicateParagraphs(text) {
|
|
1708
|
+
const source = String(text || "").trimEnd();
|
|
1709
|
+
if (!source) return source;
|
|
1710
|
+
const separator = /\n{2,}/.test(source) ? "\n\n" : "\n";
|
|
1711
|
+
const blocks = source
|
|
1712
|
+
.split(separator === "\n\n" ? /\n{2,}/ : /\n+/)
|
|
1713
|
+
.map((block) => String(block || "").trim())
|
|
1714
|
+
.filter(Boolean);
|
|
1715
|
+
if (blocks.length < 2) return source;
|
|
1716
|
+
|
|
1717
|
+
const seen = new Set();
|
|
1718
|
+
const kept = [];
|
|
1719
|
+
let removed = false;
|
|
1720
|
+
|
|
1721
|
+
blocks.forEach((block) => {
|
|
1722
|
+
const key = normalizeRepeatKey(
|
|
1723
|
+
block
|
|
1724
|
+
.replace(/^\d+\.\s*/gm, "")
|
|
1725
|
+
.replace(/^[-*]\s*/gm, "")
|
|
1726
|
+
);
|
|
1727
|
+
const isComparable = key.length >= 32 || block.length >= 48;
|
|
1728
|
+
if (isComparable && seen.has(key)) {
|
|
1729
|
+
removed = true;
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
if (isComparable) seen.add(key);
|
|
1733
|
+
kept.push(block);
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
return removed ? kept.join(separator).trimEnd() : source;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
function trimRestartedSentenceBlock(text) {
|
|
1740
|
+
const source = String(text || "").trimEnd();
|
|
1741
|
+
if (!source || source.length < 80) return source;
|
|
1742
|
+
|
|
1743
|
+
const sentenceItems = (source.match(/[^.!?\n]+[.!?]?(?:\s+|$)|[^\n]+\n?/g) || [])
|
|
1744
|
+
.map((raw) => ({
|
|
1745
|
+
raw,
|
|
1746
|
+
key: normalizeRepeatKey(
|
|
1747
|
+
String(raw || "")
|
|
1748
|
+
.replace(/^\d+\.\s*/g, "")
|
|
1749
|
+
.replace(/^[-*]\s*/g, "")
|
|
1750
|
+
)
|
|
1751
|
+
}))
|
|
1752
|
+
.filter((item) => item.key);
|
|
1753
|
+
if (sentenceItems.length < 4) return source;
|
|
1754
|
+
|
|
1755
|
+
const firstKey = sentenceItems[0].key;
|
|
1756
|
+
if (firstKey.length < 24) return source;
|
|
1757
|
+
const seed = firstKey.slice(0, Math.min(36, firstKey.length));
|
|
1758
|
+
|
|
1759
|
+
for (let index = 2; index < sentenceItems.length; index += 1) {
|
|
1760
|
+
const key = sentenceItems[index].key;
|
|
1761
|
+
if (!key || key.length < 16) continue;
|
|
1762
|
+
if (
|
|
1763
|
+
key === firstKey ||
|
|
1764
|
+
key.startsWith(seed) ||
|
|
1765
|
+
firstKey.startsWith(key.slice(0, Math.min(24, key.length)))
|
|
1766
|
+
) {
|
|
1767
|
+
return sentenceItems.slice(0, index).map((item) => item.raw).join("").trimEnd();
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
return source;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
function trimRepeatedSentenceHistory(text) {
|
|
1775
|
+
const source = String(text || "").trimEnd();
|
|
1776
|
+
if (!source || source.length < 80) return source;
|
|
1777
|
+
|
|
1778
|
+
const sentenceItems = (source.match(/[^.!?\n]+[.!?]?(?:\s+|$)|[^\n]+\n?/g) || [])
|
|
1779
|
+
.map((raw) => ({
|
|
1780
|
+
raw,
|
|
1781
|
+
key: normalizeRepeatKey(
|
|
1782
|
+
String(raw || "")
|
|
1783
|
+
.replace(/^\d+\.\s*/g, "")
|
|
1784
|
+
.replace(/^[-*]\s*/g, "")
|
|
1785
|
+
)
|
|
1786
|
+
}))
|
|
1787
|
+
.filter((item) => item.key);
|
|
1788
|
+
if (sentenceItems.length < 4) return source;
|
|
1789
|
+
|
|
1790
|
+
const seen = new Set();
|
|
1791
|
+
const kept = [];
|
|
1792
|
+
let removed = false;
|
|
1793
|
+
for (const item of sentenceItems) {
|
|
1794
|
+
const comparable = item.key.length >= 24 || String(item.raw || "").trim().length >= 32;
|
|
1795
|
+
if (comparable && seen.has(item.key)) {
|
|
1796
|
+
removed = true;
|
|
1797
|
+
break;
|
|
1798
|
+
}
|
|
1799
|
+
if (comparable) seen.add(item.key);
|
|
1800
|
+
kept.push(item.raw);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
return removed ? kept.join("").trimEnd() : source;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
function trimTrailingDuplicateSentenceBlock(text) {
|
|
1807
|
+
const source = String(text || "").trimEnd();
|
|
1808
|
+
if (!source || source.length < 80) return source;
|
|
1809
|
+
|
|
1810
|
+
const sentenceItems = (source.match(/[^.!?\n]+[.!?]?(?:\s+|$)|[^\n]+\n?/g) || [])
|
|
1811
|
+
.map((raw) => ({
|
|
1812
|
+
raw,
|
|
1813
|
+
key: normalizeRepeatKey(
|
|
1814
|
+
String(raw || "")
|
|
1815
|
+
.replace(/^\d+\.\s*/g, "")
|
|
1816
|
+
.replace(/^[-*]\s*/g, "")
|
|
1817
|
+
)
|
|
1818
|
+
}))
|
|
1819
|
+
.filter((item) => item.key);
|
|
1820
|
+
if (sentenceItems.length < 3) return source;
|
|
1821
|
+
|
|
1822
|
+
const lastItem = sentenceItems[sentenceItems.length - 1];
|
|
1823
|
+
if (lastItem.key.length < 18 || String(lastItem.raw || "").trim().length < 24) return source;
|
|
1824
|
+
|
|
1825
|
+
const earlierIndex = sentenceItems.slice(0, -1).map((item) => item.key).lastIndexOf(lastItem.key);
|
|
1826
|
+
if (earlierIndex >= 0) {
|
|
1827
|
+
return sentenceItems.slice(0, earlierIndex + 1).map((item) => item.raw).join("").trimEnd();
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
return source;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function hasExcessiveRepetition(text) {
|
|
1834
|
+
const source = String(text || "");
|
|
1835
|
+
if (!source || source.length < 80) return false;
|
|
1836
|
+
|
|
1837
|
+
const lines = source
|
|
1838
|
+
.split("\n")
|
|
1839
|
+
.map((line) => normalizeRepeatKey(line))
|
|
1840
|
+
.filter(Boolean);
|
|
1841
|
+
|
|
1842
|
+
if (lines.length >= 3) {
|
|
1843
|
+
const recent = lines.slice(-3);
|
|
1844
|
+
if (recent[0] && recent[0] === recent[1] && recent[1] === recent[2]) {
|
|
1845
|
+
return true;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
const windowSize = Math.min(80, Math.floor(source.length / 3));
|
|
1850
|
+
if (windowSize >= 24) {
|
|
1851
|
+
const a = normalizeRepeatKey(source.slice(-windowSize));
|
|
1852
|
+
const b = normalizeRepeatKey(source.slice(-windowSize * 2, -windowSize));
|
|
1853
|
+
if (a && a === b) {
|
|
1854
|
+
return true;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
if (lines.length >= 6) {
|
|
1859
|
+
const recent = lines.slice(-6).map((line) =>
|
|
1860
|
+
line
|
|
1861
|
+
.replace(/^\d+\.\s*/g, "")
|
|
1862
|
+
.replace(/^[-*]\s*/g, "")
|
|
1863
|
+
);
|
|
1864
|
+
const uniqueRecent = new Set(recent.filter(Boolean));
|
|
1865
|
+
if (uniqueRecent.size <= 3) {
|
|
1866
|
+
return true;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
if (hasRepeatedListPattern(source)) {
|
|
1871
|
+
return true;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
if (hasRepeatedBlockSequence(source)) {
|
|
1875
|
+
return true;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (trimDuplicateParagraphs(source) !== source.trimEnd()) {
|
|
1879
|
+
return true;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (trimRestartedSentenceBlock(source) !== source.trimEnd()) {
|
|
1883
|
+
return true;
|
|
1884
|
+
}
|
|
1885
|
+
if (trimRepeatedSentenceHistory(source) !== source.trimEnd()) {
|
|
1886
|
+
return true;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function trimRestartedPrefixBlock(text) {
|
|
1893
|
+
let value = String(text || "").trimEnd();
|
|
1894
|
+
if (!value || value.length < 80) return value;
|
|
1895
|
+
|
|
1896
|
+
const plain = String(value)
|
|
1897
|
+
.replace(/<think>[\s\S]*?(<\/think>|$)/gi, " ")
|
|
1898
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
1899
|
+
.replace(/<[^>]+>/g, " ")
|
|
1900
|
+
.replace(/\s+/g, " ")
|
|
1901
|
+
.trim();
|
|
1902
|
+
if (plain.length < 80) return value;
|
|
1903
|
+
|
|
1904
|
+
const prefixSizes = [120, 96, 84, 72, 60, 48, 36];
|
|
1905
|
+
for (const size of prefixSizes) {
|
|
1906
|
+
if (plain.length < size * 2) continue;
|
|
1907
|
+
const prefix = plain.slice(0, size).trim();
|
|
1908
|
+
if (prefix.length < Math.min(32, size)) continue;
|
|
1909
|
+
const restartIndex = plain.indexOf(prefix, Math.max(size + 12, Math.floor(size * 1.25)));
|
|
1910
|
+
if (restartIndex < 0) continue;
|
|
1911
|
+
|
|
1912
|
+
const uniqueChars = new Set(prefix.replace(/\s+/g, ""));
|
|
1913
|
+
if (uniqueChars.size < 8) continue;
|
|
1914
|
+
|
|
1915
|
+
const cutSnippet = plain.slice(restartIndex, restartIndex + Math.min(160, prefix.length + 40)).trim();
|
|
1916
|
+
if (!cutSnippet || cutSnippet.length < Math.min(28, Math.floor(size * 0.6))) continue;
|
|
1917
|
+
|
|
1918
|
+
const rawRestartIndex = value.indexOf(cutSnippet);
|
|
1919
|
+
if (rawRestartIndex > 0) {
|
|
1920
|
+
return value.slice(0, rawRestartIndex).trimEnd();
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
return value;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
async function consumeLocalStream(stream) {
|
|
1928
|
+
let renderedText = "";
|
|
1929
|
+
let aggregatedText = "";
|
|
1930
|
+
let repetitionHits = 0;
|
|
1931
|
+
|
|
1932
|
+
for await (const chunk of stream) {
|
|
1933
|
+
if (stopSignal) {
|
|
1934
|
+
if (typeof localEngine.interruptGenerate === "function") {
|
|
1935
|
+
localEngine.interruptGenerate();
|
|
1936
|
+
}
|
|
1937
|
+
break;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
const rawCurrentText = sanitizeStreamText(readLocalChunkText(chunk));
|
|
1941
|
+
if (!rawCurrentText) continue;
|
|
1942
|
+
aggregatedText = aggregatedText
|
|
1943
|
+
? (aggregatedText + trimIncomingOverlap(aggregatedText, rawCurrentText))
|
|
1944
|
+
: rawCurrentText;
|
|
1945
|
+
|
|
1946
|
+
const cleanedText = cleanupAssistantBoilerplateAscii(aggregatedText);
|
|
1947
|
+
const currentText = trimRepeatedSentenceHistory(
|
|
1948
|
+
trimRestartedSentenceBlock(trimRestartedPrefixBlock(cleanedText || aggregatedText))
|
|
1949
|
+
);
|
|
1950
|
+
let nextText = trimRepeatedSentenceHistory(
|
|
1951
|
+
trimTrailingDuplicateSentenceBlock(
|
|
1952
|
+
trimRestartedSentenceBlock(trimDuplicateParagraphs(trimRepeatedTail(currentText)))
|
|
1953
|
+
)
|
|
1954
|
+
);
|
|
1955
|
+
if (!nextText) continue;
|
|
1956
|
+
nextText = trimRestartedPrefixBlock(nextText);
|
|
1957
|
+
nextText = trimRepeatedSentenceHistory(
|
|
1958
|
+
trimTrailingDuplicateSentenceBlock(
|
|
1959
|
+
trimRestartedSentenceBlock(trimDuplicateParagraphs(nextText))
|
|
1960
|
+
)
|
|
1961
|
+
);
|
|
1962
|
+
|
|
1963
|
+
if (normalizeRepeatKey(nextText) === normalizeRepeatKey(renderedText)) {
|
|
1964
|
+
repetitionHits += 1;
|
|
1965
|
+
if (repetitionHits >= 3) {
|
|
1966
|
+
stopSignal = true;
|
|
1967
|
+
if (typeof localEngine.interruptGenerate === "function") {
|
|
1968
|
+
try {
|
|
1969
|
+
localEngine.interruptGenerate();
|
|
1970
|
+
} catch (error) {}
|
|
1971
|
+
}
|
|
1972
|
+
break;
|
|
1973
|
+
}
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
if (hasExcessiveRepetition(nextText)) {
|
|
1978
|
+
repetitionHits += 1;
|
|
1979
|
+
nextText = trimRepeatedSentenceHistory(
|
|
1980
|
+
trimRestartedSentenceBlock(trimDuplicateParagraphs(trimRepeatedTail(nextText)))
|
|
1981
|
+
);
|
|
1982
|
+
if (!nextText || repetitionHits >= 2) {
|
|
1983
|
+
stopSignal = true;
|
|
1984
|
+
if (typeof localEngine.interruptGenerate === "function") {
|
|
1985
|
+
try {
|
|
1986
|
+
localEngine.interruptGenerate();
|
|
1987
|
+
} catch (error) {}
|
|
1988
|
+
}
|
|
1989
|
+
break;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
if (normalizeRepeatKey(nextText) === normalizeRepeatKey(renderedText)) {
|
|
1994
|
+
repetitionHits += 1;
|
|
1995
|
+
if (repetitionHits >= 3) {
|
|
1996
|
+
stopSignal = true;
|
|
1997
|
+
if (typeof localEngine.interruptGenerate === "function") {
|
|
1998
|
+
try {
|
|
1999
|
+
localEngine.interruptGenerate();
|
|
2000
|
+
} catch (error) {}
|
|
2001
|
+
}
|
|
2002
|
+
break;
|
|
2003
|
+
}
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
repetitionHits = 0;
|
|
2008
|
+
renderedText = trimTrailingDuplicateSentenceBlock(nextText);
|
|
2009
|
+
callback(renderedText, { replace: true, isStreaming: true });
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
if (!hasRenderableLocalText(renderedText || "") && hasRenderableLocalText(aggregatedText || "")) {
|
|
2013
|
+
const fallbackText = trimRepeatedSentenceHistory(
|
|
2014
|
+
trimRestartedSentenceBlock(
|
|
2015
|
+
trimTrailingDuplicateSentenceBlock(
|
|
2016
|
+
trimDuplicateParagraphs(
|
|
2017
|
+
trimRestartedPrefixBlock(cleanupAssistantBoilerplateAscii(aggregatedText))
|
|
2018
|
+
)
|
|
2019
|
+
)
|
|
2020
|
+
)
|
|
2021
|
+
);
|
|
2022
|
+
if (hasRenderableLocalText(fallbackText || "")) {
|
|
2023
|
+
renderedText = fallbackText;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
return trimTrailingDuplicateSentenceBlock(renderedText);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
async function generateWithOptions(generationOptions) {
|
|
2031
|
+
const stream = await localEngine.generateChat(messages, generationOptions);
|
|
2032
|
+
return consumeLocalStream(stream);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
async function generateWithRawOptions(generationOptions) {
|
|
2036
|
+
const stream = await localEngine.generateChat(messages, generationOptions);
|
|
2037
|
+
let rawRendered = "";
|
|
2038
|
+
for await (const chunk of stream) {
|
|
2039
|
+
if (stopSignal) {
|
|
2040
|
+
if (typeof localEngine.interruptGenerate === "function") {
|
|
2041
|
+
try {
|
|
2042
|
+
localEngine.interruptGenerate();
|
|
2043
|
+
} catch (error) {}
|
|
2044
|
+
}
|
|
2045
|
+
break;
|
|
2046
|
+
}
|
|
2047
|
+
const rawPiece = sanitizeStreamText(readLocalChunkText(chunk));
|
|
2048
|
+
if (!rawPiece) continue;
|
|
2049
|
+
rawRendered = rawRendered
|
|
2050
|
+
? (rawRendered + trimIncomingOverlap(rawRendered, rawPiece))
|
|
2051
|
+
: rawPiece;
|
|
2052
|
+
if (hasRenderableLocalText(rawRendered)) {
|
|
2053
|
+
callback(rawRendered, { replace: true, isStreaming: true });
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return trimTrailingDuplicateSentenceBlock(String(rawRendered || "").trim());
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
async function generateCharacterEmergencyReply() {
|
|
2060
|
+
if (!isCharacterLiteMode) return "";
|
|
2061
|
+
const lastUser = [...messages].reverse().find((msg) => (
|
|
2062
|
+
msg && String(msg.role || "").toLowerCase() === "user" && String(msg.content || "").trim()
|
|
2063
|
+
));
|
|
2064
|
+
if (!lastUser) return "";
|
|
2065
|
+
const emergencyMessages = [
|
|
2066
|
+
{ role: "system", content: "Stay in character. Reply naturally in the user's language in 1 short sentence. No meta." },
|
|
2067
|
+
{ role: "user", content: String(lastUser.content || "").slice(0, 220) }
|
|
2068
|
+
];
|
|
2069
|
+
const stream = await localEngine.generateChat(emergencyMessages, {
|
|
2070
|
+
useCache: false,
|
|
2071
|
+
nPredict: 56,
|
|
2072
|
+
sampling: { temp: 0.2, top_k: 16, top_p: 0.82, penalty_repeat: 1.08 }
|
|
2073
|
+
});
|
|
2074
|
+
let text = "";
|
|
2075
|
+
for await (const chunk of stream) {
|
|
2076
|
+
if (stopSignal) break;
|
|
2077
|
+
const part = sanitizeStreamText(readLocalChunkText(chunk));
|
|
2078
|
+
if (!part) continue;
|
|
2079
|
+
text = text ? (text + trimIncomingOverlap(text, part)) : part;
|
|
2080
|
+
}
|
|
2081
|
+
return trimTrailingDuplicateSentenceBlock(String(text || "").trim());
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
let renderedText = await generateWithOptions({
|
|
2085
|
+
...(isCharacterLiteMode
|
|
2086
|
+
? { nPredict: 24 }
|
|
2087
|
+
: (isSuperlightTier ? { nPredict: 512 } : {})),
|
|
2088
|
+
useCache: !(isLightTierEffective || isCharacterLiteMode),
|
|
2089
|
+
sampling: speedConfig.sampling || (isCharacterLiteMode
|
|
2090
|
+
? { temp: 0.16, top_k: 10, top_p: 0.74, penalty_repeat: 1.18 }
|
|
2091
|
+
: (isSuperlightTier
|
|
2092
|
+
? { temp: 0.1, top_k: 1, top_p: 0.5, penalty_repeat: 1.0 }
|
|
2093
|
+
: (isLightTierEffective
|
|
2094
|
+
? { temp: 0.18, top_k: 12, top_p: 0.78, penalty_repeat: 1.22 }
|
|
2095
|
+
: { temp: 0.45, top_k: 32, top_p: 0.88, penalty_repeat: 1.14 })))
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
if (!hasRenderableLocalText(renderedText || "")) {
|
|
2099
|
+
if (typeof localEngine.clearChatState === "function") {
|
|
2100
|
+
try {
|
|
2101
|
+
await localEngine.clearChatState();
|
|
2102
|
+
} catch (error) {}
|
|
2103
|
+
}
|
|
2104
|
+
renderedText = await generateWithOptions({
|
|
2105
|
+
useCache: false,
|
|
2106
|
+
sampling: {
|
|
2107
|
+
temp: 0.16,
|
|
2108
|
+
top_k: 24,
|
|
2109
|
+
top_p: 0.84,
|
|
2110
|
+
penalty_repeat: 1.46
|
|
2111
|
+
}
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
if (!hasRenderableLocalText(renderedText || "")) {
|
|
2116
|
+
renderedText = await generateWithRawOptions({
|
|
2117
|
+
useCache: false,
|
|
2118
|
+
nPredict: isCharacterLiteMode ? 64 : (isLightTierEffective ? 120 : 220),
|
|
2119
|
+
sampling: {
|
|
2120
|
+
temp: 0.22,
|
|
2121
|
+
top_k: 28,
|
|
2122
|
+
top_p: 0.9,
|
|
2123
|
+
penalty_repeat: 1.1
|
|
2124
|
+
}
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
if (!hasRenderableLocalText(renderedText || "")) {
|
|
2129
|
+
try {
|
|
2130
|
+
const emergency = await generateCharacterEmergencyReply();
|
|
2131
|
+
if (hasRenderableLocalText(emergency || "")) {
|
|
2132
|
+
renderedText = emergency;
|
|
2133
|
+
callback(renderedText, { replace: true, isStreaming: false });
|
|
2134
|
+
}
|
|
2135
|
+
} catch (error) {}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
if (!hasRenderableLocalText(renderedText || "")) {
|
|
2139
|
+
if (isCharacterChat) {
|
|
2140
|
+
callback(getCharacterRecoveryText(outputLocale), { replace: true, isStreaming: false });
|
|
2141
|
+
} else {
|
|
2142
|
+
callback(emptyResponseText, { replace: true, isStreaming: false });
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
};
|
|
2146
|
+
|
|
2147
|
+
window.updateMainSubmitButtonState = function () {
|
|
2148
|
+
const btn = document.getElementById("btn-submit");
|
|
2149
|
+
const icon = document.getElementById("icon-submit");
|
|
2150
|
+
if (!btn || !icon) return;
|
|
2151
|
+
const stopMode = !!(isGenerating || window.__ISAI_MAIN_GENERATING__);
|
|
2152
|
+
btn.style.display = "inline-flex";
|
|
2153
|
+
btn.style.alignItems = "center";
|
|
2154
|
+
btn.style.justifyContent = "center";
|
|
2155
|
+
btn.style.pointerEvents = "auto";
|
|
2156
|
+
btn.classList.add("input-action-btn");
|
|
2157
|
+
btn.classList.remove("stop-mode", "is-generating");
|
|
2158
|
+
btn.setAttribute("aria-label", stopMode ? t("stop") : t("submit"));
|
|
2159
|
+
btn.setAttribute("title", stopMode ? t("stop") : t("submit"));
|
|
2160
|
+
btn.dataset.state = stopMode ? "stop" : "submit";
|
|
2161
|
+
btn.style.backgroundColor = "";
|
|
2162
|
+
btn.style.color = "#ffffff";
|
|
2163
|
+
icon.className = stopMode
|
|
2164
|
+
? "ri-square-fill text-[14px] text-white"
|
|
2165
|
+
: "ri-arrow-up-s-line text-[14px] text-white";
|
|
2166
|
+
};
|
|
2167
|
+
|
|
2168
|
+
window.handleMainSubmitButton = function () {
|
|
2169
|
+
if (isGenerating || window.__ISAI_MAIN_GENERATING__) {
|
|
2170
|
+
if (typeof window.stopGeneration === "function") {
|
|
2171
|
+
window.stopGeneration();
|
|
2172
|
+
}
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
executeAction("right");
|
|
2176
|
+
};
|
|
2177
|
+
|
|
2178
|
+
stopGeneration = window.stopGeneration = function () {
|
|
2179
|
+
const wasGenerating = !!(isGenerating || window.__ISAI_MAIN_GENERATING__);
|
|
2180
|
+
if (!wasGenerating) {
|
|
2181
|
+
window.updateMainSubmitButtonState();
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
if (abortController) {
|
|
2185
|
+
abortController.abort();
|
|
2186
|
+
abortController = null;
|
|
2187
|
+
}
|
|
2188
|
+
if (localEngine && typeof localEngine.interruptGenerate === "function") {
|
|
2189
|
+
try {
|
|
2190
|
+
localEngine.interruptGenerate();
|
|
2191
|
+
} catch (error) {}
|
|
2192
|
+
}
|
|
2193
|
+
stopSignal = true;
|
|
2194
|
+
isGenerating = false;
|
|
2195
|
+
window.__ISAI_MAIN_GENERATING__ = false;
|
|
2196
|
+
if (typeof showLoader === "function") showLoader(false);
|
|
2197
|
+
window.updateMainSubmitButtonState();
|
|
2198
|
+
};
|
|
2199
|
+
|
|
2200
|
+
if (typeof window.setMode === "function" && !window.__ISAI_SETMODE_PATCHED__) {
|
|
2201
|
+
const originalSetMode = window.setMode;
|
|
2202
|
+
window.setMode = function () {
|
|
2203
|
+
const result = originalSetMode.apply(this, arguments);
|
|
2204
|
+
setTimeout(function () {
|
|
2205
|
+
if (typeof window.updateMainSubmitButtonState === "function") {
|
|
2206
|
+
window.updateMainSubmitButtonState();
|
|
2207
|
+
}
|
|
2208
|
+
}, 0);
|
|
2209
|
+
return result;
|
|
2210
|
+
};
|
|
2211
|
+
window.__ISAI_SETMODE_PATCHED__ = true;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
if (typeof window.setVoiceState === "function" && !window.__ISAI_VOICESTATE_PATCHED__) {
|
|
2215
|
+
const originalSetVoiceState = window.setVoiceState;
|
|
2216
|
+
window.setVoiceState = function () {
|
|
2217
|
+
const result = originalSetVoiceState.apply(this, arguments);
|
|
2218
|
+
if (typeof window.updateMainSubmitButtonState === "function") {
|
|
2219
|
+
window.updateMainSubmitButtonState();
|
|
2220
|
+
}
|
|
2221
|
+
return result;
|
|
2222
|
+
};
|
|
2223
|
+
window.__ISAI_VOICESTATE_PATCHED__ = true;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
document.addEventListener("click", function (event) {
|
|
2227
|
+
const wrapper = document.getElementById("local-model-tier-wrapper");
|
|
2228
|
+
if (!wrapper) return;
|
|
2229
|
+
if (wrapper.contains(event.target)) return;
|
|
2230
|
+
closeLocalModelShortcutMenu();
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
2234
|
+
setTimeout(async function () {
|
|
2235
|
+
applyLocalModelProfileToConfig();
|
|
2236
|
+
await syncDownloadedStateFromCache();
|
|
2237
|
+
isLocalActive = false;
|
|
2238
|
+
setLocalActivePreference(false);
|
|
2239
|
+
renderLocalModelTierSelector();
|
|
2240
|
+
syncLocalModelTierVisibility();
|
|
2241
|
+
window.updateMainSubmitButtonState();
|
|
2242
|
+
updateLocalBtnState();
|
|
2243
|
+
updateWebGPUBtnState();
|
|
2244
|
+
}, 0);
|
|
2245
|
+
});
|
|
2246
|
+
})();
|