/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * `PlanCard` — render an AuthoringPlan with edit affordances. * * The plan-before-code UX: the AI proposes a structured plan; the user * trims contributions, prunes capabilities, edits the summary, then * approves. After approval, the chat panel routes through the actual * bundle synthesis (a follow-up that consumes this card's `onApprove`). * * Spec: docs/architecture/ai-customization/04-ai-authoring.md §4. */ import { useMemo, useState } from 'react'; import { Check, ChevronRight, Edit3, ShieldAlert, Sparkles, X } from 'lucide-react'; import { computeRisks, overallTier, parseCapability, type AuthoringPlan, type CapabilityRisk, type RiskTier, } from '@ifc-lite/extensions'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; interface PlanCardProps { /** The plan to show. Editable copy is stored in component state. */ plan: AuthoringPlan; /** Called when the user confirms the (possibly edited) plan. */ onApprove(plan: AuthoringPlan): void; /** Called when the user cancels / dismisses. */ onCancel(): void; /** Hide the inline edit fields. Useful for read-only review of past plans. */ readOnly?: boolean; } export function PlanCard({ plan, onApprove, onCancel, readOnly }: PlanCardProps) { const [draft, setDraft] = useState(plan); const risks = useMemo(() => { const parsed = draft.capabilities .map((raw) => parseCapability(raw)) .filter((r): r is { ok: true; value: ReturnType extends { ok: true; value: infer V } ? V : never } => r.ok) .map((r) => r.value); return computeRisks(parsed); }, [draft.capabilities]); const overall = overallTier(risks); const toggleCapability = (raw: string) => { setDraft((p) => ({ ...p, capabilities: p.capabilities.includes(raw) ? p.capabilities.filter((c) => c !== raw) : [...p.capabilities, raw], })); }; const removeContribution = (idx: number) => { setDraft((p) => ({ ...p, contributions: p.contributions.filter((_, i) => i !== idx), })); }; return (
{overall === 'red' ? : }
Authoring plan
{readOnly ? (
{draft.summary}
) : ( setDraft((p) => ({ ...p, summary: e.target.value }))} className="mt-1 text-sm font-medium" aria-label="Plan summary" /> )}

{draft.rationale}

{draft.contributions.length === 0 ? (
No contributions.
) : (
    {draft.contributions.map((c, i) => (
  • {c.kind} {c.label} {c.slot && ( {c.slot} )} {!readOnly && ( )}
  • ))}
)}
{draft.capabilities.length === 0 ? (
No capabilities requested.
) : (
    {risks.map((risk) => (
  • {!readOnly && ( toggleCapability(risk.capability.raw)} aria-label={`Toggle capability ${risk.capability.raw}`} /> )}
    {risk.capability.raw}
    {risk.description}
  • ))}
)}
{draft.triggers.map((t) => ( {t} ))}
{draft.tests.length > 0 && (
    {draft.tests.map((t, i) => (
  • {t.name}
    Fixture: {t.fixture}
    {t.assertionSummary}
  • ))}
)} {draft.notes && (
{draft.notes}
)} {!readOnly && (
)}
); } function RiskBadge({ tier }: { tier: RiskTier }) { return ( {tier} ); }