@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.
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,940 +0,0 @@
1
- import { useEffect, useState, useCallback } from 'react';
2
- import { Mail, ShieldCheck, Trash2, Send, RefreshCw, Archive, Flag, Search, ChevronLeft, ChevronRight, Loader2, Settings2, Calendar, Hash, AlertCircle, CheckCircle2, RotateCcw, Eye, Cpu, Clock, Code, Brain, Zap, Info, ExternalLink } from 'lucide-react';
3
- import { Button } from './ui/button';
4
- import { Card } from './ui/card';
5
- import { Input } from './ui/input';
6
- import { useApp } from '../context/AppContext';
7
- import { useTerminal } from '../context/TerminalContext';
8
- import { api } from '../lib/api';
9
- import { toast } from './Toast';
10
- import { LoadingSpinner, CardLoader } from './LoadingSpinner';
11
- import { EmailAccount, Email, UserSettings, ProcessingEvent } from '../lib/types';
12
- import { cn } from '../lib/utils';
13
- import { useRealtimeEmails } from '../hooks/useRealtimeEmails';
14
- import { sounds } from '../lib/sounds';
15
- import {
16
- Dialog,
17
- DialogContent,
18
- DialogDescription,
19
- DialogHeader,
20
- DialogTitle,
21
- } from './ui/dialog';
22
-
23
- export function AITraceModal({
24
- email,
25
- isOpen,
26
- onOpenChange
27
- }: {
28
- email: Email | null,
29
- isOpen: boolean,
30
- onOpenChange: (open: boolean) => void
31
- }) {
32
- const [events, setEvents] = useState<ProcessingEvent[]>([]);
33
- const [isLoading, setIsLoading] = useState(false);
34
-
35
- useEffect(() => {
36
- if (isOpen && email) {
37
- fetchEvents();
38
- }
39
- }, [isOpen, email]);
40
-
41
- const fetchEvents = async () => {
42
- if (!email) return;
43
- setIsLoading(true);
44
- try {
45
- const response = await api.getEmailEvents(email.id);
46
- if (response.data) {
47
- setEvents(response.data.events);
48
- }
49
- } catch (error) {
50
- console.error('Failed to fetch trace:', error);
51
- } finally {
52
- setIsLoading(false);
53
- }
54
- };
55
-
56
- const getIcon = (type: string) => {
57
- switch (type) {
58
- case 'analysis': return <Brain className="w-4 h-4 text-purple-500" />;
59
- case 'action': return <Zap className="w-4 h-4 text-emerald-500" />;
60
- case 'error': return <AlertCircle className="w-4 h-4 text-red-500" />;
61
- default: return <Info className="w-4 h-4 text-blue-500" />;
62
- }
63
- };
64
-
65
- return (
66
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
67
- <DialogContent className="sm:max-w-2xl max-h-[85vh] flex flex-col p-0 overflow-hidden">
68
- <DialogHeader className="p-6 border-b">
69
- <div className="flex items-center gap-2">
70
- <Cpu className="w-5 h-5 text-primary" />
71
- <DialogTitle>AI Processing Trace</DialogTitle>
72
- </div>
73
- <DialogDescription>
74
- Step-by-step log of how the AI analyzed and acted on this email.
75
- </DialogDescription>
76
- </DialogHeader>
77
-
78
- <div className="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar bg-secondary/5">
79
- {isLoading ? (
80
- <div className="py-20 flex justify-center"><LoadingSpinner /></div>
81
- ) : events.length === 0 ? (
82
- <div className="py-20 text-center text-muted-foreground italic font-mono text-sm">
83
- No granular trace events found for this email.
84
- </div>
85
- ) : (
86
- events.map((event, i) => (
87
- <div key={event.id} className="relative pl-8">
88
- {/* Timeline Line */}
89
- {i !== events.length - 1 && (
90
- <div className="absolute left-[15px] top-8 bottom-[-24px] w-px bg-border" />
91
- )}
92
-
93
- {/* Icon Badge */}
94
- <div className="absolute left-0 top-0 w-8 h-8 rounded-full border bg-background flex items-center justify-center z-10 shadow-sm">
95
- {getIcon(event.event_type)}
96
- </div>
97
-
98
- <div className="space-y-2">
99
- <div className="flex items-center justify-between">
100
- <span className="text-xs font-bold uppercase tracking-wider text-foreground/70">
101
- {event.agent_state}
102
- </span>
103
- <span className="text-[10px] text-muted-foreground flex items-center gap-1">
104
- <Clock className="w-3 h-3" />
105
- {new Date(event.created_at).toLocaleTimeString()}
106
- </span>
107
- </div>
108
-
109
- {/* Event Details */}
110
- <div className="bg-card border rounded-lg p-4 shadow-sm">
111
- {event.event_type === 'info' && (
112
- <p className="text-sm text-foreground/90">{event.details?.message}</p>
113
- )}
114
-
115
- {event.event_type === 'analysis' && (
116
- <div className="space-y-4">
117
- <div className="grid grid-cols-2 gap-2">
118
- <div className="text-[10px] bg-secondary px-2 py-1 rounded">
119
- <span className="text-muted-foreground mr-1">Category:</span>
120
- <span className="font-bold uppercase">{event.details?.category || 'Analyzing...'}</span>
121
- </div>
122
- <div className="text-[10px] bg-secondary px-2 py-1 rounded">
123
- <span className="text-muted-foreground mr-1">Sentiment:</span>
124
- <span className="font-bold uppercase">{event.details?.sentiment || 'Analyzing...'}</span>
125
- </div>
126
- </div>
127
-
128
- {event.details?.system_prompt && (
129
- <div className="space-y-1">
130
- <div className="text-[9px] font-bold text-muted-foreground uppercase flex items-center gap-1">
131
- <Code className="w-3 h-3" /> System Prompt
132
- </div>
133
- <pre className="text-[10px] bg-secondary/50 p-2 rounded border overflow-x-auto whitespace-pre-wrap max-h-40 overflow-y-auto font-mono">
134
- {event.details?.system_prompt}
135
- </pre>
136
- </div>
137
- )}
138
-
139
- {event.details?._raw_response && (
140
- <div className="space-y-1">
141
- <div className="text-[9px] font-bold text-muted-foreground uppercase flex items-center gap-1">
142
- <Code className="w-3 h-3" /> Raw LLM Response
143
- </div>
144
- <pre className="text-[10px] bg-emerald-500/5 p-2 rounded border border-emerald-500/10 overflow-x-auto font-mono">
145
- {JSON.stringify(JSON.parse(event.details._raw_response), null, 2)}
146
- </pre>
147
- </div>
148
- )}
149
- </div>
150
- )}
151
-
152
- {event.event_type === 'action' && (
153
- <div className="flex items-center justify-between">
154
- <div>
155
- <p className="text-sm font-bold text-emerald-600 dark:text-emerald-400 capitalize">
156
- Executed: {event.details?.action}
157
- </p>
158
- <p className="text-xs text-muted-foreground italic">
159
- "{event.details?.reason}"
160
- </p>
161
- </div>
162
- <CheckCircle2 className="w-5 h-5 text-emerald-500" />
163
- </div>
164
- )}
165
-
166
- {event.event_type === 'error' && (
167
- <div className="space-y-2">
168
- <p className="text-sm text-red-600 dark:text-red-400 font-bold">
169
- {event.details?.error}
170
- </p>
171
- {event.details?.raw_response && (
172
- <pre className="text-[10px] bg-red-500/5 p-2 rounded border border-red-500/10 overflow-x-auto whitespace-pre-wrap font-mono">
173
- {event.details.raw_response}
174
- </pre>
175
- )}
176
- </div>
177
- )}
178
- </div>
179
- </div>
180
- </div>
181
- ))
182
- )}
183
- </div>
184
- </DialogContent>
185
- </Dialog>
186
- );
187
- }
188
-
189
- const CATEGORY_COLORS: Record<string, string> = {
190
- spam: 'bg-destructive/10 text-destructive',
191
- newsletter: 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
192
- support: 'bg-orange-500/10 text-orange-600 dark:text-orange-400',
193
- client: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
194
- internal: 'bg-purple-500/10 text-purple-600 dark:text-purple-400',
195
- personal: 'bg-pink-500/10 text-pink-600 dark:text-pink-400',
196
- other: 'bg-gray-500/10 text-gray-600 dark:text-gray-400',
197
- };
198
-
199
- const ACTION_ICONS = {
200
- delete: Trash2,
201
- archive: Archive,
202
- reply: Send,
203
- flag: Flag,
204
- none: ShieldCheck,
205
- };
206
-
207
- export function Dashboard() {
208
- const { state, actions, dispatch } = useApp();
209
- const { openTerminal } = useTerminal();
210
- const [isLoading, setIsLoading] = useState(true);
211
- const [isSyncing, setIsSyncing] = useState(false);
212
- const [searchQuery, setSearchQuery] = useState('');
213
- const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
214
- const [selectedEmail, setSelectedEmail] = useState<Email | null>(null);
215
- const [actionLoading, setActionLoading] = useState<Record<string, string>>({});
216
- const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
217
- const [isTraceOpen, setIsTraceOpen] = useState(false);
218
- const [traceEmail, setTraceEmail] = useState<Email | null>(null);
219
-
220
- // Realtime subscription for live email updates
221
- const handleRealtimeInsert = useCallback((email: Email) => {
222
- dispatch({ type: 'ADD_EMAIL', payload: email });
223
-
224
- // Play feedback
225
- if (email.ai_analysis?.priority === 'High') {
226
- sounds.playAlert();
227
- toast.success('High Priority Email Processed!');
228
- } else {
229
- sounds.playNotify();
230
- toast.info('New email processed');
231
- }
232
- }, [dispatch]);
233
-
234
- const handleRealtimeUpdate = useCallback((email: Email) => {
235
- dispatch({ type: 'UPDATE_EMAIL', payload: email });
236
- }, [dispatch]);
237
-
238
- const handleRealtimeDelete = useCallback((emailId: string) => {
239
- // Refresh the list when an email is deleted
240
- loadEmails(state.emailsOffset);
241
- }, [state.emailsOffset]);
242
-
243
- const { isSubscribed } = useRealtimeEmails({
244
- userId: state.user?.id,
245
- onInsert: handleRealtimeInsert,
246
- onUpdate: handleRealtimeUpdate,
247
- onDelete: handleRealtimeDelete,
248
- enabled: state.isAuthenticated,
249
- });
250
-
251
- useEffect(() => {
252
- // Only fetch emails if user is authenticated
253
- if (state.isAuthenticated) {
254
- loadEmails();
255
- } else {
256
- setIsLoading(false);
257
- }
258
- }, [selectedCategory, state.isAuthenticated]);
259
-
260
- const loadEmails = async (offset = 0) => {
261
- setIsLoading(true);
262
- await actions.fetchEmails({
263
- category: selectedCategory || undefined,
264
- search: searchQuery || undefined,
265
- offset,
266
- });
267
- setIsLoading(false);
268
- };
269
-
270
- const handleSync = async () => {
271
- if (state.accounts.length === 0) {
272
- toast.warning('Please connect an email account first');
273
- return;
274
- }
275
- openTerminal();
276
- setIsSyncing(true);
277
- const success = await actions.triggerSync();
278
- setIsSyncing(false);
279
- if (success) {
280
- sounds.playSuccess();
281
- toast.success('Sync completed! Check your emails.');
282
- } else {
283
- toast.error('Sync failed. Check account status for details.');
284
- }
285
- };
286
-
287
- const handleAction = async (email: Email, action: string) => {
288
- // For delete, require confirmation
289
- if (action === 'delete' && deleteConfirm !== email.id) {
290
- setDeleteConfirm(email.id);
291
- return;
292
- }
293
-
294
- // Clear delete confirmation
295
- setDeleteConfirm(null);
296
-
297
- // Set loading state for this specific email+action
298
- setActionLoading(prev => ({ ...prev, [email.id]: action }));
299
-
300
- const success = await actions.executeAction(email.id, action);
301
-
302
- // Clear loading state
303
- setActionLoading(prev => {
304
- const updated = { ...prev };
305
- delete updated[email.id];
306
- return updated;
307
- });
308
-
309
- if (success) {
310
- toast.success(`Email ${action === 'delete' ? 'deleted' : action === 'archive' ? 'archived' : 'updated'}`);
311
- // Refresh list after delete to remove the email
312
- if (action === 'delete') {
313
- loadEmails(state.emailsOffset);
314
- }
315
- }
316
- };
317
-
318
- const cancelDelete = () => {
319
- setDeleteConfirm(null);
320
- };
321
-
322
- const handleSearch = (e: React.FormEvent) => {
323
- e.preventDefault();
324
- loadEmails();
325
- };
326
-
327
- const handlePageChange = (direction: 'prev' | 'next') => {
328
- const newOffset = direction === 'next'
329
- ? state.emailsOffset + 20
330
- : Math.max(0, state.emailsOffset - 20);
331
- loadEmails(newOffset);
332
- };
333
-
334
- const handleViewTrace = (email: Email) => {
335
- setTraceEmail(email);
336
- setIsTraceOpen(true);
337
- };
338
-
339
- return (
340
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-8 animate-in fade-in duration-500">
341
- {/* Main Content */}
342
- <section className="lg:col-span-2 space-y-4">
343
- {/* Header */}
344
- <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-4">
345
- <h2 className="text-lg font-semibold flex items-center gap-2">
346
- <Mail className="w-5 h-5 text-primary" />
347
- Recent Analysis
348
- </h2>
349
- <div className="flex gap-2 w-full sm:w-auto">
350
- <Button
351
- onClick={handleSync}
352
- size="sm"
353
- variant="outline"
354
- className="shadow-sm"
355
- disabled={isSyncing}
356
- >
357
- <RefreshCw className={cn("w-3.5 h-3.5 mr-2", isSyncing && "animate-spin")} />
358
- {isSyncing ? 'Syncing...' : 'Sync Now'}
359
- </Button>
360
- <span className="text-xs font-medium text-muted-foreground bg-secondary px-2 py-1 rounded-md border border-border flex items-center">
361
- {state.emailsTotal} emails
362
- </span>
363
- </div>
364
- </div>
365
-
366
- {/* Search and Filters */}
367
- <div className="flex flex-col sm:flex-row gap-3">
368
- <form onSubmit={handleSearch} className="flex-1 flex gap-2">
369
- <div className="relative flex-1">
370
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
371
- <Input
372
- placeholder="Search emails..."
373
- value={searchQuery}
374
- onChange={(e) => setSearchQuery(e.target.value)}
375
- className="pl-9"
376
- />
377
- </div>
378
- <Button type="submit" size="sm">Search</Button>
379
- </form>
380
- <div className="flex gap-1 flex-wrap">
381
- <Button
382
- size="sm"
383
- variant={selectedCategory === null ? 'secondary' : 'ghost'}
384
- onClick={() => setSelectedCategory(null)}
385
- >
386
- All
387
- </Button>
388
- {['spam', 'client', 'newsletter', 'support'].map(cat => (
389
- <Button
390
- key={cat}
391
- size="sm"
392
- variant={selectedCategory === cat ? 'secondary' : 'ghost'}
393
- onClick={() => setSelectedCategory(cat)}
394
- className="capitalize"
395
- >
396
- {cat}
397
- </Button>
398
- ))}
399
- </div>
400
- </div>
401
-
402
- {/* Email List */}
403
- {isLoading ? (
404
- <CardLoader />
405
- ) : state.emails.length === 0 ? (
406
- <Card className="p-20 text-center shadow-sm">
407
- <div className="w-16 h-16 bg-primary/10 text-primary rounded-full flex items-center justify-center mx-auto mb-6">
408
- <Mail className="w-8 h-8" />
409
- </div>
410
- <h3 className="text-lg font-medium">No emails found</h3>
411
- <p className="text-muted-foreground mt-2 mb-6">
412
- {state.accounts.length === 0
413
- ? 'Connect your email account to get started.'
414
- : 'Try syncing or adjusting your filters.'}
415
- </p>
416
- {state.accounts.length > 0 && (
417
- <Button onClick={() => {
418
- openTerminal();
419
- handleSync();
420
- }} disabled={isSyncing}>
421
- <RefreshCw className={cn("w-4 h-4 mr-2", isSyncing && "animate-spin")} />
422
- Sync Now
423
- </Button>
424
- )}
425
- </Card>
426
- ) : (
427
- <>
428
- {state.emails.map(email => (
429
- <EmailCard
430
- key={email.id}
431
- email={email}
432
- onAction={handleAction}
433
- onViewTrace={handleViewTrace}
434
- onSelect={() => setSelectedEmail(email)}
435
- isSelected={selectedEmail?.id === email.id}
436
- loadingAction={actionLoading[email.id]}
437
- isDeletePending={deleteConfirm === email.id}
438
- onCancelDelete={cancelDelete}
439
- />
440
- ))}
441
-
442
- <AITraceModal
443
- isOpen={isTraceOpen}
444
- onOpenChange={setIsTraceOpen}
445
- email={traceEmail}
446
- />
447
-
448
- {/* Pagination */}
449
- {state.emailsTotal > 20 && (
450
- <div className="flex items-center justify-between pt-4">
451
- <Button
452
- variant="outline"
453
- size="sm"
454
- onClick={() => handlePageChange('prev')}
455
- disabled={state.emailsOffset === 0}
456
- >
457
- <ChevronLeft className="w-4 h-4 mr-1" />
458
- Previous
459
- </Button>
460
- <span className="text-sm text-muted-foreground">
461
- {state.emailsOffset + 1} - {Math.min(state.emailsOffset + 20, state.emailsTotal)} of {state.emailsTotal}
462
- </span>
463
- <Button
464
- variant="outline"
465
- size="sm"
466
- onClick={() => handlePageChange('next')}
467
- disabled={state.emailsOffset + 20 >= state.emailsTotal}
468
- >
469
- Next
470
- <ChevronRight className="w-4 h-4 ml-1" />
471
- </Button>
472
- </div>
473
- )}
474
- </>
475
- )}
476
- </section>
477
-
478
- {/* Sidebar */}
479
- <aside className="space-y-6">
480
- {/* Connection Status */}
481
- <Card className={cn(
482
- "p-6 border-primary/20",
483
- isSubscribed ? "bg-primary/5" : "bg-muted/50"
484
- )}>
485
- <h3 className="font-semibold text-primary mb-1">Realtime Sync</h3>
486
- <p className="text-muted-foreground text-xs mb-3">
487
- {isSubscribed
488
- ? "Live updates enabled"
489
- : "Waiting for connection..."}
490
- </p>
491
- <div className={cn(
492
- "flex items-center gap-2 text-[10px] font-mono w-fit px-2 py-1 rounded-full border",
493
- isSubscribed
494
- ? "text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 border-emerald-500/20"
495
- : "text-yellow-600 dark:text-yellow-400 bg-yellow-500/10 border-yellow-500/20"
496
- )}>
497
- <div className={cn(
498
- "w-1.5 h-1.5 rounded-full",
499
- isSubscribed ? "bg-emerald-500 animate-pulse" : "bg-yellow-500"
500
- )} />
501
- {isSubscribed ? "CONNECTED" : "DISCONNECTED"}
502
- </div>
503
- </Card>
504
-
505
- {/* Sync Settings per Account */}
506
- <SyncSettings
507
- accounts={state.accounts}
508
- onUpdate={actions.updateAccount}
509
- onSync={actions.triggerSync}
510
- settings={state.settings}
511
- onUpdateSettings={actions.updateSettings}
512
- openTerminal={openTerminal}
513
- />
514
-
515
- {/* Quick Stats */}
516
- <Card className="p-6">
517
- <h3 className="font-semibold mb-4">Quick Stats</h3>
518
- <div className="space-y-3">
519
- <div className="flex justify-between items-center">
520
- <span className="text-sm text-muted-foreground">Total Processed</span>
521
- <span className="font-medium">{state.emailsTotal}</span>
522
- </div>
523
- <div className="flex justify-between items-center">
524
- <span className="text-sm text-muted-foreground">Connected Accounts</span>
525
- <span className="font-medium">{state.accounts.length}</span>
526
- </div>
527
- <div className="flex justify-between items-center">
528
- <span className="text-sm text-muted-foreground">Active Rules</span>
529
- <span className="font-medium">{state.rules.filter(r => r.is_enabled).length}</span>
530
- </div>
531
- </div>
532
- </Card>
533
-
534
- {/* Selected Email Detail */}
535
- {selectedEmail && (
536
- <Card className="p-6">
537
- <h3 className="font-semibold mb-4">Email Details</h3>
538
- <div className="space-y-3 text-sm">
539
- <div>
540
- <span className="text-muted-foreground">From:</span>
541
- <p className="font-medium truncate">{selectedEmail.sender}</p>
542
- </div>
543
- <div>
544
- <span className="text-muted-foreground">Subject:</span>
545
- <p className="font-medium">{selectedEmail.subject}</p>
546
- </div>
547
- {selectedEmail.ai_analysis && (
548
- <>
549
- <div>
550
- <span className="text-muted-foreground">Summary:</span>
551
- <p className="text-xs mt-1">{selectedEmail.ai_analysis.summary}</p>
552
- </div>
553
- {selectedEmail.ai_analysis.key_points && (
554
- <div>
555
- <span className="text-muted-foreground">Key Points:</span>
556
- <ul className="text-xs mt-1 list-disc list-inside">
557
- {selectedEmail.ai_analysis.key_points.map((point, i) => (
558
- <li key={i}>{point}</li>
559
- ))}
560
- </ul>
561
- </div>
562
- )}
563
- {selectedEmail.ai_analysis.draft_response && (
564
- <div className="mt-4 p-3 bg-emerald-500/5 border border-emerald-500/20 rounded-lg">
565
- <div className="flex items-center gap-2 mb-2 text-emerald-600 dark:text-emerald-400">
566
- <Send className="w-3.5 h-3.5" />
567
- <span className="text-xs font-bold uppercase">AI Draft Reply</span>
568
- </div>
569
- <p className="text-xs leading-relaxed whitespace-pre-wrap italic text-foreground/80">
570
- {selectedEmail.ai_analysis.draft_response}
571
- </p>
572
- <p className="mt-2 text-[9px] text-muted-foreground">
573
- * This draft is already saved in your {selectedEmail.email_accounts?.provider === 'gmail' ? 'Gmail' : 'Outlook'} Drafts folder.
574
- </p>
575
- </div>
576
- )}
577
- </>
578
- )}
579
- </div>
580
- </Card>
581
- )}
582
- </aside>
583
- </div>
584
- );
585
- }
586
-
587
- interface SyncSettingsProps {
588
- accounts: EmailAccount[];
589
- onUpdate: (accountId: string, updates: Partial<EmailAccount>) => Promise<boolean>;
590
- onSync: (accountId: string) => void;
591
- settings: UserSettings | null;
592
- onUpdateSettings: (updates: Partial<UserSettings>) => Promise<boolean>;
593
- openTerminal: () => void;
594
- }
595
-
596
- function SyncSettings({ accounts, onUpdate, onSync, settings, onUpdateSettings, openTerminal }: SyncSettingsProps) {
597
- const [updating, setUpdating] = useState<string | null>(null);
598
- const [updatingSettings, setUpdatingSettings] = useState(false);
599
-
600
- const handleUpdate = async (accountId: string, updates: Partial<EmailAccount>) => {
601
- setUpdating(accountId);
602
- await onUpdate(accountId, updates);
603
- setUpdating(null);
604
- };
605
-
606
- if (accounts.length === 0) return null;
607
-
608
- return (
609
- <Card className="p-6">
610
- <h3 className="font-semibold mb-4 flex items-center gap-2">
611
- <Settings2 className="w-4 h-4 text-primary" />
612
- Sync Scope
613
- </h3>
614
-
615
- <div className="mb-6 p-3 bg-muted/30 rounded-lg space-y-2">
616
- <div className="flex justify-between items-center">
617
- <label className="text-[11px] font-medium flex items-center gap-1">
618
- Sync Interval (min)
619
- </label>
620
- {updatingSettings && <Loader2 className="w-3 h-3 animate-spin text-primary" />}
621
- </div>
622
- <Input
623
- type="number"
624
- min={1}
625
- max={60}
626
- className="h-8 text-xs"
627
- value={settings?.sync_interval_minutes || 5}
628
- onChange={async (e) => {
629
- const val = parseInt(e.target.value, 10) || 5;
630
- setUpdatingSettings(true);
631
- await onUpdateSettings({ sync_interval_minutes: val });
632
- setUpdatingSettings(false);
633
- }}
634
- />
635
- <p className="text-[9px] text-muted-foreground">
636
- Background sync frequency for all accounts.
637
- </p>
638
- </div>
639
- <div className="space-y-6">
640
- {accounts.map(account => (
641
- <div key={account.id} className="space-y-3 pb-4 border-b last:border-0 last:pb-0">
642
- <div className="flex justify-between items-center">
643
- <span className="text-xs font-medium truncate max-w-[150px]" title={account.email_address}>
644
- {account.email_address}
645
- </span>
646
- <div className="flex items-center gap-1">
647
- {account.last_sync_status === 'syncing' ? (
648
- <Loader2 className="w-3 h-3 text-primary animate-spin" />
649
- ) : account.last_sync_status === 'success' ? (
650
- <CheckCircle2 className="w-3 h-3 text-emerald-500" />
651
- ) : account.last_sync_status === 'error' ? (
652
- <span title={account.last_sync_error || 'Error'}>
653
- <AlertCircle className="w-3 h-3 text-destructive" />
654
- </span>
655
- ) : null}
656
- <Button
657
- variant="ghost"
658
- size="icon"
659
- className="h-6 w-6"
660
- onClick={() => {
661
- openTerminal();
662
- onSync(account.id);
663
- }}
664
- disabled={account.last_sync_status === 'syncing'}
665
- >
666
- <RefreshCw className={cn("w-3 h-3", account.last_sync_status === 'syncing' && "animate-spin")} />
667
- </Button>
668
- <Button
669
- variant="ghost"
670
- size="icon"
671
- className="h-6 w-6 text-muted-foreground hover:text-orange-500"
672
- title="Reset Checkpoint (Force Full Re-sync from Start Date)"
673
- onClick={() => onUpdate(account.id, { last_sync_checkpoint: null })}
674
- disabled={account.last_sync_status === 'syncing'}
675
- >
676
- <RotateCcw className="w-3 h-3" />
677
- </Button>
678
- </div>
679
- </div>
680
-
681
- <div className="grid grid-cols-[1.5fr_1fr] gap-2">
682
- <div className="space-y-1">
683
- <label className="text-[10px] text-muted-foreground flex items-center gap-1">
684
- <Calendar className="w-2.5 h-2.5" /> Sync From
685
- </label>
686
- <Input
687
- type="datetime-local"
688
- className="h-7 text-[10px] px-2 py-0 w-full"
689
- value={(() => {
690
- // 1. Priority: User-defined start date
691
- if (account.sync_start_date) return account.sync_start_date.substring(0, 16);
692
-
693
- // 2. Fallback: Last known checkpoint (data time)
694
- if (account.last_sync_checkpoint) {
695
- if (account.provider === 'gmail') {
696
- try {
697
- const ms = parseInt(account.last_sync_checkpoint);
698
- if (!isNaN(ms)) return new Date(ms).toISOString().substring(0, 16);
699
- } catch (e) { /* ignore */ }
700
- } else {
701
- // Outlook checkpoint is already ISO
702
- return account.last_sync_checkpoint.substring(0, 16);
703
- }
704
- }
705
-
706
- // 3. Last fallback: Last sync execution time
707
- if (account.last_sync_at) return account.last_sync_at.substring(0, 16);
708
-
709
- return '';
710
- })()}
711
- onChange={(e) => handleUpdate(account.id, {
712
- sync_start_date: e.target.value ? new Date(e.target.value).toISOString() : null
713
- })}
714
- disabled={updating === account.id}
715
- />
716
- </div>
717
- <div className="space-y-1">
718
- <label className="text-[10px] text-muted-foreground flex items-center gap-1">
719
- <Hash className="w-2.5 h-2.5" /> Max Emails
720
- </label>
721
- <Input
722
- type="number"
723
- className="h-7 text-[10px] px-2 py-0"
724
- value={account.sync_max_emails_per_run || 50}
725
- onChange={(e) => handleUpdate(account.id, {
726
- sync_max_emails_per_run: parseInt(e.target.value, 10) || 50
727
- })}
728
- disabled={updating === account.id}
729
- />
730
- </div>
731
- </div>
732
- {account.last_sync_at && (
733
- <p className="text-[9px] text-muted-foreground">
734
- Last sync: {new Date(account.last_sync_at).toLocaleString()}
735
- </p>
736
- )}
737
- {account.last_sync_error && (
738
- <p className="text-[9px] text-destructive italic line-clamp-1" title={account.last_sync_error}>
739
- Error: {account.last_sync_error}
740
- </p>
741
- )}
742
- </div>
743
- ))}
744
- </div>
745
- </Card>
746
- );
747
- }
748
-
749
- interface EmailCardProps {
750
- email: Email;
751
- onAction: (email: Email, action: string) => void;
752
- onViewTrace: (email: Email) => void;
753
- onSelect: () => void;
754
- isSelected: boolean;
755
- loadingAction?: string;
756
- isDeletePending?: boolean;
757
- onCancelDelete?: () => void;
758
- }
759
-
760
- function EmailCard({ email, onAction, onViewTrace, onSelect, isSelected, loadingAction, isDeletePending, onCancelDelete }: EmailCardProps) {
761
- if (!email) return null;
762
- const categoryClass = CATEGORY_COLORS[email.category || 'other'];
763
- const isLoading = !!loadingAction;
764
-
765
- const getExternalMailUrl = () => {
766
- if (!email.email_accounts) return '#';
767
- const { provider, email_address } = email.email_accounts;
768
-
769
- if (provider === 'gmail') {
770
- // Gmail deep link using the message ID
771
- return `https://mail.google.com/mail/u/${email_address}/#all/${email.external_id}`;
772
- } else {
773
- // Outlook/M365 deep link
774
- return `https://outlook.office.com/mail/deeplink/read/${encodeURIComponent(email.external_id)}`;
775
- }
776
- };
777
-
778
- return (
779
- <Card
780
- className={cn(
781
- "hover:shadow-md transition-shadow group cursor-pointer",
782
- isSelected && "ring-2 ring-primary"
783
- )}
784
- onClick={onSelect}
785
- >
786
- <div className="p-5">
787
- <div className="flex justify-between items-start mb-3">
788
- <div className="flex gap-3">
789
- <div className={cn(
790
- "w-8 h-8 rounded-full flex items-center justify-center font-bold text-white text-xs",
791
- email.category === 'spam' ? 'bg-destructive' : 'bg-primary'
792
- )}>
793
- {email.sender?.[0]?.toUpperCase() || '?'}
794
- </div>
795
- <div className="min-w-0">
796
- <h3 className="font-semibold text-sm line-clamp-1 group-hover:text-primary transition-colors">
797
- {email.subject || 'No Subject'}
798
- </h3>
799
- <p className="text-xs text-muted-foreground truncate">{email.sender}</p>
800
- </div>
801
- </div>
802
- <div className="flex flex-col items-end gap-1 flex-shrink-0">
803
- <span className={cn("px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider", categoryClass)}>
804
- {email.category || 'unknown'}
805
- </span>
806
- <span className="text-[10px] text-muted-foreground">
807
- {email.date ? new Date(email.date).toLocaleDateString() : ''}
808
- </span>
809
- </div>
810
- </div>
811
-
812
- <p className="text-muted-foreground text-sm mb-4 line-clamp-2 leading-relaxed">
813
- {email.body_snippet}
814
- </p>
815
-
816
- <div className="bg-secondary/30 p-3 rounded-lg border border-border/50 flex justify-between items-center">
817
- <div className="flex items-center gap-2 text-xs font-medium">
818
- <ShieldCheck className="w-3.5 h-3.5 text-emerald-500" />
819
- Suggested:
820
- {(email.suggested_actions && email.suggested_actions.length > 0) ? (
821
- <div className="flex gap-1 flex-wrap">
822
- {email.suggested_actions.map(action => (
823
- <span key={action} className="text-foreground border border-border/50 px-1.5 py-0.5 rounded capitalize bg-background/50">
824
- {action}
825
- </span>
826
- ))}
827
- </div>
828
- ) : (
829
- <span className="text-foreground">{email.suggested_action || 'none'}</span>
830
- )}
831
-
832
- {(email.actions_taken && email.actions_taken.length > 0) ? (
833
- <span className="text-muted-foreground ml-2 truncate max-w-[100px]" title={email.actions_taken.join(', ')}>
834
- (Done: {email.actions_taken.join(', ')})
835
- </span>
836
- ) : email.action_taken ? (
837
- <span className="text-muted-foreground ml-2">
838
- (Done: {email.action_taken})
839
- </span>
840
- ) : null}
841
- </div>
842
- <div className="flex gap-1 items-center" onClick={(e) => e.stopPropagation()}>
843
- {isDeletePending ? (
844
- // Delete confirmation UI
845
- <div className="flex items-center gap-1 animate-in fade-in duration-200">
846
- <span className="text-xs text-destructive mr-1">Delete?</span>
847
- <Button
848
- variant="destructive"
849
- size="sm"
850
- className="h-7 px-2 text-xs"
851
- onClick={() => onAction(email, 'delete')}
852
- disabled={isLoading}
853
- >
854
- {loadingAction === 'delete' ? (
855
- <Loader2 className="w-3 h-3 animate-spin" />
856
- ) : (
857
- 'Yes'
858
- )}
859
- </Button>
860
- <Button
861
- variant="outline"
862
- size="sm"
863
- className="h-7 px-2 text-xs"
864
- onClick={onCancelDelete}
865
- disabled={isLoading}
866
- >
867
- No
868
- </Button>
869
- </div>
870
- ) : (
871
- // Normal action buttons
872
- <>
873
- <a
874
- href={getExternalMailUrl()}
875
- target="_blank"
876
- rel="noreferrer"
877
- className="inline-flex items-center justify-center h-7 w-7 rounded-md text-muted-foreground hover:text-primary hover:bg-secondary/50 transition-colors"
878
- title={`Open in ${email.email_accounts?.provider === 'gmail' ? 'Gmail' : 'Outlook'}`}
879
- >
880
- <ExternalLink className="w-3.5 h-3.5" />
881
- </a>
882
- <Button
883
- variant="ghost"
884
- size="icon"
885
- className="h-7 w-7 text-muted-foreground hover:text-primary"
886
- onClick={() => onViewTrace(email)}
887
- title="View AI Trace (Prompt/Response)"
888
- >
889
- <Eye className="w-3.5 h-3.5" />
890
- </Button>
891
- <Button
892
- variant="ghost"
893
- size="icon"
894
- className="h-7 w-7 hover:text-destructive"
895
- onClick={() => onAction(email, 'delete')}
896
- disabled={isLoading}
897
- title="Delete"
898
- >
899
- {loadingAction === 'delete' ? (
900
- <Loader2 className="w-3.5 h-3.5 animate-spin" />
901
- ) : (
902
- <Trash2 className="w-3.5 h-3.5" />
903
- )}
904
- </Button>
905
- <Button
906
- variant="ghost"
907
- size="icon"
908
- className="h-7 w-7 hover:text-blue-500"
909
- onClick={() => onAction(email, 'archive')}
910
- disabled={isLoading}
911
- title="Archive"
912
- >
913
- {loadingAction === 'archive' ? (
914
- <Loader2 className="w-3.5 h-3.5 animate-spin" />
915
- ) : (
916
- <Archive className="w-3.5 h-3.5" />
917
- )}
918
- </Button>
919
- <Button
920
- variant="ghost"
921
- size="icon"
922
- className="h-7 w-7 hover:text-primary"
923
- onClick={() => onAction(email, 'flag')}
924
- disabled={isLoading}
925
- title="Flag"
926
- >
927
- {loadingAction === 'flag' ? (
928
- <Loader2 className="w-3.5 h-3.5 animate-spin" />
929
- ) : (
930
- <Flag className="w-3.5 h-3.5" />
931
- )}
932
- </Button>
933
- </>
934
- )}
935
- </div>
936
- </div>
937
- </div>
938
- </Card>
939
- );
940
- }