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
240 lines
8.0 KiB
TypeScript
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>
|
|
);
|
|
};
|