sitcod3 commited on
Commit
766f889
·
verified ·
1 Parent(s): bf27873

Upload src/pages/Inbox.tsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/pages/Inbox.tsx +307 -0
src/pages/Inbox.tsx ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { apiClient } from "../lib/apiClient";
3
+ import {
4
+ Inbox as InboxIcon,
5
+ Search,
6
+ Send,
7
+ Sparkles,
8
+ CheckCircle2,
9
+ AlertCircle,
10
+ Clock,
11
+ Mail,
12
+ Calendar,
13
+ User,
14
+ Building,
15
+ ChevronRight,
16
+ Paperclip,
17
+ Smile,
18
+ RefreshCw
19
+ } from "lucide-react";
20
+ import { useState } from "react";
21
+ import DOMPurify from "dompurify";
22
+ import StatusBadge from "../components/StatusBadge";
23
+ import RichTextEditor from "../components/RichTextEditor";
24
+ import toast from "react-hot-toast";
25
+
26
+ const CATEGORIES = [
27
+ { key: "ALL", label: "All Messages" },
28
+ { key: "INTERESTED", label: "Interested" },
29
+ { key: "MEETING_BOOKED", label: "Meeting Booked" },
30
+ { key: "REPLIED", label: "Replied" },
31
+ { key: "NOT_INTERESTED", label: "Not Interested" },
32
+ ];
33
+
34
+ export default function Inbox() {
35
+ const queryClient = useQueryClient();
36
+ const [selectedCategory, setSelectedCategory] = useState("ALL");
37
+ const [selectedMessageId, setSelectedMessageId] = useState<string | null>(null);
38
+ const [replyBody, setReplyBody] = useState("");
39
+
40
+ const { data: messages = [], isLoading, isError, refetch } = useQuery({
41
+ queryKey: ["inbox-messages", selectedCategory],
42
+ queryFn: async () => {
43
+ const res = await apiClient.get("/inbox", {
44
+ params: selectedCategory !== "ALL" ? { category: selectedCategory } : {},
45
+ });
46
+ return res.data;
47
+ },
48
+ });
49
+
50
+ const selectedMessage = (Array.isArray(messages) ? messages : []).find((m: any) => m.id === selectedMessageId) || (Array.isArray(messages) ? messages[0] : null);
51
+
52
+ const updateCategoryMutation = useMutation({
53
+ mutationFn: async ({ messageId, category }: { messageId: string; category: string }) => {
54
+ const res = await apiClient.post("/inbox/categorize", { thread_id: messageId, category });
55
+ return res.data;
56
+ },
57
+ onSuccess: () => {
58
+ toast.success("Lead categorization updated");
59
+ queryClient.invalidateQueries({ queryKey: ["inbox-messages"] });
60
+ },
61
+ onError: () => {
62
+ toast.error("Failed to update lead category");
63
+ },
64
+ });
65
+
66
+ const sendReplyMutation = useMutation({
67
+ mutationFn: async ({ campaignId, messageId, body }: { campaignId: string; messageId: string; body: string }) => {
68
+ const res = await apiClient.post(`/campaigns/${campaignId}/reply-email-thread`, { thread_id: messageId, reply_body: body });
69
+ return res.data;
70
+ },
71
+ onSuccess: () => {
72
+ toast.success("Reply dispatched to lead");
73
+ setReplyBody("");
74
+ queryClient.invalidateQueries({ queryKey: ["inbox-messages"] });
75
+ },
76
+ onError: () => {
77
+ toast.error("Failed to send reply");
78
+ }
79
+ });
80
+
81
+ return (
82
+ <div className="h-[calc(100vh-4rem)] flex flex-col overflow-hidden">
83
+ {/* Top Controls */}
84
+ <div className="h-14 border-b border-zinc-800 bg-zinc-950 px-6 flex items-center justify-between shrink-0">
85
+ <div className="flex items-center gap-3">
86
+ <div className="p-1.5 bg-indigo-500/10 text-indigo-400 rounded-lg">
87
+ <InboxIcon className="w-5 h-5" />
88
+ </div>
89
+ <div>
90
+ <h1 className="text-base font-bold text-zinc-100 flex items-center gap-2">
91
+ Unified Master Inbox
92
+ <span className="text-[10px] font-bold uppercase tracking-wider bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-0.5 rounded-full">
93
+ AI Sentiment Active
94
+ </span>
95
+ </h1>
96
+ </div>
97
+ </div>
98
+
99
+ <div className="flex items-center gap-2">
100
+ <button
101
+ onClick={() => refetch()}
102
+ className="p-1.5 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-900 rounded-lg transition-colors"
103
+ title="Refresh Inboxes"
104
+ >
105
+ <RefreshCw className="w-4 h-4" />
106
+ </button>
107
+ </div>
108
+ </div>
109
+
110
+ {/* 2-Pane Inbox Workspace */}
111
+ <div className="flex-1 flex overflow-hidden">
112
+ {/* Left Pane: Message List */}
113
+ <div className="w-80 md:w-96 border-r border-zinc-800 flex flex-col bg-zinc-950/60 shrink-0">
114
+ {/* Categories Filter Tabs */}
115
+ <div className="p-3 border-b border-zinc-800 flex gap-1 overflow-x-auto">
116
+ {CATEGORIES.map((cat) => (
117
+ <button
118
+ key={cat.key}
119
+ onClick={() => setSelectedCategory(cat.key)}
120
+ className={`px-2.5 py-1 rounded-md text-xs font-medium whitespace-nowrap transition-colors ${
121
+ selectedCategory === cat.key
122
+ ? "bg-indigo-600 text-white"
123
+ : "bg-zinc-900 text-zinc-400 hover:text-zinc-200"
124
+ }`}
125
+ >
126
+ {cat.label}
127
+ </button>
128
+ ))}
129
+ </div>
130
+
131
+ {/* Messages Scroll Area */}
132
+ <div className="flex-1 overflow-y-auto divide-y divide-zinc-800/60">
133
+ {isLoading && (
134
+ <div className="p-8 text-center text-xs text-zinc-400" role="status">Loading message threads…</div>
135
+ )}
136
+
137
+ {isError && (
138
+ <div className="p-8 text-center text-xs text-rose-300" role="alert">Unable to load message threads.</div>
139
+ )}
140
+
141
+ {!isLoading && !isError && messages.map((msg: any) => {
142
+ const isSelected = msg.id === selectedMessage?.id;
143
+ return (
144
+ <div
145
+ key={msg.id}
146
+ onClick={() => setSelectedMessageId(msg.id)}
147
+ onKeyDown={(event) => {
148
+ if (event.key === "Enter" || event.key === " ") setSelectedMessageId(msg.id);
149
+ }}
150
+ role="button"
151
+ tabIndex={0}
152
+ aria-pressed={isSelected}
153
+ className={`p-4 transition-colors cursor-pointer ${
154
+ isSelected ? "bg-indigo-950/30 border-l-2 border-indigo-500" : "hover:bg-zinc-900/40"
155
+ }`}
156
+ >
157
+ <div className="flex items-start justify-between gap-2">
158
+ <span className="font-semibold text-xs text-zinc-100 truncate">
159
+ {msg.lead_name || msg.lead_email}
160
+ </span>
161
+ <span className="text-[10px] text-zinc-500 shrink-0">
162
+ {new Date(msg.received_at || Date.now()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
163
+ </span>
164
+ </div>
165
+
166
+ <h4 className="text-xs font-medium text-zinc-300 mt-1 truncate">
167
+ {msg.subject}
168
+ </h4>
169
+
170
+ <p className="text-[11px] text-zinc-500 mt-1 line-clamp-2 leading-relaxed">
171
+ {msg.messages?.at(-1)?.body_html?.replace(/<[^>]*>?/gm, "") || "No message preview available"}
172
+ </p>
173
+
174
+ <div className="mt-2.5 flex items-center justify-between">
175
+ <StatusBadge status={msg.category || "REPLIED"} size="sm" />
176
+ <span className="text-[10px] text-zinc-400 font-mono">
177
+ {msg.company_name}
178
+ </span>
179
+ </div>
180
+ </div>
181
+ );
182
+ })}
183
+
184
+ {messages.length === 0 && (
185
+ <div className="p-8 text-center text-xs text-zinc-500">
186
+ No replies found for this category filter.
187
+ </div>
188
+ )}
189
+ </div>
190
+ </div>
191
+
192
+ {/* Right Pane: Thread View & Response Composer */}
193
+ {selectedMessage ? (
194
+ <div className="flex-1 flex flex-col bg-zinc-950 overflow-hidden">
195
+ {/* Thread Header */}
196
+ <div className="p-5 border-b border-zinc-800 bg-zinc-900/30 flex items-center justify-between shrink-0">
197
+ <div className="flex items-center gap-3">
198
+ <div className="w-9 h-9 rounded-full bg-indigo-600/20 text-indigo-400 flex items-center justify-center font-bold text-sm border border-indigo-500/30">
199
+ {selectedMessage.lead_name ? selectedMessage.lead_name[0] : "L"}
200
+ </div>
201
+ <div>
202
+ <h3 className="text-sm font-bold text-zinc-100 flex items-center gap-2">
203
+ {selectedMessage.subject}
204
+ </h3>
205
+ <div className="text-xs text-zinc-400 flex items-center gap-2 mt-0.5">
206
+ <span>{selectedMessage.lead_name} &lt;{selectedMessage.lead_email}&gt;</span>
207
+ <span>•</span>
208
+ <span className="font-mono text-zinc-500">{selectedMessage.company_name}</span>
209
+ </div>
210
+ </div>
211
+ </div>
212
+
213
+ {/* Categorization Dropdown */}
214
+ <div className="flex items-center gap-2">
215
+ <span className="text-xs text-zinc-500">AI Intent:</span>
216
+ <select
217
+ value={selectedMessage.category || "REPLIED"}
218
+ onChange={(e) =>
219
+ updateCategoryMutation.mutate({
220
+ messageId: selectedMessage.id,
221
+ category: e.target.value,
222
+ })
223
+ }
224
+ className="bg-zinc-900 border border-zinc-800 rounded-lg py-1.5 px-3 text-xs text-zinc-200 focus:outline-none focus:border-indigo-500"
225
+ >
226
+ <option value="INTERESTED">Interested</option>
227
+ <option value="MEETING_BOOKED">Meeting Booked</option>
228
+ <option value="REPLIED">Replied</option>
229
+ <option value="NOT_INTERESTED">Not Interested</option>
230
+ </select>
231
+ </div>
232
+ </div>
233
+
234
+ {/* Conversation Messages */}
235
+ <div className="flex-1 overflow-y-auto p-6 space-y-4">
236
+ {(selectedMessage.messages || []).map((message: any) => {
237
+ const isInbound = message.direction === "INBOUND";
238
+ return (
239
+ <div key={message.id} className={`max-w-2xl ${isInbound ? "ml-auto bg-indigo-950/40 border-indigo-500/30" : "bg-zinc-900 border-zinc-800"} border rounded-xl p-4 text-xs space-y-2`}>
240
+ <div className={`flex items-center justify-between ${isInbound ? "text-indigo-300 border-indigo-500/20" : "text-zinc-400 border-zinc-800"} pb-2 border-b`}>
241
+ <span className="font-semibold">{isInbound ? "Prospect reply" : "Outbound message"}</span>
242
+ <span className="text-[10px] text-zinc-400">{new Date(message.timestamp).toLocaleString()}</span>
243
+ </div>
244
+ <div
245
+ className="text-zinc-200 leading-relaxed prose prose-invert max-w-none text-xs"
246
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(message.body_html || "") }}
247
+ />
248
+ </div>
249
+ );
250
+ })}
251
+ </div>
252
+
253
+ {/* Reply Composer Bottom Section */}
254
+ <div className="p-4 border-t border-zinc-800 bg-zinc-950 shrink-0 space-y-3">
255
+ <RichTextEditor
256
+ content={replyBody}
257
+ onChange={setReplyBody}
258
+ placeholder="Compose your reply or insert calendar meeting booking link..."
259
+ />
260
+
261
+ <div className="flex items-center justify-between">
262
+ <div className="flex items-center gap-2">
263
+ <button
264
+ type="button"
265
+ onClick={() => setReplyBody("<p>Hi, how does Tuesday at 2 PM EST work for a quick 15-min call? Here is my calendar: https://cal.com/alex/15min</p>")}
266
+ className="px-2.5 py-1 bg-zinc-900 hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 border border-zinc-800 rounded-md text-[11px] transition-colors"
267
+ >
268
+ + Insert Booking Link
269
+ </button>
270
+ <button
271
+ type="button"
272
+ onClick={() => setReplyBody("<p>Thanks for getting back to me! I have attached our case study PDF here.</p>")}
273
+ className="px-2.5 py-1 bg-zinc-900 hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 border border-zinc-800 rounded-md text-[11px] transition-colors"
274
+ >
275
+ + Insert Case Study
276
+ </button>
277
+ </div>
278
+
279
+ <button
280
+ type="button"
281
+ onClick={() => {
282
+ if (!replyBody) {
283
+ toast.error("Please write a reply message");
284
+ return;
285
+ }
286
+ sendReplyMutation.mutate({
287
+ campaignId: selectedMessage.campaign_id,
288
+ messageId: selectedMessage.id,
289
+ body: replyBody,
290
+ });
291
+ }}
292
+ className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-medium text-xs rounded-lg transition-colors flex items-center gap-2 shadow-md"
293
+ >
294
+ <Send className="w-3.5 h-3.5" /> Send Reply
295
+ </button>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ ) : (
300
+ <div className="flex-1 flex items-center justify-center text-xs text-zinc-500">
301
+ Select a message thread from the left pane to view conversation.
302
+ </div>
303
+ )}
304
+ </div>
305
+ </div>
306
+ );
307
+ }