solana-multiagent/components/ui/chat-message.jsx

263 lines
6.3 KiB
React
Raw Permalink Normal View History

2025-02-17 15:21:20 +07:00
import { cn } from "@/lib/utils";
import { MarkdownContent } from "@/components/ui/markdown-content";
import { cva } from "class-variance-authority";
import { SparklesIcon, UserIcon, WrenchIcon } from "lucide-react";
import React from "react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Button } from "@/components/ui/button";
const chatMessageVariants = cva("flex gap-4 w-full", {
variants: {
variant: {
default: "",
bubble: "",
full: "p-5",
},
type: {
incoming: "justify-start mr-auto",
outgoing: "justify-end ml-auto",
tool: "justify-end ml-auto",
},
},
compoundVariants: [
{
variant: "full",
type: "outgoing",
className: "bg-muted",
},
{
variant: "full",
type: "incoming",
className: "bg-background",
},
{
variant: "full",
type: "tool",
className: "bg-accent",
},
],
defaultVariants: {
variant: "default",
type: "incoming",
},
});
const ChatMessageContext = React.createContext(null);
const useChatMessage = () => {
const context = React.useContext(ChatMessageContext);
return context;
};
const ChatMessage = React.forwardRef((
{
className,
variant = "default",
type = "incoming",
id,
children,
...props
},
ref,
) => {
return (
<ChatMessageContext.Provider value={{ variant, type, id }}>
<div
ref={ref}
className={cn(chatMessageVariants({ variant, type, className }))}
{...props}>
{children}
</div>
</ChatMessageContext.Provider>
);
});
ChatMessage.displayName = "ChatMessage";
// Avatar component
const chatMessageAvatarVariants = cva(
"w-8 h-8 flex items-center rounded-full justify-center ring-1 shrink-0 bg-transparent overflow-hidden",
{
variants: {
type: {
incoming: "ring-border",
outgoing: "ring-muted-foreground/30",
tool: "ring-accent-foreground/30",
},
},
defaultVariants: {
type: "incoming",
},
}
);
const ChatMessageAvatar = React.forwardRef(({ className, icon: iconProps, imageSrc, ...props }, ref) => {
const context = useChatMessage();
const type = context?.type ?? "incoming";
const icon =
iconProps ?? (type === "incoming" ? <SparklesIcon /> : type === "tool" ? <WrenchIcon /> : <UserIcon />);
return (
<div
ref={ref}
className={cn(chatMessageAvatarVariants({ type, className }))}
{...props}>
{imageSrc ? (
<img src={imageSrc} alt="Avatar" className="h-full w-full object-cover" />
) : (
<div className="translate-y-px [&_svg]:size-4 [&_svg]:shrink-0">
{icon}
</div>
)}
</div>
);
});
ChatMessageAvatar.displayName = "ChatMessageAvatar";
// Content component
const chatMessageContentVariants = cva("flex flex-col gap-2", {
variants: {
variant: {
default: "",
bubble: "rounded-xl px-3 py-2",
full: "",
},
type: {
incoming: "",
outgoing: "",
tool: "",
},
},
compoundVariants: [
{
variant: "bubble",
type: "incoming",
className: "bg-secondary text-secondary-foreground",
},
{
variant: "bubble",
type: "outgoing",
className: "bg-primary text-primary-foreground",
},
{
variant: "bubble",
type: "tool",
className: "bg-accent text-accent-foreground",
},
],
defaultVariants: {
variant: "default",
type: "incoming",
},
});
const InterrupterCard = ({ invocationId, args, onDecision }) => {
const [decisions, setDecisions] = React.useState(
args.toolCalls.reduce((acc, tool) => ({ ...acc, [tool.id]: null }), {})
);
console.log("decisions",decisions);
const handleDecision = (key, decision) => {
const newDecisions = { ...decisions, [key]: decision };
setDecisions(newDecisions);
if (Object.values(newDecisions).every(value => value !== null)) {
onDecision(newDecisions);
}
};
console.log("args",args);
return (
<Card>
<CardHeader>
<CardTitle>Interrupter</CardTitle>
<CardDescription>Invocation ID: {invocationId}</CardDescription>
</CardHeader>
<CardContent>
{args['toolCalls'].map((tool) => (
<div key={tool.id} className="flex justify-between items-center mb-2">
<div>
<p className="font-semibold">Function: {tool.name}</p>
<ul className="list-disc">
{Object.entries(tool.args).map(([key, value]) => (
<li key={key}>
{key}: {value}
</li>
))}
</ul>
</div>
<div>
<Button variant="outline" onClick={() => handleDecision(tool.id, true)} disabled={decisions[tool.id] !== null} className="mr-2">
Approve
</Button>
<Button variant="outline" onClick={() => handleDecision(tool.id, false)} disabled={decisions[tool.id] !== null}>
Deny
</Button>
</div>
</div>
))}
</CardContent>
</Card>
);
};
const ChatMessageContent = React.forwardRef(({ className, content, toolInvocations, id: idProp,interruptSubmitter, children, ...props }, ref) => {
const context = useChatMessage();
const variant = context?.variant ?? "default";
const type = context?.type ?? "incoming";
const id = idProp ?? context?.id ?? "";
return (
<div ref={ref} className={cn(chatMessageContentVariants({ variant, type, className }))} {...props}>
{content.length > 0 && <MarkdownContent id={id} content={content} />}
{toolInvocations && toolInvocations.length > 0 && (
<div className="space-y-4 mt-4">
{toolInvocations.map((toolInvocation) => (
toolInvocation.toolName === "interrupter" ? (
<InterrupterCard
key={toolInvocation.toolCallId}
invocationId={toolInvocation.toolCallId}
args={toolInvocation.args}
onDecision={(decisions) => {
console.log(decisions);
interruptSubmitter(null,{
data: decisions,
allowEmptySubmit: true,
});
}
}
/>
) : (
<Card key={toolInvocation.toolCallId}>
<CardHeader>
<CardTitle>{toolInvocation.toolName}</CardTitle>
<CardDescription>Invocation ID: {toolInvocation.toolCallId}</CardDescription>
</CardHeader>
<CardContent>
<p>Function: {toolInvocation.toolName}</p>
<p>Parameters: {JSON.stringify(toolInvocation.args)}</p>
</CardContent>
<CardFooter>
<p>Status: {toolInvocation.state}</p>
</CardFooter>
</Card>
)
))}
</div>
)}
{children}
</div>
);
});
ChatMessageContent.displayName = "ChatMessageContent";
export { ChatMessage, ChatMessageAvatar, ChatMessageContent };