@pheem49/mint 1.9.0 → 1.10.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 (90) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +285 -140
  4. package/bin/mint +0 -0
  5. package/crates/mint-cli/Cargo.toml +1 -0
  6. package/crates/mint-cli/src/actions.rs +233 -0
  7. package/crates/mint-cli/src/agent.rs +287 -43
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/interactive.rs +2888 -0
  10. package/crates/mint-cli/src/main.rs +336 -2854
  11. package/crates/mint-cli/src/markdown.rs +294 -0
  12. package/crates/mint-cli/src/onboard.rs +44 -1
  13. package/crates/mint-cli/src/updater.rs +48 -6
  14. package/crates/mint-core/Cargo.toml +1 -0
  15. package/crates/mint-core/src/agent_loop.rs +1 -0
  16. package/crates/mint-core/src/api_server.rs +376 -13
  17. package/crates/mint-core/src/browser.rs +629 -101
  18. package/crates/mint-core/src/channels.rs +105 -6
  19. package/crates/mint-core/src/chat.rs +52 -4
  20. package/crates/mint-core/src/code_tools.rs +29 -11
  21. package/crates/mint-core/src/config.rs +118 -0
  22. package/crates/mint-core/src/image_gen.rs +294 -33
  23. package/crates/mint-core/src/lib.rs +5 -2
  24. package/crates/mint-core/src/mcp.rs +119 -6
  25. package/crates/mint-core/src/orchestration.rs +133 -11
  26. package/crates/mint-core/src/pictures.rs +64 -1
  27. package/crates/mint-core/src/skills.rs +82 -6
  28. package/crates/mint-core/src/video_gen.rs +235 -0
  29. package/package.json +8 -2
  30. package/shared-modules/luajit/luajit.json +2 -2
  31. package/skills-lock.json +41 -0
  32. package/src/renderer/index-web.html +1 -1
  33. package/src/renderer/index.html +1 -1
  34. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  35. package/src/renderer/shared/platform.ts +3 -0
  36. package/src/renderer/shared/types.ts +4 -0
  37. package/src/renderer/shared/utils/approval.tsx +86 -7
  38. package/src/renderer/shared/utils/markdown.tsx +217 -88
  39. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  40. package/src/renderer/src/components/ChatPanel.tsx +461 -64
  41. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  42. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  43. package/src/renderer/src/components/MintDashboard.tsx +150 -5
  44. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  45. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  46. package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
  47. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  48. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  49. package/src/renderer/src/components/SettingsWindow.tsx +38 -2
  50. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  51. package/src/renderer/src/css/image-studio.css +106 -0
  52. package/src/renderer/src/css/settings.css +158 -0
  53. package/src/renderer/src/css/styles.css +352 -1
  54. package/src/renderer/src/css/veo-studio.css +626 -0
  55. package/src/renderer/src/index.css +1 -0
  56. package/src/renderer/src/tauri.ts +144 -17
  57. package/src/renderer/src-web/components/ChatPanel.tsx +360 -88
  58. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  59. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  60. package/src/renderer/src-web/components/MintDashboard.tsx +137 -4
  61. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  62. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  63. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
  64. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  65. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  66. package/src/renderer/src-web/components/SettingsWindow.tsx +35 -2
  67. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  68. package/src/renderer/src-web/css/image-studio.css +106 -0
  69. package/src/renderer/src-web/css/settings.css +158 -0
  70. package/src/renderer/src-web/css/styles.css +353 -1
  71. package/src/renderer/src-web/css/veo-studio.css +650 -0
  72. package/src/renderer/src-web/index.css +1 -0
  73. package/src/renderer/src-web/tauri.ts +135 -16
  74. package/src-tauri/src/lib.rs +114 -8
  75. package/src-tauri/src/webhooks.rs +45 -31
  76. package/src-tauri/src/workflows.rs +1 -0
  77. package/src-tauri/tauri.conf.json +1 -1
  78. package/temp_gridgeist/AGENTS.md +8 -0
  79. package/temp_gridgeist/CHANGELOG.md +130 -0
  80. package/temp_gridgeist/LICENSE +21 -0
  81. package/temp_gridgeist/README.md +323 -0
  82. package/temp_gridgeist/README.th.md +325 -0
  83. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  84. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  85. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  86. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  87. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  88. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  89. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  90. package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
@@ -1,7 +1,22 @@
1
1
  import { useEffect, useMemo, useState } from 'react'
