@realtimex/email-automator 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/server.ts +4 -8
- package/api/src/config/index.ts +6 -3
- package/bin/email-automator-setup.js +2 -3
- package/bin/email-automator.js +7 -11
- package/dist/api/server.js +109 -0
- package/dist/api/src/config/index.js +88 -0
- package/dist/api/src/middleware/auth.js +119 -0
- package/dist/api/src/middleware/errorHandler.js +78 -0
- package/dist/api/src/middleware/index.js +4 -0
- package/dist/api/src/middleware/rateLimit.js +57 -0
- package/dist/api/src/middleware/validation.js +111 -0
- package/dist/api/src/routes/actions.js +173 -0
- package/dist/api/src/routes/auth.js +106 -0
- package/dist/api/src/routes/emails.js +100 -0
- package/dist/api/src/routes/health.js +33 -0
- package/dist/api/src/routes/index.js +19 -0
- package/dist/api/src/routes/migrate.js +61 -0
- package/dist/api/src/routes/rules.js +104 -0
- package/dist/api/src/routes/settings.js +178 -0
- package/dist/api/src/routes/sync.js +118 -0
- package/dist/api/src/services/eventLogger.js +41 -0
- package/dist/api/src/services/gmail.js +350 -0
- package/dist/api/src/services/intelligence.js +243 -0
- package/dist/api/src/services/microsoft.js +256 -0
- package/dist/api/src/services/processor.js +503 -0
- package/dist/api/src/services/scheduler.js +210 -0
- package/dist/api/src/services/supabase.js +59 -0
- package/dist/api/src/utils/contentCleaner.js +94 -0
- package/dist/api/src/utils/crypto.js +68 -0
- package/dist/api/src/utils/logger.js +119 -0
- package/package.json +5 -5
- 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,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
|
-
}
|