@realtimex/email-automator 2.1.1 → 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.
Files changed (48) hide show
  1. package/api/server.ts +0 -6
  2. package/api/src/config/index.ts +3 -0
  3. package/bin/email-automator-setup.js +2 -3
  4. package/bin/email-automator.js +23 -7
  5. package/package.json +1 -2
  6. package/src/App.tsx +0 -622
  7. package/src/components/AccountSettings.tsx +0 -310
  8. package/src/components/AccountSettingsPage.tsx +0 -390
  9. package/src/components/Configuration.tsx +0 -1345
  10. package/src/components/Dashboard.tsx +0 -940
  11. package/src/components/ErrorBoundary.tsx +0 -71
  12. package/src/components/LiveTerminal.tsx +0 -308
  13. package/src/components/LoadingSpinner.tsx +0 -39
  14. package/src/components/Login.tsx +0 -371
  15. package/src/components/Logo.tsx +0 -57
  16. package/src/components/SetupWizard.tsx +0 -388
  17. package/src/components/Toast.tsx +0 -109
  18. package/src/components/migration/MigrationBanner.tsx +0 -97
  19. package/src/components/migration/MigrationModal.tsx +0 -458
  20. package/src/components/migration/MigrationPulseIndicator.tsx +0 -38
  21. package/src/components/mode-toggle.tsx +0 -24
  22. package/src/components/theme-provider.tsx +0 -72
  23. package/src/components/ui/alert.tsx +0 -66
  24. package/src/components/ui/button.tsx +0 -57
  25. package/src/components/ui/card.tsx +0 -75
  26. package/src/components/ui/dialog.tsx +0 -133
  27. package/src/components/ui/input.tsx +0 -22
  28. package/src/components/ui/label.tsx +0 -24
  29. package/src/components/ui/otp-input.tsx +0 -184
  30. package/src/context/AppContext.tsx +0 -422
  31. package/src/context/MigrationContext.tsx +0 -53
  32. package/src/context/TerminalContext.tsx +0 -31
  33. package/src/core/actions.ts +0 -76
  34. package/src/core/auth.ts +0 -108
  35. package/src/core/intelligence.ts +0 -76
  36. package/src/core/processor.ts +0 -112
  37. package/src/hooks/useRealtimeEmails.ts +0 -111
  38. package/src/index.css +0 -140
  39. package/src/lib/api-config.ts +0 -42
  40. package/src/lib/api-old.ts +0 -228
  41. package/src/lib/api.ts +0 -421
  42. package/src/lib/migration-check.ts +0 -264
  43. package/src/lib/sounds.ts +0 -120
  44. package/src/lib/supabase-config.ts +0 -117
  45. package/src/lib/supabase.ts +0 -28
  46. package/src/lib/types.ts +0 -166
  47. package/src/lib/utils.ts +0 -6
  48. 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
-