Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

240 lines
8.0 KiB
TypeScript

"use client";
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
import {
Reasoning,
ReasoningContent,
ReasoningRoot,
ReasoningText,
ReasoningTrigger,
} from "@/components/assistant-ui/reasoning";
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
import {
ToolGroupContent,
ToolGroupRoot,
ToolGroupTrigger,
} from "@/components/assistant-ui/tool-group";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import {
UnknownComponentFallback,
componentsAllowlist,
} from "@/components/gui";
import {
RENDER_GUI_TOOL_NAME,
isLeakedRenderGuiText,
parseRenderGuiResult,
} from "@/lib/render-gui-tool";
import { cn } from "@/lib/utils";
import type { GenerativeUISpec } from "@assistant-ui/react";
import {
ActionBarPrimitive,
AuiIf,
BranchPickerPrimitive,
ErrorPrimitive,
getMcpAppFromToolPart,
MessagePrimitive,
useAuiState,
} from "@assistant-ui/react";
import {
CheckIcon,
ChevronLeftIcon,
ChevronRightIcon,
CopyIcon,
RefreshCwIcon,
} from "lucide-react";
import { type FC, useEffect, useRef } from "react";
type AssistantMessageGuiProps = {
/** Set false in dev to surface the dropped-bridge warning. */
bridgeEnabled?: boolean;
};
const AllowlistedGenerativeUI = ({ spec }: { spec?: GenerativeUISpec }) => (
<MessagePrimitive.GenerativeUI
spec={spec}
components={componentsAllowlist}
Fallback={UnknownComponentFallback}
/>
);
const useDroppedGenerativeUIWarning = (bridgeEnabled: boolean) => {
const content = useAuiState((s) => s.message.content);
const warned = useRef(false);
useEffect(() => {
if (
process.env.NODE_ENV === "production" ||
bridgeEnabled ||
warned.current
) {
return;
}
const hasRenderGui = content.some(
(part) =>
part.type === "tool-call" &&
part.toolName === RENDER_GUI_TOOL_NAME &&
part.result != null &&
parseRenderGuiResult(part.result) != null,
);
const hasUnhandledGenerativeUI = content.some(
(part) => part.type === "generative-ui",
);
if (hasRenderGui || hasUnhandledGenerativeUI) {
warned.current = true;
console.warn(
"[generative-ui] Message contains render_gui or generative-ui parts but the bridge is disabled. " +
"Enable the bridge in assistant-message-gui.tsx or wire MessagePrimitive.GenerativeUI.",
);
}
}, [bridgeEnabled, content]);
};
export const AssistantMessageGui: FC<AssistantMessageGuiProps> = ({
bridgeEnabled = true,
}) => {
useDroppedGenerativeUIWarning(bridgeEnabled);
const hasBridgedRenderGui = useAuiState((s) =>
s.message.content.some(
(part) =>
part.type === "tool-call" &&
part.toolName === RENDER_GUI_TOOL_NAME &&
parseRenderGuiResult(part.result) != null,
),
);
const ACTION_BAR_PT = "pt-1.5";
const ACTION_BAR_HEIGHT = `-mb-7.5 min-h-7.5 ${ACTION_BAR_PT}`;
return (
<MessagePrimitive.Root
data-slot="aui_assistant-message-root"
data-role="assistant"
className="fade-in slide-in-from-bottom-1 animate-in relative duration-150"
>
<div
data-slot="aui_assistant-message-content"
// [contain-intrinsic-size:auto_24px] fixes issue #4104, don't change without checking for regressions
className="text-foreground px-2 leading-relaxed wrap-break-word [contain-intrinsic-size:auto_24px] [content-visibility:auto]"
>
<MessagePrimitive.GroupedParts
groupBy={(part) => {
if (part.type === "reasoning") {
return ["group-chainOfThought", "group-reasoning"];
}
if (part.type === "tool-call") {
if (getMcpAppFromToolPart(part)) return [];
if (part.toolName === RENDER_GUI_TOOL_NAME) return [];
return ["group-chainOfThought", "group-tool"];
}
return [];
}}
>
{({ part, children }) => {
switch (part.type) {
case "group-chainOfThought":
return <div data-slot="aui_chain-of-thought">{children}</div>;
case "group-reasoning": {
const running = part.status.type === "running";
return (
<ReasoningRoot streaming={running}>
<ReasoningTrigger active={running} />
<ReasoningContent aria-busy={running}>
<ReasoningText>{children}</ReasoningText>
</ReasoningContent>
</ReasoningRoot>
);
}
case "group-tool":
return (
<ToolGroupRoot>
<ToolGroupTrigger
count={part.indices.length}
active={part.status.type === "running"}
/>
<ToolGroupContent>{children}</ToolGroupContent>
</ToolGroupRoot>
);
case "text":
if (
bridgeEnabled &&
hasBridgedRenderGui &&
isLeakedRenderGuiText(part.text)
) {
return null;
}
return <MarkdownText />;
case "reasoning":
return <Reasoning {...part} />;
case "generative-ui":
return bridgeEnabled ? <AllowlistedGenerativeUI /> : null;
case "tool-call": {
if (bridgeEnabled && part.toolName === RENDER_GUI_TOOL_NAME) {
const spec = parseRenderGuiResult(part.result);
if (spec) return <AllowlistedGenerativeUI spec={spec} />;
}
return part.toolUI ?? <ToolFallback {...part} />;
}
default:
return null;
}
}}
</MessagePrimitive.GroupedParts>
<MessagePrimitive.Error>
<ErrorPrimitive.Root className="aui-message-error-root border-destructive bg-destructive/10 text-destructive dark:bg-destructive/5 mt-2 rounded-md border p-3 text-sm dark:text-red-200">
<ErrorPrimitive.Message className="aui-message-error-message line-clamp-2" />
</ErrorPrimitive.Root>
</MessagePrimitive.Error>
</div>
<div
data-slot="aui_assistant-message-footer"
className={cn("ms-2 flex items-center", ACTION_BAR_HEIGHT)}
>
<BranchPickerPrimitive.Root
hideWhenSingleBranch
className="aui-branch-picker-root text-muted-foreground -ms-2 me-2 inline-flex items-center text-xs"
>
<BranchPickerPrimitive.Previous asChild>
<TooltipIconButton tooltip="Previous">
<ChevronLeftIcon />
</TooltipIconButton>
</BranchPickerPrimitive.Previous>
<span className="aui-branch-picker-state font-medium">
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
</span>
<BranchPickerPrimitive.Next asChild>
<TooltipIconButton tooltip="Next">
<ChevronRightIcon />
</TooltipIconButton>
</BranchPickerPrimitive.Next>
</BranchPickerPrimitive.Root>
<ActionBarPrimitive.Root
hideWhenRunning
autohide="not-last"
className="aui-assistant-action-bar-root text-muted-foreground -ms-1 flex gap-1"
>
<ActionBarPrimitive.Copy asChild>
<TooltipIconButton tooltip="Copy">
<AuiIf condition={(s) => s.message.isCopied}>
<CheckIcon />
</AuiIf>
<AuiIf condition={(s) => !s.message.isCopied}>
<CopyIcon />
</AuiIf>
</TooltipIconButton>
</ActionBarPrimitive.Copy>
<ActionBarPrimitive.Reload asChild>
<TooltipIconButton tooltip="Refresh">
<RefreshCwIcon />
</TooltipIconButton>
</ActionBarPrimitive.Reload>
</ActionBarPrimitive.Root>
</div>
</MessagePrimitive.Root>
);
};