Download src/pages/Inbox.tsx from sitcod3/modul-s: direct link, hf CLI and curl.
- Browser
- Download file 13.4 kB
-
https://huggingface.co/spaces/sitcod3/modul-s/resolve/main/src/pages/Inbox.tsx
- Command line
-
hf download hf://spaces/sitcod3/modul-s/src/pages/Inbox.tsx
-
curl -L -o Inbox.tsx https://huggingface.co/spaces/sitcod3/modul-s/resolve/main/src/pages/Inbox.tsx
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} <{selectedMessage.from_email}></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> | |
| ); | |
| } | |