Files
agent-chat-ui/src/components/thread/messages/ai.tsx
2025-03-07 15:46:00 +01:00

118 lines
3.6 KiB
TypeScript

import { useStreamContext } from "@/providers/Stream";
import { Checkpoint, Message } from "@langchain/langgraph-sdk";
import { getContentString } from "../utils";
import { BranchSwitcher, CommandBar } from "./shared";
import { MarkdownText } from "../markdown-text";
import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui";
import { cn } from "@/lib/utils";
import { ToolCalls, ToolResult } from "./tool-calls";
import { StringParam, useQueryParam } from "use-query-params";
function CustomComponent({
message,
thread,
}: {
message: Message;
thread: ReturnType<typeof useStreamContext>;
}) {
const [apiUrl] = useQueryParam("apiUrl", StringParam);
const meta = thread.getMessagesMetadata(message);
const seenState = meta?.firstSeenState;
const customComponent = seenState?.values.ui
?.slice()
.reverse()
.find(
({ additional_kwargs }) =>
additional_kwargs.run_id === seenState.metadata?.run_id,
);
if (!customComponent) {
return null;
}
return (
<div key={message.id}>
{customComponent && (
<LoadExternalComponent
apiUrl={apiUrl ?? undefined}
assistantId="agent"
stream={thread}
message={customComponent}
meta={{ ui: customComponent }}
/>
)}
</div>
);
}
export function AssistantMessage({
message,
isLoading,
handleRegenerate,
}: {
message: Message;
isLoading: boolean;
handleRegenerate: (parentCheckpoint: Checkpoint | null | undefined) => void;
}) {
const contentString = getContentString(message.content);
const thread = useStreamContext();
const meta = thread.getMessagesMetadata(message);
const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
const hasToolCalls =
"tool_calls" in message &&
message.tool_calls &&
message.tool_calls.length > 0;
const isToolResult = message.type === "tool";
return (
<div className="flex items-start mr-auto gap-2 group">
{isToolResult ? (
<ToolResult message={message} />
) : (
<div className="flex flex-col gap-2">
{contentString.length > 0 && (
<div className="py-1">
<MarkdownText>{contentString}</MarkdownText>
</div>
)}
{hasToolCalls && <ToolCalls toolCalls={message.tool_calls} />}
<CustomComponent message={message} thread={thread} />
<div
className={cn(
"flex gap-2 items-center mr-auto transition-opacity",
"opacity-0 group-focus-within:opacity-100 group-hover:opacity-100",
)}
>
<BranchSwitcher
branch={meta?.branch}
branchOptions={meta?.branchOptions}
onSelect={(branch) => thread.setBranch(branch)}
isLoading={isLoading}
/>
<CommandBar
content={contentString}
isLoading={isLoading}
isAiMessage={true}
handleRegenerate={() => handleRegenerate(parentCheckpoint)}
/>
</div>
</div>
)}
</div>
);
}
export function AssistantMessageLoading() {
return (
<div className="flex items-start mr-auto gap-2">
<div className="flex items-center gap-1 rounded-2xl bg-muted px-4 py-2 h-8">
<div className="w-1.5 h-1.5 rounded-full bg-foreground/50 animate-[pulse_1.5s_ease-in-out_infinite]"></div>
<div className="w-1.5 h-1.5 rounded-full bg-foreground/50 animate-[pulse_1.5s_ease-in-out_0.5s_infinite]"></div>
<div className="w-1.5 h-1.5 rounded-full bg-foreground/50 animate-[pulse_1.5s_ease-in-out_1s_infinite]"></div>
</div>
</div>
);
}