276 lines
7.6 KiB
TypeScript
276 lines
7.6 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
type ReasoningGroupComponent,
|
|
type ReasoningMessagePartComponent,
|
|
useAuiState,
|
|
useScrollLock,
|
|
} from "@assistant-ui/react";
|
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
import { BrainIcon, ChevronDownIcon } from "lucide-react";
|
|
import { memo, useCallback, useRef, useState } from "react";
|
|
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from "@/components/ui/collapsible";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const ANIMATION_DURATION = 200;
|
|
|
|
const reasoningVariants = cva("aui-reasoning-root mb-4 w-full", {
|
|
variants: {
|
|
variant: {
|
|
outline: "rounded-lg border px-3 py-2",
|
|
ghost: "",
|
|
muted: "rounded-lg bg-muted/50 px-3 py-2",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "outline",
|
|
},
|
|
});
|
|
|
|
export type ReasoningRootProps = Omit<
|
|
React.ComponentProps<typeof Collapsible>,
|
|
"open" | "onOpenChange"
|
|
> &
|
|
VariantProps<typeof reasoningVariants> & {
|
|
open?: boolean;
|
|
onOpenChange?: (open: boolean) => void;
|
|
defaultOpen?: boolean;
|
|
};
|
|
|
|
function ReasoningRoot({
|
|
className,
|
|
variant,
|
|
open: controlledOpen,
|
|
onOpenChange: controlledOnOpenChange,
|
|
defaultOpen = false,
|
|
children,
|
|
...props
|
|
}: ReasoningRootProps) {
|
|
const collapsibleRef = useRef<HTMLDivElement>(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) => {
|
|
if (!open) {
|
|
lockScroll();
|
|
}
|
|
if (!isControlled) {
|
|
setUncontrolledOpen(open);
|
|
}
|
|
controlledOnOpenChange?.(open);
|
|
},
|
|
[lockScroll, isControlled, controlledOnOpenChange],
|
|
);
|
|
|
|
return (
|
|
<Collapsible
|
|
ref={collapsibleRef}
|
|
data-slot="reasoning-root"
|
|
data-variant={variant}
|
|
open={isOpen}
|
|
onOpenChange={handleOpenChange}
|
|
className={cn(
|
|
"group/reasoning-root",
|
|
reasoningVariants({ variant, className }),
|
|
)}
|
|
style={
|
|
{
|
|
"--animation-duration": `${ANIMATION_DURATION}ms`,
|
|
} as React.CSSProperties
|
|
}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</Collapsible>
|
|
);
|
|
}
|
|
|
|
function ReasoningFade({ className, ...props }: React.ComponentProps<"div">) {
|
|
return (
|
|
<div
|
|
data-slot="reasoning-fade"
|
|
className={cn(
|
|
"aui-reasoning-fade pointer-events-none absolute inset-x-0 bottom-0 z-10 h-8",
|
|
"bg-[linear-gradient(to_top,var(--color-background),transparent)]",
|
|
"group-data-[variant=muted]/reasoning-root:bg-[linear-gradient(to_top,hsl(var(--muted)/0.5),transparent)]",
|
|
"fade-in-0 animate-in",
|
|
"group-data-[state=open]/collapsible-content:animate-out",
|
|
"group-data-[state=open]/collapsible-content:fade-out-0",
|
|
"group-data-[state=open]/collapsible-content:delay-[calc(var(--animation-duration)*0.75)]",
|
|
"group-data-[state=open]/collapsible-content:fill-mode-forwards",
|
|
"duration-(--animation-duration)",
|
|
"group-data-[state=open]/collapsible-content:duration-(--animation-duration)",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function ReasoningTrigger({
|
|
active,
|
|
duration,
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof CollapsibleTrigger> & {
|
|
active?: boolean;
|
|
duration?: number;
|
|
}) {
|
|
const durationText = duration ? ` (${duration}s)` : "";
|
|
|
|
return (
|
|
<CollapsibleTrigger
|
|
data-slot="reasoning-trigger"
|
|
className={cn(
|
|
"aui-reasoning-trigger group/trigger flex max-w-[75%] items-center gap-2 py-1 text-muted-foreground text-sm transition-colors hover:text-foreground",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
<BrainIcon
|
|
data-slot="reasoning-trigger-icon"
|
|
className="aui-reasoning-trigger-icon size-4 shrink-0"
|
|
/>
|
|
<span
|
|
data-slot="reasoning-trigger-label"
|
|
className="aui-reasoning-trigger-label-wrapper relative inline-block leading-none"
|
|
>
|
|
<span>Reasoning{durationText}</span>
|
|
{active ? (
|
|
<span
|
|
aria-hidden
|
|
data-slot="reasoning-trigger-shimmer"
|
|
className="aui-reasoning-trigger-shimmer shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none"
|
|
>
|
|
Reasoning{durationText}
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
<ChevronDownIcon
|
|
data-slot="reasoning-trigger-chevron"
|
|
className={cn(
|
|
"aui-reasoning-trigger-chevron mt-0.5 size-4 shrink-0",
|
|
"transition-transform duration-(--animation-duration) ease-out",
|
|
"group-data-[state=closed]/trigger:-rotate-90",
|
|
"group-data-[state=open]/trigger:rotate-0",
|
|
)}
|
|
/>
|
|
</CollapsibleTrigger>
|
|
);
|
|
}
|
|
|
|
function ReasoningContent({
|
|
className,
|
|
children,
|
|
...props
|
|
}: React.ComponentProps<typeof CollapsibleContent>) {
|
|
return (
|
|
<CollapsibleContent
|
|
data-slot="reasoning-content"
|
|
className={cn(
|
|
"aui-reasoning-content relative overflow-hidden text-muted-foreground text-sm outline-none",
|
|
"group/collapsible-content ease-out",
|
|
"data-[state=closed]:animate-collapsible-up",
|
|
"data-[state=open]:animate-collapsible-down",
|
|
"data-[state=closed]:fill-mode-forwards",
|
|
"data-[state=closed]:pointer-events-none",
|
|
"data-[state=open]:duration-(--animation-duration)",
|
|
"data-[state=closed]:duration-(--animation-duration)",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
<ReasoningFade />
|
|
</CollapsibleContent>
|
|
);
|
|
}
|
|
|
|
function ReasoningText({ className, ...props }: React.ComponentProps<"div">) {
|
|
return (
|
|
<div
|
|
data-slot="reasoning-text"
|
|
className={cn(
|
|
"aui-reasoning-text relative z-0 max-h-64 space-y-4 overflow-y-auto pt-2 pb-2 pl-6 leading-relaxed",
|
|
"transform-gpu transition-[transform,opacity]",
|
|
"group-data-[state=open]/collapsible-content:animate-in",
|
|
"group-data-[state=closed]/collapsible-content:animate-out",
|
|
"group-data-[state=open]/collapsible-content:fade-in-0",
|
|
"group-data-[state=closed]/collapsible-content:fade-out-0",
|
|
"group-data-[state=open]/collapsible-content:slide-in-from-top-4",
|
|
"group-data-[state=closed]/collapsible-content:slide-out-to-top-4",
|
|
"group-data-[state=open]/collapsible-content:duration-(--animation-duration)",
|
|
"group-data-[state=closed]/collapsible-content:duration-(--animation-duration)",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const ReasoningImpl: ReasoningMessagePartComponent = () => <MarkdownText />;
|
|
|
|
const ReasoningGroupImpl: ReasoningGroupComponent = ({
|
|
children,
|
|
startIndex,
|
|
endIndex,
|
|
}) => {
|
|
const isReasoningStreaming = useAuiState((s) => {
|
|
if (s.message.status?.type !== "running") return false;
|
|
const lastIndex = s.message.parts.length - 1;
|
|
if (lastIndex < 0) return false;
|
|
const lastType = s.message.parts[lastIndex]?.type;
|
|
if (lastType !== "reasoning") return false;
|
|
return lastIndex >= startIndex && lastIndex <= endIndex;
|
|
});
|
|
|
|
return (
|
|
<ReasoningRoot defaultOpen={isReasoningStreaming}>
|
|
<ReasoningTrigger active={isReasoningStreaming} />
|
|
<ReasoningContent aria-busy={isReasoningStreaming}>
|
|
<ReasoningText>{children}</ReasoningText>
|
|
</ReasoningContent>
|
|
</ReasoningRoot>
|
|
);
|
|
};
|
|
|
|
const Reasoning = memo(
|
|
ReasoningImpl,
|
|
) as unknown as ReasoningMessagePartComponent & {
|
|
Root: typeof ReasoningRoot;
|
|
Trigger: typeof ReasoningTrigger;
|
|
Content: typeof ReasoningContent;
|
|
Text: typeof ReasoningText;
|
|
Fade: typeof ReasoningFade;
|
|
};
|
|
|
|
Reasoning.displayName = "Reasoning";
|
|
Reasoning.Root = ReasoningRoot;
|
|
Reasoning.Trigger = ReasoningTrigger;
|
|
Reasoning.Content = ReasoningContent;
|
|
Reasoning.Text = ReasoningText;
|
|
Reasoning.Fade = ReasoningFade;
|
|
|
|
const ReasoningGroup = memo(ReasoningGroupImpl);
|
|
ReasoningGroup.displayName = "ReasoningGroup";
|
|
|
|
export {
|
|
Reasoning,
|
|
ReasoningContent,
|
|
ReasoningFade,
|
|
ReasoningGroup,
|
|
ReasoningRoot,
|
|
ReasoningText,
|
|
ReasoningTrigger,
|
|
reasoningVariants,
|
|
};
|