import capitalize from "lodash/capitalize"; import { GripVertical, MinusCircleIcon } from "lucide-react"; import { memo, useState, useCallback } from "react"; import { type ChatMessage, ChatMessageRole, ChatMessageType, type ChatMessageWithId, type LLMToolCall, type PlaceholderMessage, } from "@langfuse/shared"; import { Button } from "@/src/components/ui/button"; import { Card, CardContent } from "@/src/components/ui/card"; import { CodeMirrorEditor } from "@/src/components/editor"; import type { MessagesContext } from "./types"; import { useSortable } from "@dnd-kit/sortable"; import { cn } from "@/src/utils/tailwind"; import { CSS } from "@dnd-kit/utilities"; import { ToolCallCard } from "./ToolCallCard"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/src/components/ui/select"; type ChatMessageProps = Pick< MessagesContext, | "deleteMessage" | "updateMessage" | "availableRoles" | "toolCallIds" | "replaceMessage" > & { message: ChatMessageWithId; index: number }; const ROLES: ChatMessageRole[] = [ ChatMessageRole.User, ChatMessageRole.System, ChatMessageRole.Developer, ChatMessageRole.Assistant, ChatMessageRole.Tool, ] as const; const getRoleNamePlaceholder = (role: string) => { switch (role) { case ChatMessageRole.System: return "a system message"; case ChatMessageRole.Developer: return "a developer message"; case ChatMessageRole.Assistant: return "an assistant message"; case ChatMessageRole.User: return "a user message"; case ChatMessageRole.Tool: return "a tool response message"; case "placeholder": return "placeholder name (e.g. chat_history)"; default: return `a ${role}`; } }; const ToolCalls: React.FC<{ toolCalls: LLMToolCall[] }> = ({ toolCalls }) => { if (!toolCalls || toolCalls.length === 0) return null; return (
{toolCalls.map((toolCall) => ( ))}
); }; export const ChatMessageComponent: React.FC = ({ message, updateMessage, deleteMessage, replaceMessage, availableRoles, index: _index, toolCallIds, }) => { const [roleIndex, setRoleIndex] = useState(1); const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: message.id }); const toggleRole = () => { // Only allow role toggling for messages that have a role property (not placeholder messages) if (!("role" in message)) return; // if user has set custom roles, available roles will be non-empty and we toggle through custom and default roles (assistant, user) if (!!availableRoles && Boolean(availableRoles.length)) { let randomRole = availableRoles[roleIndex % availableRoles.length]; if (randomRole === message.role) { randomRole = availableRoles[(roleIndex + 1) % availableRoles.length]; } replaceMessage(message.id, { content: message.content, role: randomRole, type: ChatMessageType.PublicAPICreated, }); setRoleIndex(roleIndex + 1); } else { // if user has not set custom roles, we toggle through default roles (assistant, user) // Allow all roles including system and developer at any position const eligibleRoles = ROLES.filter( (r) => r !== ChatMessageRole.Tool || (toolCallIds && toolCallIds.length > 0), ); const currentIndex = eligibleRoles.indexOf( ("role" in message ? message.role : ChatMessageRole.User) as ChatMessageRole, ); const nextRole = eligibleRoles[(currentIndex + 1) % eligibleRoles.length]; if (nextRole === ChatMessageRole.User) { replaceMessage(message.id, { content: message.content, role: nextRole, type: ChatMessageType.User, }); } else if (nextRole === ChatMessageRole.Assistant) { replaceMessage(message.id, { content: message.content, role: nextRole, type: ChatMessageType.AssistantText, }); } else if (nextRole === ChatMessageRole.Tool) { replaceMessage(message.id, { content: message.content, role: nextRole, type: ChatMessageType.ToolResult, toolCallId: toolCallIds?.[0] ?? "", }); } else if (nextRole === ChatMessageRole.Developer) { replaceMessage(message.id, { content: message.content, role: nextRole, type: ChatMessageType.Developer, }); } else if (nextRole === ChatMessageRole.System) { replaceMessage(message.id, { content: message.content, role: nextRole, type: ChatMessageType.System, }); } else if (nextRole === ChatMessageRole.Model) { replaceMessage(message.id, { content: message.content, role: nextRole, type: ChatMessageType.ModelText, }); } else { const exhaustiveCheck: never = nextRole; console.error(`Unhandled role: ${exhaustiveCheck}`); } } }; const onValueChange = useCallback( (value: string) => { if (message.type === ChatMessageType.Placeholder) { updateMessage(message.type, message.id, "name", value); } else { updateMessage(message.type, message.id, "content", value); } }, [message.id, message.type, updateMessage], ); const onPlaceholderNameChange = useCallback( (value: string) => { if (message.type === ChatMessageType.Placeholder) { updateMessage(message.type, message.id, "name", value); } }, [message.id, message.type, updateMessage], ); const showToolCallSelect = message.type === ChatMessageType.ToolResult; const isPlaceholder = message.type === ChatMessageType.Placeholder; return (
{isPlaceholder ? ( placeholder ) : ( )}
{showToolCallSelect && ( )} {isPlaceholder ? ( ) : ( )}
{message.type === ChatMessageType.AssistantToolCall && ( )}
); }; const MemoizedEditor = memo(function MemoizedEditor(props: { value: string; role: ChatMessage["role"]; onChange: (value: string) => void; }) { const { value, role, onChange } = props; const placeholder = `Enter ${getRoleNamePlaceholder(role)} here.`; return ( ); });