@realtimex/email-automator 2.2.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/server.ts +0 -6
- package/api/src/config/index.ts +3 -0
- package/bin/email-automator-setup.js +2 -3
- package/bin/email-automator.js +23 -7
- package/package.json +1 -2
- package/src/App.tsx +0 -622
- package/src/components/AccountSettings.tsx +0 -310
- package/src/components/AccountSettingsPage.tsx +0 -390
- package/src/components/Configuration.tsx +0 -1345
- package/src/components/Dashboard.tsx +0 -940
- package/src/components/ErrorBoundary.tsx +0 -71
- package/src/components/LiveTerminal.tsx +0 -308
- package/src/components/LoadingSpinner.tsx +0 -39
- package/src/components/Login.tsx +0 -371
- package/src/components/Logo.tsx +0 -57
- package/src/components/SetupWizard.tsx +0 -388
- package/src/components/Toast.tsx +0 -109
- package/src/components/migration/MigrationBanner.tsx +0 -97
- package/src/components/migration/MigrationModal.tsx +0 -458
- package/src/components/migration/MigrationPulseIndicator.tsx +0 -38
- package/src/components/mode-toggle.tsx +0 -24
- package/src/components/theme-provider.tsx +0 -72
- package/src/components/ui/alert.tsx +0 -66
- package/src/components/ui/button.tsx +0 -57
- package/src/components/ui/card.tsx +0 -75
- package/src/components/ui/dialog.tsx +0 -133
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/otp-input.tsx +0 -184
- package/src/context/AppContext.tsx +0 -422
- package/src/context/MigrationContext.tsx +0 -53
- package/src/context/TerminalContext.tsx +0 -31
- package/src/core/actions.ts +0 -76
- package/src/core/auth.ts +0 -108
- package/src/core/intelligence.ts +0 -76
- package/src/core/processor.ts +0 -112
- package/src/hooks/useRealtimeEmails.ts +0 -111
- package/src/index.css +0 -140
- package/src/lib/api-config.ts +0 -42
- package/src/lib/api-old.ts +0 -228
- package/src/lib/api.ts +0 -421
- package/src/lib/migration-check.ts +0 -264
- package/src/lib/sounds.ts +0 -120
- package/src/lib/supabase-config.ts +0 -117
- package/src/lib/supabase.ts +0 -28
- package/src/lib/types.ts +0 -166
- package/src/lib/utils.ts +0 -6
- package/src/main.tsx +0 -10
|
@@ -1,1345 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState, useRef } from 'react';
|
|
2
|
-
import { ShieldCheck, Database, RefreshCw, Plus, Check, Trash2, Power, ExternalLink, Upload, Paperclip, X, Clock } from 'lucide-react';
|
|
3
|
-
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from './ui/card';
|
|
4
|
-
import { Button } from './ui/button';
|
|
5
|
-
import { Input } from './ui/input';
|
|
6
|
-
import { useApp } from '../context/AppContext';
|
|
7
|
-
import { api } from '../lib/api';
|
|
8
|
-
import { supabase } from '../lib/supabase';
|
|
9
|
-
import { toast } from './Toast';
|
|
10
|
-
import { LoadingSpinner } from './LoadingSpinner';
|
|
11
|
-
import { EmailAccount, Rule, UserSettings, RuleAttachment } from '../lib/types';
|
|
12
|
-
import {
|
|
13
|
-
Dialog,
|
|
14
|
-
DialogContent,
|
|
15
|
-
DialogDescription,
|
|
16
|
-
DialogFooter,
|
|
17
|
-
DialogHeader,
|
|
18
|
-
DialogTitle,
|
|
19
|
-
} from './ui/dialog';
|
|
20
|
-
|
|
21
|
-
interface ExtendedUserSettings extends UserSettings {
|
|
22
|
-
google_client_id?: string;
|
|
23
|
-
google_client_secret?: string;
|
|
24
|
-
microsoft_client_id?: string;
|
|
25
|
-
microsoft_client_secret?: string;
|
|
26
|
-
microsoft_tenant_id?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function Configuration() {
|
|
30
|
-
const { state, actions } = useApp();
|
|
31
|
-
const [isConnecting, setIsConnecting] = useState(false);
|
|
32
|
-
const [isOutlookConnecting, setIsOutlookConnecting] = useState(false);
|
|
33
|
-
const [outlookDeviceCode, setOutlookDeviceCode] = useState<{
|
|
34
|
-
userCode: string;
|
|
35
|
-
verificationUri: string;
|
|
36
|
-
message: string;
|
|
37
|
-
deviceCode: string;
|
|
38
|
-
} | null>(null);
|
|
39
|
-
const [savingSettings, setSavingSettings] = useState(false);
|
|
40
|
-
const [testingLlm, setTestingLlm] = useState(false);
|
|
41
|
-
const [localSettings, setLocalSettings] = useState<Partial<ExtendedUserSettings>>({});
|
|
42
|
-
|
|
43
|
-
// Gmail credentials modal state
|
|
44
|
-
const [showGmailModal, setShowGmailModal] = useState(false);
|
|
45
|
-
const [gmailModalStep, setGmailModalStep] = useState<'credentials' | 'code'>('credentials');
|
|
46
|
-
const [credentialsJson, setCredentialsJson] = useState('');
|
|
47
|
-
const [gmailClientId, setGmailClientId] = useState('');
|
|
48
|
-
const [gmailClientSecret, setGmailClientSecret] = useState('');
|
|
49
|
-
const [gmailAuthCode, setGmailAuthCode] = useState('');
|
|
50
|
-
const [savingCredentials, setSavingCredentials] = useState(false);
|
|
51
|
-
const [connectingGmail, setConnectingGmail] = useState(false);
|
|
52
|
-
|
|
53
|
-
// Outlook credentials modal state
|
|
54
|
-
const [showOutlookModal, setShowOutlookModal] = useState(false);
|
|
55
|
-
const [outlookModalStep, setOutlookModalStep] = useState<'credentials' | 'device-code'>('credentials');
|
|
56
|
-
const [outlookClientId, setOutlookClientId] = useState('');
|
|
57
|
-
const [outlookTenantId, setOutlookTenantId] = useState('');
|
|
58
|
-
const [savingOutlookCredentials, setSavingOutlookCredentials] = useState(false);
|
|
59
|
-
|
|
60
|
-
// Rule creation state
|
|
61
|
-
const [showRuleModal, setShowRuleModal] = useState(false);
|
|
62
|
-
const [newRuleName, setNewRuleName] = useState('');
|
|
63
|
-
const [newRuleKey, setNewRuleKey] = useState('category');
|
|
64
|
-
const [newRuleValue, setNewRuleValue] = useState('newsletter');
|
|
65
|
-
const [newRuleAction, setNewRuleAction] = useState('archive');
|
|
66
|
-
const [newRuleOlderThan, setNewRuleOlderThan] = useState('');
|
|
67
|
-
const [newRuleInstructions, setNewRuleInstructions] = useState('');
|
|
68
|
-
const [newRuleAttachments, setNewRuleAttachments] = useState<RuleAttachment[]>([]);
|
|
69
|
-
const [isUploading, setIsUploading] = useState(false);
|
|
70
|
-
const [savingRule, setSavingRule] = useState(false);
|
|
71
|
-
const [loadingSetting, setLoadingSetting] = useState<string | null>(null);
|
|
72
|
-
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
actions.fetchAccounts();
|
|
75
|
-
actions.fetchRules();
|
|
76
|
-
actions.fetchSettings();
|
|
77
|
-
}, []);
|
|
78
|
-
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
if (state.settings) {
|
|
81
|
-
setLocalSettings(state.settings);
|
|
82
|
-
}
|
|
83
|
-
}, [state.settings]);
|
|
84
|
-
|
|
85
|
-
const handleToggleSpam = async () => {
|
|
86
|
-
const rule = state.rules.find(r => r.name === 'Auto-Trash Spam');
|
|
87
|
-
if (!rule) {
|
|
88
|
-
toast.error('System rule not found. Please sync your database.');
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
setLoadingSetting('auto_trash_spam');
|
|
92
|
-
await actions.toggleRule(rule.id);
|
|
93
|
-
setLoadingSetting(null);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const handleToggleDrafts = async () => {
|
|
97
|
-
const rule = state.rules.find(r => r.name === 'Smart Drafts');
|
|
98
|
-
if (!rule) {
|
|
99
|
-
toast.error('System rule not found. Please sync your database.');
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
setLoadingSetting('smart_drafts');
|
|
103
|
-
await actions.toggleRule(rule.id);
|
|
104
|
-
setLoadingSetting(null);
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// Ref for scrolling
|
|
108
|
-
const credentialsRef = useRef<HTMLDivElement>(null);
|
|
109
|
-
|
|
110
|
-
// Start OAuth flow (called after credentials are saved)
|
|
111
|
-
const startGmailOAuth = async () => {
|
|
112
|
-
setIsConnecting(true);
|
|
113
|
-
try {
|
|
114
|
-
const response = await api.getGmailAuthUrl();
|
|
115
|
-
if (response.data?.url) {
|
|
116
|
-
// Open OAuth popup
|
|
117
|
-
const popup = window.open(response.data.url, 'gmail-auth', 'width=600,height=700');
|
|
118
|
-
|
|
119
|
-
// Listen for callback
|
|
120
|
-
const checkPopup = setInterval(() => {
|
|
121
|
-
if (popup?.closed) {
|
|
122
|
-
clearInterval(checkPopup);
|
|
123
|
-
setIsConnecting(false);
|
|
124
|
-
actions.fetchAccounts();
|
|
125
|
-
}
|
|
126
|
-
}, 1000);
|
|
127
|
-
} else if (response.error) {
|
|
128
|
-
const errMsg = typeof response.error === 'string' ? response.error : response.error.message;
|
|
129
|
-
console.error('[Configuration] Gmail auth error:', response.error);
|
|
130
|
-
toast.error(errMsg || 'Failed to start connection. Please ensure you are logged in.');
|
|
131
|
-
setIsConnecting(false);
|
|
132
|
-
}
|
|
133
|
-
} catch (error) {
|
|
134
|
-
toast.error('Failed to start Gmail connection');
|
|
135
|
-
setIsConnecting(false);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
// Handle "Connect Gmail" button click - show modal
|
|
140
|
-
const handleConnectGmail = () => {
|
|
141
|
-
// Reset modal state
|
|
142
|
-
setGmailModalStep('credentials');
|
|
143
|
-
setCredentialsJson('');
|
|
144
|
-
setGmailClientId('');
|
|
145
|
-
setGmailClientSecret('');
|
|
146
|
-
setGmailAuthCode('');
|
|
147
|
-
setShowGmailModal(true);
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// Parse credentials.json and extract client_id/secret
|
|
151
|
-
const handleCredentialsJsonChange = (json: string) => {
|
|
152
|
-
setCredentialsJson(json);
|
|
153
|
-
try {
|
|
154
|
-
const parsed = JSON.parse(json);
|
|
155
|
-
// Handle both formats: { installed: {...} } or { web: {...} } or direct
|
|
156
|
-
const creds = parsed.installed || parsed.web || parsed;
|
|
157
|
-
if (creds.client_id) {
|
|
158
|
-
setGmailClientId(creds.client_id);
|
|
159
|
-
}
|
|
160
|
-
if (creds.client_secret) {
|
|
161
|
-
setGmailClientSecret(creds.client_secret);
|
|
162
|
-
}
|
|
163
|
-
// Also show the app type for user awareness
|
|
164
|
-
if (parsed.installed) {
|
|
165
|
-
toast.success('Detected Desktop app credentials');
|
|
166
|
-
} else if (parsed.web) {
|
|
167
|
-
toast.info('Detected Web app - make sure redirect URI is configured in Google Cloud Console');
|
|
168
|
-
}
|
|
169
|
-
} catch {
|
|
170
|
-
// Invalid JSON, ignore - user might be typing
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
// Save credentials and start OAuth
|
|
175
|
-
const handleSaveAndConnect = async () => {
|
|
176
|
-
if (!gmailClientId || !gmailClientSecret) {
|
|
177
|
-
toast.error('Please provide both Client ID and Client Secret');
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
setSavingCredentials(true);
|
|
182
|
-
try {
|
|
183
|
-
// Save credentials to user_settings
|
|
184
|
-
const success = await actions.updateSettings({
|
|
185
|
-
...localSettings,
|
|
186
|
-
google_client_id: gmailClientId,
|
|
187
|
-
google_client_secret: gmailClientSecret,
|
|
188
|
-
} as any);
|
|
189
|
-
|
|
190
|
-
if (success) {
|
|
191
|
-
// Update local state
|
|
192
|
-
setLocalSettings(s => ({
|
|
193
|
-
...s,
|
|
194
|
-
google_client_id: gmailClientId,
|
|
195
|
-
google_client_secret: gmailClientSecret,
|
|
196
|
-
}));
|
|
197
|
-
|
|
198
|
-
// Get OAuth URL and open popup
|
|
199
|
-
const response = await api.getGmailAuthUrl();
|
|
200
|
-
if (response.data?.url) {
|
|
201
|
-
// Open OAuth in new tab (popup might be blocked)
|
|
202
|
-
window.open(response.data.url, '_blank');
|
|
203
|
-
|
|
204
|
-
// Move to step 2 - paste code
|
|
205
|
-
setGmailModalStep('code');
|
|
206
|
-
toast.success('Please authorize in the opened tab, then paste the code here');
|
|
207
|
-
} else {
|
|
208
|
-
const errMsg = typeof response.error === 'string' ? response.error : response.error?.message;
|
|
209
|
-
toast.error(errMsg || 'Failed to get OAuth URL');
|
|
210
|
-
}
|
|
211
|
-
} else {
|
|
212
|
-
toast.error('Failed to save credentials');
|
|
213
|
-
}
|
|
214
|
-
} catch (error) {
|
|
215
|
-
toast.error('Failed to save credentials');
|
|
216
|
-
} finally {
|
|
217
|
-
setSavingCredentials(false);
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
// Submit authorization code to complete Gmail connection
|
|
222
|
-
const handleSubmitAuthCode = async () => {
|
|
223
|
-
if (!gmailAuthCode.trim()) {
|
|
224
|
-
toast.error('Please paste the authorization code');
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
setConnectingGmail(true);
|
|
229
|
-
try {
|
|
230
|
-
const response = await api.connectGmail(gmailAuthCode.trim());
|
|
231
|
-
if (response.data?.success) {
|
|
232
|
-
toast.success('Gmail account connected successfully!');
|
|
233
|
-
setShowGmailModal(false);
|
|
234
|
-
actions.fetchAccounts();
|
|
235
|
-
} else {
|
|
236
|
-
const errMsg = typeof response.error === 'string' ? response.error : response.error?.message;
|
|
237
|
-
toast.error(errMsg || 'Failed to connect Gmail');
|
|
238
|
-
}
|
|
239
|
-
} catch (error) {
|
|
240
|
-
toast.error('Failed to connect Gmail');
|
|
241
|
-
} finally {
|
|
242
|
-
setConnectingGmail(false);
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
// Handle "Connect Outlook" button click - show modal
|
|
247
|
-
const handleConnectOutlook = () => {
|
|
248
|
-
// Reset modal state
|
|
249
|
-
setOutlookModalStep('credentials');
|
|
250
|
-
setOutlookClientId('');
|
|
251
|
-
setOutlookTenantId('');
|
|
252
|
-
setShowOutlookModal(true);
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
// Save Outlook credentials and start device flow
|
|
256
|
-
const handleSaveOutlookAndConnect = async () => {
|
|
257
|
-
if (!outlookClientId) {
|
|
258
|
-
toast.error('Please provide the Client ID');
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
setSavingOutlookCredentials(true);
|
|
263
|
-
try {
|
|
264
|
-
// Save credentials to user_settings
|
|
265
|
-
const success = await actions.updateSettings({
|
|
266
|
-
...localSettings,
|
|
267
|
-
microsoft_client_id: outlookClientId,
|
|
268
|
-
microsoft_tenant_id: outlookTenantId || 'common',
|
|
269
|
-
} as any);
|
|
270
|
-
|
|
271
|
-
if (success) {
|
|
272
|
-
// Update local state
|
|
273
|
-
setLocalSettings(s => ({
|
|
274
|
-
...s,
|
|
275
|
-
microsoft_client_id: outlookClientId,
|
|
276
|
-
microsoft_tenant_id: outlookTenantId || 'common',
|
|
277
|
-
}));
|
|
278
|
-
|
|
279
|
-
// Start device flow
|
|
280
|
-
const response = await api.startMicrosoftDeviceFlow();
|
|
281
|
-
if (response.data) {
|
|
282
|
-
setOutlookDeviceCode(response.data);
|
|
283
|
-
setOutlookModalStep('device-code');
|
|
284
|
-
pollOutlookLogin(response.data.deviceCode, response.data.interval);
|
|
285
|
-
} else {
|
|
286
|
-
const errMsg = typeof response.error === 'string' ? response.error : response.error?.message;
|
|
287
|
-
toast.error(errMsg || 'Failed to start device flow');
|
|
288
|
-
}
|
|
289
|
-
} else {
|
|
290
|
-
toast.error('Failed to save credentials');
|
|
291
|
-
}
|
|
292
|
-
} catch (error) {
|
|
293
|
-
toast.error('Failed to save credentials');
|
|
294
|
-
} finally {
|
|
295
|
-
setSavingOutlookCredentials(false);
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
// Original device flow start (used after credentials saved)
|
|
300
|
-
const startOutlookDeviceFlow = async () => {
|
|
301
|
-
setIsOutlookConnecting(true);
|
|
302
|
-
try {
|
|
303
|
-
const response = await api.startMicrosoftDeviceFlow();
|
|
304
|
-
if (response.data) {
|
|
305
|
-
setOutlookDeviceCode(response.data);
|
|
306
|
-
pollOutlookLogin(response.data.deviceCode, response.data.interval);
|
|
307
|
-
} else {
|
|
308
|
-
toast.error('Failed to start Outlook connection');
|
|
309
|
-
setIsOutlookConnecting(false);
|
|
310
|
-
}
|
|
311
|
-
} catch (error) {
|
|
312
|
-
toast.error('Failed to start Outlook connection');
|
|
313
|
-
setIsOutlookConnecting(false);
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const pollOutlookLogin = async (deviceCode: string, interval: number) => {
|
|
318
|
-
const pollInterval = setInterval(async () => {
|
|
319
|
-
try {
|
|
320
|
-
const response = await api.pollMicrosoftDeviceCode(deviceCode);
|
|
321
|
-
if (response.data?.status === 'completed') {
|
|
322
|
-
clearInterval(pollInterval);
|
|
323
|
-
setOutlookDeviceCode(null);
|
|
324
|
-
setIsOutlookConnecting(false);
|
|
325
|
-
setShowOutlookModal(false); // Close modal on success
|
|
326
|
-
toast.success('Outlook account connected');
|
|
327
|
-
actions.fetchAccounts();
|
|
328
|
-
} else if (response.error) {
|
|
329
|
-
if (typeof response.error === 'object' && response.error.code !== 'authorization_pending') {
|
|
330
|
-
// Stop polling on real errors
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
} catch (e) {
|
|
334
|
-
// Network glitches shouldn't kill polling immediately
|
|
335
|
-
}
|
|
336
|
-
}, interval * 1000);
|
|
337
|
-
|
|
338
|
-
// Safety timeout after 15 minutes
|
|
339
|
-
setTimeout(() => {
|
|
340
|
-
clearInterval(pollInterval);
|
|
341
|
-
if (isOutlookConnecting) {
|
|
342
|
-
setOutlookDeviceCode(null);
|
|
343
|
-
setIsOutlookConnecting(false);
|
|
344
|
-
setShowOutlookModal(false);
|
|
345
|
-
toast.error('Connection timed out. Please try again.');
|
|
346
|
-
}
|
|
347
|
-
}, 15 * 60 * 1000);
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
const handleCreateRule = async () => {
|
|
351
|
-
if (!newRuleName) {
|
|
352
|
-
toast.error('Please name your rule');
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
setSavingRule(true);
|
|
357
|
-
try {
|
|
358
|
-
const condition: Record<string, any> = { [newRuleKey]: newRuleValue };
|
|
359
|
-
if (newRuleOlderThan) {
|
|
360
|
-
condition.older_than_days = parseInt(newRuleOlderThan, 10);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const success = await actions.createRule({
|
|
364
|
-
name: newRuleName,
|
|
365
|
-
condition,
|
|
366
|
-
action: newRuleAction as any,
|
|
367
|
-
instructions: newRuleAction === 'draft' ? newRuleInstructions : undefined,
|
|
368
|
-
attachments: newRuleAction === 'draft' ? newRuleAttachments : [],
|
|
369
|
-
is_enabled: true
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
if (success) {
|
|
373
|
-
toast.success('Rule created');
|
|
374
|
-
setShowRuleModal(false);
|
|
375
|
-
setNewRuleName('');
|
|
376
|
-
setNewRuleOlderThan('');
|
|
377
|
-
setNewRuleInstructions('');
|
|
378
|
-
setNewRuleAttachments([]);
|
|
379
|
-
actions.fetchRules();
|
|
380
|
-
} else {
|
|
381
|
-
toast.error('Failed to create rule');
|
|
382
|
-
}
|
|
383
|
-
} catch (error) {
|
|
384
|
-
toast.error('Failed to create rule');
|
|
385
|
-
} finally {
|
|
386
|
-
setSavingRule(false);
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
const handleDisconnect = async (accountId: string) => {
|
|
391
|
-
if (!confirm('Are you sure you want to disconnect this account?')) return;
|
|
392
|
-
|
|
393
|
-
const success = await actions.disconnectAccount(accountId);
|
|
394
|
-
if (success) {
|
|
395
|
-
toast.success('Account disconnected');
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
const handleSaveSettings = async () => {
|
|
400
|
-
setSavingSettings(true);
|
|
401
|
-
const success = await actions.updateSettings(localSettings as any);
|
|
402
|
-
setSavingSettings(false);
|
|
403
|
-
|
|
404
|
-
if (success) {
|
|
405
|
-
toast.success('Settings saved');
|
|
406
|
-
}
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
const handleTestConnection = async () => {
|
|
410
|
-
setTestingLlm(true);
|
|
411
|
-
try {
|
|
412
|
-
const result = await api.testLlm({
|
|
413
|
-
llm_model: localSettings.llm_model || null,
|
|
414
|
-
llm_base_url: localSettings.llm_base_url || null,
|
|
415
|
-
llm_api_key: localSettings.llm_api_key || null,
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
if (result.data?.success) {
|
|
419
|
-
toast.success(result.data.message);
|
|
420
|
-
} else {
|
|
421
|
-
toast.error(result.data?.message || 'Connection test failed');
|
|
422
|
-
}
|
|
423
|
-
} catch (error) {
|
|
424
|
-
toast.error('Failed to test connection');
|
|
425
|
-
} finally {
|
|
426
|
-
setTestingLlm(false);
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
const handleToggleRule = async (ruleId: string) => {
|
|
431
|
-
await actions.toggleRule(ruleId);
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
435
|
-
const files = e.target.files;
|
|
436
|
-
if (!files || files.length === 0) return;
|
|
437
|
-
|
|
438
|
-
setIsUploading(true);
|
|
439
|
-
const file = files[0];
|
|
440
|
-
const fileExt = file.name.split('.').pop();
|
|
441
|
-
const fileName = `${Math.random().toString(36).substring(2)}.${fileExt}`;
|
|
442
|
-
const filePath = `${state.user.id}/${fileName}`;
|
|
443
|
-
|
|
444
|
-
try {
|
|
445
|
-
const { error: uploadError } = await supabase.storage
|
|
446
|
-
.from('rule-attachments')
|
|
447
|
-
.upload(filePath, file);
|
|
448
|
-
|
|
449
|
-
if (uploadError) throw uploadError;
|
|
450
|
-
|
|
451
|
-
const newAttachment: RuleAttachment = {
|
|
452
|
-
name: file.name,
|
|
453
|
-
path: filePath,
|
|
454
|
-
type: file.type,
|
|
455
|
-
size: file.size
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
setNewRuleAttachments(prev => [...prev, newAttachment]);
|
|
459
|
-
toast.success('File uploaded');
|
|
460
|
-
} catch (error) {
|
|
461
|
-
console.error('Upload error:', error);
|
|
462
|
-
toast.error('Failed to upload file');
|
|
463
|
-
} finally {
|
|
464
|
-
setIsUploading(false);
|
|
465
|
-
// Reset input
|
|
466
|
-
e.target.value = '';
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
const removeAttachment = (path: string) => {
|
|
471
|
-
setNewRuleAttachments(prev => prev.filter(a => a.path !== path));
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
const getProviderIcon = (provider: string) => {
|
|
475
|
-
if (provider === 'gmail') {
|
|
476
|
-
return (
|
|
477
|
-
<div className="w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center text-red-600 dark:text-red-400 font-bold">
|
|
478
|
-
G
|
|
479
|
-
</div>
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
return (
|
|
483
|
-
<div className="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold">
|
|
484
|
-
O
|
|
485
|
-
</div>
|
|
486
|
-
);
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
return (
|
|
490
|
-
<div className="space-y-8 animate-in fade-in duration-500">
|
|
491
|
-
{/* Gmail Credentials Modal */}
|
|
492
|
-
<Dialog open={showGmailModal} onOpenChange={setShowGmailModal}>
|
|
493
|
-
<DialogContent className="sm:max-w-lg">
|
|
494
|
-
<DialogHeader>
|
|
495
|
-
<DialogTitle className="flex items-center gap-2">
|
|
496
|
-
<div className="w-8 h-8 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center text-red-600 dark:text-red-400 font-bold text-sm">
|
|
497
|
-
G
|
|
498
|
-
</div>
|
|
499
|
-
Connect Gmail Account
|
|
500
|
-
</DialogTitle>
|
|
501
|
-
<DialogDescription>
|
|
502
|
-
{gmailModalStep === 'credentials'
|
|
503
|
-
? 'Enter your Google OAuth credentials to connect your Gmail account.'
|
|
504
|
-
: 'Paste the authorization code from Google to complete the connection.'}
|
|
505
|
-
</DialogDescription>
|
|
506
|
-
</DialogHeader>
|
|
507
|
-
|
|
508
|
-
{gmailModalStep === 'credentials' ? (
|
|
509
|
-
<>
|
|
510
|
-
<div className="space-y-4 py-4">
|
|
511
|
-
{/* Paste JSON option */}
|
|
512
|
-
<div className="space-y-2">
|
|
513
|
-
<label className="text-sm font-medium flex items-center gap-2">
|
|
514
|
-
<Upload className="w-4 h-4" />
|
|
515
|
-
Paste credentials.json
|
|
516
|
-
</label>
|
|
517
|
-
<textarea
|
|
518
|
-
className="w-full h-24 p-3 text-xs font-mono border rounded-lg bg-secondary/30 resize-none focus:outline-none focus:ring-2 focus:ring-primary"
|
|
519
|
-
placeholder='{"installed":{"client_id":"...","client_secret":"..."}}'
|
|
520
|
-
value={credentialsJson}
|
|
521
|
-
onChange={(e) => handleCredentialsJsonChange(e.target.value)}
|
|
522
|
-
/>
|
|
523
|
-
<p className="text-xs text-muted-foreground">
|
|
524
|
-
Download from Google Cloud Console → APIs & Services → Credentials
|
|
525
|
-
</p>
|
|
526
|
-
</div>
|
|
527
|
-
|
|
528
|
-
<div className="relative">
|
|
529
|
-
<div className="absolute inset-0 flex items-center">
|
|
530
|
-
<span className="w-full border-t" />
|
|
531
|
-
</div>
|
|
532
|
-
<div className="relative flex justify-center text-xs uppercase">
|
|
533
|
-
<span className="bg-background px-2 text-muted-foreground">or enter manually</span>
|
|
534
|
-
</div>
|
|
535
|
-
</div>
|
|
536
|
-
|
|
537
|
-
{/* Manual entry */}
|
|
538
|
-
<div className="space-y-3">
|
|
539
|
-
<div className="space-y-2">
|
|
540
|
-
<label className="text-sm font-medium">Client ID</label>
|
|
541
|
-
<Input
|
|
542
|
-
placeholder="xxx.apps.googleusercontent.com"
|
|
543
|
-
value={gmailClientId}
|
|
544
|
-
onChange={(e) => setGmailClientId(e.target.value)}
|
|
545
|
-
/>
|
|
546
|
-
</div>
|
|
547
|
-
<div className="space-y-2">
|
|
548
|
-
<label className="text-sm font-medium">Client Secret</label>
|
|
549
|
-
<Input
|
|
550
|
-
type="password"
|
|
551
|
-
placeholder="GOCSPX-..."
|
|
552
|
-
value={gmailClientSecret}
|
|
553
|
-
onChange={(e) => setGmailClientSecret(e.target.value)}
|
|
554
|
-
/>
|
|
555
|
-
</div>
|
|
556
|
-
</div>
|
|
557
|
-
</div>
|
|
558
|
-
|
|
559
|
-
<DialogFooter>
|
|
560
|
-
<Button variant="outline" onClick={() => setShowGmailModal(false)}>
|
|
561
|
-
Cancel
|
|
562
|
-
</Button>
|
|
563
|
-
<Button
|
|
564
|
-
onClick={handleSaveAndConnect}
|
|
565
|
-
disabled={savingCredentials || !gmailClientId || !gmailClientSecret}
|
|
566
|
-
>
|
|
567
|
-
{savingCredentials ? (
|
|
568
|
-
<LoadingSpinner size="sm" className="mr-2" />
|
|
569
|
-
) : (
|
|
570
|
-
<Check className="w-4 h-4 mr-2" />
|
|
571
|
-
)}
|
|
572
|
-
Save & Connect
|
|
573
|
-
</Button>
|
|
574
|
-
</DialogFooter>
|
|
575
|
-
</>
|
|
576
|
-
) : (
|
|
577
|
-
<>
|
|
578
|
-
{/* Step 2: Paste Authorization Code */}
|
|
579
|
-
<div className="space-y-4 py-4">
|
|
580
|
-
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
|
581
|
-
<p className="text-sm text-blue-800 dark:text-blue-200">
|
|
582
|
-
1. A new tab opened with Google Sign-In<br />
|
|
583
|
-
2. Sign in and authorize the app<br />
|
|
584
|
-
3. Copy the authorization code shown<br />
|
|
585
|
-
4. Paste it below
|
|
586
|
-
</p>
|
|
587
|
-
</div>
|
|
588
|
-
|
|
589
|
-
<div className="space-y-2">
|
|
590
|
-
<label className="text-sm font-medium">Authorization Code</label>
|
|
591
|
-
<Input
|
|
592
|
-
placeholder="4/0AQlEd8x..."
|
|
593
|
-
value={gmailAuthCode}
|
|
594
|
-
onChange={(e) => setGmailAuthCode(e.target.value)}
|
|
595
|
-
className="font-mono"
|
|
596
|
-
/>
|
|
597
|
-
</div>
|
|
598
|
-
</div>
|
|
599
|
-
|
|
600
|
-
<DialogFooter>
|
|
601
|
-
<Button variant="outline" onClick={() => setGmailModalStep('credentials')}>
|
|
602
|
-
Back
|
|
603
|
-
</Button>
|
|
604
|
-
<Button
|
|
605
|
-
onClick={handleSubmitAuthCode}
|
|
606
|
-
disabled={connectingGmail || !gmailAuthCode.trim()}
|
|
607
|
-
>
|
|
608
|
-
{connectingGmail ? (
|
|
609
|
-
<LoadingSpinner size="sm" className="mr-2" />
|
|
610
|
-
) : (
|
|
611
|
-
<Check className="w-4 h-4 mr-2" />
|
|
612
|
-
)}
|
|
613
|
-
Connect
|
|
614
|
-
</Button>
|
|
615
|
-
</DialogFooter>
|
|
616
|
-
</>
|
|
617
|
-
)}
|
|
618
|
-
</DialogContent>
|
|
619
|
-
</Dialog>
|
|
620
|
-
|
|
621
|
-
{/* Outlook Credentials Modal */}
|
|
622
|
-
<Dialog open={showOutlookModal} onOpenChange={setShowOutlookModal}>
|
|
623
|
-
<DialogContent className="sm:max-w-lg">
|
|
624
|
-
<DialogHeader>
|
|
625
|
-
<DialogTitle className="flex items-center gap-2">
|
|
626
|
-
<div className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold text-sm">
|
|
627
|
-
O
|
|
628
|
-
</div>
|
|
629
|
-
Connect Outlook Account
|
|
630
|
-
</DialogTitle>
|
|
631
|
-
<DialogDescription>
|
|
632
|
-
{outlookModalStep === 'credentials'
|
|
633
|
-
? 'Enter your Microsoft Azure App credentials to connect your Outlook account.'
|
|
634
|
-
: 'Follow the instructions to authorize the application.'}
|
|
635
|
-
</DialogDescription>
|
|
636
|
-
</DialogHeader>
|
|
637
|
-
|
|
638
|
-
{outlookModalStep === 'credentials' ? (
|
|
639
|
-
<>
|
|
640
|
-
<div className="space-y-4 py-4">
|
|
641
|
-
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
|
642
|
-
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
|
643
|
-
Note: You need an Azure App Registration for this to work.
|
|
644
|
-
</p>
|
|
645
|
-
</div>
|
|
646
|
-
|
|
647
|
-
<div className="space-y-3">
|
|
648
|
-
<div className="space-y-2">
|
|
649
|
-
<label className="text-sm font-medium">Client ID (Application ID)</label>
|
|
650
|
-
<Input
|
|
651
|
-
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
652
|
-
value={outlookClientId}
|
|
653
|
-
onChange={(e) => setOutlookClientId(e.target.value)}
|
|
654
|
-
/>
|
|
655
|
-
</div>
|
|
656
|
-
<div className="space-y-2">
|
|
657
|
-
<label className="text-sm font-medium">Tenant ID (Optional)</label>
|
|
658
|
-
<Input
|
|
659
|
-
placeholder="common"
|
|
660
|
-
value={outlookTenantId}
|
|
661
|
-
onChange={(e) => setOutlookTenantId(e.target.value)}
|
|
662
|
-
/>
|
|
663
|
-
<p className="text-[10px] text-muted-foreground">
|
|
664
|
-
Default is "common". Use your specific Tenant ID for organization accounts.
|
|
665
|
-
</p>
|
|
666
|
-
</div>
|
|
667
|
-
</div>
|
|
668
|
-
</div>
|
|
669
|
-
|
|
670
|
-
<DialogFooter>
|
|
671
|
-
<Button variant="outline" onClick={() => setShowOutlookModal(false)}>
|
|
672
|
-
Cancel
|
|
673
|
-
</Button>
|
|
674
|
-
<Button
|
|
675
|
-
onClick={handleSaveOutlookAndConnect}
|
|
676
|
-
disabled={savingOutlookCredentials || !outlookClientId}
|
|
677
|
-
>
|
|
678
|
-
{savingOutlookCredentials ? (
|
|
679
|
-
<LoadingSpinner size="sm" className="mr-2" />
|
|
680
|
-
) : (
|
|
681
|
-
<Check className="w-4 h-4 mr-2" />
|
|
682
|
-
)}
|
|
683
|
-
Save & Connect
|
|
684
|
-
</Button>
|
|
685
|
-
</DialogFooter>
|
|
686
|
-
</>
|
|
687
|
-
) : (
|
|
688
|
-
<>
|
|
689
|
-
{outlookDeviceCode && (
|
|
690
|
-
<div className="space-y-4 py-4">
|
|
691
|
-
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
|
692
|
-
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">
|
|
693
|
-
Action Required
|
|
694
|
-
</h4>
|
|
695
|
-
<p className="text-sm text-blue-800 dark:text-blue-200 mb-4">
|
|
696
|
-
{outlookDeviceCode.message}
|
|
697
|
-
</p>
|
|
698
|
-
<div className="flex flex-col gap-3">
|
|
699
|
-
<div className="flex items-center gap-2 bg-white dark:bg-black/20 p-2 rounded border border-blue-200 dark:border-blue-800">
|
|
700
|
-
<code className="text-lg font-mono font-bold flex-1 text-center select-all">
|
|
701
|
-
{outlookDeviceCode.userCode}
|
|
702
|
-
</code>
|
|
703
|
-
</div>
|
|
704
|
-
<Button
|
|
705
|
-
variant="default"
|
|
706
|
-
className="w-full bg-blue-600 hover:bg-blue-700"
|
|
707
|
-
onClick={() => window.open(outlookDeviceCode.verificationUri, '_blank')}
|
|
708
|
-
>
|
|
709
|
-
Open Microsoft Login
|
|
710
|
-
<ExternalLink className="w-4 h-4 ml-2" />
|
|
711
|
-
</Button>
|
|
712
|
-
</div>
|
|
713
|
-
</div>
|
|
714
|
-
<div className="flex justify-center">
|
|
715
|
-
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
716
|
-
<LoadingSpinner size="sm" />
|
|
717
|
-
Waiting for authorization...
|
|
718
|
-
</div>
|
|
719
|
-
</div>
|
|
720
|
-
</div>
|
|
721
|
-
)}
|
|
722
|
-
|
|
723
|
-
<DialogFooter>
|
|
724
|
-
<Button variant="outline" onClick={() => setShowOutlookModal(false)}>
|
|
725
|
-
Cancel
|
|
726
|
-
</Button>
|
|
727
|
-
</DialogFooter>
|
|
728
|
-
</>
|
|
729
|
-
)}
|
|
730
|
-
</DialogContent>
|
|
731
|
-
</Dialog>
|
|
732
|
-
|
|
733
|
-
{/* Create Rule Modal */}
|
|
734
|
-
<Dialog open={showRuleModal} onOpenChange={setShowRuleModal}>
|
|
735
|
-
<DialogContent>
|
|
736
|
-
<DialogHeader>
|
|
737
|
-
<DialogTitle>Create Auto-Pilot Rule</DialogTitle>
|
|
738
|
-
<DialogDescription>
|
|
739
|
-
Define a condition based on AI analysis to trigger an action.
|
|
740
|
-
</DialogDescription>
|
|
741
|
-
</DialogHeader>
|
|
742
|
-
|
|
743
|
-
<div className="space-y-4 py-4">
|
|
744
|
-
<div className="space-y-2">
|
|
745
|
-
<label className="text-sm font-medium">Rule Name</label>
|
|
746
|
-
<Input
|
|
747
|
-
placeholder="e.g. Archive Newsletters"
|
|
748
|
-
value={newRuleName}
|
|
749
|
-
onChange={(e) => setNewRuleName(e.target.value)}
|
|
750
|
-
/>
|
|
751
|
-
</div>
|
|
752
|
-
|
|
753
|
-
<div className="grid grid-cols-2 gap-4">
|
|
754
|
-
<div className="space-y-2">
|
|
755
|
-
<label className="text-sm font-medium">If Condition Field</label>
|
|
756
|
-
<select
|
|
757
|
-
className="w-full p-2 border rounded-md bg-background text-sm"
|
|
758
|
-
value={newRuleKey}
|
|
759
|
-
onChange={(e) => {
|
|
760
|
-
setNewRuleKey(e.target.value);
|
|
761
|
-
// Set default values for certain keys
|
|
762
|
-
if (e.target.value === 'category') setNewRuleValue('newsletter');
|
|
763
|
-
else if (e.target.value === 'sentiment') setNewRuleValue('Positive');
|
|
764
|
-
else if (e.target.value === 'priority') setNewRuleValue('High');
|
|
765
|
-
else setNewRuleValue('');
|
|
766
|
-
}}
|
|
767
|
-
>
|
|
768
|
-
<optgroup label="AI Analysis">
|
|
769
|
-
<option value="category">Category</option>
|
|
770
|
-
<option value="sentiment">Sentiment</option>
|
|
771
|
-
<option value="priority">Priority</option>
|
|
772
|
-
</optgroup>
|
|
773
|
-
<optgroup label="Metadata">
|
|
774
|
-
<option value="sender_email">Specific Email (Exact)</option>
|
|
775
|
-
<option value="sender_domain">Email Domain (@...)</option>
|
|
776
|
-
<option value="sender_contains">Sender contains...</option>
|
|
777
|
-
<option value="subject_contains">Subject contains...</option>
|
|
778
|
-
<option value="body_contains">Body contains...</option>
|
|
779
|
-
</optgroup>
|
|
780
|
-
</select>
|
|
781
|
-
</div>
|
|
782
|
-
<div className="space-y-2">
|
|
783
|
-
<label className="text-sm font-medium">Equals Value</label>
|
|
784
|
-
{newRuleKey === 'category' ? (
|
|
785
|
-
<select
|
|
786
|
-
className="w-full p-2 border rounded-md bg-background text-sm"
|
|
787
|
-
value={newRuleValue}
|
|
788
|
-
onChange={(e) => setNewRuleValue(e.target.value)}
|
|
789
|
-
>
|
|
790
|
-
<option value="newsletter">Newsletter</option>
|
|
791
|
-
<option value="spam">Spam</option>
|
|
792
|
-
<option value="promotional">Promotional</option>
|
|
793
|
-
<option value="transactional">Transactional</option>
|
|
794
|
-
<option value="social">Social</option>
|
|
795
|
-
<option value="support">Support</option>
|
|
796
|
-
<option value="client">Client</option>
|
|
797
|
-
<option value="internal">Internal</option>
|
|
798
|
-
<option value="personal">Personal</option>
|
|
799
|
-
<option value="other">Other</option>
|
|
800
|
-
</select>
|
|
801
|
-
) : newRuleKey === 'sentiment' ? (
|
|
802
|
-
<select
|
|
803
|
-
className="w-full p-2 border rounded-md bg-background text-sm"
|
|
804
|
-
value={newRuleValue}
|
|
805
|
-
onChange={(e) => setNewRuleValue(e.target.value)}
|
|
806
|
-
>
|
|
807
|
-
<option value="Positive">Positive</option>
|
|
808
|
-
<option value="Neutral">Neutral</option>
|
|
809
|
-
<option value="Negative">Negative</option>
|
|
810
|
-
</select>
|
|
811
|
-
) : newRuleKey === 'priority' ? (
|
|
812
|
-
<select
|
|
813
|
-
className="w-full p-2 border rounded-md bg-background text-sm"
|
|
814
|
-
value={newRuleValue}
|
|
815
|
-
onChange={(e) => setNewRuleValue(e.target.value)}
|
|
816
|
-
>
|
|
817
|
-
<option value="High">High</option>
|
|
818
|
-
<option value="Medium">Medium</option>
|
|
819
|
-
<option value="Low">Low</option>
|
|
820
|
-
</select>
|
|
821
|
-
) : (
|
|
822
|
-
<Input
|
|
823
|
-
placeholder={
|
|
824
|
-
newRuleKey === 'sender_domain' ? 'rta.vn' :
|
|
825
|
-
newRuleKey === 'sender_email' ? 'john@example.com' :
|
|
826
|
-
'Keywords...'
|
|
827
|
-
}
|
|
828
|
-
value={newRuleValue}
|
|
829
|
-
onChange={(e) => setNewRuleValue(e.target.value)}
|
|
830
|
-
/>
|
|
831
|
-
)}
|
|
832
|
-
</div>
|
|
833
|
-
</div>
|
|
834
|
-
|
|
835
|
-
<div className="space-y-2">
|
|
836
|
-
<label className="text-sm font-medium flex items-center gap-2">
|
|
837
|
-
<Clock className="w-4 h-4" />
|
|
838
|
-
Only if email is older than... (Optional)
|
|
839
|
-
</label>
|
|
840
|
-
<div className="flex items-center gap-2">
|
|
841
|
-
<Input
|
|
842
|
-
type="number"
|
|
843
|
-
min="0"
|
|
844
|
-
placeholder="0"
|
|
845
|
-
className="w-24"
|
|
846
|
-
value={newRuleOlderThan}
|
|
847
|
-
onChange={(e) => setNewRuleOlderThan(e.target.value)}
|
|
848
|
-
/>
|
|
849
|
-
<span className="text-sm text-muted-foreground">days</span>
|
|
850
|
-
</div>
|
|
851
|
-
<p className="text-[10px] text-muted-foreground">
|
|
852
|
-
Leave empty or 0 to apply rule immediately upon receipt.
|
|
853
|
-
</p>
|
|
854
|
-
</div>
|
|
855
|
-
|
|
856
|
-
<div className="space-y-2">
|
|
857
|
-
<label className="text-sm font-medium">Then Perform Action</label>
|
|
858
|
-
<select
|
|
859
|
-
className="w-full p-2 border rounded-md bg-background"
|
|
860
|
-
value={newRuleAction}
|
|
861
|
-
onChange={(e) => setNewRuleAction(e.target.value)}
|
|
862
|
-
>
|
|
863
|
-
<option value="archive">Archive Email</option>
|
|
864
|
-
<option value="delete">Delete Email</option>
|
|
865
|
-
<option value="draft">Draft Reply</option>
|
|
866
|
-
<option value="read">Mark as Read</option>
|
|
867
|
-
<option value="star">Star / Flag</option>
|
|
868
|
-
</select>
|
|
869
|
-
</div>
|
|
870
|
-
|
|
871
|
-
{newRuleAction === 'draft' && (
|
|
872
|
-
<>
|
|
873
|
-
<div className="space-y-2 animate-in slide-in-from-top-2 duration-200">
|
|
874
|
-
<label className="text-sm font-medium">Draft Instructions (Context)</label>
|
|
875
|
-
<textarea
|
|
876
|
-
className="w-full p-2 border rounded-md bg-background min-h-[80px] text-sm"
|
|
877
|
-
placeholder="e.g. Tell them I'm busy until Friday, but interested in the proposal."
|
|
878
|
-
value={newRuleInstructions}
|
|
879
|
-
onChange={(e) => setNewRuleInstructions(e.target.value)}
|
|
880
|
-
/>
|
|
881
|
-
<p className="text-[10px] text-muted-foreground">
|
|
882
|
-
Specific context for the AI ghostwriter.
|
|
883
|
-
</p>
|
|
884
|
-
</div>
|
|
885
|
-
|
|
886
|
-
<div className="space-y-2 animate-in slide-in-from-top-2 duration-200">
|
|
887
|
-
<label className="text-sm font-medium flex items-center gap-2">
|
|
888
|
-
<Paperclip className="w-4 h-4" />
|
|
889
|
-
Attachments (Optional)
|
|
890
|
-
</label>
|
|
891
|
-
|
|
892
|
-
<div className="flex flex-col gap-2">
|
|
893
|
-
{newRuleAttachments.map(file => (
|
|
894
|
-
<div key={file.path} className="flex items-center justify-between p-2 bg-secondary/50 rounded border text-xs">
|
|
895
|
-
<span className="truncate max-w-[200px]">{file.name}</span>
|
|
896
|
-
<Button
|
|
897
|
-
variant="ghost"
|
|
898
|
-
size="sm"
|
|
899
|
-
className="h-6 w-6 p-0 text-destructive"
|
|
900
|
-
onClick={() => removeAttachment(file.path)}
|
|
901
|
-
>
|
|
902
|
-
<X className="w-3 h-3" />
|
|
903
|
-
</Button>
|
|
904
|
-
</div>
|
|
905
|
-
))}
|
|
906
|
-
|
|
907
|
-
<div className="relative">
|
|
908
|
-
<input
|
|
909
|
-
type="file"
|
|
910
|
-
className="absolute inset-0 opacity-0 cursor-pointer"
|
|
911
|
-
onChange={handleFileUpload}
|
|
912
|
-
disabled={isUploading}
|
|
913
|
-
/>
|
|
914
|
-
<Button
|
|
915
|
-
variant="outline"
|
|
916
|
-
size="sm"
|
|
917
|
-
className="w-full border-dashed"
|
|
918
|
-
disabled={isUploading}
|
|
919
|
-
>
|
|
920
|
-
{isUploading ? (
|
|
921
|
-
<LoadingSpinner size="sm" className="mr-2" />
|
|
922
|
-
) : (
|
|
923
|
-
<Plus className="w-3 h-3 mr-2" />
|
|
924
|
-
)}
|
|
925
|
-
{isUploading ? 'Uploading...' : 'Add Attachment'}
|
|
926
|
-
</Button>
|
|
927
|
-
</div>
|
|
928
|
-
</div>
|
|
929
|
-
<p className="text-[10px] text-muted-foreground">
|
|
930
|
-
Files will be attached to every draft generated by this rule.
|
|
931
|
-
</p>
|
|
932
|
-
</div>
|
|
933
|
-
</>
|
|
934
|
-
)}
|
|
935
|
-
</div>
|
|
936
|
-
|
|
937
|
-
<DialogFooter>
|
|
938
|
-
<Button variant="outline" onClick={() => setShowRuleModal(false)}>
|
|
939
|
-
Cancel
|
|
940
|
-
</Button>
|
|
941
|
-
<Button onClick={handleCreateRule} disabled={savingRule}>
|
|
942
|
-
{savingRule ? <LoadingSpinner size="sm" className="mr-2" /> : <Plus className="w-4 h-4 mr-2" />}
|
|
943
|
-
Create Rule
|
|
944
|
-
</Button>
|
|
945
|
-
</DialogFooter>
|
|
946
|
-
</DialogContent>
|
|
947
|
-
</Dialog>
|
|
948
|
-
|
|
949
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
950
|
-
{/* Email Accounts Section */}
|
|
951
|
-
<Card>
|
|
952
|
-
<CardHeader>
|
|
953
|
-
<CardTitle className="flex items-center gap-2">
|
|
954
|
-
<Database className="w-5 h-5 text-primary" />
|
|
955
|
-
Connected Accounts
|
|
956
|
-
</CardTitle>
|
|
957
|
-
<CardDescription>Manage your email providers</CardDescription>
|
|
958
|
-
</CardHeader>
|
|
959
|
-
<CardContent className="space-y-4">
|
|
960
|
-
{state.accounts.length === 0 ? (
|
|
961
|
-
<p className="text-sm text-muted-foreground text-center py-4">
|
|
962
|
-
No accounts connected yet
|
|
963
|
-
</p>
|
|
964
|
-
) : (
|
|
965
|
-
state.accounts.map((account: EmailAccount) => (
|
|
966
|
-
<div
|
|
967
|
-
key={account.id}
|
|
968
|
-
className="flex items-center justify-between p-4 border rounded-lg bg-card"
|
|
969
|
-
>
|
|
970
|
-
<div className="flex items-center gap-3">
|
|
971
|
-
{getProviderIcon(account.provider)}
|
|
972
|
-
<div>
|
|
973
|
-
<h4 className="font-medium capitalize">{account.provider}</h4>
|
|
974
|
-
<p className="text-xs text-muted-foreground">
|
|
975
|
-
{account.email_address}
|
|
976
|
-
</p>
|
|
977
|
-
</div>
|
|
978
|
-
</div>
|
|
979
|
-
<div className="flex items-center gap-2">
|
|
980
|
-
{account.is_active ? (
|
|
981
|
-
<span className="text-xs text-emerald-600 bg-emerald-500/10 px-2 py-1 rounded-full">
|
|
982
|
-
Active
|
|
983
|
-
</span>
|
|
984
|
-
) : (
|
|
985
|
-
<span className="text-xs text-yellow-600 bg-yellow-500/10 px-2 py-1 rounded-full">
|
|
986
|
-
Inactive
|
|
987
|
-
</span>
|
|
988
|
-
)}
|
|
989
|
-
<Button
|
|
990
|
-
variant="outline"
|
|
991
|
-
size="sm"
|
|
992
|
-
className="text-destructive hover:text-destructive"
|
|
993
|
-
onClick={() => handleDisconnect(account.id)}
|
|
994
|
-
>
|
|
995
|
-
<Trash2 className="w-4 h-4" />
|
|
996
|
-
</Button>
|
|
997
|
-
</div>
|
|
998
|
-
</div>
|
|
999
|
-
))
|
|
1000
|
-
)}
|
|
1001
|
-
|
|
1002
|
-
<div className="flex flex-col gap-2">
|
|
1003
|
-
<Button
|
|
1004
|
-
className="w-full border-dashed"
|
|
1005
|
-
variant="outline"
|
|
1006
|
-
onClick={handleConnectGmail}
|
|
1007
|
-
disabled={isConnecting || isOutlookConnecting}
|
|
1008
|
-
>
|
|
1009
|
-
{isConnecting ? (
|
|
1010
|
-
<LoadingSpinner size="sm" className="mr-2" />
|
|
1011
|
-
) : (
|
|
1012
|
-
<Plus className="w-4 h-4 mr-2" />
|
|
1013
|
-
)}
|
|
1014
|
-
Connect Gmail Account
|
|
1015
|
-
</Button>
|
|
1016
|
-
|
|
1017
|
-
<Button
|
|
1018
|
-
className="w-full border-dashed"
|
|
1019
|
-
variant="outline"
|
|
1020
|
-
onClick={handleConnectOutlook}
|
|
1021
|
-
disabled={isConnecting || isOutlookConnecting}
|
|
1022
|
-
>
|
|
1023
|
-
{isOutlookConnecting && !outlookDeviceCode ? (
|
|
1024
|
-
<LoadingSpinner size="sm" className="mr-2" />
|
|
1025
|
-
) : (
|
|
1026
|
-
<Plus className="w-4 h-4 mr-2" />
|
|
1027
|
-
)}
|
|
1028
|
-
Connect Outlook Account
|
|
1029
|
-
</Button>
|
|
1030
|
-
</div>
|
|
1031
|
-
|
|
1032
|
-
{outlookDeviceCode && (
|
|
1033
|
-
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg animate-in slide-in-from-top-2">
|
|
1034
|
-
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">
|
|
1035
|
-
Microsoft Sign-In Required
|
|
1036
|
-
</h4>
|
|
1037
|
-
<p className="text-sm text-blue-800 dark:text-blue-200 mb-4">
|
|
1038
|
-
{outlookDeviceCode.message}
|
|
1039
|
-
</p>
|
|
1040
|
-
<div className="flex flex-col gap-3">
|
|
1041
|
-
<div className="flex items-center gap-2 bg-white dark:bg-black/20 p-2 rounded border border-blue-200 dark:border-blue-800">
|
|
1042
|
-
<code className="text-lg font-mono font-bold flex-1 text-center select-all">
|
|
1043
|
-
{outlookDeviceCode.userCode}
|
|
1044
|
-
</code>
|
|
1045
|
-
</div>
|
|
1046
|
-
<Button
|
|
1047
|
-
variant="default"
|
|
1048
|
-
className="w-full bg-blue-600 hover:bg-blue-700"
|
|
1049
|
-
onClick={() => window.open(outlookDeviceCode.verificationUri, '_blank')}
|
|
1050
|
-
>
|
|
1051
|
-
Open Microsoft Login
|
|
1052
|
-
<ExternalLink className="w-4 h-4 ml-2" />
|
|
1053
|
-
</Button>
|
|
1054
|
-
<p className="text-xs text-center text-muted-foreground mt-2">
|
|
1055
|
-
Waiting for you to sign in...
|
|
1056
|
-
</p>
|
|
1057
|
-
</div>
|
|
1058
|
-
</div>
|
|
1059
|
-
)}
|
|
1060
|
-
</CardContent>
|
|
1061
|
-
</Card>
|
|
1062
|
-
|
|
1063
|
-
{/* Auto-Pilot Rules Section */}
|
|
1064
|
-
<Card>
|
|
1065
|
-
<CardHeader>
|
|
1066
|
-
<CardTitle className="flex items-center gap-2">
|
|
1067
|
-
<ShieldCheck className="w-5 h-5 text-emerald-500" />
|
|
1068
|
-
Auto-Pilot Rules
|
|
1069
|
-
</CardTitle>
|
|
1070
|
-
<CardDescription>Configure AI automation behavior</CardDescription>
|
|
1071
|
-
</CardHeader>
|
|
1072
|
-
<CardContent className="space-y-4">
|
|
1073
|
-
{/* Built-in toggles */}
|
|
1074
|
-
<div className="flex justify-between items-center py-3 border-b border-border">
|
|
1075
|
-
<div>
|
|
1076
|
-
<h4 className="font-medium text-sm">Auto-Trash Spam</h4>
|
|
1077
|
-
<p className="text-xs text-muted-foreground">
|
|
1078
|
-
Automatically delete emails categorized as spam
|
|
1079
|
-
</p>
|
|
1080
|
-
</div>
|
|
1081
|
-
<Button
|
|
1082
|
-
variant={state.rules.find(r => r.name === 'Auto-Trash Spam')?.is_enabled ? 'default' : 'outline'}
|
|
1083
|
-
size="sm"
|
|
1084
|
-
onClick={handleToggleSpam}
|
|
1085
|
-
disabled={loadingSetting === 'auto_trash_spam'}
|
|
1086
|
-
>
|
|
1087
|
-
{loadingSetting === 'auto_trash_spam' ? (
|
|
1088
|
-
<LoadingSpinner size="sm" className="mr-1" />
|
|
1089
|
-
) : (
|
|
1090
|
-
<Power className="w-4 h-4 mr-1" />
|
|
1091
|
-
)}
|
|
1092
|
-
{state.rules.find(r => r.name === 'Auto-Trash Spam')?.is_enabled ? 'On' : 'Off'}
|
|
1093
|
-
</Button>
|
|
1094
|
-
</div>
|
|
1095
|
-
|
|
1096
|
-
<div className="flex justify-between items-center py-3 border-b border-border">
|
|
1097
|
-
<div>
|
|
1098
|
-
<h4 className="font-medium text-sm">Smart Drafts</h4>
|
|
1099
|
-
<p className="text-xs text-muted-foreground">
|
|
1100
|
-
Generate draft replies for important emails
|
|
1101
|
-
</p>
|
|
1102
|
-
</div>
|
|
1103
|
-
<Button
|
|
1104
|
-
variant={state.rules.find(r => r.name === 'Smart Drafts')?.is_enabled ? 'default' : 'outline'}
|
|
1105
|
-
size="sm"
|
|
1106
|
-
onClick={handleToggleDrafts}
|
|
1107
|
-
disabled={loadingSetting === 'smart_drafts'}
|
|
1108
|
-
>
|
|
1109
|
-
{loadingSetting === 'smart_drafts' ? (
|
|
1110
|
-
<LoadingSpinner size="sm" className="mr-1" />
|
|
1111
|
-
) : (
|
|
1112
|
-
<Power className="w-4 h-4 mr-1" />
|
|
1113
|
-
)}
|
|
1114
|
-
{state.rules.find(r => r.name === 'Smart Drafts')?.is_enabled ? 'On' : 'Off'}
|
|
1115
|
-
</Button>
|
|
1116
|
-
</div>
|
|
1117
|
-
|
|
1118
|
-
{/* Custom Rules */}
|
|
1119
|
-
<div className="pt-2">
|
|
1120
|
-
<div className="flex items-center justify-between mb-2">
|
|
1121
|
-
<h4 className="text-sm font-medium">Custom Rules</h4>
|
|
1122
|
-
<Button variant="ghost" size="sm" onClick={() => setShowRuleModal(true)}>
|
|
1123
|
-
<Plus className="w-4 h-4 mr-1" /> Add Rule
|
|
1124
|
-
</Button>
|
|
1125
|
-
</div>
|
|
1126
|
-
|
|
1127
|
-
{state.rules.length === 0 && (
|
|
1128
|
-
<p className="text-xs text-muted-foreground text-center py-2 border border-dashed rounded-lg">
|
|
1129
|
-
No custom rules yet
|
|
1130
|
-
</p>
|
|
1131
|
-
)}
|
|
1132
|
-
|
|
1133
|
-
{state.rules.length > 0 && state.rules
|
|
1134
|
-
.filter(r => r.name !== 'Auto-Trash Spam' && r.name !== 'Smart Drafts')
|
|
1135
|
-
.map((rule: Rule) => (
|
|
1136
|
-
<div
|
|
1137
|
-
key={rule.id}
|
|
1138
|
-
className="p-3 bg-secondary/30 rounded-lg mb-2"
|
|
1139
|
-
>
|
|
1140
|
-
<div className="flex justify-between items-center mb-1">
|
|
1141
|
-
<div>
|
|
1142
|
-
<span className="text-sm font-medium">{rule.name}</span>
|
|
1143
|
-
<span className="text-xs text-muted-foreground ml-2">
|
|
1144
|
-
→ {rule.action}
|
|
1145
|
-
</span>
|
|
1146
|
-
</div>
|
|
1147
|
-
<div className="flex items-center gap-2">
|
|
1148
|
-
<Button
|
|
1149
|
-
variant={rule.is_enabled ? 'default' : 'outline'}
|
|
1150
|
-
size="sm"
|
|
1151
|
-
onClick={() => handleToggleRule(rule.id)}
|
|
1152
|
-
className="h-7 px-2"
|
|
1153
|
-
>
|
|
1154
|
-
<Power className="w-3.5 h-3.5 mr-1" />
|
|
1155
|
-
{rule.is_enabled ? 'On' : 'Off'}
|
|
1156
|
-
</Button>
|
|
1157
|
-
<Button
|
|
1158
|
-
variant="ghost"
|
|
1159
|
-
size="sm"
|
|
1160
|
-
className="text-destructive"
|
|
1161
|
-
onClick={() => actions.deleteRule(rule.id)}
|
|
1162
|
-
>
|
|
1163
|
-
<Trash2 className="w-3 h-3" />
|
|
1164
|
-
</Button>
|
|
1165
|
-
</div>
|
|
1166
|
-
</div>
|
|
1167
|
-
{rule.instructions && (
|
|
1168
|
-
<p className="text-[10px] text-muted-foreground italic border-t border-border/50 pt-1 mt-1 truncate">
|
|
1169
|
-
"{rule.instructions}"
|
|
1170
|
-
</p>
|
|
1171
|
-
)}
|
|
1172
|
-
{rule.attachments && rule.attachments.length > 0 && (
|
|
1173
|
-
<div className="flex gap-1 mt-1">
|
|
1174
|
-
{rule.attachments.map(a => (
|
|
1175
|
-
<div key={a.path} className="flex items-center gap-1 text-[9px] bg-blue-500/10 text-blue-400 px-1.5 py-0.5 rounded border border-blue-500/20">
|
|
1176
|
-
<Paperclip className="w-2.5 h-2.5" />
|
|
1177
|
-
{a.name}
|
|
1178
|
-
</div>
|
|
1179
|
-
))}
|
|
1180
|
-
</div>
|
|
1181
|
-
)}
|
|
1182
|
-
</div>
|
|
1183
|
-
))}
|
|
1184
|
-
</div>
|
|
1185
|
-
|
|
1186
|
-
</CardContent>
|
|
1187
|
-
</Card>
|
|
1188
|
-
</div>
|
|
1189
|
-
|
|
1190
|
-
{/* LLM Settings Section */}
|
|
1191
|
-
<Card>
|
|
1192
|
-
<CardHeader>
|
|
1193
|
-
<CardTitle className="flex items-center gap-2">
|
|
1194
|
-
<RefreshCw className="w-5 h-5 text-indigo-500" />
|
|
1195
|
-
Model Configuration
|
|
1196
|
-
</CardTitle>
|
|
1197
|
-
<CardDescription>Configure Local LLM or API settings</CardDescription>
|
|
1198
|
-
</CardHeader>
|
|
1199
|
-
<CardContent className="space-y-4">
|
|
1200
|
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
1201
|
-
<div className="space-y-2">
|
|
1202
|
-
<label className="text-sm font-medium">Model Name</label>
|
|
1203
|
-
<Input
|
|
1204
|
-
placeholder="gpt-4o-mini"
|
|
1205
|
-
value={localSettings.llm_model || ''}
|
|
1206
|
-
onChange={(e) => setLocalSettings(s => ({ ...s, llm_model: e.target.value }))}
|
|
1207
|
-
/>
|
|
1208
|
-
</div>
|
|
1209
|
-
<div className="space-y-2">
|
|
1210
|
-
<label className="text-sm font-medium">Base URL</label>
|
|
1211
|
-
<Input
|
|
1212
|
-
placeholder="https://api.openai.com/v1"
|
|
1213
|
-
value={localSettings.llm_base_url || ''}
|
|
1214
|
-
onChange={(e) => setLocalSettings(s => ({ ...s, llm_base_url: e.target.value }))}
|
|
1215
|
-
/>
|
|
1216
|
-
<p className="text-[10px] text-muted-foreground">
|
|
1217
|
-
Use http://localhost:11434/v1 for Ollama
|
|
1218
|
-
</p>
|
|
1219
|
-
</div>
|
|
1220
|
-
<div className="space-y-2">
|
|
1221
|
-
<label className="text-sm font-medium">API Key</label>
|
|
1222
|
-
<Input
|
|
1223
|
-
type="password"
|
|
1224
|
-
placeholder="sk-..."
|
|
1225
|
-
value={localSettings.llm_api_key || ''}
|
|
1226
|
-
onChange={(e) => setLocalSettings(s => ({ ...s, llm_api_key: e.target.value }))}
|
|
1227
|
-
/>
|
|
1228
|
-
</div>
|
|
1229
|
-
</div>
|
|
1230
|
-
<div className="flex justify-end mt-4 gap-2">
|
|
1231
|
-
<Button
|
|
1232
|
-
variant="outline"
|
|
1233
|
-
onClick={handleTestConnection}
|
|
1234
|
-
disabled={testingLlm}
|
|
1235
|
-
>
|
|
1236
|
-
{testingLlm ? (
|
|
1237
|
-
<LoadingSpinner size="sm" className="mr-2" />
|
|
1238
|
-
) : (
|
|
1239
|
-
<RefreshCw className="w-4 h-4 mr-2" />
|
|
1240
|
-
)}
|
|
1241
|
-
Check Connection
|
|
1242
|
-
</Button>
|
|
1243
|
-
<Button onClick={handleSaveSettings} disabled={savingSettings}>
|
|
1244
|
-
{savingSettings ? (
|
|
1245
|
-
<LoadingSpinner size="sm" className="mr-2" />
|
|
1246
|
-
) : (
|
|
1247
|
-
<Check className="w-4 h-4 mr-2" />
|
|
1248
|
-
)}
|
|
1249
|
-
Save Configuration
|
|
1250
|
-
</Button>
|
|
1251
|
-
</div>
|
|
1252
|
-
</CardContent>
|
|
1253
|
-
</Card>
|
|
1254
|
-
|
|
1255
|
-
{/* Provider Credentials (BYOK) Section */}
|
|
1256
|
-
<div ref={credentialsRef}>
|
|
1257
|
-
<Card>
|
|
1258
|
-
<CardHeader>
|
|
1259
|
-
<CardTitle className="flex items-center gap-2">
|
|
1260
|
-
<ShieldCheck className="w-5 h-5 text-orange-500" />
|
|
1261
|
-
Provider Credentials (Advanced)
|
|
1262
|
-
</CardTitle>
|
|
1263
|
-
<CardDescription>
|
|
1264
|
-
Bring Your Own Keys (BYOK). Configure your own OAuth credentials here.
|
|
1265
|
-
Leave empty to use system defaults.
|
|
1266
|
-
</CardDescription>
|
|
1267
|
-
</CardHeader>
|
|
1268
|
-
<CardContent className="space-y-6">
|
|
1269
|
-
{/* Google */}
|
|
1270
|
-
<div className="space-y-4 border-b pb-4">
|
|
1271
|
-
<h4 className="font-medium flex items-center gap-2">
|
|
1272
|
-
<span className="w-2 h-2 rounded-full bg-red-500" /> Google / Gmail
|
|
1273
|
-
</h4>
|
|
1274
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
1275
|
-
<div className="space-y-2">
|
|
1276
|
-
<label className="text-sm font-medium">Client ID</label>
|
|
1277
|
-
<Input
|
|
1278
|
-
type="password"
|
|
1279
|
-
placeholder="...apps.googleusercontent.com"
|
|
1280
|
-
value={localSettings.google_client_id || ''}
|
|
1281
|
-
onChange={(e) => setLocalSettings(s => ({ ...s, google_client_id: e.target.value }))}
|
|
1282
|
-
/>
|
|
1283
|
-
</div>
|
|
1284
|
-
<div className="space-y-2">
|
|
1285
|
-
<label className="text-sm font-medium">Client Secret</label>
|
|
1286
|
-
<Input
|
|
1287
|
-
type="password"
|
|
1288
|
-
placeholder="GOCSPX-..."
|
|
1289
|
-
value={localSettings.google_client_secret || ''}
|
|
1290
|
-
onChange={(e) => setLocalSettings(s => ({ ...s, google_client_secret: e.target.value }))}
|
|
1291
|
-
/>
|
|
1292
|
-
</div>
|
|
1293
|
-
</div>
|
|
1294
|
-
</div>
|
|
1295
|
-
|
|
1296
|
-
{/* Microsoft */}
|
|
1297
|
-
<div className="space-y-4">
|
|
1298
|
-
<h4 className="font-medium flex items-center gap-2">
|
|
1299
|
-
<span className="w-2 h-2 rounded-full bg-blue-500" /> Microsoft / Outlook
|
|
1300
|
-
</h4>
|
|
1301
|
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
1302
|
-
<div className="space-y-2">
|
|
1303
|
-
<label className="text-sm font-medium">Client ID</label>
|
|
1304
|
-
<Input
|
|
1305
|
-
type="password"
|
|
1306
|
-
value={localSettings.microsoft_client_id || ''}
|
|
1307
|
-
onChange={(e) => setLocalSettings(s => ({ ...s, microsoft_client_id: e.target.value }))}
|
|
1308
|
-
/>
|
|
1309
|
-
</div>
|
|
1310
|
-
<div className="space-y-2">
|
|
1311
|
-
<label className="text-sm font-medium">Client Secret (Optional)</label>
|
|
1312
|
-
<Input
|
|
1313
|
-
type="password"
|
|
1314
|
-
value={localSettings.microsoft_client_secret || ''}
|
|
1315
|
-
onChange={(e) => setLocalSettings(s => ({ ...s, microsoft_client_secret: e.target.value }))}
|
|
1316
|
-
/>
|
|
1317
|
-
</div>
|
|
1318
|
-
<div className="space-y-2">
|
|
1319
|
-
<label className="text-sm font-medium">Tenant ID</label>
|
|
1320
|
-
<Input
|
|
1321
|
-
placeholder="common"
|
|
1322
|
-
value={localSettings.microsoft_tenant_id || ''}
|
|
1323
|
-
onChange={(e) => setLocalSettings(s => ({ ...s, microsoft_tenant_id: e.target.value }))}
|
|
1324
|
-
/>
|
|
1325
|
-
</div>
|
|
1326
|
-
</div>
|
|
1327
|
-
</div>
|
|
1328
|
-
|
|
1329
|
-
<div className="flex justify-end mt-4">
|
|
1330
|
-
<Button onClick={handleSaveSettings} disabled={savingSettings} variant="secondary">
|
|
1331
|
-
{savingSettings ? (
|
|
1332
|
-
<LoadingSpinner size="sm" className="mr-2" />
|
|
1333
|
-
) : (
|
|
1334
|
-
<Check className="w-4 h-4 mr-2" />
|
|
1335
|
-
)}
|
|
1336
|
-
Save Credentials
|
|
1337
|
-
</Button>
|
|
1338
|
-
</div>
|
|
1339
|
-
</CardContent>
|
|
1340
|
-
</Card>
|
|
1341
|
-
</div>
|
|
1342
|
-
</div>
|
|
1343
|
-
);
|
|
1344
|
-
}
|
|
1345
|
-
|