import React from "react"; import { Flag, Mail, MessageSquare, Plus, Search } from "lucide-react"; import { cn } from "@/lib/utils"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { AiConvChannelFilter, AiConvFilterTab, AiConvListItemData, AiConvStatus, } from "./types"; import { ContactAvatar, displayContactName, PANEL_HEADER_HEIGHT, } from "./helpers"; // --------------------------------------------------------------------------- // ConversationStatusChip // --------------------------------------------------------------------------- type BadgeVariant = "success" | "default" | "warning" | "secondary"; const STATUS_CONFIG: Record< AiConvStatus, { label: string; variant: BadgeVariant; dotClass: string } > = { "ai-active": { label: "AI Active", variant: "success", dotClass: "bg-success", }, manual: { label: "Manual", variant: "default", dotClass: "bg-primary", }, "needs-attention": { label: "Needs Attention", variant: "warning", dotClass: "bg-warning", }, closed: { label: "Closed", variant: "secondary", dotClass: "bg-muted-foreground/50", }, }; export interface ConversationStatusChipProps { status: AiConvStatus; showDot?: boolean; className?: string; } export function ConversationStatusChip({ status, showDot = false, className, }: ConversationStatusChipProps) { const { label, variant, dotClass } = STATUS_CONFIG[status]; return ( {showDot && ( )} {label} ); } // --------------------------------------------------------------------------- // ConversationListItem // --------------------------------------------------------------------------- export interface ConversationListItemProps { data: AiConvListItemData; isActive?: boolean; /** * Visual variant: "panel" (default) is the 3-row sidebar card used by the * Conversations inbox; "row" is a single-line, Gmail-style row (name · * subject · snippet · timestamp) for full-width embedded lists where the * user drills into a thread instead of using a side panel. */ variant?: "panel" | "row"; onClick?: (id: string) => void; onRead?: (id: string) => void; } export function ConversationListItem({ data, isActive, variant = "panel", onClick, onRead, }: ConversationListItemProps) { if (variant === "row") { const unread = Boolean(data.unreadCount); return ( ); } return ( ); } // --------------------------------------------------------------------------- // ConversationList // --------------------------------------------------------------------------- function filterConversations( conversations: AiConvListItemData[], query: string, filter: AiConvFilterTab, channelFilter: AiConvChannelFilter, ): AiConvListItemData[] { const q = query.toLowerCase(); return conversations.filter((c) => { const matchesFilter = filter === "all" || (filter === "emails" ? (c.channel ?? "chat") === "email" : c.status === filter); const matchesChannel = channelFilter === "all" || (c.channel ?? "chat") === channelFilter; const matchesSearch = !q || c.contact.name.toLowerCase().includes(q) || c.lastMessage.toLowerCase().includes(q); return matchesFilter && matchesChannel && matchesSearch; }); } const FILTER_TABS: { id: AiConvFilterTab; label: string }[] = [ { id: "all", label: "All" }, { id: "emails", label: "Emails" }, { id: "needs-attention", label: "Urgent" }, { id: "closed", label: "Archived" }, { id: "ai-active", label: "AI Active" }, ]; export interface ConversationListProps { conversations: AiConvListItemData[]; activeId?: string; /** * Embedded mode for contexts scoped to a handful of conversations (e.g. * one opportunity's applicants): swaps the search + filter-tab chrome for * a slim labelled header (overline label + count badge) and lists every * conversation unfiltered. Defaults to false. */ compact?: boolean; /** Header label shown in compact mode. Defaults to "Conversations". */ compactLabel?: string; /** * Shows a "New Email" button in the compact header when set. For scoped * contexts (e.g. one opportunity's applicants) where the broker may need * to start a new thread instead of only replying to existing ones. */ onCompose?: () => void; /** * Visual variant applied to every list item — "panel" (default, sidebar * card) or "row" (single-line, Gmail-style; pair with `compact` for * full-width embedded lists). */ itemVariant?: "panel" | "row"; searchQuery?: string; activeFilter?: AiConvFilterTab; channelFilter?: AiConvChannelFilter; hasMore?: boolean; isLoadingMore?: boolean; onSearchChange?: (v: string) => void; onFilterChange?: (f: AiConvFilterTab) => void; onChannelFilterChange?: (f: AiConvChannelFilter) => void; onSelect?: (id: string) => void; onRead?: (id: string) => void; onLoadMore?: () => void; className?: string; } export function ConversationList({ conversations, activeId, compact = false, compactLabel = "Conversations", onCompose, itemVariant = "panel", searchQuery = "", activeFilter = "all", channelFilter = "all", hasMore, isLoadingMore, onSearchChange, onFilterChange, onSelect, onRead, onLoadMore, className, }: ConversationListProps) { return (
{compact ? (
{compactLabel} {conversations.length} {onCompose && ( )}
) : (
onSearchChange?.(e.target.value)} placeholder="Search conversations..." className="h-8 pl-8 text-body-small" />
v && onFilterChange?.(v as AiConvFilterTab)} className="w-full" > {FILTER_TABS.map((tab) => ( {tab.label} ))}
)} {/* List */}
{(() => { const filtered = compact ? conversations : filterConversations( conversations, searchQuery, activeFilter, channelFilter, ); return filtered.length === 0 ? (

No conversations

{searchQuery && ( )} {!searchQuery && activeFilter !== "all" && ( )}
) : ( <> {filtered.map((item) => ( ))} {hasMore && (
)} ); })()}
); }