lead-to-policy/src/components/insurance/AiSuggestionPanel.tsx
Bhanu Prakash Sai Potteri 52a691fd75 feat: form RBAC guard, file fields, AI eligibility card, worklist recency sort + UI polish
- SchemaGuard: gate all activity bodies on form-screens (403 → permission card, not live form); dedup ActivityForm/RecommendBody
- ActivityForm: generic file-field upload (Onboard policy doc etc)
- UnderwritingBody: AI eligibility-notes parser (+raw fallback) in navy card, drop placeholder assess() panel
- DocumentUploadCard: equal-height cards + Replace/Remove
- Worklist + leads: sort latest-first by updated_at (instance_id tiebreak)
- Modal: cap 85vh, scroll body; slim themed scrollbars (schedule/rail/modal/kanban)
- Schedule agenda + Kanban columns: cap height, scroll internally; minimal kanban card
- Topbar: back button each screen; Avatar lucide type fix

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:10:11 +05:30

93 lines
3.6 KiB
TypeScript

import { useState } from 'react';
import { Sparkles, ChevronDown, ChevronUp, Check, AlertTriangle } from 'lucide-react';
import { cn } from '../../lib/cn';
import { Avatar } from '../core/Avatar';
import { AIDecisionCard } from './AIDecisionCard';
import type { AIDecisionCardProps } from './AIDecisionCard';
export interface AiSuggestionPanelProps extends AIDecisionCardProps {
/** Eyebrow heading above the card. @default "AI suggestion" */
label?: string;
/** Start as a one-line strip, expandable to the full card. @default true */
collapsible?: boolean;
className?: string;
}
/**
* Sidebar framing for an AI employee's decision inside an activity form.
* Defaults to a compact one-line strip (avatar · name · confidence · summary)
* so it sits comfortably beside small forms, and expands to the full
* AIDecisionCard on click. Pass `collapsible={false}` to always show the card.
*/
export function AiSuggestionPanel({
label = 'AI suggestion',
collapsible = true,
className,
...card
}: AiSuggestionPanelProps) {
const [open, setOpen] = useState(!collapsible);
const {
employee = 'Aria',
confidence = 0.85,
threshold = 0.7,
summary,
} = card;
const autonomous = confidence >= threshold;
const accent = autonomous ? 'var(--status-autonomous)' : 'var(--status-escalated)';
const accentSoft = autonomous ? 'var(--status-autonomous-soft)' : 'var(--status-escalated-soft)';
const pct = Math.round(Math.max(0, Math.min(1, confidence)) * 100);
return (
<div className={cn('flex flex-col gap-2.5', className)}>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1.5 text-2xs font-bold uppercase tracking-[0.06em] text-faint">
<Sparkles size={12} className="text-sunrise-500" />
{label}
</div>
{collapsible && open && (
<button
type="button"
onClick={() => setOpen(false)}
className="inline-flex items-center gap-1 bg-none border-none p-0 cursor-pointer font-sans text-2xs font-semibold text-link"
>
<ChevronUp size={12} />
Collapse
</button>
)}
</div>
{open ? (
<AIDecisionCard {...card} />
) : (
<button
type="button"
onClick={() => setOpen(true)}
className="group relative w-full text-left bg-card rounded-lg border border-border-subtle shadow-sm hover:shadow-md transition-shadow overflow-hidden font-sans flex items-center gap-3 pl-[14px] pr-3 py-2.5"
>
{/* accent rail */}
<span className="absolute top-0 left-0 bottom-0 w-1" style={{ background: accent }} />
<Avatar name={employee} ai size={30} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-bold text-strong leading-tight truncate">{employee}</span>
<span
className="inline-flex items-center gap-1 text-2xs font-bold tracking-[0.03em] px-1.5 py-0.5 rounded-pill shrink-0"
style={{ background: accentSoft, color: accent }}
>
{autonomous ? <Check size={10} strokeWidth={3} /> : <AlertTriangle size={10} strokeWidth={3} />}
{pct}%
</span>
</div>
{summary && <div className="text-xs text-muted mt-0.5 truncate">{summary}</div>}
</div>
<ChevronDown
size={16}
className="shrink-0 text-faint transition-transform group-hover:translate-y-0.5"
/>
</button>
)}
</div>
);
}