141 lines
5.1 KiB
TypeScript
141 lines
5.1 KiB
TypeScript
import { useState } from 'react';
|
||
import type { CSSProperties } from 'react';
|
||
import { ChevronRight, FileText } from 'lucide-react';
|
||
import { cn } from '../../lib/cn';
|
||
import { Avatar } from '../core/Avatar';
|
||
import { ConfidenceRing } from './ConfidenceRing';
|
||
import { ReasoningBody } from './ReasoningBody';
|
||
|
||
export type Citation = string | { title: string };
|
||
|
||
export interface AIDecisionCardProps {
|
||
employee?: string;
|
||
role?: string;
|
||
activity?: string;
|
||
/** Confidence 0–1. @default 0.85 */
|
||
confidence?: number;
|
||
summary?: string;
|
||
reasoning?: string | null;
|
||
citations?: Citation[];
|
||
time?: string | null;
|
||
defaultExpanded?: boolean;
|
||
/** Autonomy threshold. @default 0.7 */
|
||
threshold?: number;
|
||
className?: string;
|
||
style?: CSSProperties;
|
||
}
|
||
|
||
/**
|
||
* AI DECISION CARD — Aria's signature component.
|
||
* Which AI employee acted, the activity, a confidence ring, a one-line
|
||
* reasoning summary, expandable full reasoning, and cited-knowledge chips.
|
||
*/
|
||
export function AIDecisionCard({
|
||
employee = 'Aria',
|
||
role = 'Lead Qualifier',
|
||
activity,
|
||
confidence = 0.85,
|
||
summary,
|
||
reasoning = null,
|
||
citations = [],
|
||
time = null,
|
||
defaultExpanded = false,
|
||
threshold = 0.7,
|
||
className,
|
||
style,
|
||
}: AIDecisionCardProps) {
|
||
const [open, setOpen] = useState(defaultExpanded);
|
||
const autonomous = confidence >= threshold;
|
||
const accent = autonomous ? 'var(--status-autonomous)' : 'var(--status-escalated)';
|
||
const accentSoft = autonomous ? 'var(--status-autonomous-soft)' : 'var(--status-escalated-soft)';
|
||
|
||
return (
|
||
<article
|
||
className={cn('relative bg-card rounded-lg border border-border-subtle shadow-md overflow-hidden font-sans', className)}
|
||
style={style}
|
||
>
|
||
{/* accent rail */}
|
||
<span
|
||
className={cn('absolute top-0 left-0 bottom-0 w-1', autonomous && 'bg-sunrise')}
|
||
style={autonomous ? undefined : { background: 'var(--status-escalated)' }}
|
||
/>
|
||
|
||
{/* header band — identity + confidence, tinted by autonomy state */}
|
||
<header
|
||
className="flex items-center gap-3 pl-[22px] pr-5 py-3.5 border-b border-border-subtle"
|
||
style={{ background: accentSoft }}
|
||
>
|
||
<Avatar name={employee} ai size={38} />
|
||
<div className="min-w-0 flex-1">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<span className="text-base font-bold text-strong leading-tight">{employee}</span>
|
||
<span className="text-[10px] font-bold uppercase tracking-[0.06em] text-faint bg-card border border-border-subtle px-[7px] py-[3px] rounded-pill">
|
||
{role}
|
||
</span>
|
||
</div>
|
||
<div className="text-xs text-muted mt-0.5 truncate">
|
||
{activity}
|
||
{time && <span className="text-faint"> · {time}</span>}
|
||
</div>
|
||
</div>
|
||
<div className="shrink-0">
|
||
<ConfidenceRing value={confidence} size={64} threshold={threshold} />
|
||
</div>
|
||
</header>
|
||
|
||
<div className="pl-[22px] pr-5 py-[18px]">
|
||
{/* status pill */}
|
||
<div
|
||
className="inline-flex items-center gap-1.5 text-2xs font-bold tracking-[0.03em] px-[11px] py-[5px] rounded-pill mb-2.5"
|
||
style={{ background: accentSoft, color: accent }}
|
||
>
|
||
<span className="w-1.5 h-1.5 rounded-full" style={{ background: accent }} />
|
||
{autonomous ? 'AUTONOMOUS DECISION' : 'ESCALATED TO HUMAN'}
|
||
</div>
|
||
|
||
{/* one-line reasoning summary */}
|
||
<p className="m-0 text-base leading-normal text-body">{summary}</p>
|
||
|
||
{/* expandable full reasoning */}
|
||
{reasoning && (
|
||
<div className="mt-3">
|
||
<button
|
||
type="button"
|
||
onClick={() => setOpen((o) => !o)}
|
||
className="inline-flex items-center gap-1.5 bg-none border-none p-0 cursor-pointer font-sans text-xs font-semibold text-link"
|
||
>
|
||
<ChevronRight size={14} className={cn('transition-transform duration-200', open && 'rotate-90')} />
|
||
{open ? 'Hide full reasoning' : 'Show full reasoning'}
|
||
</button>
|
||
{open && (
|
||
<div className="mt-2.5 px-4 py-3.5 bg-sunk rounded-lg">
|
||
<ReasoningBody text={reasoning} accent={accent} accentSoft={accentSoft} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* citation chips */}
|
||
{citations.length > 0 && (
|
||
<div className="mt-3.5">
|
||
<div className="text-[10px] font-bold uppercase tracking-[0.06em] text-faint mb-[7px]">
|
||
Cited knowledge
|
||
</div>
|
||
<div className="flex flex-wrap gap-[7px]">
|
||
{citations.map((c, i) => (
|
||
<span
|
||
key={i}
|
||
className="inline-flex items-center gap-1.5 text-xs font-medium text-muted bg-card border border-border-default rounded-sm px-2.5 py-[5px]"
|
||
>
|
||
<FileText size={12} className="text-sunrise-500" />
|
||
{typeof c === 'string' ? c : c.title}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</article>
|
||
);
|
||
}
|