# Message Part Grouping URL: /docs/ui/part-grouping Organize message parts into custom groups with flexible grouping functions. *** title: Message Part Grouping description: Organize message parts into custom groups with flexible grouping functions. ---------------------------------------------------------------------------------------- import { ParametersTable } from "@/components/docs/tables/ParametersTable"; import { PartGroupingSample } from "@/components/docs/samples/part-grouping-sample"; This feature is experimental and the API may change in future versions. ## Basic Usage Use the `MessagePrimitive.Unstable_PartsGrouped` component with a custom grouping function: ```tsx twoslash title="/components/assistant-ui/thread.tsx" import { FC, PropsWithChildren } from "react"; import { MessagePrimitive } from "@assistant-ui/react"; const AssistantActionBar: FC = () => null; const BranchPicker: FC<{ className?: string }> = () => null; // ---cut--- const AssistantMessage: FC = () => { return (
{ // Your custom grouping logic here return [{ groupKey: undefined, indices: [0, 1, 2] }]; }} components={{ Group: ({ groupKey, indices, children }) => { // Your custom group rendering return
{children}
; }, }} />
); }; ``` ## How Grouping Works The grouping function receives all message parts and returns an array of groups. Each group contains: * `groupKey`: A string identifier for the group (or `undefined` for ungrouped parts) * `indices`: An array of indices indicating which parts belong to this group ## Use Cases & Examples ### Group by Parent ID Group related content together using a parent-child relationship: ```tsx import { FC, PropsWithChildren, useState } from "react"; import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; const groupByParentId = (parts: readonly any[]) => { const groups = new Map(); for (let i = 0; i < parts.length; i++) { const part = parts[i]; const groupId = part?.parentId ?? `__ungrouped_${i}`; const indices = groups.get(groupId) ?? []; indices.push(i); groups.set(groupId, indices); } return Array.from(groups.entries()).map(([groupId, indices]) => ({ groupKey: groupId.startsWith("__ungrouped_") ? undefined : groupId, indices, })); }; // Usage with collapsible groups const CollapsibleGroup: FC< PropsWithChildren<{ groupKey: string | undefined; indices: number[] }> > = ({ groupKey, indices, children }) => { const [isCollapsed, setIsCollapsed] = useState(false); if (!groupKey) return <>{children}; return (
{!isCollapsed &&
{children}
}
); }; ``` ### Group by Tool Name Organize tool calls by their type: ```tsx import { FC, PropsWithChildren } from "react"; const groupByToolName = (parts: readonly any[]) => { const groups = new Map(); for (let i = 0; i < parts.length; i++) { const part = parts[i]; // Group tool calls by tool name, everything else ungrouped const groupKey = part.type === "tool-call" ? part.toolName : `__other_${i}`; const indices = groups.get(groupKey) ?? []; indices.push(i); groups.set(groupKey, indices); } return Array.from(groups.entries()).map(([groupKey, indices]) => ({ groupKey: groupKey.startsWith("__other_") ? undefined : groupKey, indices, })); }; // Render tool groups with custom styling const ToolGroup: FC< PropsWithChildren<{ groupKey: string | undefined; indices: number[] }> > = ({ groupKey, indices, children }) => { if (!groupKey) return <>{children}; return (
Tool: {groupKey} ({indices.length} calls)
{children}
); }; ``` ### Group Consecutive Text Parts Combine multiple text parts into cohesive blocks: ```tsx import { FC, PropsWithChildren } from "react"; type MessagePartGroup = { groupKey: string | undefined; indices: number[]; }; const groupConsecutiveText = (parts: readonly any[]) => { const groups: MessagePartGroup[] = []; let currentGroup: number[] = []; let isTextGroup = false; for (let i = 0; i < parts.length; i++) { const isText = parts[i].type === "text"; if (isText === isTextGroup && currentGroup.length > 0) { currentGroup.push(i); } else { if (currentGroup.length > 0) { groups.push({ groupKey: isTextGroup ? "text-block" : undefined, indices: currentGroup, }); } currentGroup = [i]; isTextGroup = isText; } } if (currentGroup.length > 0) { groups.push({ groupKey: isTextGroup ? "text-block" : undefined, indices: currentGroup, }); } return groups; }; // Render text blocks with special formatting const TextBlockGroup: FC< PropsWithChildren<{ groupKey: string | undefined; indices: number[] }> > = ({ groupKey, indices, children }) => { if (groupKey === "text-block") { return (
{children}
); } return <>{children}; }; ``` ### Group by Content Type Separate different types of content for distinct visual treatment: ```tsx type MessagePartGroup = { groupKey: string | undefined; indices: number[]; }; const groupByContentType = (parts: readonly any[]) => { const typeGroups = new Map(); for (let i = 0; i < parts.length; i++) { const part = parts[i]; const type = part.type; const indices = typeGroups.get(type) ?? []; indices.push(i); typeGroups.set(type, indices); } // Order groups by type priority const typeOrder = [ "reasoning", "tool-call", "source", "text", "image", "file", ]; const orderedGroups: MessagePartGroup[] = []; for (const type of typeOrder) { const indices = typeGroups.get(type); if (indices && indices.length > 0) { orderedGroups.push({ groupKey: type, indices }); } } // Add any remaining types for (const [type, indices] of typeGroups) { if (!typeOrder.includes(type)) { orderedGroups.push({ groupKey: type, indices }); } } return orderedGroups; }; ``` ### Group by Custom Metadata Use any custom metadata in your parts for grouping: ```tsx import { FC, PropsWithChildren } from "react"; type MessagePartGroup = { groupKey: string | undefined; indices: number[]; }; const groupByPriority = (parts: readonly any[]) => { const priorityGroups = new Map(); for (let i = 0; i < parts.length; i++) { const part = parts[i]; // Assume parts have a custom priority field const priority = part.priority || "normal"; const indices = priorityGroups.get(priority) ?? []; indices.push(i); priorityGroups.set(priority, indices); } // Order by priority const priorityOrder = ["high", "normal", "low"]; const orderedGroups: MessagePartGroup[] = []; for (const priority of priorityOrder) { const indices = priorityGroups.get(priority); if (indices) { orderedGroups.push({ groupKey: priority, indices }); } } return orderedGroups; }; // Render with priority indicators const PriorityGroup: FC< PropsWithChildren<{ groupKey: string | undefined; indices: number[] }> > = ({ groupKey, indices, children }) => { if (!groupKey) return <>{children}; const priorityStyles = { high: "border-red-500 bg-red-50", normal: "border-gray-300 bg-white", low: "border-gray-200 bg-gray-50", }; return (
{groupKey} Priority
{children}
); }; ``` ## Integration with Assistant Streams When using assistant-stream libraries, you can add custom metadata to parts: ### Python (assistant-stream) ```python from assistant_stream import create_run async def my_run(controller): # Add parts with custom parentId research_controller = controller.with_parent_id("research-123") await research_controller.add_tool_call("search", {"query": "climate data"}) research_controller.append_text("Key findings from the research:") # Add parts with custom metadata controller.append_part({ "type": "text", "text": "High priority finding", "priority": "high", "category": "findings" }) ``` ### TypeScript (assistant-stream) ```typescript import { createAssistantStream } from "@assistant-ui/react/assistant-stream"; const stream = createAssistantStream(async (controller) => { // Add parts with parentId const researchController = controller.withParentId("research-123"); await researchController.addToolCallPart({ toolName: "search", args: { query: "climate data" }, }); // Add parts with custom metadata controller.appendPart({ type: "text", text: "High priority finding", priority: "high", category: "findings", }); }); ``` ## API Reference ### MessagePrimitive.Unstable\_PartsGrouped MessagePartGroup[]", description: "Function that takes an array of message parts and returns an array of groups. Each group contains a groupKey (for identification) and an array of indices.", required: true, }, { name: "components", type: "object", description: "Component configuration for rendering different types of message content and groups.", children: [ { type: "Components", parameters: [ { name: "Empty", type: "EmptyMessagePartComponent", description: "Component for rendering empty messages", }, { name: "Text", type: "TextMessagePartComponent", description: "Component for rendering text content", }, { name: "Reasoning", type: "ReasoningMessagePartComponent", description: "Component for rendering reasoning content (typically hidden)", }, { name: "Source", type: "SourceMessagePartComponent", description: "Component for rendering source content", }, { name: "Image", type: "ImageMessagePartComponent", description: "Component for rendering image content", }, { name: "File", type: "FileMessagePartComponent", description: "Component for rendering file content", }, { name: "Unstable_Audio", type: "Unstable_AudioMessagePartComponent", description: "Component for rendering audio content (experimental)", }, { name: "tools", type: "object | { Override: ComponentType }", description: "Configuration for tool call rendering. Can be an object with by_name map and Fallback component, or an Override component.", }, { name: "Group", type: "ComponentType>", description: "Component for rendering grouped message parts. Receives groupKey, indices array, and children to render.", }, ], }, ], }, ]} /> ### MessagePartGroup Type ```typescript type MessagePartGroup = { groupKey: string | undefined; // The group identifier (undefined for ungrouped parts) indices: number[]; // Array of part indices belonging to this group }; ``` ### Group Component Props The Group component receives: * `groupKey`: The group identifier (or `undefined` for ungrouped parts) * `indices`: Array of indices for the parts in this group * `children`: The rendered message part components ## Best Practices 1. **Keep grouping logic simple**: Complex grouping functions can impact performance 2. **Handle ungrouped parts**: Always consider parts that don't match any group criteria 3. **Maintain order**: Consider the visual flow when ordering groups 4. **Use meaningful keys**: Group keys should be descriptive for debugging 5. **Test edge cases**: Empty messages, single parts, and large numbers of parts ## Common Patterns ### Conditional Grouping Only group when certain conditions are met: ```tsx const conditionalGrouping = (parts: readonly any[]) => { // Only group if there are enough parts if (parts.length < 5) { return [{ groupKey: undefined, indices: parts.map((_, i) => i) }]; } // Your grouping logic here }; ``` ### Nested Grouping Create hierarchical groups: ```tsx const nestedGrouping = (parts: readonly any[]) => { // First level: group by type // Second level: group by subtype or metadata // Implementation depends on your specific needs }; ``` ### Dynamic Group Rendering Adjust group appearance based on content: ```tsx import { FC, PropsWithChildren } from "react"; import { useAssistantState } from "@assistant-ui/react"; const DynamicGroup: FC< PropsWithChildren<{ groupKey: string | undefined; indices: number[] }> > = ({ groupKey, indices, children }) => { const parts = useAssistantState(({ message }) => message.content); const groupParts = indices.map((i) => parts[i]); // Analyze group content const hasErrors = groupParts.some((p) => p.error); const isLoading = groupParts.some((p) => p.status?.type === "running"); if (!groupKey) return <>{children}; return (
{children}
); }; ```