import * as React from "react"; import { PanelRight } from "lucide-react"; import { cn } from "@/lib/utils"; import { formatCurrentMonthYear } from "@/lib/format-date"; import { Button } from "@/components/ui/button"; import { PageTopBar } from "@/components/ui/page-top-bar"; import { PolicyAIPanel } from "./policy-ai-panel"; import { PolicyAIHistoryPanel } from "./policy-ai-history-panel"; import { PolicyAIContextSidebar } from "./policy-ai-context-sidebar"; import type { PolicyAIMessage, PolicyConversationItem, PolicyQueryContext, } from "./policy-ai-primitives"; // --------------------------------------------------------------------------- // Props // --------------------------------------------------------------------------- export interface PolicyAIPageProps { // ── Chat area (pass-through to PolicyAIPanel inline) ── messages?: PolicyAIMessage[]; suggestedQuestions?: Record; isStreaming?: boolean; isLoading?: boolean; thinkingSteps?: string[]; onSendMessage?: (text: string) => void; onAttachFile?: (files: FileList) => void; onAttachImage?: (files: FileList) => void; onReset?: () => void; /** True when older messages can be loaded for the active conversation. */ hasMoreMessages?: boolean; /** True while a `onLoadMoreMessages` request is in flight. */ isLoadingMoreMessages?: boolean; /** Fired when the user scrolls near the top of the chat — fetch older messages. */ onLoadMoreMessages?: () => void; // ── History panel (left) ── conversations?: PolicyConversationItem[]; activeConversationId?: string; onSelectConversation?: (id: string) => void; onNewChat?: () => void; /** Whether more conversations can be loaded (cursor pagination). */ hasMoreConversations?: boolean; /** Load the next page of conversations. */ onLoadMoreConversations?: () => void; /** Whether a conversations load-more request is in flight. */ isLoadingMoreConversations?: boolean; /** * Notified when the history search query changes. Use to drive a server-side * search (the panel always also filters the current list client-side). */ onSearchConversations?: (query: string) => void; // ── Context sidebar (right) ── /** * Whether the context panel starts visible. * Renamed from `showContextPanel` to make uncontrolled semantics explicit — * the toggle button owns the state after mount. */ defaultShowContextPanel?: boolean; bankCount?: number; categoryCount?: number; lastUpdated?: string; /** * Suggested follow-up questions (e.g. backend-generated). When provided, these * override the built-in `deriveFollowUps` heuristic; falls back to it when omitted. */ suggestedFollowUps?: string[]; /** Opens the Support Agent panel — wired to PageTopBar "Ask Support" button. */ onAskSupport?: () => void; className?: string; } // --------------------------------------------------------------------------- // Internal helpers // --------------------------------------------------------------------------- /** Derives suggested follow-up questions from the last assistant message's query context. */ function deriveFollowUps(context: PolicyQueryContext | undefined): string[] { if (!context) return []; const { queryType, policyType, bankName } = context; if (queryType === "cross_bank_comparison") { return [ `Which of these banks is best overall for ${context.categories[0] ?? policyType}?`, bankName ? `Does ${bankName} have any exceptions?` : "Which bank has the most flexible policy?", "Show me the ranked list of lenders.", ]; } if (queryType === "ranking") { return [ `What are the full details for the #1 ranked bank?`, `Which banks in the list accept cases under 80% LVR?`, `Compare the top 3 banks side by side.`, ]; } if (queryType === "single_bank") { return [ `How does ${bankName ?? "this bank"} compare to other lenders?`, `What documentation does ${bankName ?? "this bank"} require?`, `Which banks have a better policy than ${bankName ?? "this bank"}?`, ]; } if (queryType === "threshold_filter") { return [ `What is the maximum LVR across all filtered banks?`, `Are any of these banks on a specialist product?`, `Rank the filtered banks best to worst.`, ]; } return []; } // --------------------------------------------------------------------------- // PolicyAIPage (Template) // --------------------------------------------------------------------------- /** * Dedicated full-page layout for the Policy AI feature. * * 3-panel layout modelled on the AI Conversations page: * - **Left** (260px): `PolicyAIHistoryPanel` — recent conversations + search * - **Center** (flex-1): `PolicyAIPanel` in inline mode — chat + response cards * - **Right** (280px, toggle-able): `PolicyAIContextSidebar` — coverage stats + * last query context + suggested follow-ups * * @example * */ export function PolicyAIPage({ // Chat messages = [], suggestedQuestions, isStreaming = false, isLoading = false, thinkingSteps, onSendMessage, onAttachFile, onAttachImage, onReset, hasMoreMessages, isLoadingMoreMessages, onLoadMoreMessages, // History conversations = [], activeConversationId, onSelectConversation, onNewChat, hasMoreConversations = false, onLoadMoreConversations, isLoadingMoreConversations = false, onSearchConversations, // Context defaultShowContextPanel = true, bankCount = 0, categoryCount = 0, lastUpdated = formatCurrentMonthYear(), suggestedFollowUps, onAskSupport, className, }: PolicyAIPageProps) { const [showContextPanel, setShowContextPanel] = React.useState( defaultShowContextPanel, ); const [historySearch, setHistorySearch] = React.useState(""); // Walk backwards to find the most recent assistant message that carries a // queryContext. This keeps the sidebar populated while a new (streaming) // reply is pending — the in-flight message won't carry a queryContext yet. // Single-pass: no intermediate array allocation. const lastQueryContext = React.useMemo(() => { for (let i = messages.length - 1; i >= 0; i--) { const m = messages[i]; if (m.role === "assistant" && m.queryContext) return m.queryContext; } return undefined; }, [messages]); // Prefer caller-supplied (e.g. backend-generated) follow-ups; otherwise derive // from the last query context. deriveFollowUps is cheap — no useMemo needed. const followUps = suggestedFollowUps ?? deriveFollowUps(lastQueryContext); const handleNewChat = () => { onReset?.(); onNewChat?.(); }; // Derive once — used for both aria-label and title on the toggle button. const panelToggleLabel = showContextPanel ? "Hide context panel" : "Show context panel"; return (
{/* ── Page header — uses shared PageTopBar (same as Loan CRM, Contact, etc.) ── */} setShowContextPanel((v) => !v)} aria-label={panelToggleLabel} aria-pressed={showContextPanel} title={panelToggleLabel} >
); }