sitcod3 commited on
Commit
343a096
·
verified ·
1 Parent(s): 31dc3a4

Upload src/pages/Inbox.tsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/pages/Inbox.tsx +303 -0
src/pages/Inbox.tsx ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>("msg_001");
38
+ const [replyBody, setReplyBody] = useState("");
39
+
40
+ const { data: messages = [], isLoading, 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/${messageId}/category`, { category });
55
+ return res.data;
56
+ },
57
+ onSuccess: () => {
58
+ toast.success("Lead categorization updated");
59
+ refetch();
60
+ },
61
+ });
62
+
63
+ const sendReplyMutation = useMutation({
64
+ mutationFn: async ({ messageId, body }: { messageId: string; body: string }) => {
65
+ const res = await apiClient.post(`/inbox/${messageId}/reply`, { reply_body: body });
66
+ return res.data;
67
+ },
68
+ onSuccess: () => {
69
+ toast.success("Reply dispatched to lead!");
70
+ setReplyBody("");
71
+ refetch();
72
+ },
73
+ onError: () => {
74
+ toast.error("Failed to send reply");
75
+ }
76
+ });
77
+
78
+ return (
79
+ <div className="h-[calc(100vh-4rem)] flex flex-col overflow-hidden">
80
+ {/* Top Controls */}
81
+ <div className="h-14 border-b border-zinc-800 bg-zinc-950 px-6 flex items-center justify-between shrink-0">
82
+ <div className="flex items-center gap-3">
83
+ <div className="p-1.5 bg-indigo-500/10 text-indigo-400 rounded-lg">
84
+ <InboxIcon className="w-5 h-5" />
85
+ </div>
86
+ <div>
87
+ <h1 className="text-base font-bold text-zinc-100 flex items-center gap-2">
88
+ Unified Master Inbox
89
+ <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">
90
+ AI Sentiment Active
91
+ </span>
92
+ </h1>
93
+ </div>
94
+ </div>
95
+
96
+ <div className="flex items-center gap-2">
97
+ <button
98
+ onClick={() => refetch()}
99
+ className="p-1.5 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-900 rounded-lg transition-colors"
100
+ title="Refresh Inboxes"
101
+ >
102
+ <RefreshCw className="w-4 h-4" />
103
+ </button>
104
+ </div>
105
+ </div>
106
+
107
+ {/* 2-Pane Inbox Workspace */}
108
+ <div className="flex-1 flex overflow-hidden">
109
+ {/* Left Pane: Message List */}
110
+ <div className="w-80 md:w-96 border-r border-zinc-800 flex flex-col bg-zinc-950/60 shrink-0">
111
+ {/* Categories Filter Tabs */}
112
+ <div className="p-3 border-b border-zinc-800 flex gap-1 overflow-x-auto">
113
+ {CATEGORIES.map((cat) => (
114
+ <button
115
+ key={cat.key}
116
+ onClick={() => setSelectedCategory(cat.key)}
117
+ className={`px-2.5 py-1 rounded-md text-xs font-medium whitespace-nowrap transition-colors ${
118
+ selectedCategory === cat.key
119
+ ? "bg-indigo-600 text-white"
120
+ : "bg-zinc-900 text-zinc-400 hover:text-zinc-200"
121
+ }`}
122
+ >
123
+ {cat.label}
124
+ </button>
125
+ ))}
126
+ </div>
127
+
128
+ {/* Messages Scroll Area */}
129
+ <div className="flex-1 overflow-y-auto divide-y divide-zinc-800/60">
130
+ {messages.map((msg: any) => {
131
+ const isSelected = msg.id === selectedMessage?.id;
132
+ return (
133
+ <div
134
+ key={msg.id}
135
+ onClick={() => setSelectedMessageId(msg.id)}
136
+ className={`p-4 transition-colors cursor-pointer ${
137
+ isSelected ? "bg-indigo-950/30 border-l-2 border-indigo-500" : "hover:bg-zinc-900/40"
138
+ }`}
139
+ >
140
+ <div className="flex items-start justify-between gap-2">
141
+ <span className="font-semibold text-xs text-zinc-100 truncate">
142
+ {msg.from_name || msg.from_email}
143
+ </span>
144
+ <span className="text-[10px] text-zinc-500 shrink-0">
145
+ {new Date(msg.created_at || Date.now()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
146
+ </span>
147
+ </div>
148
+
149
+ <h4 className="text-xs font-medium text-zinc-300 mt-1 truncate">
150
+ {msg.subject}
151
+ </h4>
152
+
153
+ <p className="text-[11px] text-zinc-500 mt-1 line-clamp-2 leading-relaxed">
154
+ {msg.preview_body || msg.body?.replace(/<[^>]*>?/gm, "")}
155
+ </p>
156
+
157
+ <div className="mt-2.5 flex items-center justify-between">
158
+ <StatusBadge status={msg.lead_category || "INTERESTED"} size="sm" />
159
+ <span className="text-[10px] text-zinc-400 font-mono">
160
+ {msg.company_name}
161
+ </span>
162
+ </div>
163
+ </div>
164
+ );
165
+ })}
166
+
167
+ {messages.length === 0 && (
168
+ <div className="p-8 text-center text-xs text-zinc-500">
169
+ No replies found for this category filter.
170
+ </div>
171
+ )}
172
+ </div>
173
+ </div>
174
+
175
+ {/* Right Pane: Thread View & Response Composer */}
176
+ {selectedMessage ? (
177
+ <div className="flex-1 flex flex-col bg-zinc-950 overflow-hidden">
178
+ {/* Thread Header */}
179
+ <div className="p-5 border-b border-zinc-800 bg-zinc-900/30 flex items-center justify-between shrink-0">
180
+ <div className="flex items-center gap-3">
181
+ <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">
182
+ {selectedMessage.from_name ? selectedMessage.from_name[0] : "L"}
183
+ </div>
184
+ <div>
185
+ <h3 className="text-sm font-bold text-zinc-100 flex items-center gap-2">
186
+ {selectedMessage.subject}
187
+ </h3>
188
+ <div className="text-xs text-zinc-400 flex items-center gap-2 mt-0.5">
189
+ <span>{selectedMessage.from_name} &lt;{selectedMessage.from_email}&gt;</span>
190
+ <span>•</span>
191
+ <span className="font-mono text-zinc-500">{selectedMessage.company_name}</span>
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ {/* Categorization Dropdown */}
197
+ <div className="flex items-center gap-2">
198
+ <span className="text-xs text-zinc-500">AI Intent:</span>
199
+ <select
200
+ value={selectedMessage.lead_category || "INTERESTED"}
201
+ onChange={(e) =>
202
+ updateCategoryMutation.mutate({
203
+ messageId: selectedMessage.id,
204
+ category: e.target.value,
205
+ })
206
+ }
207
+ 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"
208
+ >
209
+ <option value="INTERESTED">Interested</option>
210
+ <option value="MEETING_BOOKED">Meeting Booked</option>
211
+ <option value="REPLIED">Replied</option>
212
+ <option value="NOT_INTERESTED">Not Interested</option>
213
+ </select>
214
+ </div>
215
+ </div>
216
+
217
+ {/* Conversation Messages */}
218
+ <div className="flex-1 overflow-y-auto p-6 space-y-4">
219
+ {/* Outbound Sent Message */}
220
+ <div className="max-w-2xl bg-zinc-900 border border-zinc-800 rounded-xl p-4 text-xs space-y-2">
221
+ <div className="flex items-center justify-between text-zinc-400 pb-2 border-b border-zinc-800">
222
+ <span className="font-medium text-zinc-300">Outbound Step #1 (Sent by Sequence)</span>
223
+ <span className="text-[10px]">Yesterday</span>
224
+ </div>
225
+ <div className="text-zinc-300 leading-relaxed">
226
+ Hi {selectedMessage.from_name?.split(" ")[0] || "there"},<br /><br />
227
+ Wanted to see how your team is managing deliverability and scale this quarter?<br /><br />
228
+ Best regards,<br />
229
+ Alex
230
+ </div>
231
+ </div>
232
+
233
+ {/* Inbound Prospect Reply */}
234
+ <div className="max-w-2xl ml-auto bg-indigo-950/40 border border-indigo-500/30 rounded-xl p-4 text-xs space-y-2">
235
+ <div className="flex items-center justify-between text-indigo-300 pb-2 border-b border-indigo-500/20">
236
+ <span className="font-semibold text-indigo-200">
237
+ {selectedMessage.from_name} (Prospect Reply)
238
+ </span>
239
+ <span className="text-[10px] text-zinc-400">Today</span>
240
+ </div>
241
+ <div
242
+ className="text-zinc-200 leading-relaxed prose prose-invert max-w-none text-xs"
243
+ dangerouslySetInnerHTML={{
244
+ __html: DOMPurify.sanitize(selectedMessage.body || "Thanks for reaching out! Let's schedule 15 minutes."),
245
+ }}
246
+ />
247
+ </div>
248
+ </div>
249
+
250
+ {/* Reply Composer Bottom Section */}
251
+ <div className="p-4 border-t border-zinc-800 bg-zinc-950 shrink-0 space-y-3">
252
+ <RichTextEditor
253
+ content={replyBody}
254
+ onChange={setReplyBody}
255
+ placeholder="Compose your reply or insert calendar meeting booking link..."
256
+ />
257
+
258
+ <div className="flex items-center justify-between">
259
+ <div className="flex items-center gap-2">
260
+ <button
261
+ type="button"
262
+ 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>")}
263
+ 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"
264
+ >
265
+ + Insert Booking Link
266
+ </button>
267
+ <button
268
+ type="button"
269
+ onClick={() => setReplyBody("<p>Thanks for getting back to me! I have attached our case study PDF here.</p>")}
270
+ 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"
271
+ >
272
+ + Insert Case Study
273
+ </button>
274
+ </div>
275
+
276
+ <button
277
+ type="button"
278
+ onClick={() => {
279
+ if (!replyBody) {
280
+ toast.error("Please write a reply message");
281
+ return;
282
+ }
283
+ sendReplyMutation.mutate({
284
+ messageId: selectedMessage.id,
285
+ body: replyBody,
286
+ });
287
+ }}
288
+ 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"
289
+ >
290
+ <Send className="w-3.5 h-3.5" /> Send Reply
291
+ </button>
292
+ </div>
293
+ </div>
294
+ </div>
295
+ ) : (
296
+ <div className="flex-1 flex items-center justify-center text-xs text-zinc-500">
297
+ Select a message thread from the left pane to view conversation.
298
+ </div>
299
+ )}
300
+ </div>
301
+ </div>
302
+ );
303
+ }