2
- import { type PictureEntry, convertFileSrc } from '../tauri'
2
+ import { type PictureEntry, convertFileSrc, isTauriRuntime, getLocalApiBase } from '../tauri'
3
3
  import type { DashboardView } from './DashboardSidebar'
4
4
 
5
+ const getPictureSrc = (picture: PictureEntry, useThumbnail = false) => {
6
+ if (isTauriRuntime()) {
7
+ if (useThumbnail && (picture.thumbnailPath || picture.thumbnailUrl)) {
8
+ return convertFileSrc(picture.thumbnailPath || picture.thumbnailUrl || '');
9
+ }
10
+ return convertFileSrc(picture.path || picture.url || '');
11
+ } else {
12
+ const apiBase = getLocalApiBase();
13
+ if (useThumbnail && (picture.thumbnailPath || picture.thumbnailUrl)) {
14
+ return `${apiBase}/thumbnails/${picture.id}.thumb.png`;
15
+ }
16
+ return `${apiBase}/pictures/${encodeURIComponent(picture.filename)}`;
17
+ }
18
+ }
19
+
5
20
  interface PicturesLibraryProps {
6
21
  view: DashboardView
7
22
  pictures: PictureEntry[]
@@ -10,15 +25,39 @@ interface PicturesLibraryProps {
10
25
  }
11
26
 
12
27
  export default function PicturesLibrary({ view, pictures, onSetView, onRefreshPictures }: PicturesLibraryProps) {
28
+ const [filterType, setFilterType] = useState<'photo' | 'video'>('photo')
13
29
  const [visibleCount, setVisibleCount] = useState(24)
30
+ const [failedImages, setFailedImages] = useState<Set<string>>(new Set())
31
+
32
+ const filteredPictures = useMemo(() => {
33
+ return pictures.filter((picture) => {
34
+ const pathLower = (picture.path || '').toLowerCase()
35
+ const urlLower = (picture.url || '').toLowerCase()
36
+ const mimeLower = (picture.mimeType || '').toLowerCase()
37
+
38
+ const isVideo =
39
+ mimeLower.startsWith('video/') ||
40
+ pathLower.endsWith('.mp4') ||
41
+ pathLower.endsWith('.webm') ||
42
+ pathLower.endsWith('.mov') ||
43
+ pathLower.endsWith('.avi') ||
44
+ pathLower.endsWith('.mkv') ||
45
+ urlLower.endsWith('.mp4') ||
46
+ urlLower.endsWith('.webm') ||
47
+ urlLower.endsWith('.mov')
48
+
49
+ return filterType === 'video' ? isVideo : !isVideo
50
+ })
51
+ }, [pictures, filterType])
52
+
14
53
  const visiblePictures = useMemo(
15
- () => pictures.slice(0, visibleCount),
16
- [pictures, visibleCount],
54
+ () => filteredPictures.slice(0, visibleCount),
55
+ [filteredPictures, visibleCount],
17
56
  )
18
57
 
19
58
  useEffect(() => {
20
59
  setVisibleCount(24)
21
- }, [view, pictures])
60
+ }, [view, pictures, filterType])
22
61
 
