"use client"; import { memo, useCallback, useRef, useState } from "react"; import { AlertCircleIcon, CheckIcon, ChevronDownIcon, LoaderIcon, XCircleIcon, } from "lucide-react"; import { useScrollLock, useToolCallElapsed, type ToolApprovalOption, type ToolCallMessagePart, type ToolCallMessagePartProps, type ToolCallMessagePartStatus, type ToolCallMessagePartComponent, } from "@assistant-ui/react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; const ANIMATION_DURATION = 200; const pressable = "active:scale-[0.98]"; export type ToolFallbackRootProps = Omit< React.ComponentProps, "open" | "onOpenChange" > & { open?: boolean; onOpenChange?: (open: boolean) => void; defaultOpen?: boolean; }; function ToolFallbackRoot({ className, open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen = false, children, ...props }: ToolFallbackRootProps) { const collapsibleRef = useRef(null); const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION); const isControlled = controlledOpen !== undefined; const isOpen = isControlled ? controlledOpen : uncontrolledOpen; const handleOpenChange = useCallback( (open: boolean) => { lockScroll(); if (!isControlled) { setUncontrolledOpen(open); } controlledOnOpenChange?.(open); }, [lockScroll, isControlled, controlledOnOpenChange], ); return ( {children} ); } type ToolStatus = ToolCallMessagePartStatus["type"]; const statusIconMap: Record = { running: LoaderIcon, complete: CheckIcon, incomplete: XCircleIcon, "requires-action": AlertCircleIcon, }; const formatToolDuration = (ms: number) => { if (ms < 1000) return "<1s"; const seconds = ms / 1000; if (seconds < 10) return `${(Math.floor(seconds * 10) / 10).toFixed(1)}s`; if (seconds < 60) return `${Math.floor(seconds)}s`; return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`; }; function ToolFallbackDuration({ className, ...props }: React.ComponentProps<"span">) { const elapsedMs = useToolCallElapsed(); if (elapsedMs === undefined) return null; return ( {formatToolDuration(elapsedMs)} ); } function ToolFallbackTrigger({ toolName, status, className, ...props }: React.ComponentProps & { toolName: string; status?: ToolCallMessagePartStatus; }) { const statusType = status?.type ?? "complete"; const isRunning = statusType === "running"; const isCancelled = status?.type === "incomplete" && status.reason === "cancelled"; const Icon = statusIconMap[statusType]; const label = isCancelled ? "已中止 Skill" : isRunning ? "正在使用 Skill" : statusType === "incomplete" ? "Skill 调用失败" : "已使用 Skill"; return ( {label}: {toolName} {isRunning && ( {label}: {toolName} )} ); } function ToolFallbackContent({ className, children, ...props }: React.ComponentProps) { return (
{children}
); } function ToolFallbackArgs({ argsText, className, ...props }: React.ComponentProps<"div"> & { argsText?: string; }) { if (!argsText) return null; return (
        {argsText}
      
); } function ToolFallbackResult({ result, className, ...props }: React.ComponentProps<"div"> & { result?: unknown; }) { if (result === undefined) return null; return (

执行结果

        {typeof result === "string" ? result : JSON.stringify(result, null, 2)}
      
); } function ToolFallbackError({ status, className, ...props }: React.ComponentProps<"div"> & { status?: ToolCallMessagePartStatus; }) { if (status?.type !== "incomplete") return null; const error = status.error; const errorText = error ? typeof error === "string" ? error : JSON.stringify(error) : null; if (!errorText) return null; const isCancelled = status.reason === "cancelled"; const headerText = isCancelled ? "中止原因" : "错误"; return (

{headerText}

{errorText}

); } const APPROVED_RESULT = "Approved by user"; const DENIED_RESULT = "User denied tool execution"; const APPROVAL_OPTION_DEFAULT_LABELS: Record = { "allow-once": "Allow", "allow-always": "Always allow", "reject-once": "Deny", "reject-always": "Always deny", }; const isAllowKind = (kind: string) => kind === "allow-once" || kind === "allow-always"; const approvalOptionLabel = (option: ToolApprovalOption) => option.label ?? (Object.hasOwn(APPROVAL_OPTION_DEFAULT_LABELS, option.kind) ? APPROVAL_OPTION_DEFAULT_LABELS[option.kind] : undefined) ?? option.id; function ToolFallbackApproval({ className, addResult, resume, interrupt, approval, respondToApproval, ...props }: React.ComponentProps<"div"> & Partial< Pick > & { interrupt?: ToolCallMessagePart["interrupt"]; approval?: ToolCallMessagePart["approval"]; }) { const [submitted, setSubmitted] = useState(false); const [confirmingId, setConfirmingId] = useState(null); if ( approval != null && (approval.approved !== undefined || approval.resolution !== undefined) ) return null; // Custom (`_`-prefixed) kinds cannot be resolved to a boolean by the kit; // hosts using custom kinds render their own bar. A declared option list is // a host constraint: the kit never adds an approval path beyond it, but // always preserves a refusal path. const declaredOptions = respondToApproval ? approval?.options : undefined; const options = declaredOptions?.filter((o) => Object.hasOwn(APPROVAL_OPTION_DEFAULT_LABELS, o.kind), ); const respond = (approved: boolean) => { if (submitted) return; if ( approval != null && approval.approved === undefined && respondToApproval ) { respondToApproval({ approved }); } else if (interrupt) { resume?.({ approved }); } else { addResult?.(approved ? APPROVED_RESULT : DENIED_RESULT); } setSubmitted(true); }; const respondWithOption = (option: ToolApprovalOption) => { if (submitted) return; respondToApproval?.({ optionId: option.id }); setSubmitted(true); setConfirmingId(null); }; const handleOption = (option: ToolApprovalOption) => { if (option.confirm) { setConfirmingId(option.id); } else { respondWithOption(option); } }; const confirming = confirmingId != null ? options?.find((o) => o.id === confirmingId) : undefined; if (confirming) { const confirmMeta = typeof confirming.confirm === "object" ? confirming.confirm : undefined; const confirmDescription = confirmMeta?.description ?? confirming.description; return (

{confirmMeta?.title ?? `${approvalOptionLabel(confirming)}?`}

{confirmDescription && (

{confirmDescription}

)} {confirming.grants && confirming.grants.length > 0 && (
    {confirming.grants.map((grant) => (
  • {grant}
  • ))}
)}
); } if (declaredOptions && declaredOptions.length > 0) { const allowOptions = options?.filter((o) => isAllowKind(o.kind)) ?? []; const rejectOptions = options?.filter((o) => !isAllowKind(o.kind)) ?? []; return (
{[...allowOptions, ...rejectOptions].map((option) => ( ))} {rejectOptions.length === 0 && ( )}
); } return (
); } const ToolFallbackImpl: ToolCallMessagePartComponent = ({ toolName, argsText, result, status, addResult, resume, interrupt, approval, respondToApproval, }) => { const isCancelled = status?.type === "incomplete" && status.reason === "cancelled"; const isRequiresAction = status?.type === "requires-action"; const [open, setOpen] = useState(isRequiresAction); const [prevRequiresAction, setPrevRequiresAction] = useState(isRequiresAction); if (isRequiresAction !== prevRequiresAction) { setPrevRequiresAction(isRequiresAction); if (isRequiresAction) setOpen(true); } return ( {isRequiresAction && ( )} {!isCancelled && } ); }; const ToolFallback = memo( ToolFallbackImpl, ) as unknown as ToolCallMessagePartComponent & { Root: typeof ToolFallbackRoot; Trigger: typeof ToolFallbackTrigger; Content: typeof ToolFallbackContent; Args: typeof ToolFallbackArgs; Result: typeof ToolFallbackResult; Error: typeof ToolFallbackError; Approval: typeof ToolFallbackApproval; }; ToolFallback.displayName = "ToolFallback"; ToolFallback.Root = ToolFallbackRoot; ToolFallback.Trigger = ToolFallbackTrigger; ToolFallback.Content = ToolFallbackContent; ToolFallback.Args = ToolFallbackArgs; ToolFallback.Result = ToolFallbackResult; ToolFallback.Error = ToolFallbackError; ToolFallback.Approval = ToolFallbackApproval; export { ToolFallback, ToolFallbackRoot, ToolFallbackTrigger, ToolFallbackContent, ToolFallbackArgs, ToolFallbackResult, ToolFallbackError, ToolFallbackApproval, };