levix-bot 3.3.0-rc.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +13 -9
  2. package/app.cjs +49 -46
  3. package/package.json +9 -1
  4. package/public/dashboard/assets/index-BBBXWzM0.js +391 -0
  5. package/public/dashboard/assets/index-pYaivHON.css +1 -0
  6. package/public/dashboard/index.html +19 -0
  7. package/public/dashboard.css +391 -66
  8. package/public/dashboard.js +593 -42
  9. package/scheduler.cjs +9 -14
  10. package/src/auth/use-database-auth-state.js +2 -4
  11. package/src/bootstrap/core.js +2 -5
  12. package/src/bootstrap/panel.js +10 -5
  13. package/src/cli.js +3 -5
  14. package/src/commands/autoschedule.cjs +1 -4
  15. package/src/commands/blacklist.cjs +22 -20
  16. package/src/commands/block.cjs +2 -4
  17. package/src/commands/calc.cjs +9 -16
  18. package/src/commands/digit.cjs +97 -0
  19. package/src/commands/gemini.cjs +147 -161
  20. package/src/commands/group/add.cjs +7 -16
  21. package/src/commands/group/antiSpam.cjs +5 -11
  22. package/src/commands/group/antilink.cjs +34 -28
  23. package/src/commands/group/approveall.cjs +3 -11
  24. package/src/commands/group/clearwarns.cjs +2 -5
  25. package/src/commands/group/demote.cjs +3 -8
  26. package/src/commands/group/kick.cjs +12 -15
  27. package/src/commands/group/media.cjs +17 -24
  28. package/src/commands/group/members.cjs +1 -2
  29. package/src/commands/group/promote.cjs +4 -12
  30. package/src/commands/group/removeall.cjs +2 -5
  31. package/src/commands/group/setpp.cjs +1 -1
  32. package/src/commands/group/setrules.cjs +2 -8
  33. package/src/commands/group/setwarn.cjs +4 -2
  34. package/src/commands/group/tagadmins.cjs +2 -5
  35. package/src/commands/group/warn.cjs +26 -24
  36. package/src/commands/group/warns.cjs +1 -2
  37. package/src/commands/group/welcome.cjs +13 -17
  38. package/src/commands/group.cjs +7 -10
  39. package/src/commands/help.cjs +6 -12
  40. package/src/commands/lang.cjs +104 -0
  41. package/src/commands/listschedules.cjs +2 -6
  42. package/src/commands/loop.cjs +6 -13
  43. package/src/commands/memory.cjs +19 -32
  44. package/src/commands/mod.cjs +97 -35
  45. package/src/commands/perm.cjs +18 -56
  46. package/src/commands/ping.cjs +1 -5
  47. package/src/commands/poll.cjs +1 -1
  48. package/src/commands/rand.cjs +2 -5
  49. package/src/commands/schedule.cjs +4 -8
  50. package/src/commands/score.cjs +8 -2
  51. package/src/commands/setprefix.cjs +2 -2
  52. package/src/commands/shortlink.cjs +7 -15
  53. package/src/commands/shutdown.cjs +1 -1
  54. package/src/commands/sticker.cjs +222 -0
  55. package/src/commands/stt.cjs +136 -68
  56. package/src/commands/todo.cjs +9 -15
  57. package/src/commands/tts.cjs +10 -16
  58. package/src/commands/unblock.cjs +2 -6
  59. package/src/commands/weather.cjs +5 -12
  60. package/src/config/ai-persona.md +5 -3
  61. package/src/config/available-models.json +1156 -0
  62. package/src/config/baileys.config.js +2 -1
  63. package/src/config/brand.esm.js +1 -10
  64. package/src/config/constants.js +1 -1
  65. package/src/config/defaults.cjs +66 -66
  66. package/src/config/lock.cjs +2 -2
  67. package/src/config/modelCapabilities.cjs +172 -0
  68. package/src/config/runtime-config.cjs +8 -13
  69. package/src/config/secrets.cjs +18 -10
  70. package/src/config/settings.cjs +94 -10
  71. package/src/config/vault.cjs +107 -0
  72. package/src/core/connection.js +13 -12
  73. package/src/core/events.js +41 -23
  74. package/src/core/proxy.js +7 -7
  75. package/src/core/session.js +91 -48
  76. package/src/core/socket.js +12 -15
  77. package/src/db/db.cjs +14 -6
  78. package/src/db/store.cjs +221 -87
  79. package/src/domain/caddy.js +1 -1
  80. package/src/domain/command.js +40 -19
  81. package/src/domain/detect.js +1 -1
  82. package/src/domain/system.js +2 -5
  83. package/src/handlers/command.handler.js +22 -43
  84. package/src/handlers/group.handler.js +13 -13
  85. package/src/handlers/message.handler.js +11 -21
  86. package/src/index.js +8 -4
  87. package/src/middleware/antispam.middleware.js +46 -23
  88. package/src/middleware/blacklist.middleware.js +14 -9
  89. package/src/middleware/forward-tracking.middleware.js +3 -2
  90. package/src/middleware/permissions.middleware.js +7 -10
  91. package/src/panel/login-throttle.cjs +43 -0
  92. package/src/panel/session-auth.cjs +17 -0
  93. package/src/routes/dashboard.api.esm.js +394 -72
  94. package/src/services/aiAgent.cjs +136 -49
  95. package/src/services/aiProviders.cjs +193 -47
  96. package/src/services/aiRouter.cjs +137 -0
  97. package/src/services/aiTools.cjs +305 -128
  98. package/src/services/llmDiscovery.cjs +481 -0
  99. package/src/services/modelRegistry.cjs +412 -0
  100. package/src/utils/datetime.cjs +2 -2
  101. package/src/utils/lid-helper.esm.js +7 -7
  102. package/src/utils/logger.cjs +1 -1
  103. package/src/utils/markdownParser.cjs +159 -0
  104. package/src/utils/memory.cjs +43 -13
  105. package/src/utils/messageContent.cjs +40 -0
  106. package/src/utils/normalizeJid.esm.js +7 -7
  107. package/src/utils/numerals.cjs +75 -0
  108. package/src/utils/permissions.esm.js +10 -15
  109. package/src/utils/providerUrl.cjs +281 -0
  110. package/src/utils/recurrence.cjs +7 -11
  111. package/src/utils/sendBotMessage.esm.js +4 -14
  112. package/src/utils/statusMessage.cjs +5 -13
  113. package/src/utils/storage.cjs +10 -0
  114. package/src/utils/storage.esm.js +11 -0
  115. package/src/utils/textDecode.cjs +4 -9
  116. package/src/utils/thumbnail.cjs +8 -14
  117. package/src/utils/typing.esm.js +12 -12
  118. package/views/dashboard.ejs +134 -106
