levix-bot 3.3.0-rc.2 → 4.0.0-beta
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 +11 -7
- package/app.cjs +42 -45
- package/package.json +9 -1
- package/public/dashboard/assets/index-C1b7ijfu.js +391 -0
- package/public/dashboard/assets/index-pYaivHON.css +1 -0
- package/public/dashboard/index.html +19 -0
- package/public/dashboard.css +391 -66
- package/public/dashboard.js +593 -42
- package/scheduler.cjs +9 -14
- package/src/auth/use-database-auth-state.js +2 -4
- package/src/bootstrap/core.js +2 -5
- package/src/bootstrap/panel.js +10 -5
- package/src/cli.js +3 -5
- package/src/commands/autoschedule.cjs +1 -4
- package/src/commands/blacklist.cjs +22 -20
- package/src/commands/block.cjs +2 -4
- package/src/commands/calc.cjs +9 -16
- package/src/commands/digit.cjs +97 -0
- package/src/commands/gemini.cjs +147 -161
- package/src/commands/group/add.cjs +7 -16
- package/src/commands/group/antiSpam.cjs +5 -11
- package/src/commands/group/antilink.cjs +34 -28
- package/src/commands/group/approveall.cjs +3 -11
- package/src/commands/group/clearwarns.cjs +2 -5
- package/src/commands/group/demote.cjs +3 -8
- package/src/commands/group/kick.cjs +12 -15
- package/src/commands/group/media.cjs +17 -24
- package/src/commands/group/members.cjs +1 -2
- package/src/commands/group/promote.cjs +4 -12
- package/src/commands/group/removeall.cjs +2 -5
- package/src/commands/group/setpp.cjs +1 -1
- package/src/commands/group/setrules.cjs +2 -8
- package/src/commands/group/setwarn.cjs +4 -2
- package/src/commands/group/tagadmins.cjs +2 -5
- package/src/commands/group/warn.cjs +26 -24
- package/src/commands/group/warns.cjs +1 -2
- package/src/commands/group/welcome.cjs +13 -17
- package/src/commands/group.cjs +7 -10
- package/src/commands/help.cjs +6 -12
- package/src/commands/lang.cjs +104 -0
- package/src/commands/listschedules.cjs +2 -6
- package/src/commands/loop.cjs +6 -13
- package/src/commands/memory.cjs +19 -32
- package/src/commands/mod.cjs +97 -35
- package/src/commands/perm.cjs +18 -56
- package/src/commands/ping.cjs +1 -5
- package/src/commands/poll.cjs +1 -1
- package/src/commands/rand.cjs +2 -5
- package/src/commands/schedule.cjs +4 -8
- package/src/commands/score.cjs +8 -2
- package/src/commands/setprefix.cjs +2 -2
- package/src/commands/shortlink.cjs +7 -15
- package/src/commands/shutdown.cjs +1 -1
- package/src/commands/sticker.cjs +222 -0
- package/src/commands/stt.cjs +136 -68
- package/src/commands/todo.cjs +9 -15
- package/src/commands/tts.cjs +10 -16
- package/src/commands/unblock.cjs +2 -6
- package/src/commands/weather.cjs +5 -12
- package/src/config/ai-persona.md +5 -3
- package/src/config/available-models.json +1156 -0
- package/src/config/baileys.config.js +2 -1
- package/src/config/brand.esm.js +1 -10
- package/src/config/constants.js +1 -1
- package/src/config/defaults.cjs +66 -66
- package/src/config/lock.cjs +2 -2
- package/src/config/modelCapabilities.cjs +172 -0
- package/src/config/runtime-config.cjs +8 -13
- package/src/config/secrets.cjs +18 -10
- package/src/config/settings.cjs +94 -10
- package/src/config/vault.cjs +107 -0
- package/src/core/connection.js +13 -12
- package/src/core/events.js +41 -23
- package/src/core/proxy.js +7 -7
- package/src/core/session.js +91 -48
- package/src/core/socket.js +12 -15
- package/src/db/db.cjs +14 -6
- package/src/db/store.cjs +221 -87
- package/src/domain/caddy.js +1 -1
- package/src/domain/command.js +40 -19
- package/src/domain/detect.js +1 -1
- package/src/domain/system.js +2 -5
- package/src/handlers/command.handler.js +22 -43
- package/src/handlers/group.handler.js +13 -13
- package/src/handlers/message.handler.js +11 -21
- package/src/index.js +8 -4
- package/src/middleware/antispam.middleware.js +46 -23
- package/src/middleware/blacklist.middleware.js +14 -9
- package/src/middleware/forward-tracking.middleware.js +3 -2
- package/src/middleware/permissions.middleware.js +7 -10
- package/src/panel/login-throttle.cjs +43 -0
- package/src/panel/session-auth.cjs +17 -0
- package/src/routes/dashboard.api.esm.js +394 -72
- package/src/services/aiAgent.cjs +136 -49
- package/src/services/aiProviders.cjs +193 -47
- package/src/services/aiRouter.cjs +137 -0
- package/src/services/aiTools.cjs +305 -125
- package/src/services/llmDiscovery.cjs +481 -0
- package/src/services/modelRegistry.cjs +412 -0
- package/src/utils/datetime.cjs +2 -2
- package/src/utils/lid-helper.esm.js +7 -7
- package/src/utils/logger.cjs +1 -1
- package/src/utils/markdownParser.cjs +159 -0
- package/src/utils/memory.cjs +43 -13
- package/src/utils/messageContent.cjs +40 -0
- package/src/utils/normalizeJid.esm.js +7 -7
- package/src/utils/numerals.cjs +75 -0
- package/src/utils/permissions.esm.js +10 -15
- package/src/utils/providerUrl.cjs +281 -0
- package/src/utils/recurrence.cjs +7 -11
- package/src/utils/sendBotMessage.esm.js +4 -14
- package/src/utils/statusMessage.cjs +5 -13
- package/src/utils/storage.cjs +10 -0
- package/src/utils/storage.esm.js +11 -0
- package/src/utils/textDecode.cjs +4 -9
- package/src/utils/thumbnail.cjs +8 -14
- package/src/utils/typing.esm.js +12 -12
- package/views/dashboard.ejs +134 -106
package/public/dashboard.js
CHANGED
|
@@ -121,6 +121,234 @@
|
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// --- i18n -----------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
const LANG_KEY = "levix_lang";
|
|
127
|
+
let currentLang = "en";
|
|
128
|
+
|
|
129
|
+
const I18N = {
|
|
130
|
+
en: {
|
|
131
|
+
overview: "Overview",
|
|
132
|
+
connection: "Connection",
|
|
133
|
+
commands: "Commands",
|
|
134
|
+
ai: "AI & memory",
|
|
135
|
+
groups: "Groups",
|
|
136
|
+
roles: "Roles",
|
|
137
|
+
data: "Tables",
|
|
138
|
+
data_label: "Data",
|
|
139
|
+
control: "Control",
|
|
140
|
+
settings: "Settings",
|
|
141
|
+
workspace: "Workspace",
|
|
142
|
+
settings_sub: "Configure your bot options and AI models. Changes take effect immediately.",
|
|
143
|
+
settings_tab_ai: "AI Assistant",
|
|
144
|
+
settings_tab_general: "General",
|
|
145
|
+
settings_tab_security: "Security",
|
|
146
|
+
settings_tab_whatsapp: "WhatsApp Proxy",
|
|
147
|
+
settings_tab_media: "Media & Storage",
|
|
148
|
+
pw_heading: "Control panel password",
|
|
149
|
+
pw_current: "Current password",
|
|
150
|
+
pw_next: "New password",
|
|
151
|
+
pw_change_btn: "Change password",
|
|
152
|
+
pw_hint: "This password protects your dashboard. It is stored securely as a hash.",
|
|
153
|
+
ai_wizard_title: "AI Provider Setup",
|
|
154
|
+
ai_wizard_sub: "Choose which provider powers your assistant's chat responses.",
|
|
155
|
+
provider_gemini: "Google Gemini",
|
|
156
|
+
provider_gemini_desc: "Free key, native multimodal vision, and Google Search grounding.",
|
|
157
|
+
provider_groq: "Groq",
|
|
158
|
+
provider_groq_desc: "Ultra-fast Llama 3 & Whisper STT inference.",
|
|
159
|
+
provider_openai: "OpenAI / Compatible",
|
|
160
|
+
provider_openai_desc: "GPT-4o, OpenRouter, Together AI, or any compatible endpoint.",
|
|
161
|
+
provider_ollama: "Ollama (Local)",
|
|
162
|
+
provider_ollama_desc: "Self-hosted models running locally on your hardware.",
|
|
163
|
+
provider_anthropic: "Anthropic Claude",
|
|
164
|
+
provider_anthropic_desc: "Claude 3.5 & Sonnet reasoning models.",
|
|
165
|
+
apply_preset: "Apply Preset",
|
|
166
|
+
active_provider: "Active Provider",
|
|
167
|
+
api_key: "API Key",
|
|
168
|
+
base_url: "Base URL",
|
|
169
|
+
model_name: "Model Name",
|
|
170
|
+
capabilities_title: "Model Capabilities & Media",
|
|
171
|
+
capabilities_sub: "Configure vision inspection and voice speech-to-text.",
|
|
172
|
+
vision_toggle: "Vision & Image Analysis",
|
|
173
|
+
vision_toggle_hint: "Allows the AI to inspect and understand photos sent in messages.",
|
|
174
|
+
stt_title: "Voice Transcription (STT)",
|
|
175
|
+
stt_hint: "Which engine transcribes voice messages when using !stt.",
|
|
176
|
+
agent_mode_toggle: "Autonomous Agent Mode",
|
|
177
|
+
agent_mode_hint: "Enables multi-turn tool calling (web search, memory, bot actions).",
|
|
178
|
+
google_search_toggle: "Google Search Grounding",
|
|
179
|
+
google_search_hint: "Allows Gemini to look up current live web data.",
|
|
180
|
+
advanced_btn: "Advanced AI Settings…",
|
|
181
|
+
ai_advanced_title: "Advanced AI Settings",
|
|
182
|
+
save: "Save",
|
|
183
|
+
saved: "Saved",
|
|
184
|
+
saved_here: "saved here",
|
|
185
|
+
default_val: "default",
|
|
186
|
+
restart_to_apply: "restart to apply",
|
|
187
|
+
not_set: "not set",
|
|
188
|
+
cleared: "Cleared",
|
|
189
|
+
clear: "Clear",
|
|
190
|
+
done: "Done",
|
|
191
|
+
close: "Close",
|
|
192
|
+
supports_vision_badge: "Vision Supported",
|
|
193
|
+
supports_audio_badge: "Audio/STT Ready",
|
|
194
|
+
recommended: "Recommended",
|
|
195
|
+
detected: "Detected",
|
|
196
|
+
connected: "Connected",
|
|
197
|
+
disconnected: "Disconnected",
|
|
198
|
+
idle: "Idle",
|
|
199
|
+
starting: "Starting…",
|
|
200
|
+
linking: "Linking…",
|
|
201
|
+
reconnecting: "Reconnecting…",
|
|
202
|
+
unlink: "Unlink",
|
|
203
|
+
start_bot: "Start",
|
|
204
|
+
stop_bot: "Stop",
|
|
205
|
+
reconnect_bot: "Reconnect to apply",
|
|
206
|
+
},
|
|
207
|
+
ar: {
|
|
208
|
+
overview: "نظرة عامة",
|
|
209
|
+
connection: "الاتصال",
|
|
210
|
+
commands: "الأوامر",
|
|
211
|
+
ai: "الذكاء الاصطناعي والذاكرة",
|
|
212
|
+
groups: "المجموعات",
|
|
213
|
+
roles: "الصلاحيات",
|
|
214
|
+
data: "الجداول والبيانات",
|
|
215
|
+
data_label: "البيانات",
|
|
216
|
+
control: "التحكم",
|
|
217
|
+
settings: "الإعدادات",
|
|
218
|
+
workspace: "مساحة العمل",
|
|
219
|
+
settings_sub: "لوحة التحكم المركزية لضبط إعدادات البوت والذكاء الاصطناعي. تُطبق التغييرات فوراً.",
|
|
220
|
+
settings_tab_ai: "الذكاء الاصطناعي",
|
|
221
|
+
settings_tab_general: "عام",
|
|
222
|
+
settings_tab_security: "الأمان",
|
|
223
|
+
settings_tab_whatsapp: "بروكسي واتساب",
|
|
224
|
+
settings_tab_media: "الوسائط والتخزين",
|
|
225
|
+
pw_heading: "كلمة مرور لوحة التحكم",
|
|
226
|
+
pw_current: "كلمة المرور الحالية",
|
|
227
|
+
pw_next: "كلمة المرور الجديدة",
|
|
228
|
+
pw_change_btn: "تغيير كلمة المرور",
|
|
229
|
+
pw_hint: "هذه الكلمة تحمي حساب واتساب المربوط. تُحفظ بشكل مشفر تماماً.",
|
|
230
|
+
ai_wizard_title: "إعداد مزود الذكاء الاصطناعي",
|
|
231
|
+
ai_wizard_sub: "اختر المزود والموديل الذي يشغّل محادثات المساعد الخاص بك.",
|
|
232
|
+
provider_gemini: "جوجل جيميني",
|
|
233
|
+
provider_gemini_desc: "مفتاح مجاني مع دعم فائق لقراءة الصور والبحث المباشر.",
|
|
234
|
+
provider_groq: "جروك (Groq)",
|
|
235
|
+
provider_groq_desc: "سرعة خارقة في الرد وتفريغ الصوت بموديلات Llama و Whisper.",
|
|
236
|
+
provider_openai: "OpenAI / متوافق",
|
|
237
|
+
provider_openai_desc: "موديلات GPT-4o أو OpenRouter أو Together ومختلف المزودات.",
|
|
238
|
+
provider_ollama: "أولاما (محلي)",
|
|
239
|
+
provider_ollama_desc: "موديلات تعمل محلياً على جهازك دون اتصال خارجي.",
|
|
240
|
+
provider_anthropic: "أنثروبيك كلود",
|
|
241
|
+
provider_anthropic_desc: "موديلات Claude 3.5 & Sonnet لقدرات تحليلية متقدمة.",
|
|
242
|
+
apply_preset: "تطبيق الإعداد الجاهز",
|
|
243
|
+
active_provider: "المزود النشط",
|
|
244
|
+
api_key: "مفتاح API",
|
|
245
|
+
base_url: "رابط الخدمة (Base URL)",
|
|
246
|
+
model_name: "اسم الموديل",
|
|
247
|
+
capabilities_title: "قدرات الموديل ومعالجة الوسائط",
|
|
248
|
+
capabilities_sub: "تحديد ما إذا كان الموديل سيتعامل مع الصور والتسجيلات الصوتية.",
|
|
249
|
+
vision_toggle: "تحليل وقراءة الصور (Vision)",
|
|
250
|
+
vision_toggle_hint: "يسمح للذكاء الاصطناعي برؤية وفهم الصور والمستندات المرسلة في الشات.",
|
|
251
|
+
stt_title: "تفريغ الصوتيات إلى نص (STT)",
|
|
252
|
+
stt_hint: "المحرك المستخدم في تحويل الرسائل الصوتية لنصوص عند طلب !stt.",
|
|
253
|
+
agent_mode_toggle: "وضع الوكيل الذكي (Agent Mode)",
|
|
254
|
+
agent_mode_hint: "تفعيل البحث بالإنترنت، تسجيل الملاحظات في الذاكرة، وأدوات المجموعات.",
|
|
255
|
+
google_search_toggle: "البحث الحي عبر جوجل",
|
|
256
|
+
google_search_hint: "يتيح لموديل جيميني جلب أحدث المعلومات والأخبار الحية من محرك بحث جوجل.",
|
|
257
|
+
advanced_btn: "إعدادات متقدمة للذكاء الاصطناعي…",
|
|
258
|
+
ai_advanced_title: "إعدادات متقدمة للذكاء الاصطناعي",
|
|
259
|
+
save: "حفظ",
|
|
260
|
+
saved: "تم الحفظ بنجاح",
|
|
261
|
+
saved_here: "محفوظ هنا",
|
|
262
|
+
default_val: "افتراضي",
|
|
263
|
+
restart_to_apply: "إعادة التشغيل للتطبيق",
|
|
264
|
+
not_set: "غير محدد",
|
|
265
|
+
cleared: "تم الحذف",
|
|
266
|
+
clear: "مسح",
|
|
267
|
+
done: "تم",
|
|
268
|
+
close: "إغلاق",
|
|
269
|
+
supports_vision_badge: "يدعم تحليل الصور",
|
|
270
|
+
supports_audio_badge: "يدعم تفريغ الصوتيات",
|
|
271
|
+
recommended: "موصى به",
|
|
272
|
+
detected: "تم اكتشافه",
|
|
273
|
+
connected: "متصل",
|
|
274
|
+
disconnected: "غير متصل",
|
|
275
|
+
idle: "خامل",
|
|
276
|
+
starting: "جاري البدء…",
|
|
277
|
+
linking: "جاري الربط…",
|
|
278
|
+
reconnecting: "جاري إعادة الاتصال…",
|
|
279
|
+
unlink: "إلغاء الربط",
|
|
280
|
+
start_bot: "تشغيل",
|
|
281
|
+
stop_bot: "إيقاف",
|
|
282
|
+
reconnect_bot: "إعادة الاتصال لتطبيق التغييرات",
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
function t(key) {
|
|
287
|
+
return I18N[currentLang]?.[key] || I18N.en[key] || key;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function applyLang(lang) {
|
|
291
|
+
currentLang = lang === "ar" ? "ar" : "en";
|
|
292
|
+
document.documentElement.setAttribute("lang", currentLang);
|
|
293
|
+
document.documentElement.setAttribute("dir", currentLang === "ar" ? "rtl" : "ltr");
|
|
294
|
+
if (currentLang === "ar") {
|
|
295
|
+
document.documentElement.classList.add("lang-ar");
|
|
296
|
+
} else {
|
|
297
|
+
document.documentElement.classList.remove("lang-ar");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const langTag = $("#lang-tag");
|
|
301
|
+
if (langTag) {
|
|
302
|
+
langTag.textContent = currentLang === "ar" ? "EN" : "عربي";
|
|
303
|
+
}
|
|
304
|
+
const langBtn = $("#lang-btn");
|
|
305
|
+
if (langBtn) {
|
|
306
|
+
const title = currentLang === "ar" ? "Switch to English" : "التبديل إلى العربية";
|
|
307
|
+
langBtn.setAttribute("title", title);
|
|
308
|
+
langBtn.setAttribute("aria-label", title);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
localStorage.setItem(LANG_KEY, currentLang);
|
|
313
|
+
} catch {}
|
|
314
|
+
|
|
315
|
+
document.querySelectorAll("[data-i18n]").forEach((el) => {
|
|
316
|
+
const key = el.getAttribute("data-i18n");
|
|
317
|
+
const val = t(key);
|
|
318
|
+
if (val) el.textContent = val;
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
document.querySelectorAll("[data-i18n-title]").forEach((el) => {
|
|
322
|
+
const key = el.getAttribute("data-i18n-title");
|
|
323
|
+
const val = t(key);
|
|
324
|
+
if (val) el.setAttribute("title", val);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (currentView && VIEWS[currentView]) {
|
|
328
|
+
$("#page-title").textContent = t(currentView) || VIEWS[currentView].title;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function initLang() {
|
|
333
|
+
let chosen = "en";
|
|
334
|
+
try {
|
|
335
|
+
const saved = localStorage.getItem(LANG_KEY);
|
|
336
|
+
if (saved === "ar" || saved === "en") chosen = saved;
|
|
337
|
+
else {
|
|
338
|
+
const nav = (navigator.languages && navigator.languages[0]) || navigator.language || "";
|
|
339
|
+
if (nav.toLowerCase().startsWith("ar")) chosen = "ar";
|
|
340
|
+
}
|
|
341
|
+
} catch {}
|
|
342
|
+
applyLang(chosen);
|
|
343
|
+
|
|
344
|
+
$("#lang-btn")?.addEventListener("click", () => {
|
|
345
|
+
applyLang(currentLang === "ar" ? "en" : "ar");
|
|
346
|
+
if (currentView && VIEWS[currentView]?.load) {
|
|
347
|
+
VIEWS[currentView].load().catch(() => {});
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
124
352
|
// --- routing ------------------------------------------------------------
|
|
125
353
|
|
|
126
354
|
const VIEWS = {};
|
|
@@ -149,7 +377,7 @@
|
|
|
149
377
|
if (active) link.setAttribute("aria-current", "page");
|
|
150
378
|
else link.removeAttribute("aria-current");
|
|
151
379
|
});
|
|
152
|
-
$("#page-title").textContent = VIEWS[target].title;
|
|
380
|
+
$("#page-title").textContent = I18N[currentLang]?.[target] || VIEWS[target].title;
|
|
153
381
|
setSidebar(false);
|
|
154
382
|
|
|
155
383
|
VIEWS[target].load();
|
|
@@ -233,12 +461,12 @@
|
|
|
233
461
|
setStatus(stats.connection);
|
|
234
462
|
|
|
235
463
|
const cards = [
|
|
236
|
-
["Groups", stats.totalGroups, "Communities", '<svg viewBox="0 0 24 24"
|
|
237
|
-
["Commands", stats.commandCount, "Available", '<svg viewBox="0 0 24 24"><
|
|
238
|
-
["Open debts", stats.totalDebts, "Unsettled", '<svg viewBox="0 0 24 24"
|
|
239
|
-
["Warnings", stats.totalWarnings, "Issued", '<svg viewBox="0 0 24 24"><path d="
|
|
240
|
-
["Notes", stats.totalNotes, "Saved", '<svg viewBox="0 0 24 24"><path d="
|
|
241
|
-
["Todo lists", stats.totalTodos, "Tracked", '<svg viewBox="0 0 24 24"><path d="
|
|
464
|
+
["Groups", stats.totalGroups, "Communities", '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>'],
|
|
465
|
+
["Commands", stats.commandCount, "Available", '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>'],
|
|
466
|
+
["Open debts", stats.totalDebts, "Unsettled", '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" x2="12" y1="2" y2="22"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>'],
|
|
467
|
+
["Warnings", stats.totalWarnings, "Issued", '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" x2="12" y1="9" y2="13"/><line x1="12" x2="12.01" y1="17" y2="17"/></svg>'],
|
|
468
|
+
["Notes", stats.totalNotes, "Saved", '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>'],
|
|
469
|
+
["Todo lists", stats.totalTodos, "Tracked", '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 11 3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>'],
|
|
242
470
|
];
|
|
243
471
|
|
|
244
472
|
$("#stat-cards").innerHTML = cards
|
|
@@ -1209,33 +1437,36 @@
|
|
|
1209
1437
|
|
|
1210
1438
|
// --- settings -----------------------------------------------------------
|
|
1211
1439
|
|
|
1440
|
+
let activeSettingsTab = "ai";
|
|
1441
|
+
|
|
1212
1442
|
function settingField(setting) {
|
|
1443
|
+
if (!setting) return "";
|
|
1213
1444
|
const id = `set-${setting.key}`;
|
|
1214
1445
|
const source =
|
|
1215
1446
|
setting.source === "dashboard"
|
|
1216
|
-
? '<span class="source dashboard">
|
|
1217
|
-
: '<span class="source">
|
|
1447
|
+
? '<span class="source dashboard">' + t("saved_here") + '</span>'
|
|
1448
|
+
: '<span class="source">' + t("default_val") + '</span>';
|
|
1218
1449
|
const restart = setting.restart
|
|
1219
|
-
? '<span class="source">
|
|
1450
|
+
? '<span class="source">' + t("restart_to_apply") + '</span>'
|
|
1220
1451
|
: "";
|
|
1221
1452
|
|
|
1222
1453
|
let control;
|
|
1223
1454
|
if (setting.type === "secret") {
|
|
1224
1455
|
control = `
|
|
1225
1456
|
<div class="inline">
|
|
1226
|
-
<input type="password" id="${id}" placeholder="${setting.configured ? "•••••••• (set)" : "
|
|
1457
|
+
<input type="password" id="${id}" placeholder="${setting.configured ? "•••••••• (set)" : t("not_set")}"
|
|
1227
1458
|
autocomplete="new-password"${setting.configured ? ' data-configured="1"' : ""} />
|
|
1228
|
-
<button class="btn btn-sm" type="button" data-save-setting="${esc(setting.key)}"
|
|
1229
|
-
${setting.configured ? `<button class="btn btn-sm btn-danger" type="button" data-clear-setting="${esc(setting.key)}"
|
|
1459
|
+
<button class="btn btn-sm" type="button" data-save-setting="${esc(setting.key)}">${t("save")}</button>
|
|
1460
|
+
${setting.configured ? `<button class="btn btn-sm btn-danger" type="button" data-clear-setting="${esc(setting.key)}">${t("clear")}</button>` : ""}
|
|
1230
1461
|
</div>`;
|
|
1231
1462
|
} else if (setting.type === "bool") {
|
|
1232
1463
|
control = `
|
|
1233
1464
|
<div class="inline">
|
|
1234
1465
|
<label class="switch">
|
|
1235
|
-
<input type="checkbox" id="${id}" ${setting.value ? "checked" : ""} />
|
|
1466
|
+
<input type="checkbox" id="${id}" ${setting.value ? "checked" : ""} data-auto-save="${esc(setting.key)}" />
|
|
1236
1467
|
<span class="track"></span>
|
|
1237
1468
|
</label>
|
|
1238
|
-
<button class="btn btn-sm" type="button" data-save-setting="${esc(setting.key)}"
|
|
1469
|
+
<button class="btn btn-sm" type="button" data-save-setting="${esc(setting.key)}">${t("save")}</button>
|
|
1239
1470
|
</div>`;
|
|
1240
1471
|
} else if (Array.isArray(setting.choices) && setting.choices.length) {
|
|
1241
1472
|
control = `
|
|
@@ -1248,7 +1479,7 @@
|
|
|
1248
1479
|
)
|
|
1249
1480
|
.join("")}
|
|
1250
1481
|
</select>
|
|
1251
|
-
<button class="btn btn-sm" type="button" data-save-setting="${esc(setting.key)}"
|
|
1482
|
+
<button class="btn btn-sm" type="button" data-save-setting="${esc(setting.key)}">${t("save")}</button>
|
|
1252
1483
|
</div>`;
|
|
1253
1484
|
} else {
|
|
1254
1485
|
control = `
|
|
@@ -1257,12 +1488,12 @@
|
|
|
1257
1488
|
value="${esc(setting.value)}"
|
|
1258
1489
|
${setting.min !== null ? `min="${setting.min}"` : ""}
|
|
1259
1490
|
${setting.max !== null ? `max="${setting.max}"` : ""} />
|
|
1260
|
-
<button class="btn btn-sm" type="button" data-save-setting="${esc(setting.key)}"
|
|
1491
|
+
<button class="btn btn-sm" type="button" data-save-setting="${esc(setting.key)}">${t("save")}</button>
|
|
1261
1492
|
</div>`;
|
|
1262
1493
|
}
|
|
1263
1494
|
|
|
1264
1495
|
return `
|
|
1265
|
-
<div class="setting-item">
|
|
1496
|
+
<div class="setting-item" id="item-${esc(setting.key)}">
|
|
1266
1497
|
<div class="inline setting-label-row">
|
|
1267
1498
|
<label for="${id}" class="setting-label">${esc(setting.label)}</label>
|
|
1268
1499
|
${source}${restart}
|
|
@@ -1272,28 +1503,260 @@
|
|
|
1272
1503
|
</div>`;
|
|
1273
1504
|
}
|
|
1274
1505
|
|
|
1506
|
+
function renderAiSettingsTab(container, byKey) {
|
|
1507
|
+
const currentProvider = byKey.get("ai_provider")?.value || "gemini";
|
|
1508
|
+
const openaiBaseUrl = String(byKey.get("openai_base_url")?.value || "");
|
|
1509
|
+
const activeModel =
|
|
1510
|
+
currentProvider === "gemini"
|
|
1511
|
+
? byKey.get("gemini_model")?.value
|
|
1512
|
+
: currentProvider === "openai"
|
|
1513
|
+
? byKey.get("openai_model")?.value
|
|
1514
|
+
: byKey.get("anthropic_model")?.value;
|
|
1515
|
+
|
|
1516
|
+
const mStr = String(activeModel || "").toLowerCase();
|
|
1517
|
+
const supportsVision =
|
|
1518
|
+
currentProvider === "gemini" ||
|
|
1519
|
+
currentProvider === "anthropic" ||
|
|
1520
|
+
mStr.includes("vision") ||
|
|
1521
|
+
mStr.includes("vl") ||
|
|
1522
|
+
mStr.includes("llava") ||
|
|
1523
|
+
mStr.includes("4o") ||
|
|
1524
|
+
mStr.includes("pixtral") ||
|
|
1525
|
+
mStr.includes("qwen") ||
|
|
1526
|
+
mStr.includes("llama-3.2-11b") ||
|
|
1527
|
+
mStr.includes("llama-3.2-90b");
|
|
1528
|
+
|
|
1529
|
+
let activeCard = currentProvider;
|
|
1530
|
+
if (currentProvider === "openai") {
|
|
1531
|
+
if (openaiBaseUrl.includes("groq.com")) activeCard = "groq";
|
|
1532
|
+
else if (openaiBaseUrl.includes("11434") || openaiBaseUrl.includes("ollama")) activeCard = "ollama";
|
|
1533
|
+
else activeCard = "openai";
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
let providerFieldsHtml = "";
|
|
1537
|
+
if (currentProvider === "gemini") {
|
|
1538
|
+
const keys = ["gemini_api_key", "gemini_model"];
|
|
1539
|
+
providerFieldsHtml = keys.map((k) => settingField(byKey.get(k))).join("");
|
|
1540
|
+
} else if (currentProvider === "openai") {
|
|
1541
|
+
const keys = ["openai_api_key", "openai_base_url", "openai_model"];
|
|
1542
|
+
providerFieldsHtml = `
|
|
1543
|
+
<div class="preset-chips">
|
|
1544
|
+
<span class="hint" style="align-self:center; font-weight:600;">${t("apply_preset")}:</span>
|
|
1545
|
+
<button class="preset-chip" type="button" data-preset="groq">⚡ Groq (Llama 3.3)</button>
|
|
1546
|
+
<button class="preset-chip" type="button" data-preset="openai">🧠 OpenAI (GPT-4o)</button>
|
|
1547
|
+
<button class="preset-chip" type="button" data-preset="ollama">🦙 Ollama (Local)</button>
|
|
1548
|
+
<button class="preset-chip" type="button" data-preset="openrouter">🌐 OpenRouter</button>
|
|
1549
|
+
</div>
|
|
1550
|
+
${keys.map((k) => settingField(byKey.get(k))).join("")}
|
|
1551
|
+
`;
|
|
1552
|
+
} else if (currentProvider === "anthropic") {
|
|
1553
|
+
const keys = ["anthropic_api_key", "anthropic_model"];
|
|
1554
|
+
providerFieldsHtml = keys.map((k) => settingField(byKey.get(k))).join("");
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const capKeys = ["bot_language", "ai_vision_enabled", "ai_stt_provider"];
|
|
1558
|
+
if (
|
|
1559
|
+
byKey.get("ai_stt_provider")?.value === "openai" ||
|
|
1560
|
+
(byKey.get("ai_stt_provider")?.value === "auto" && currentProvider === "openai")
|
|
1561
|
+
) {
|
|
1562
|
+
capKeys.push("openai_stt_model");
|
|
1563
|
+
}
|
|
1564
|
+
capKeys.push("ai_agent");
|
|
1565
|
+
if (currentProvider === "gemini") {
|
|
1566
|
+
capKeys.push("ai_google_search");
|
|
1567
|
+
}
|
|
1568
|
+
const capFieldsHtml = capKeys.map((k) => settingField(byKey.get(k))).join("");
|
|
1569
|
+
|
|
1570
|
+
container.innerHTML = `
|
|
1571
|
+
<!-- Step 1: Provider Selection Cards -->
|
|
1572
|
+
<div class="card">
|
|
1573
|
+
<div class="card-head">
|
|
1574
|
+
<div>
|
|
1575
|
+
<h2>${t("ai_wizard_title")}</h2>
|
|
1576
|
+
<div class="hint">${t("ai_wizard_sub")}</div>
|
|
1577
|
+
</div>
|
|
1578
|
+
</div>
|
|
1579
|
+
|
|
1580
|
+
<div class="provider-grid">
|
|
1581
|
+
<div class="provider-card ${activeCard === "gemini" ? "active" : ""}" data-pick-provider="gemini">
|
|
1582
|
+
<div class="provider-card-head">
|
|
1583
|
+
<div class="provider-title"><span>🌟</span> ${t("provider_gemini")}</div>
|
|
1584
|
+
<span class="provider-active-badge">${t("active_provider")}</span>
|
|
1585
|
+
</div>
|
|
1586
|
+
<div class="provider-desc">${t("provider_gemini_desc")}</div>
|
|
1587
|
+
</div>
|
|
1588
|
+
|
|
1589
|
+
<div class="provider-card ${activeCard === "groq" ? "active" : ""}" data-pick-provider="groq">
|
|
1590
|
+
<div class="provider-card-head">
|
|
1591
|
+
<div class="provider-title"><span>⚡</span> ${t("provider_groq")}</div>
|
|
1592
|
+
<span class="provider-active-badge">${t("active_provider")}</span>
|
|
1593
|
+
</div>
|
|
1594
|
+
<div class="provider-desc">${t("provider_groq_desc")}</div>
|
|
1595
|
+
</div>
|
|
1596
|
+
|
|
1597
|
+
<div class="provider-card ${activeCard === "openai" ? "active" : ""}" data-pick-provider="openai">
|
|
1598
|
+
<div class="provider-card-head">
|
|
1599
|
+
<div class="provider-title"><span>🧠</span> ${t("provider_openai")}</div>
|
|
1600
|
+
<span class="provider-active-badge">${t("active_provider")}</span>
|
|
1601
|
+
</div>
|
|
1602
|
+
<div class="provider-desc">${t("provider_openai_desc")}</div>
|
|
1603
|
+
</div>
|
|
1604
|
+
|
|
1605
|
+
<div class="provider-card ${activeCard === "ollama" ? "active" : ""}" data-pick-provider="ollama">
|
|
1606
|
+
<div class="provider-card-head">
|
|
1607
|
+
<div class="provider-title"><span>🦙</span> ${t("provider_ollama")}</div>
|
|
1608
|
+
<span class="provider-active-badge">${t("active_provider")}</span>
|
|
1609
|
+
</div>
|
|
1610
|
+
<div class="provider-desc">${t("provider_ollama_desc")}</div>
|
|
1611
|
+
</div>
|
|
1612
|
+
|
|
1613
|
+
<div class="provider-card ${activeCard === "anthropic" ? "active" : ""}" data-pick-provider="anthropic">
|
|
1614
|
+
<div class="provider-card-head">
|
|
1615
|
+
<div class="provider-title"><span>🔮</span> ${t("provider_anthropic")}</div>
|
|
1616
|
+
<span class="provider-active-badge">${t("active_provider")}</span>
|
|
1617
|
+
</div>
|
|
1618
|
+
<div class="provider-desc">${t("provider_anthropic_desc")}</div>
|
|
1619
|
+
</div>
|
|
1620
|
+
</div>
|
|
1621
|
+
</div>
|
|
1622
|
+
|
|
1623
|
+
<!-- Step 2: Credentials & Model for Active Provider -->
|
|
1624
|
+
<div class="card">
|
|
1625
|
+
<div class="card-head">
|
|
1626
|
+
<h2>🔑 ${t("active_provider")}: ${esc(currentProvider.toUpperCase())} ${activeCard !== currentProvider ? `(${esc(activeCard.toUpperCase())})` : ""}</h2>
|
|
1627
|
+
</div>
|
|
1628
|
+
${providerFieldsHtml}
|
|
1629
|
+
</div>
|
|
1630
|
+
|
|
1631
|
+
<!-- Step 3: Model Capabilities & Media Toggles (Point 3) -->
|
|
1632
|
+
<div class="card">
|
|
1633
|
+
<div class="card-head">
|
|
1634
|
+
<div>
|
|
1635
|
+
<h2>🎛️ ${t("capabilities_title")}</h2>
|
|
1636
|
+
<div class="hint">${t("capabilities_sub")}</div>
|
|
1637
|
+
</div>
|
|
1638
|
+
</div>
|
|
1639
|
+
${capFieldsHtml}
|
|
1640
|
+
<div style="margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line-soft); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
|
|
1641
|
+
<button class="btn btn-secondary" id="open-ai-advanced" type="button">
|
|
1642
|
+
⚙️ ${t("advanced_btn")}
|
|
1643
|
+
</button>
|
|
1644
|
+
</div>
|
|
1645
|
+
</div>
|
|
1646
|
+
`;
|
|
1647
|
+
|
|
1648
|
+
const advContainer = $("#ai-advanced-fields");
|
|
1649
|
+
if (advContainer) {
|
|
1650
|
+
const advKeys = [
|
|
1651
|
+
"ai_max_tool_steps",
|
|
1652
|
+
"ai_tool_timeout_ms",
|
|
1653
|
+
"ai_max_history",
|
|
1654
|
+
"memory_context_chars",
|
|
1655
|
+
"gemini_stt_model",
|
|
1656
|
+
"gemini_image_model",
|
|
1657
|
+
"google_search_api_key",
|
|
1658
|
+
"google_search_cx",
|
|
1659
|
+
];
|
|
1660
|
+
advContainer.innerHTML = advKeys
|
|
1661
|
+
.map((k) => byKey.get(k))
|
|
1662
|
+
.filter(Boolean)
|
|
1663
|
+
.map(settingField)
|
|
1664
|
+
.join("");
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1275
1668
|
VIEWS.settings = {
|
|
1276
1669
|
title: "Settings",
|
|
1277
1670
|
load: guard(async () => {
|
|
1278
|
-
const { settings } = await api("/settings");
|
|
1671
|
+
const { settings, prefix } = await api("/settings");
|
|
1672
|
+
const byKey = new Map(settings.map((s) => [s.key, s]));
|
|
1279
1673
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1674
|
+
$$(".settings-tab").forEach((b) => {
|
|
1675
|
+
const isSelected = b.dataset.tab === activeSettingsTab;
|
|
1676
|
+
b.classList.toggle("active", isSelected);
|
|
1677
|
+
b.setAttribute("aria-selected", isSelected ? "true" : "false");
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
const secContent = $("#settings-tab-security");
|
|
1681
|
+
if (secContent) {
|
|
1682
|
+
secContent.style.display = activeSettingsTab === "security" ? "block" : "none";
|
|
1286
1683
|
}
|
|
1287
1684
|
|
|
1288
|
-
$("#settings-list")
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
)
|
|
1296
|
-
.
|
|
1685
|
+
const listEl = $("#settings-list");
|
|
1686
|
+
if (!listEl) return;
|
|
1687
|
+
|
|
1688
|
+
if (activeSettingsTab === "ai") {
|
|
1689
|
+
renderAiSettingsTab(listEl, byKey);
|
|
1690
|
+
} else if (activeSettingsTab === "general") {
|
|
1691
|
+
const generalKeys = ["bot_timezone", "bot_min_delay_ms", "bot_max_delay_ms"];
|
|
1692
|
+
const items = generalKeys.map((k) => byKey.get(k)).filter(Boolean);
|
|
1693
|
+
listEl.innerHTML = `
|
|
1694
|
+
<div class="card" style="margin-bottom: 20px;">
|
|
1695
|
+
<div class="card-head"><h2>⌨️ Command Prefix (بادئة الأوامر)</h2></div>
|
|
1696
|
+
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 14px;">
|
|
1697
|
+
<input type="text" id="gen-prefix-input" maxlength="3" class="field" style="max-width: 90px; font-family: monospace; font-size: 1.15rem; text-align: center;" value="${escapeHtml(prefix || "!")}">
|
|
1698
|
+
<button type="button" class="btn btn-primary" id="gen-prefix-save">Save Prefix</button>
|
|
1699
|
+
<span class="hint" style="margin: 0;">e.g. <code>!help</code> or <code>/help</code></span>
|
|
1700
|
+
</div>
|
|
1701
|
+
</div>
|
|
1702
|
+
<div class="card">
|
|
1703
|
+
<div class="card-head"><h2>⚙️ ${t("settings_tab_general")}</h2></div>
|
|
1704
|
+
${items.map(settingField).join("")}
|
|
1705
|
+
</div>`;
|
|
1706
|
+
|
|
1707
|
+
const pInput = $("#gen-prefix-input");
|
|
1708
|
+
const pSave = $("#gen-prefix-save");
|
|
1709
|
+
if (pSave && pInput) {
|
|
1710
|
+
pSave.addEventListener("click", guard(async () => {
|
|
1711
|
+
const val = pInput.value.trim();
|
|
1712
|
+
if (!val || val.length > 3) {
|
|
1713
|
+
toast("Prefix must be 1 to 3 characters", "bad");
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
await api("/settings", { method: "PATCH", body: { key: "prefix", value: val } });
|
|
1717
|
+
toast(`Prefix updated to "${val}"`, "ok");
|
|
1718
|
+
}));
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
} else if (activeSettingsTab === "security") {
|
|
1722
|
+
const serverKeys = ["port", "bind_address", "trust_proxy", "public_domain", "dashboard_origin"];
|
|
1723
|
+
const items = serverKeys.map((k) => byKey.get(k)).filter(Boolean);
|
|
1724
|
+
listEl.innerHTML = `
|
|
1725
|
+
<div class="card">
|
|
1726
|
+
<div class="card-head"><h2>🌐 ${t("settings_tab_security")} & Server</h2></div>
|
|
1727
|
+
${items.map(settingField).join("")}
|
|
1728
|
+
</div>`;
|
|
1729
|
+
} else if (activeSettingsTab === "whatsapp") {
|
|
1730
|
+
const proxyKeys = [
|
|
1731
|
+
"whatsapp_proxy_enabled",
|
|
1732
|
+
"whatsapp_proxy_protocol",
|
|
1733
|
+
"whatsapp_proxy_host",
|
|
1734
|
+
"whatsapp_proxy_port",
|
|
1735
|
+
"whatsapp_proxy_username",
|
|
1736
|
+
"whatsapp_proxy_password",
|
|
1737
|
+
];
|
|
1738
|
+
const items = proxyKeys.map((k) => byKey.get(k)).filter(Boolean);
|
|
1739
|
+
listEl.innerHTML = `
|
|
1740
|
+
<div class="card">
|
|
1741
|
+
<div class="card-head"><h2>🌐 ${t("settings_tab_whatsapp")}</h2></div>
|
|
1742
|
+
${items.map(settingField).join("")}
|
|
1743
|
+
</div>`;
|
|
1744
|
+
} else if (activeSettingsTab === "media") {
|
|
1745
|
+
const mediaKeys = [
|
|
1746
|
+
"thumbnail_remote",
|
|
1747
|
+
"thumbnail_remote_max_mb",
|
|
1748
|
+
"thumbnail_remote_timeout_ms",
|
|
1749
|
+
"ffmpeg_path",
|
|
1750
|
+
"forward_score_ttl_days",
|
|
1751
|
+
"openweathermap_api_key",
|
|
1752
|
+
];
|
|
1753
|
+
const items = mediaKeys.map((k) => byKey.get(k)).filter(Boolean);
|
|
1754
|
+
listEl.innerHTML = `
|
|
1755
|
+
<div class="card">
|
|
1756
|
+
<div class="card-head"><h2>📁 ${t("settings_tab_media")}</h2></div>
|
|
1757
|
+
${items.map(settingField).join("")}
|
|
1758
|
+
</div>`;
|
|
1759
|
+
}
|
|
1297
1760
|
}),
|
|
1298
1761
|
};
|
|
1299
1762
|
|
|
@@ -1310,18 +1773,107 @@
|
|
|
1310
1773
|
})
|
|
1311
1774
|
);
|
|
1312
1775
|
|
|
1313
|
-
|
|
1776
|
+
// Tab switcher
|
|
1777
|
+
$("#settings-nav-tabs")?.addEventListener("click", (e) => {
|
|
1778
|
+
const tabBtn = e.target.closest("[data-tab]");
|
|
1779
|
+
if (!tabBtn) return;
|
|
1780
|
+
activeSettingsTab = tabBtn.dataset.tab;
|
|
1781
|
+
VIEWS.settings.load();
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
// Modal triggers
|
|
1785
|
+
document.addEventListener("click", (e) => {
|
|
1786
|
+
if (e.target.closest("#open-ai-advanced")) {
|
|
1787
|
+
const modal = $("#ai-advanced-modal");
|
|
1788
|
+
if (modal) modal.style.display = "flex";
|
|
1789
|
+
} else if (e.target.closest("#ai-advanced-close") || e.target.closest("#ai-advanced-done")) {
|
|
1790
|
+
const modal = $("#ai-advanced-modal");
|
|
1791
|
+
if (modal) modal.style.display = "none";
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
// Provider Card Picker
|
|
1796
|
+
document.addEventListener("click", (e) => {
|
|
1797
|
+
const card = e.target.closest("[data-pick-provider]");
|
|
1798
|
+
if (!card) return;
|
|
1799
|
+
const pick = card.dataset.pickProvider;
|
|
1800
|
+
guard(async () => {
|
|
1801
|
+
if (pick === "groq") {
|
|
1802
|
+
await api("/settings", { method: "PATCH", body: { key: "ai_provider", value: "openai" } });
|
|
1803
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_base_url", value: "https://api.groq.com/openai/v1" } });
|
|
1804
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_model", value: "llama-3.3-70b-versatile" } });
|
|
1805
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_stt_model", value: "whisper-large-v3" } });
|
|
1806
|
+
await api("/settings", { method: "PATCH", body: { key: "ai_vision_enabled", value: true } });
|
|
1807
|
+
} else if (pick === "ollama") {
|
|
1808
|
+
await api("/settings", { method: "PATCH", body: { key: "ai_provider", value: "openai" } });
|
|
1809
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_base_url", value: "http://127.0.0.1:11434/v1" } });
|
|
1810
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_model", value: "llama3.2" } });
|
|
1811
|
+
} else if (pick === "openai") {
|
|
1812
|
+
await api("/settings", { method: "PATCH", body: { key: "ai_provider", value: "openai" } });
|
|
1813
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_base_url", value: "https://api.openai.com/v1" } });
|
|
1814
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_model", value: "gpt-4o-mini" } });
|
|
1815
|
+
} else if (pick === "gemini") {
|
|
1816
|
+
await api("/settings", { method: "PATCH", body: { key: "ai_provider", value: "gemini" } });
|
|
1817
|
+
} else if (pick === "anthropic") {
|
|
1818
|
+
await api("/settings", { method: "PATCH", body: { key: "ai_provider", value: "anthropic" } });
|
|
1819
|
+
await api("/settings", { method: "PATCH", body: { key: "anthropic_model", value: "claude-sonnet-4-5" } });
|
|
1820
|
+
}
|
|
1821
|
+
toast(t("saved"), "ok");
|
|
1822
|
+
VIEWS.settings.load();
|
|
1823
|
+
})();
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
// Preset chips
|
|
1827
|
+
document.addEventListener("click", (e) => {
|
|
1828
|
+
const chip = e.target.closest("[data-preset]");
|
|
1829
|
+
if (!chip) return;
|
|
1830
|
+
const preset = chip.dataset.preset;
|
|
1831
|
+
guard(async () => {
|
|
1832
|
+
if (preset === "groq") {
|
|
1833
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_base_url", value: "https://api.groq.com/openai/v1" } });
|
|
1834
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_model", value: "llama-3.3-70b-versatile" } });
|
|
1835
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_stt_model", value: "whisper-large-v3" } });
|
|
1836
|
+
await api("/settings", { method: "PATCH", body: { key: "ai_vision_enabled", value: true } });
|
|
1837
|
+
} else if (preset === "openai") {
|
|
1838
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_base_url", value: "https://api.openai.com/v1" } });
|
|
1839
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_model", value: "gpt-4o-mini" } });
|
|
1840
|
+
} else if (preset === "ollama") {
|
|
1841
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_base_url", value: "http://127.0.0.1:11434/v1" } });
|
|
1842
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_model", value: "llama3.2" } });
|
|
1843
|
+
} else if (preset === "openrouter") {
|
|
1844
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_base_url", value: "https://openrouter.ai/api/v1" } });
|
|
1845
|
+
await api("/settings", { method: "PATCH", body: { key: "openai_model", value: "google/gemini-2.5-flash" } });
|
|
1846
|
+
}
|
|
1847
|
+
toast(t("saved"), "ok");
|
|
1848
|
+
VIEWS.settings.load();
|
|
1849
|
+
})();
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
// Auto-save switches on change
|
|
1853
|
+
document.addEventListener("change", (e) => {
|
|
1854
|
+
const autoSaveKey = e.target?.dataset?.autoSave;
|
|
1855
|
+
if (autoSaveKey && e.target.type === "checkbox") {
|
|
1856
|
+
const key = autoSaveKey;
|
|
1857
|
+
const value = e.target.checked;
|
|
1858
|
+
guard(async () => {
|
|
1859
|
+
await api("/settings", { method: "PATCH", body: { key, value } });
|
|
1860
|
+
toast(t("saved"), "ok");
|
|
1861
|
+
VIEWS.settings.load();
|
|
1862
|
+
if (key.startsWith("whatsapp_proxy_")) loadStats().catch(() => {});
|
|
1863
|
+
})();
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1867
|
+
// Button-based save and clear
|
|
1868
|
+
document.addEventListener("click", (event) => {
|
|
1314
1869
|
const save = event.target.closest("[data-save-setting]");
|
|
1315
1870
|
const clear = event.target.closest("[data-clear-setting]");
|
|
1316
1871
|
if (!save && !clear) return;
|
|
1317
1872
|
|
|
1318
1873
|
const key = save ? save.dataset.saveSetting : clear.dataset.clearSetting;
|
|
1319
1874
|
const input = document.getElementById(`set-${key}`);
|
|
1875
|
+
if (!input) return;
|
|
1320
1876
|
|
|
1321
|
-
// An already-set secret with an empty box means "leave it alone", not
|
|
1322
|
-
// "erase it". The box is always empty — the server never sends the value
|
|
1323
|
-
// back — so saving the field as-is would wipe a working password every
|
|
1324
|
-
// time somebody edited the host next to it. Clear is the explicit way.
|
|
1325
1877
|
if (save && input.dataset.configured === "1" && input.value === "") {
|
|
1326
1878
|
toast("Left unchanged. Use Clear to remove it.");
|
|
1327
1879
|
return;
|
|
@@ -1335,10 +1887,8 @@
|
|
|
1335
1887
|
|
|
1336
1888
|
guard(async () => {
|
|
1337
1889
|
await api("/settings", { method: "PATCH", body: { key, value } });
|
|
1338
|
-
toast(clear ? "
|
|
1890
|
+
toast(clear ? t("cleared") : t("saved"), "ok");
|
|
1339
1891
|
VIEWS.settings.load();
|
|
1340
|
-
// A proxy change cannot reach a socket that is already open; the
|
|
1341
|
-
// Connection screen is where that is offered, so refresh its state.
|
|
1342
1892
|
if (key.startsWith("whatsapp_proxy_")) loadStats().catch(() => {});
|
|
1343
1893
|
})();
|
|
1344
1894
|
});
|
|
@@ -1382,6 +1932,7 @@
|
|
|
1382
1932
|
}
|
|
1383
1933
|
|
|
1384
1934
|
initTheme();
|
|
1935
|
+
initLang();
|
|
1385
1936
|
initRouting();
|
|
1386
1937
|
initConnectionActions();
|
|
1387
1938
|
{
|