23
62
  return (
24
63
  <section className={`pictures-library ${view === 'pictures' ? 'is-visible' : ''}`} aria-hidden={view !== 'pictures'}>
@@ -34,34 +73,135 @@ export default function PicturesLibrary({ view, pictures, onSetView, onRefreshPi
34
73
  </button>
35
74
  </div>
36
75
  </header>
37
- {pictures.length === 0 ? (
76
+
77
+ {/* Tabs Filter */}
78
+ <div className="pictures-type-filter">
79
+ <button
80
+ type="button"
81
+ className={`filter-tab-btn ${filterType === 'photo' ? 'active' : ''}`}
82
+ onClick={() => setFilterType('photo')}
83
+ >
84
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
85
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
86
+ <circle cx="8.5" cy="8.5" r="1.5"></circle>
87
+ <polyline points="21 15 16 10 5 21"></polyline>
88
+ </svg>
89
+ Photos
90
+ </button>
91
+ <button
92
+ type="button"
93
+ className={`filter-tab-btn ${filterType === 'video' ? 'active' : ''}`}
94
+ onClick={() => setFilterType('video')}
95
+ >
96
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
97
+ <polygon points="23 7 16 12 23 17 23 7"></polygon>
98
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
99
+ </svg>
100
+ Videos
101
+ </button>
102
+ </div>
103
+
104
+ {filteredPictures.length === 0 ? (
38
105
  <div className="pictures-empty">
39
106
  <div className="pictures-empty-icon" style={{ display: 'flex', justifyContent: 'center', marginBottom: '12px', opacity: 0.3 }}>
40
107
  <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
41
108
  <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline>
42
109
  </svg>
43
110
  </div>
44
- <p>No saved pictures yet</p>
45
- <span>Images appear here after a message with an attachment is sent successfully.</span>
111
+ <p>No saved {filterType === 'video' ? 'videos' : 'pictures'} yet</p>
112
+ <span>Items appear here after a message with an attachment is sent successfully.</span>
46
113
  </div>
47
114
  ) : (
48
115
  <>
49
116
  <div className="pictures-grid">
50
- {visiblePictures.map((picture, index) => (
51
- <article className="picture-card" key={picture.id}>
52
- <img src={convertFileSrc(picture.thumbnailPath || picture.thumbnailUrl || picture.path)} alt={picture.message || picture.filename} loading={index < 6 ? 'eager' : 'lazy'} decoding="async" />
53
- <div className="picture-card-meta"><span>{picture.message || picture.filename}</span></div>
54
- </article>
55
- ))}
117
+ {visiblePictures.map((picture, index) => {
118
+ const isVideo = filterType === 'video'
119
+ return (
120
+ <article className="picture-card" key={picture.id}>
121
+ {isVideo ? (
122
+ <div style={{ position: 'relative', width: '100%', aspectRatio: '4 / 3', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: '#1a1a1a' }}>
123
+ {failedImages.has(picture.id) ? (
124
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px', color: '#666' }}>
125
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ opacity: 0.5 }}>
126
+ <path d="M23 7l-7 5 7 5V7z"></path>
127
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
128
+ </svg>
129
+ <span style={{ fontSize: '11px', fontWeight: 500, letterSpacing: '0.05em', textTransform: 'uppercase', opacity: 0.8 }}>Video</span>
130
+ </div>
131
+ ) : (
132
+ <>
133
+ {picture.thumbnailPath || picture.thumbnailUrl ? (
134
+ <img
135
+ src={getPictureSrc(picture, true)}
136
+ alt={picture.message || picture.filename}
137
+ loading={index < 6 ? 'eager' : 'lazy'}
138
+ decoding="async"
139
+ style={{ width: '100%', height: '100%', objectFit: 'cover' }}
140
+ onError={() => setFailedImages((prev) => new Set([...prev, picture.id]))}
141
+ />
142
+ ) : (
143
+ <video
144
+ src={getPictureSrc(picture, false)}
145
+ preload="metadata"
146
+ style={{ width: '100%', height: '100%', objectFit: 'cover', pointerEvents: 'none' }}
147
+ onError={() => setFailedImages((prev) => new Set([...prev, picture.id]))}
148
+ />
149
+ )}
150
+ <div style={{
151
+ position: 'absolute',
152
+ top: '8px',
153
+ left: '8px',
154
+ display: 'flex',
155
+ alignItems: 'center',
156
+ gap: '4px',
157
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
158
+ color: '#fff',
159
+ padding: '4px 8px',
160
+ borderRadius: '4px',
161
+ fontSize: '11px',
162
+ fontWeight: 600,
163
+ pointerEvents: 'none',
164
+ backdropFilter: 'blur(4px)',
165
+ border: '1px solid rgba(255, 255, 255, 0.1)'
166
+ }}>
167
+ <span>📹 Video</span>
168
+ </div>
169
+ </>
170
+ )}
171
+ </div>
172
+ ) : (
173
+ <div style={{ position: 'relative', width: '100%', aspectRatio: '4 / 3', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: '#1a1a1a' }}>
174
+ {failedImages.has(picture.id) ? (
175
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ opacity: 0.4 }}>
176
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
177
+ <circle cx="8.5" cy="8.5" r="1.5"></circle>
178
+ <polyline points="21 15 16 10 5 21"></polyline>
179
+ </svg>
180
+ ) : (
181
+ <img
182
+ src={getPictureSrc(picture, true)}
183
+ alt={picture.message || picture.filename}
184
+ loading={index < 6 ? 'eager' : 'lazy'}
185
+ decoding="async"
186
+ style={{ width: '100%', height: '100%', objectFit: 'cover' }}
187
+ onError={() => setFailedImages((prev) => new Set([...prev, picture.id]))}
188
+ />
189
+ )}
190
+ </div>
191
+ )}
192
+ <div className="picture-card-meta"><span>{picture.message || picture.filename}</span></div>
193
+ </article>
194
+ )
195
+ })}
56
196
  </div>
57
- {visibleCount < pictures.length && (
197
+ {visibleCount < filteredPictures.length && (
58
198
  <div className="pictures-load-more-container">
59
199
  <button
60
200
  type="button"
61
201
  className="pictures-load-more-btn"
62
202
  onClick={() => setVisibleCount((prev) => prev + 24)}
63
203
  >
64
- Load More ({pictures.length - visibleCount} remaining)
204
+ Load More ({filteredPictures.length - visibleCount} remaining)
65
205
  </button>
66
206
  </div>
67
207
  )}
@@ -70,3 +210,4 @@ export default function PicturesLibrary({ view, pictures, onSetView, onRefreshPi
70
210
  </section>
71
211
  )
72
212
  }
213
+
@@ -119,25 +119,323 @@ export default function AutomationTab({
119
119
  <p className="section-kicker">Messaging</p>
120
120
  <h2 className="section-title">Native Channel Bridges</h2>
121
121
  </div>
122
- <p className="section-description">Configure credentials used directly by the Rust channel workers.</p>
122
+ <p className="section-description">
123
+ Configure messaging credentials and webhooks used by Rust channel workers to enable remote control.
124
+ </p>
123
125
  </div>
124
- <div className="form-grid">
125
- <label className="toggle-row"><span>Telegram Bot API</span><input type="checkbox" checked={config.enableTelegramBridge} onChange={(e) => updateField('enableTelegramBridge', e.target.checked)} /></label>
126
- <input type="password" placeholder="Telegram bot token" value={config.telegramBotToken} onChange={(e) => updateField('telegramBotToken', e.target.value)} />
127
- <label className="toggle-row"><span>Discord Gateway</span><input type="checkbox" checked={config.enableDiscordBridge} onChange={(e) => updateField('enableDiscordBridge', e.target.checked)} /></label>
128
- <input type="password" placeholder="Discord bot token" value={config.discordBotToken} onChange={(e) => updateField('discordBotToken', e.target.value)} />
129
- <input type="text" placeholder="Discord application ID for Rich Presence" value={config.discordApplicationId} onChange={(e) => updateField('discordApplicationId', e.target.value)} />
130
- <label className="toggle-row"><span>Slack Socket Mode</span><input type="checkbox" checked={config.enableSlackBridge} onChange={(e) => updateField('enableSlackBridge', e.target.checked)} /></label>
131
- <input type="password" placeholder="Slack bot token (xoxb-...)" value={config.slackBotToken} onChange={(e) => updateField('slackBotToken', e.target.value)} />
132
- <input type="password" placeholder="Slack app token (xapp-...)" value={config.slackAppToken} onChange={(e) => updateField('slackAppToken', e.target.value)} />
133
- <label className="toggle-row"><span>LINE Webhook</span><input type="checkbox" checked={config.enableLineBridge} onChange={(e) => updateField('enableLineBridge', e.target.checked)} /></label>
134
- <input type="password" placeholder="LINE channel access token" value={config.lineChannelAccessToken} onChange={(e) => updateField('lineChannelAccessToken', e.target.value)} />
135
- <input type="password" placeholder="LINE channel secret" value={config.lineChannelSecret} onChange={(e) => updateField('lineChannelSecret', e.target.value)} />
136
- <label className="toggle-row"><span>WhatsApp Cloud API</span><input type="checkbox" checked={config.enableWhatsappBridge} onChange={(e) => updateField('enableWhatsappBridge', e.target.checked)} /></label>
137
- <input type="password" placeholder="WhatsApp Cloud access token" value={config.whatsappCloudAccessToken} onChange={(e) => updateField('whatsappCloudAccessToken', e.target.value)} />
138
- <input type="text" placeholder="WhatsApp phone number ID" value={config.whatsappPhoneNumberId} onChange={(e) => updateField('whatsappPhoneNumberId', e.target.value)} />
139
- <input type="password" placeholder="WhatsApp verify token" value={config.whatsappVerifyToken} onChange={(e) => updateField('whatsappVerifyToken', e.target.value)} />
140
- <input type="password" placeholder="WhatsApp app secret (optional HMAC validation)" value={config.whatsappAppSecret} onChange={(e) => updateField('whatsappAppSecret', e.target.value)} />
126
+
127
+ {/* Global Instant Ack Notification */}
128
+ <div className={`bridge-card ${config.enableBridgeAckNotification !== false ? 'active' : ''}`} style={{ marginBottom: '16px' }}>
129
+ <div className="bridge-card-header">
130
+ <div className="bridge-info">
131
+ <div className="bridge-icon-badge" style={{ background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)' }}>
132
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
133
+ <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
134
+ </svg>
135
+ </div>
136
+ <div className="bridge-title-group">
137
+ <span className="bridge-title">
138
+ Instant Remote Acknowledgement
139
+ <span className={`bridge-status-pill ${config.enableBridgeAckNotification !== false ? 'active' : 'inactive'}`}>
140
+ {config.enableBridgeAckNotification !== false ? 'Enabled' : 'Disabled'}
141
+ </span>
142
+ </span>
143
+ <span className="bridge-subtitle">Send instant feedback/typing status when remote commands arrive</span>
144
+ </div>
145
+ </div>
146
+ <label className="settings-toggle-switch">
147
+ <input
148
+ type="checkbox"
149
+ checked={config.enableBridgeAckNotification !== false}
150
+ onChange={(e) => updateField('enableBridgeAckNotification', e.target.checked)}
151
+ />
152
+ <span className="settings-toggle-slider"></span>
153
+ </label>
154
+ </div>
155
+ {config.enableBridgeAckNotification !== false && (
156
+ <div className="bridge-inputs">
157
+ <input
158
+ type="text"
159
+ className="bridge-input"
160
+ placeholder="Instant Ack Message (default: [Mint Agent] Remote command received, processing...)"
161
+ value={config.bridgeAckMessage || ''}
162
+ onChange={(e) => updateField('bridgeAckMessage', e.target.value)}
163
+ />
164
+ </div>
165
+ )}
166
+ </div>
167
+
168
+ <div className="bridge-cards-container">
169
+ {/* Telegram Bot Card */}
170
+ <div className={`bridge-card ${config.enableTelegramBridge ? 'active' : ''}`}>
171
+ <div className="bridge-card-header">
172
+ <div className="bridge-info">
173
+ <div className="bridge-icon-badge bridge-icon-telegram">
174
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
175
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69.01-.03.01-.14-.07-.2-.08-.06-.19-.04-.27-.02-.12.02-1.96 1.25-5.54 3.69-.52.36-1 .54-1.42.53-.47-.01-1.37-.26-2.03-.48-.82-.27-1.47-.42-1.42-.88.03-.25.38-.51 1.07-.78 4.18-1.82 6.97-3.02 8.37-3.61 3.98-1.66 4.81-1.95 5.35-1.96.12 0 .38.03.55.17.14.12.18.28.2.4.02.11.02.26.01.39z"/>
176
+ </svg>
177
+ </div>
178
+ <div className="bridge-title-group">
179
+ <span className="bridge-title">
180
+ Telegram Bot API
181
+ <span className={`bridge-status-pill ${config.enableTelegramBridge ? 'active' : 'inactive'}`}>
182
+ {config.enableTelegramBridge ? 'Active' : 'Disabled'}
183
+ </span>
184
+ </span>
185
+ <span className="bridge-subtitle">Long Polling Engine (getUpdates)</span>
186
+ </div>
187
+ </div>
188
+ <label className="settings-toggle-switch">
189
+ <input
190
+ type="checkbox"
191
+ checked={config.enableTelegramBridge}
192
+ onChange={(e) => updateField('enableTelegramBridge', e.target.checked)}
193
+ />
194
+ <span className="settings-toggle-slider"></span>
195
+ </label>
196
+ </div>
197
+ <div className="bridge-inputs">
198
+ <input
199
+ type="password"
200
+ className="bridge-input"
201
+ placeholder="Telegram Bot Token (from @BotFather)"
202
+ value={config.telegramBotToken}
203
+ onChange={(e) => updateField('telegramBotToken', e.target.value)}
204
+ />
205
+ </div>
206
+ </div>
207
+
208
+ {/* Discord Gateway Card */}
209
+ <div className={`bridge-card ${config.enableDiscordBridge ? 'active' : ''}`}>
210
+ <div className="bridge-card-header">
211
+ <div className="bridge-info">
212
+ <div className="bridge-icon-badge bridge-icon-discord">
213
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
214
+ <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.061 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.893.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.028zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
215
+ </svg>
216
+ </div>
217
+ <div className="bridge-title-group">
218
+ <span className="bridge-title">
219
+ Discord Gateway
220
+ <span className={`bridge-status-pill ${config.enableDiscordBridge ? 'active' : 'inactive'}`}>
221
+ {config.enableDiscordBridge ? 'Active' : 'Disabled'}
222
+ </span>
223
+ </span>
224
+ <span className="bridge-subtitle">WebSocket Gateway (wss://gateway.discord.gg)</span>
225
+ </div>
226
+ </div>
227
+ <label className="settings-toggle-switch">
228
+ <input
229
+ type="checkbox"
230
+ checked={config.enableDiscordBridge}
231
+ onChange={(e) => updateField('enableDiscordBridge', e.target.checked)}
232
+ />
233
+ <span className="settings-toggle-slider"></span>
234
+ </label>
235
+ </div>
236
+ <div className="bridge-inputs">
237
+ <input
238
+ type="password"
239
+ className="bridge-input"
240
+ placeholder="Discord Bot Token"
241
+ value={config.discordBotToken}
242
+ onChange={(e) => updateField('discordBotToken', e.target.value)}
243
+ />
244
+ <input
245
+ type="text"
246
+ className="bridge-input"
247
+ placeholder="Discord Application ID (for Rich Presence)"
248
+ value={config.discordApplicationId}
249
+ onChange={(e) => updateField('discordApplicationId', e.target.value)}
250
+ />
251
+ </div>
252
+ </div>
253
+
254
+ {/* Slack Socket Mode Card */}
255
+ <div className={`bridge-card ${config.enableSlackBridge ? 'active' : ''}`}>
256
+ <div className="bridge-card-header">
257
+ <div className="bridge-info">
258
+ <div className="bridge-icon-badge bridge-icon-slack">
259
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
260
+ <path d="M6 15a2.5 2.5 0 0 1-2.5-2.5A2.5 2.5 0 0 1 6 10h2.5v2.5A2.5 2.5 0 0 1 6 15zm0-8.5A2.5 2.5 0 0 1 3.5 4 2.5 2.5 0 0 1 6 1.5 2.5 2.5 0 0 1 8.5 4v2.5H6zm8.5 0A2.5 2.5 0 0 1 12 4a2.5 2.5 0 0 1 2.5-2.5A2.5 2.5 0 0 1 17 4v2.5h-2.5zm0 8.5a2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5A2.5 2.5 0 0 1 12 17.5V15h2.5zm-8.5 2.5A2.5 2.5 0 0 1 6 20a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5H8.5V17.5zm8.5-8.5A2.5 2.5 0 0 1 17 6.5 2.5 2.5 0 0 1 19.5 9 2.5 2.5 0 0 1 17 11.5H14.5V9zm-2.5-2.5a2.5 2.5 0 0 1-2.5-2.5A2.5 2.5 0 0 1 12 1.5a2.5 2.5 0 0 1 2.5 2.5V6.5H12z"/>
261
+ </svg>
262
+ </div>
263
+ <div className="bridge-title-group">
264
+ <span className="bridge-title">
265
+ Slack Socket Mode
266
+ <span className={`bridge-status-pill ${config.enableSlackBridge ? 'active' : 'inactive'}`}>
267
+ {config.enableSlackBridge ? 'Active' : 'Disabled'}
268
+ </span>
269
+ </span>
270
+ <span className="bridge-subtitle">WebSocket Socket Mode API</span>
271
+ </div>
272
+ </div>
273
+ <label className="settings-toggle-switch">
274
+ <input
275
+ type="checkbox"
276
+ checked={config.enableSlackBridge}
277
+ onChange={(e) => updateField('enableSlackBridge', e.target.checked)}
278
+ />
279
+ <span className="settings-toggle-slider"></span>
280
+ </label>
281
+ </div>
282
+ <div className="bridge-inputs">
283
+ <input
284
+ type="password"
285
+ className="bridge-input"
286
+ placeholder="Slack Bot Token (xoxb-...)"
287
+ value={config.slackBotToken}
288
+ onChange={(e) => updateField('slackBotToken', e.target.value)}
289
+ />
290
+ <input
291
+ type="password"
292
+ className="bridge-input"
293
+ placeholder="Slack App Token (xapp-...)"
294
+ value={config.slackAppToken}
295
+ onChange={(e) => updateField('slackAppToken', e.target.value)}
296
+ />
297
+ </div>
298
+ </div>
299
+
300
+ {/* LINE Webhook Card */}
301
+ <div className={`bridge-card ${config.enableLineBridge ? 'active' : ''}`}>
302
+ <div className="bridge-card-header">
303
+ <div className="bridge-info">
304
+ <div className="bridge-icon-badge bridge-icon-line">
305
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
306
+ <path d="M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63h-2.286v1.442h2.286c.349 0 .63.285.63.63 0 .346-.281.63-.63.63h-2.917c-.349 0-.63-.284-.63-.63V8.581c0-.346.281-.63.63-.63h2.917c.349 0 .63.284.63.63 0 .346-.281.63-.63.63h-2.286V9.86h2.286zm-5.631 4.156c0 .248-.146.472-.371.57-.083.036-.172.054-.26.054-.154 0-.306-.056-.425-.165l-2.481-3.238v2.783c0 .346-.28.63-.63.63-.348 0-.63-.284-.63-.63V8.581c0-.248.146-.472.371-.57.225-.098.486-.051.664.12l2.48 3.237V8.581c0-.346.28-.63.63-.63.349 0 .63.284.63.63v5.438zm-6.843 0c0 .346-.28.63-.63.63-.349 0-.63-.284-.63-.63V8.581c0-.346.281-.63.63-.63.35 0 .63.284.63.63v5.438zm-2.434 0c0 .346-.28.63-.63.63h-2.417c-.349 0-.63-.284-.63-.63V8.581c0-.346.281-.63.63-.63.349 0 .63.284.63.63v4.808h1.787c.349 0 .63.285.63.63zM24 10.617C24 4.754 18.627 0 12 0S0 4.754 0 10.617c0 5.247 4.264 9.61 10.02 10.435.39.084.922.257 1.057.592.121.3.079.77.039 1.074l-.17.994c-.052.304-.247 1.191 1.044.649 1.292-.542 6.974-4.107 9.513-7.032C23.013 15.602 24 13.23 24 10.617z"/>
307
+ </svg>
308
+ </div>
309
+ <div className="bridge-title-group">
310
+ <span className="bridge-title">
311
+ LINE Messaging Webhook
312
+ <span className={`bridge-status-pill ${config.enableLineBridge ? 'active' : 'inactive'}`}>
313
+ {config.enableLineBridge ? 'Active' : 'Disabled'}
314
+ </span>
315
+ </span>
316
+ <span className="bridge-subtitle">HTTP Webhook Listener (Callback)</span>
317
+ </div>
318
+ </div>
319
+ <label className="settings-toggle-switch">
320
+ <input
321
+ type="checkbox"
322
+ checked={config.enableLineBridge}
323
+ onChange={(e) => updateField('enableLineBridge', e.target.checked)}
324
+ />
325
+ <span className="settings-toggle-slider"></span>
326
+ </label>
327
+ </div>
328
+ <div className="bridge-inputs">
329
+ <input
330
+ type="password"
331
+ className="bridge-input"
332
+ placeholder="LINE Channel Access Token"
333
+ value={config.lineChannelAccessToken}
334
+ onChange={(e) => updateField('lineChannelAccessToken', e.target.value)}
335
+ />
336
+ <input
337
+ type="password"
338
+ className="bridge-input"
339
+ placeholder="LINE Channel Secret"
340
+ value={config.lineChannelSecret}
341
+ onChange={(e) => updateField('lineChannelSecret', e.target.value)}
342
+ />
343
+ <div className="bridge-input-row">
344
+ <input
345
+ type="text"
346
+ className="bridge-input"
347
+ placeholder="Webhook Host (default: 127.0.0.1)"
348
+ value={config.lineWebhookHost || ''}
349
+ onChange={(e) => updateField('lineWebhookHost', e.target.value)}
350
+ />
351
+ <input
352
+ type="number"
353
+ className="bridge-input"
354
+ placeholder="Port"
355
+ value={config.lineWebhookPort || 3000}
356
+ onChange={(e) => updateField('lineWebhookPort', parseInt(e.target.value) || 3000)}
357
+ style={{ width: '120px' }}
358
+ />
359
+ </div>
360
+ </div>
361
+ </div>
362
+
363
+ {/* WhatsApp Cloud API Card */}
364
+ <div className={`bridge-card ${config.enableWhatsappBridge ? 'active' : ''}`}>
365
+ <div className="bridge-card-header">
366
+ <div className="bridge-info">
367
+ <div className="bridge-icon-badge bridge-icon-whatsapp">
368
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
369
+ <path d="M.057 24l1.687-6.163c-1.041-1.804-1.588-3.849-1.587-5.946.003-6.556 5.338-11.891 11.893-11.891 3.181.001 6.167 1.24 8.413 3.488 2.245 2.248 3.481 5.236 3.48 8.414-.003 6.557-5.338 11.892-11.893 11.892-1.99-.001-3.951-.5-5.688-1.448l-6.305 1.654zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.301-.15-1.785-.881-2.062-.982-.276-.101-.477-.15-.677.15-.199.301-.776.982-.952 1.182-.175.201-.351.226-.652.076-.301-.15-1.27-.468-2.42-1.494-.897-.8-1.502-1.788-1.678-2.09-.175-.301-.019-.464.131-.613.136-.135.301-.351.451-.526.15-.176.2-.301.3-.502.1-.201.05-.376-.025-.526-.075-.15-.677-1.631-.928-2.235-.244-.587-.492-.507-.676-.517-.175-.008-.376-.01-.576-.01-.201 0-.526.075-.802.376-.276.301-1.053 1.029-1.053 2.509 0 1.48 1.078 2.909 1.229 3.11.15.201 2.122 3.241 5.141 4.544.718.31 1.279.495 1.716.634.721.229 1.377.197 1.896.12.578-.086 1.785-.729 2.036-1.431.25-.702.25-1.304.175-1.431-.075-.127-.276-.201-.577-.352z"/>
370
+ </svg>
371
+ </div>
372
+ <div className="bridge-title-group">
373
+ <span className="bridge-title">
374
+ WhatsApp Cloud API
375
+ <span className={`bridge-status-pill ${config.enableWhatsappBridge ? 'active' : 'inactive'}`}>
376
+ {config.enableWhatsappBridge ? 'Active' : 'Disabled'}
377
+ </span>
378
+ </span>
379
+ <span className="bridge-subtitle">Meta Cloud API Webhook Listener</span>
380
+ </div>
381
+ </div>
382
+ <label className="settings-toggle-switch">
383
+ <input
384
+ type="checkbox"
385
+ checked={config.enableWhatsappBridge}
386
+ onChange={(e) => updateField('enableWhatsappBridge', e.target.checked)}
387
+ />
388
+ <span className="settings-toggle-slider"></span>
389
+ </label>
390
+ </div>
391
+ <div className="bridge-inputs">
392
+ <input
393
+ type="password"
394
+ className="bridge-input"
395
+ placeholder="WhatsApp Cloud Access Token"
396
+ value={config.whatsappCloudAccessToken}
397
+ onChange={(e) => updateField('whatsappCloudAccessToken', e.target.value)}
398
+ />
399
+ <input
400
+ type="text"
401
+ className="bridge-input"
402
+ placeholder="WhatsApp Phone Number ID"
403
+ value={config.whatsappPhoneNumberId}
404
+ onChange={(e) => updateField('whatsappPhoneNumberId', e.target.value)}
405
+ />
406
+ <input
407
+ type="password"
408
+ className="bridge-input"
409
+ placeholder="WhatsApp Verify Token"
410
+ value={config.whatsappVerifyToken}
411
+ onChange={(e) => updateField('whatsappVerifyToken', e.target.value)}
412
+ />
413
+ <input
414
+ type="password"
415
+ className="bridge-input"
416
+ placeholder="WhatsApp App Secret (optional HMAC validation)"
417
+ value={config.whatsappAppSecret}
418
+ onChange={(e) => updateField('whatsappAppSecret', e.target.value)}
419
+ />
420
+ <div className="bridge-input-row">
421
+ <input
422
+ type="text"
423
+ className="bridge-input"
424
+ placeholder="Webhook Host (default: 127.0.0.1)"
425
+ value={config.whatsappWebhookHost || ''}
426
+ onChange={(e) => updateField('whatsappWebhookHost', e.target.value)}
427
+ />
428
+ <input
429
+ type="number"
430
+ className="bridge-input"
431
+ placeholder="Port"
432
+ value={config.whatsappWebhookPort || 3001}
433
+ onChange={(e) => updateField('whatsappWebhookPort', parseInt(e.target.value) || 3001)}
434
+ style={{ width: '120px' }}
435
+ />
436
+ </div>
437
+ </div>
438
+ </div>
141
439
  </div>
142
440
  </section>
143
441