modul-s / src /pages /Inbox.tsx
sitcod3's picture
Upload src/pages/Inbox.tsx with huggingface_hub
343a096 verified
Raw History Blame Contribute Delete
13.4 kB
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { apiClient } from "../lib/apiClient";
import {
Inbox as InboxIcon,
Search,
Send,
Sparkles,
CheckCircle2,
AlertCircle,
Clock,
Mail,
Calendar,
User,
Building,
ChevronRight,
Paperclip,
Smile,
RefreshCw
} from "lucide-react";
import { useState } from "react";
import DOMPurify from "dompurify";
import StatusBadge from "../components/StatusBadge";
import RichTextEditor from "../components/RichTextEditor";
import toast from "react-hot-toast";
const CATEGORIES = [
{ key: "ALL", label: "All Messages" },
{ key: "INTERESTED", label: "Interested" },
{ key: "MEETING_BOOKED", label: "Meeting Booked" },
{ key: "REPLIED", label: "Replied" },
{ key: "NOT_INTERESTED", label: "Not Interested" },
];
export default function Inbox() {
const queryClient = useQueryClient();
const [selectedCategory, setSelectedCategory] = useState("ALL");
const [selectedMessageId, setSelectedMessageId] = useState<string | null>("msg_001");
const [replyBody, setReplyBody] = useState("");
const { data: messages = [], isLoading, refetch } = useQuery({
queryKey: ["inbox-messages", selectedCategory],
queryFn: async () => {
const res = await apiClient.get("/inbox", {
params: selectedCategory !== "ALL" ? { category: selectedCategory } : {},
});
return res.data;
},
});
const selectedMessage = (Array.isArray(messages) ? messages : []).find((m: any) => m.id === selectedMessageId) || (Array.isArray(messages) ? messages[0] : null);
const updateCategoryMutation = useMutation({
mutationFn: async ({ messageId, category }: { messageId: string; category: string }) => {
const res = await apiClient.post(`/inbox/${messageId}/category`, { category });
return res.data;
},
onSuccess: () => {
toast.success("Lead categorization updated");
refetch();
},
});
const sendReplyMutation = useMutation({
mutationFn: async ({ messageId, body }: { messageId: string; body: string }) => {
const res = await apiClient.post(`/inbox/${messageId}/reply`, { reply_body: body });
return res.data;
},
onSuccess: () => {
toast.success("Reply dispatched to lead!");
setReplyBody("");
refetch();
},
onError: () => {
toast.error("Failed to send reply");
}
});
return (
<div className="h-[calc(100vh-4rem)] flex flex-col overflow-hidden">
{/* Top Controls */}
<div className="h-14 border-b border-zinc-800 bg-zinc-950 px-6 flex items-center justify-between shrink-0">
<div className="flex items-center gap-3">
<div className="p-1.5 bg-indigo-500/10 text-indigo-400 rounded-lg">
<InboxIcon className="w-5 h-5" />
</div>
<div>
<h1 className="text-base font-bold text-zinc-100 flex items-center gap-2">
Unified Master Inbox
<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">
AI Sentiment Active
</span>
</h1>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => refetch()}
className="p-1.5 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-900 rounded-lg transition-colors"
title="Refresh Inboxes"
>
<RefreshCw className="w-4 h-4" />
</button>
</div>
</div>
{/* 2-Pane Inbox Workspace */}
<div className="flex-1 flex overflow-hidden">
{/* Left Pane: Message List */}
<div className="w-80 md:w-96 border-r border-zinc-800 flex flex-col bg-zinc-950/60 shrink-0">
{/* Categories Filter Tabs */}
<div className="p-3 border-b border-zinc-800 flex gap-1 overflow-x-auto">
{CATEGORIES.map((cat) => (
<button
key={cat.key}
onClick={() => setSelectedCategory(cat.key)}
className={`px-2.5 py-1 rounded-md text-xs font-medium whitespace-nowrap transition-colors ${
selectedCategory === cat.key
? "bg-indigo-600 text-white"
: "bg-zinc-900 text-zinc-400 hover:text-zinc-200"
}`}
>
{cat.label}
</button>
))}
</div>
{/* Messages Scroll Area */}
<div className="flex-1 overflow-y-auto divide-y divide-zinc-800/60">
{messages.map((msg: any) => {
const isSelected = msg.id === selectedMessage?.id;
return (
<div
key={msg.id}
onClick={() => setSelectedMessageId(msg.id)}
className={`p-4 transition-colors cursor-pointer ${
isSelected ? "bg-indigo-950/30 border-l-2 border-indigo-500" : "hover:bg-zinc-900/40"
}`}
>
<div className="flex items-start justify-between gap-2">
<span className="font-semibold text-xs text-zinc-100 truncate">
{msg.from_name || msg.from_email}
</span>
<span className="text-[10px] text-zinc-500 shrink-0">
{new Date(msg.created_at || Date.now()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</span>
</div>
<h4 className="text-xs font-medium text-zinc-300 mt-1 truncate">
{msg.subject}
</h4>
<p className="text-[11px] text-zinc-500 mt-1 line-clamp-2 leading-relaxed">
{msg.preview_body || msg.body?.replace(/<[^>]*>?/gm, "")}
</p>
<div className="mt-2.5 flex items-center justify-between">
<StatusBadge status={msg.lead_category || "INTERESTED"} size="sm" />
<span className="text-[10px] text-zinc-400 font-mono">
{msg.company_name}
</span>
</div>
</div>
);
})}
{messages.length === 0 && (
<div className="p-8 text-center text-xs text-zinc-500">
No replies found for this category filter.
</div>
)}
</div>
</div>
{/* Right Pane: Thread View & Response Composer */}
{selectedMessage ? (
<div className="flex-1 flex flex-col bg-zinc-950 overflow-hidden">
{/* Thread Header */}
<div className="p-5 border-b border-zinc-800 bg-zinc-900/30 flex items-center justify-between shrink-0">
<div className="flex items-center gap-3">
<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">
{selectedMessage.from_name ? selectedMessage.from_name[0] : "L"}
</div>
<div>
<h3 className="text-sm font-bold text-zinc-100 flex items-center gap-2">
{selectedMessage.subject}
</h3>
<div className="text-xs text-zinc-400 flex items-center gap-2 mt-0.5">
<span>{selectedMessage.from_name} &lt;{selectedMessage.from_email}&gt;</span>
<span>•</span>
<span className="font-mono text-zinc-500">{selectedMessage.company_name}</span>
</div>
</div>
</div>
{/* Categorization Dropdown */}
<div className="flex items-center gap-2">
<span className="text-xs text-zinc-500">AI Intent:</span>
<select
value={selectedMessage.lead_category || "INTERESTED"}
onChange={(e) =>
updateCategoryMutation.mutate({
messageId: selectedMessage.id,
category: e.target.value,
})
}
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"
>
<option value="INTERESTED">Interested</option>
<option value="MEETING_BOOKED">Meeting Booked</option>
<option value="REPLIED">Replied</option>
<option value="NOT_INTERESTED">Not Interested</option>
</select>
</div>
</div>
{/* Conversation Messages */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{/* Outbound Sent Message */}
<div className="max-w-2xl bg-zinc-900 border border-zinc-800 rounded-xl p-4 text-xs space-y-2">
<div className="flex items-center justify-between text-zinc-400 pb-2 border-b border-zinc-800">
<span className="font-medium text-zinc-300">Outbound Step #1 (Sent by Sequence)</span>
<span className="text-[10px]">Yesterday</span>
</div>
<div className="text-zinc-300 leading-relaxed">
Hi {selectedMessage.from_name?.split(" ")[0] || "there"},<br /><br />
Wanted to see how your team is managing deliverability and scale this quarter?<br /><br />
Best regards,<br />
Alex
</div>
</div>
{/* Inbound Prospect Reply */}
<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">
<div className="flex items-center justify-between text-indigo-300 pb-2 border-b border-indigo-500/20">
<span className="font-semibold text-indigo-200">
{selectedMessage.from_name} (Prospect Reply)
</span>
<span className="text-[10px] text-zinc-400">Today</span>
</div>
<div
className="text-zinc-200 leading-relaxed prose prose-invert max-w-none text-xs"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(selectedMessage.body || "Thanks for reaching out! Let's schedule 15 minutes."),
}}
/>
</div>
</div>
{/* Reply Composer Bottom Section */}
<div className="p-4 border-t border-zinc-800 bg-zinc-950 shrink-0 space-y-3">
<RichTextEditor
content={replyBody}
onChange={setReplyBody}
placeholder="Compose your reply or insert calendar meeting booking link..."
/>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<button
type="button"
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>")}
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"
>
+ Insert Booking Link
</button>
<button
type="button"
onClick={() => setReplyBody("<p>Thanks for getting back to me! I have attached our case study PDF here.</p>")}
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"
>
+ Insert Case Study
</button>
</div>
<button
type="button"
onClick={() => {
if (!replyBody) {
toast.error("Please write a reply message");
return;
}
sendReplyMutation.mutate({
messageId: selectedMessage.id,
body: replyBody,
});
}}
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"
>
<Send className="w-3.5 h-3.5" /> Send Reply
</button>
</div>
</div>
</div>
) : (
<div className="flex-1 flex items-center justify-center text-xs text-zinc-500">
Select a message thread from the left pane to view conversation.
</div>
)}
</div>
</div>
);
}