@@ -44,7 +44,7 @@ class StatusMessage {
44
44
  this.sock,
45
45
  this.jid,
46
46
  { text },
47
- { replyTo: this.replyTo, quote: this.quote, typing: false, delayMs: 0 }
47
+ { replyTo: this.replyTo, quote: this.quote, typing: false, delayMs: 0 },
48
48
  );
49
49
  this.key = sent?.key || null;
50
50
  this.lastEditAt = Date.now();
@@ -71,11 +71,7 @@ class StatusMessage {
71
71
  // `quoted: null` on purpose: the command handler wraps sock.sendMessage
72
72
  // and auto-quotes anything that doesn't set it, and an edit/delete is a
73
73
  // protocolMessage — there is nowhere to hang a contextInfo.
74
- await this.sock.sendMessage(
75
- this.jid,
76
- { text: next, edit: this.key },
77
- { quoted: null }
78
- );
74
+ await this.sock.sendMessage(this.jid, { text: next, edit: this.key }, { quoted: null });
79
75
  this.text = next;
80
76
  this.lastEditAt = Date.now();
81
77
  return true;
@@ -91,7 +87,7 @@ class StatusMessage {
91
87
  this.sock,
92
88
  this.jid,
93
89
  { text },
94
- { replyTo: this.replyTo, quote: this.quote, typing: false, delayMs: 0 }
90
+ { replyTo: this.replyTo, quote: this.quote, typing: false, delayMs: 0 },
95
91
  );
96
92
  this.key = sent?.key || null;
97
93
  this.text = text;
@@ -115,7 +111,7 @@ class StatusMessage {
115
111
  const name = err?.name || "Error";
116
112
  const message = err?.message || String(err || "غير معروف");
117
113
  return this.finish(
118
- `❌ *${arabicLead}*\n\n*النوع:* \`${name}\`\n*التفاصيل:* ${message}`.slice(0, 1500)
114
+ `❌ *${arabicLead}*\n\n*النوع:* \`${name}\`\n*التفاصيل:* ${message}`.slice(0, 1500),
119
115
  );
120
116
  }
121
117
 
@@ -124,11 +120,7 @@ class StatusMessage {
124
120
  this.closed = true;
125
121
  if (!this.key) return false;
126
122
  try {
127
- await this.sock.sendMessage(
128
- this.jid,
129
- { delete: this.key },
130
- { quoted: null }
131
- );
123
+ await this.sock.sendMessage(this.jid, { delete: this.key }, { quoted: null });
132
124
  this.key = null;
133
125
  return true;
134
126
  } catch (err) {
@@ -17,6 +17,13 @@ module.exports = {
17
17
  getGroupSettings: store.getGroupSettings,
18
18
  saveGroupSettings: store.saveGroupSettings,
19
19
  getAllGroupSettings: store.getAllGroupSettings,
20
+ upsertGroupDirectory: store.upsertGroupDirectory,
21
+ getGroupDirectory: store.getGroupDirectory,
22
+ getAllGroupDirectory: store.getAllGroupDirectory,
23
+ clearWhatsAppDirectory: store.clearWhatsAppDirectory,
24
+ isPairedSession: store.isPairedSession,
25
+ resolveUserPhone: store.resolveUserPhone,
26
+ describePeer: store.describePeer,
20
27
  // Warnings
21
28
  getUserWarnings: store.getUserWarnings,
22
29
  saveUserWarnings: store.saveUserWarnings,
@@ -77,5 +84,8 @@ module.exports = {
77
84
  setScheduleStatus: store.setScheduleStatus,
78
85
  setScheduleDelivery: store.setScheduleDelivery,
79
86
  deleteSchedule: store.deleteSchedule,
87
+ pauseAllSchedules: store.pauseAllSchedules,
80
88
  countSchedules: store.countSchedules,
89
+ // AI conversation history
90
+ deleteAllChatHistories: store.deleteAllChatHistories,
81
91
  };
@@ -16,6 +16,13 @@ export const deleteBotSetting = store.deleteBotSetting;
16
16
  export const getGroupSettings = store.getGroupSettings;
17
17
  export const saveGroupSettings = store.saveGroupSettings;
18
18
  export const getAllGroupSettings = store.getAllGroupSettings;
19
+ export const upsertGroupDirectory = store.upsertGroupDirectory;
20
+ export const getGroupDirectory = store.getGroupDirectory;
21
+ export const getAllGroupDirectory = store.getAllGroupDirectory;
22
+ export const clearWhatsAppDirectory = store.clearWhatsAppDirectory;
23
+ export const isPairedSession = store.isPairedSession;
24
+ export const resolveUserPhone = store.resolveUserPhone;
25
+ export const describePeer = store.describePeer;
19
26
 
20
27
  // ===== Warnings =====
21
28
  export const getUserWarnings = store.getUserWarnings;
@@ -91,4 +98,8 @@ export const saveSchedule = store.saveSchedule;
91
98
  export const setScheduleStatus = store.setScheduleStatus;
92
99
  export const setScheduleDelivery = store.setScheduleDelivery;
93
100
  export const deleteSchedule = store.deleteSchedule;
101
+ export const pauseAllSchedules = store.pauseAllSchedules;
94
102
  export const countSchedules = store.countSchedules;
103
+
104
+ // ===== AI conversation history =====
105
+ export const deleteAllChatHistories = store.deleteAllChatHistories;
@@ -69,12 +69,8 @@ function decodeHtmlEntities(text) {
69
69
  function decodeEscapes(text) {
70
70
  if (!text || typeof text !== "string" || !text.includes("\\")) return text;
71
71
  return text
72
- .replace(/\\u([0-9a-f]{4})/gi, (match, hex) =>
73
- String.fromCharCode(parseInt(hex, 16))
74
- )
75
- .replace(/\\x([0-9a-f]{2})/gi, (match, hex) =>
76
- String.fromCharCode(parseInt(hex, 16))
77
- )
72
+ .replace(/\\u([0-9a-f]{4})/gi, (match, hex) => String.fromCharCode(parseInt(hex, 16)))
73
+ .replace(/\\x([0-9a-f]{2})/gi, (match, hex) => String.fromCharCode(parseInt(hex, 16)))
78
74
  .replace(/\\n/g, "\n")
79
75
  .replace(/\\t/g, "\t")
80
76
  .replace(/\\r/g, "")
@@ -87,8 +83,7 @@ function decodeEscapes(text) {
87
83
  // The tell-tale pair that appears when UTF-8 bytes are decoded as latin1: a
88
84
  // lead byte (0xC2-0xDF) followed by a continuation byte that landed in the
89
85
  // latin1 supplement or the cp1252 punctuation block.
90
- const MOJIBAKE_HINT =
91
- /[Â-ß][€-¿–—‘-„†-•…‰‹›€™ŒœŠšŸŽžƒˆ˜]/;
86
+ const MOJIBAKE_HINT = /[Â-ß][€-¿–—‘-„†-•…‰‹›€™ŒœŠšŸŽžƒˆ˜]/;
92
87
 
93
88
  function textScore(text) {
94
89
  let score = 0;
@@ -179,7 +174,7 @@ function stripHtml(html) {
179
174
  .replace(/<!--[\s\S]*?-->/g, " ")
180
175
  .replace(/<\/(p|div|li|tr|h[1-6]|br)>/gi, "\n")
181
176
  .replace(/<br\s*\/?>/gi, "\n")
182
- .replace(/<[^>]+>/g, " ")
177
+ .replace(/<[^>]+>/g, " "),
183
178
  )
184
179
  .replace(NOISE_SPACE, " ")
185
180
  .replace(/\n{3,}/g, "\n\n")
@@ -105,7 +105,7 @@ function warnMissingFfmpeg(reason) {
105
105
  { ffmpeg: ffmpegBin(), reason },
106
106
  "[thumbnail] ffmpeg isn't runnable — video previews are off and images " +
107
107
  "fall back to raw-JPEG previews. Reinstall `ffmpeg-static`, or point " +
108
- "Settings -> Media -> ffmpeg path at a working binary."
108
+ "Settings -> Media -> ffmpeg path at a working binary.",
109
109
  );
110
110
  }
111
111
 
@@ -216,9 +216,7 @@ function parseProbe(stderr = "") {
216
216
  hasAudio: /Stream #\d+:\d+[^\n]*?: Audio:/.test(stderr),
217
217
  };
218
218
 
219
- const dimensions = stderr.match(
220
- /Stream #\d+:\d+[^\n]*?: Video:[^\n]*?[,\s](\d{2,5})x(\d{2,5})/
221
- );
219
+ const dimensions = stderr.match(/Stream #\d+:\d+[^\n]*?: Video:[^\n]*?[,\s](\d{2,5})x(\d{2,5})/);
222
220
  if (dimensions) {
223
221
  info.width = Number(dimensions[1]) || 0;
224
222
  info.height = Number(dimensions[2]) || 0;
@@ -226,10 +224,7 @@ function parseProbe(stderr = "") {
226
224
 
227
225
  const duration = stderr.match(/Duration:\s*(\d+):(\d{2}):(\d{2}(?:\.\d+)?)/);
228
226
  if (duration) {
229
- const seconds =
230
- Number(duration[1]) * 3600 +
231
- Number(duration[2]) * 60 +
232
- Number(duration[3]);
227
+ const seconds = Number(duration[1]) * 3600 + Number(duration[2]) * 60 + Number(duration[3]);
233
228
  // Round, but never down to 0 — a sub-second clip still has a duration.
234
229
  if (Number.isFinite(seconds) && seconds > 0) {
235
230
  info.seconds = Math.max(1, Math.round(seconds));
@@ -270,7 +265,7 @@ async function generateThumbnail(filePath, kind) {
270
265
 
271
266
  // For video, seek a second in — frame 0 of a lot of clips is a black fade.
272
267
  let result = await runFfmpeg(
273
- kind === "video" ? ["-y", "-ss", "1", ...encodeArgs] : ["-y", ...encodeArgs]
268
+ kind === "video" ? ["-y", "-ss", "1", ...encodeArgs] : ["-y", ...encodeArgs],
274
269
  );
275
270
 
276
271
  if (kind === "video" && (!result.ok || !sizeOf(target))) {
@@ -322,8 +317,7 @@ function jpegMeta(buffer) {
322
317
  if (marker === 0xd9 || marker === 0xda) break; // end of header / start of scan
323
318
  const length = buffer.readUInt16BE(offset + 2);
324
319
  // SOF0..SOF15, minus the DHT/JPG/DAC markers that share the range
325
- const isFrameHeader =
326
- marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker);
320
+ const isFrameHeader = marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker);
327
321
  if (isFrameHeader) {
328
322
  return {
329
323
  format: "jpeg",
@@ -478,7 +472,7 @@ async function resolveSource(media, kind) {
478
472
  } catch (err) {
479
473
  logger.debug(
480
474
  { err: err?.message, url },
481
- "[thumbnail] remote source could not be fetched for a preview"
475
+ "[thumbnail] remote source could not be fetched for a preview",
482
476
  );
483
477
  return null;
484
478
  }
@@ -549,7 +543,7 @@ async function withMediaThumbnail(content) {
549
543
  thumbnail = fs.readFileSync(source.file);
550
544
  logger.debug(
551
545
  { bytes: thumbnail.length },
552
- "[thumbnail] reusing the image itself as its preview"
546
+ "[thumbnail] reusing the image itself as its preview",
553
547
  );
554
548
  }
555
549
  }
@@ -585,7 +579,7 @@ async function withMediaThumbnail(content) {
585
579
  } catch (err) {
586
580
  logger.debug(
587
581
  { err: err?.message, kind, field },
588
- "[thumbnail] could not build a preview — sending without one"
582
+ "[thumbnail] could not build a preview — sending without one",
589
583
  );
590
584
  return content;
591
585
  } finally {
@@ -1,8 +1,8 @@
1
1
  // Typing simulation utility
2
- import { createRequire } from 'module';
2
+ import { createRequire } from "module";
3
3
 
4
4
  const require = createRequire(import.meta.url);
5
- const logger = require('./logger.cjs');
5
+ const logger = require("./logger.cjs");
6
6
 
7
7
  /**
8
8
  * Simulates typing with a random delay between 300-500ms
@@ -15,16 +15,16 @@ const logger = require('./logger.cjs');
15
15
  export async function simulateTyping(sock, jid, minDelay = 300, maxDelay = 500) {
16
16
  try {
17
17
  // Send typing indicator
18
- await sock.sendPresenceUpdate('composing', jid);
18
+ await sock.sendPresenceUpdate("composing", jid);
19
19
 
20
20
  // Random delay between min and max
21
21
  const delay = Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay;
22
- await new Promise(resolve => setTimeout(resolve, delay));
22
+ await new Promise((resolve) => setTimeout(resolve, delay));
23
23
 
24
24
  // Stop typing indicator
25
- await sock.sendPresenceUpdate('paused', jid);
25
+ await sock.sendPresenceUpdate("paused", jid);
26
26
  } catch (error) {
27
- logger.error({ err: error }, '[Typing] Error simulating typing');
27
+ logger.error({ err: error }, "[Typing] Error simulating typing");
28
28
  }
29
29
  }
30
30
 
@@ -45,7 +45,7 @@ export async function sendMessageWithTyping(sock, jid, message, withTyping = tru
45
45
  const sent = await sock.sendMessage(jid, message);
46
46
  return sent;
47
47
  } catch (error) {
48
- logger.error({ err: error }, '[Typing] Error sending message with typing');
48
+ logger.error({ err: error }, "[Typing] Error sending message with typing");
49
49
  throw error;
50
50
  }
51
51
  }
@@ -74,22 +74,22 @@ export function calculateTypingDelay(text, charsPerSecond = 40) {
74
74
  */
75
75
  export async function sendMessageWithRealisticTyping(sock, jid, message) {
76
76
  try {
77
- const text = message.text || message.caption || '';
77
+ const text = message.text || message.caption || "";
78
78
  const delay = calculateTypingDelay(text);
79
79
 
80
80
  // Send typing indicator
81
- await sock.sendPresenceUpdate('composing', jid);
81
+ await sock.sendPresenceUpdate("composing", jid);
82
82
 
83
83
  // Wait for calculated delay
84
- await new Promise(resolve => setTimeout(resolve, delay));
84
+ await new Promise((resolve) => setTimeout(resolve, delay));
85
85
 
86
86
  // Stop typing and send message
87
- await sock.sendPresenceUpdate('paused', jid);
87
+ await sock.sendPresenceUpdate("paused", jid);
88
88
  const sent = await sock.sendMessage(jid, message);
89
89
 
90
90
  return sent;
91
91
  } catch (error) {
92
- logger.error({ err: error }, '[Typing] Error sending message with realistic typing');
92
+ logger.error({ err: error }, "[Typing] Error sending message with realistic typing");
93
93
  throw error;
94
94
  }
95
95
  }
@@ -29,108 +29,80 @@
29
29
 
30
30
  <nav class="nav" aria-label="Primary navigation">
31
31
  <a href="#overview" data-view="overview">
32
- <svg
33
- viewBox="0 0 24 24"
34
- fill="none"
35
- stroke="currentColor"
36
- stroke-width="2"
37
- >
38
- <rect x="3" y="3" width="7" height="7" rx="1" />
39
- <rect x="14" y="3" width="7" height="7" rx="1" />
40
- <rect x="3" y="14" width="7" height="7" rx="1" />
41
- <rect x="14" y="14" width="7" height="7" rx="1" />
32
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
33
+ <rect width="7" height="7" x="3" y="3" rx="1" />
34
+ <rect width="7" height="7" x="14" y="3" rx="1" />
35
+ <rect width="7" height="7" x="14" y="14" rx="1" />
36
+ <rect width="7" height="7" x="3" y="14" rx="1" />
42
37
  </svg>
43
- Overview
38
+ <span data-i18n="overview">Overview</span>
44
39
  </a>
45
40
  <a href="#connection" data-view="connection">
46
- <svg
47
- viewBox="0 0 24 24"
48
- fill="none"
49
- stroke="currentColor"
50
- stroke-width="2"
51
- >
52
- <path d="M12 20a8 8 0 1 0-7-4l-1 4 4-1a8 8 0 0 0 4 1z" />
41
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
42
+ <path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9" />
43
+ <path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5" />
44
+ <circle cx="12" cy="12" r="2" />
45
+ <path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5" />
46
+ <path d="M19.1 4.9C23 8.8 23 15.1 19.1 19" />
53
47
  </svg>
54
- Connection
48
+ <span data-i18n="connection">Connection</span>
55
49
  </a>
56
50
 
57
- <div class="nav-label">Control</div>
51
+ <div class="nav-label" data-i18n="control">Control</div>
58
52
  <a href="#commands" data-view="commands">
59
- <svg
60
- viewBox="0 0 24 24"
61
- fill="none"
62
- stroke="currentColor"
63
- stroke-width="2"
64
- >
53
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
65
54
  <polyline points="4 17 10 11 4 5" />
66
55
  <line x1="12" y1="19" x2="20" y2="19" />
67
56
  </svg>
68
- Commands
57
+ <span data-i18n="commands">Commands</span>
69
58
  </a>
70
59
  <a href="#ai" data-view="ai">
71
- <svg
72
- viewBox="0 0 24 24"
73
- fill="none"
74
- stroke="currentColor"
75
- stroke-width="2"
76
- >
77
- <path d="M12 3v3M12 18v3M3 12h3M18 12h3" />
78
- <circle cx="12" cy="12" r="4" />
60
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
61
+ <path d="M12 8V4H8" />
62
+ <rect width="16" height="12" x="4" y="8" rx="2" />
63
+ <path d="M2 14h2" />
64
+ <path d="M20 14h2" />
65
+ <path d="M15 13v2" />
66
+ <path d="M9 13v2" />
79
67
  </svg>
80
- AI &amp; memory
68
+ <span data-i18n="ai">AI &amp; memory</span>
81
69
  </a>
82
70
  <a href="#groups" data-view="groups">
83
- <svg
84
- viewBox="0 0 24 24"
85
- fill="none"
86
- stroke="currentColor"
87
- stroke-width="2"
88
- >
89
- <circle cx="9" cy="8" r="3" />
90
- <circle cx="17" cy="9" r="2.5" />
91
- <path d="M3 19a6 6 0 0 1 12 0M15 19a5 5 0 0 1 6-4" />
71
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
72
+ <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
73
+ <circle cx="9" cy="7" r="4" />
74
+ <path d="M22 21v-2a4 4 0 0 0-3-3.87" />
75
+ <path d="M16 3.13a4 4 0 0 1 0 7.75" />
92
76
  </svg>
93
- Groups
77
+ <span data-i18n="groups">Groups</span>
94
78
  </a>
95
79
  <a href="#roles" data-view="roles">
96
- <svg
97
- viewBox="0 0 24 24"
98
- fill="none"
99
- stroke="currentColor"
100
- stroke-width="2"
101
- >
102
- <path d="M12 3l7 4v5c0 4-3 7-7 9-4-2-7-5-7-9V7z" />
80
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
81
+ <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
103
82
  </svg>
104
- Roles
83
+ <span data-i18n="roles">Roles</span>
105
84
  </a>
106
85
 
107
- <div class="nav-label">Data</div>
86
+ <div class="nav-label" data-i18n="data_label">Data</div>
108
87
  <a href="#data" data-view="data">
109
- <svg
110
- viewBox="0 0 24 24"
111
- fill="none"
112
- stroke="currentColor"
113
- stroke-width="2"
114
- >
115
- <ellipse cx="12" cy="6" rx="8" ry="3" />
116
- <path d="M4 6v12c0 1.7 3.6 3 8 3s8-1.3 8-3V6" />
117
- <path d="M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3" />
88
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
89
+ <path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
118
90
  </svg>
119
- Tables
91
+ <span data-i18n="data">Tables</span>
120
92
  </a>
121
93
  <a href="#settings" data-view="settings">
122
- <svg
123
- viewBox="0 0 24 24"
124
- fill="none"
125
- stroke="currentColor"
126
- stroke-width="2"
127
- >
128
- <circle cx="12" cy="12" r="3" />
129
- <path
130
- d="M19 12a7 7 0 0 0-.1-1l2-1.5-2-3.4-2.3 1a7 7 0 0 0-1.7-1L14.5 3h-4l-.4 2.6a7 7 0 0 0-1.7 1l-2.3-1-2 3.4L6 11a7 7 0 0 0 0 2l-2 1.5 2 3.4 2.3-1a7 7 0 0 0 1.7 1l.4 2.6h4l.4-2.6a7 7 0 0 0 1.7-1l2.3 1 2-3.4-2-1.5c.1-.3.1-.7.1-1z"
131
- />
94
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
95
+ <line x1="4" x2="4" y1="21" y2="14" />
96
+ <line x1="4" x2="4" y1="10" y2="3" />
97
+ <line x1="12" x2="12" y1="21" y2="12" />
98
+ <line x1="12" x2="12" y1="8" y2="3" />
99
+ <line x1="20" x2="20" y1="21" y2="16" />
100
+ <line x1="20" x2="20" y1="12" y2="3" />
101
+ <line x1="1" x2="7" y1="14" y2="14" />
102
+ <line x1="9" x2="15" y1="8" y2="8" />
103
+ <line x1="17" x2="23" y1="16" y2="16" />
132
104
  </svg>
133
- Settings
105
+ <span data-i18n="settings">Settings</span>
134
106
  </a>
135
107
  </nav>
136
108
 
@@ -218,6 +190,20 @@
218
190
  />
219
191
  </svg>
220
192
  </button>
193
+ <button
194
+ class="btn btn-icon lang-btn"
195
+ id="lang-btn"
196
+ type="button"
197
+ aria-label="Switch language"
198
+ title="Switch language / تغيير اللغة"
199
+ >
200
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
201
+ <circle cx="12" cy="12" r="10" />
202
+ <line x1="2" y1="12" x2="22" y2="12" />
203
+ <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
204
+ </svg>
205
+ <span class="lang-tag" id="lang-tag">AR</span>
206
+ </button>
221
207
  </div>
222
208
  <form action="/logout" method="post" class="logout-form">
223
209
  <button type="submit" class="btn btn-sm logout-btn">
@@ -693,45 +679,87 @@
693
679
  <!-- Settings ---------------------------------------------------->
694
680
  <section class="view" id="view-settings">
695
681
  <div class="section-head">
696
- <span class="eyebrow">Workspace</span>
697
- <h2>Settings</h2>
698
- <p>
682
+ <span class="eyebrow" data-i18n="workspace">Workspace</span>
683
+ <h2 data-i18n="settings">Settings</h2>
684
+ <p data-i18n="settings_sub">
699
685
  This page is the only place the bot is configured — there is no
700
686
  config file. Clear a field to go back to its default.
701
687
  </p>
702
688
  </div>
703
689
 
704
- <div class="card security-card">
705
- <div class="card-head"><h2>Control panel password</h2></div>
706
- <div class="password-grid">
707
- <label class="field">
708
- <span>Current password</span>
709
- <input
710
- type="password"
711
- id="pw-current"
712
- autocomplete="current-password"
713
- />
714
- </label>
715
- <label class="field">
716
- <span>New password</span>
717
- <input
718
- type="password"
719
- id="pw-next"
720
- autocomplete="new-password"
721
- />
722
- </label>
723
- <button class="btn btn-primary" id="pw-save" type="button">
724
- Change password
725
- </button>
726
- </div>
727
- <div class="hint">
728
- This password is the only thing between the internet and a
729
- linked WhatsApp account. It is stored as a hash — nobody,
730
- including this page, can read it back.
690
+ <!-- Settings Category Navigation -->
691
+ <div class="settings-nav-tabs" role="tablist" id="settings-nav-tabs">
692
+ <button class="settings-tab active" data-tab="ai" type="button" role="tab" aria-selected="true">
693
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>
694
+ <span data-i18n="settings_tab_ai">AI Assistant</span>
695
+ </button>
696
+ <button class="settings-tab" data-tab="general" type="button" role="tab" aria-selected="false">
697
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="4" x2="4" y1="21" y2="14"/><line x1="4" x2="4" y1="10" y2="3"/><line x1="12" x2="12" y1="21" y2="12"/><line x1="12" x2="12" y1="8" y2="3"/><line x1="20" x2="20" y1="21" y2="16"/><line x1="20" x2="20" y1="12" y2="3"/><line x1="1" x2="7" y1="14" y2="14"/><line x1="9" x2="15" y1="8" y2="8"/><line x1="17" x2="23" y1="16" y2="16"/></svg>
698
+ <span data-i18n="settings_tab_general">General</span>
699
+ </button>
700
+ <button class="settings-tab" data-tab="security" type="button" role="tab" aria-selected="false">
701
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/></svg>
702
+ <span data-i18n="settings_tab_security">Security</span>
703
+ </button>
704
+ <button class="settings-tab" data-tab="whatsapp" type="button" role="tab" aria-selected="false">
705
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
706
+ <span data-i18n="settings_tab_whatsapp">WhatsApp Proxy</span>
707
+ </button>
708
+ <button class="settings-tab" data-tab="media" type="button" role="tab" aria-selected="false">
709
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="22" x2="2" y1="12" y2="12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><line x1="6" x2="6.01" y1="16" y2="16"/><line x1="10" x2="10.01" y1="16" y2="16"/></svg>
710
+ <span data-i18n="settings_tab_media">Media & Storage</span>
711
+ </button>
712
+ </div>
713
+
714
+ <div id="settings-tab-security" class="settings-tab-content" style="display: none;">
715
+ <div class="card security-card">
716
+ <div class="card-head"><h2 data-i18n="pw_heading">Control panel password</h2></div>
717
+ <div class="password-grid">
718
+ <label class="field">
719
+ <span data-i18n="pw_current">Current password</span>
720
+ <input
721
+ type="password"
722
+ id="pw-current"
723
+ autocomplete="current-password"
724
+ />
725
+ </label>
726
+ <label class="field">
727
+ <span data-i18n="pw_next">New password</span>
728
+ <input
729
+ type="password"
730
+ id="pw-next"
731
+ autocomplete="new-password"
732
+ />
733
+ </label>
734
+ <button class="btn btn-primary" id="pw-save" type="button" data-i18n="pw_change_btn">
735
+ Change password
736
+ </button>
737
+ </div>
738
+ <div class="hint" data-i18n="pw_hint">
739
+ This password is the only thing between the internet and a
740
+ linked WhatsApp account. It is stored as a hash — nobody,
741
+ including this page, can read it back.
742
+ </div>
731
743
  </div>
732
744
  </div>
733
745
 
734
746
  <div id="settings-list"><div class="loading">Loading…</div></div>
747
+
748
+ <!-- Advanced AI Settings Modal -->
749
+ <div class="modal-backdrop" id="ai-advanced-modal" style="display: none;">
750
+ <div class="modal-card">
751
+ <div class="modal-head">
752
+ <h2 data-i18n="ai_advanced_title">Advanced AI Settings</h2>
753
+ <button class="btn btn-icon modal-close" id="ai-advanced-close" type="button" aria-label="Close">
754
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
755
+ </button>
756
+ </div>
757
+ <div class="modal-body" id="ai-advanced-fields"></div>
758
+ <div class="modal-foot">
759
+ <button class="btn btn-primary" id="ai-advanced-done" type="button" data-i18n="done">Done</button>
760
+ </div>
761
+ </div>
762
+ </div>
735
763
  </section>
736
764
  </main>
737
765
  </div>