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 && (
)}
) : (
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 && (
)}
>
);
})()}
);
}