changes
This commit is contained in:
@@ -18,7 +18,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { ToolCallDisplay } from './ToolCallDisplay';
|
import { ToolCall, ToolCallGroup } from './ToolCallDisplay';
|
||||||
import classes from './MarkdownMessage.module.css';
|
import classes from './MarkdownMessage.module.css';
|
||||||
// Import KaTeX CSS for LaTeX rendering
|
// Import KaTeX CSS for LaTeX rendering
|
||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
@@ -93,6 +93,47 @@ function parseContentWithToolCalls(content: string): ContentSegment[] {
|
|||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grouped segment type - either text or a group of consecutive tool calls
|
||||||
|
*/
|
||||||
|
interface GroupedSegment {
|
||||||
|
type: 'text' | 'toolGroup';
|
||||||
|
content?: string;
|
||||||
|
toolCalls?: ToolCall[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group consecutive tool call segments together
|
||||||
|
*/
|
||||||
|
function groupConsecutiveToolCalls(segments: ContentSegment[]): GroupedSegment[] {
|
||||||
|
const grouped: GroupedSegment[] = [];
|
||||||
|
let currentToolGroup: ToolCall[] = [];
|
||||||
|
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (segment.type === 'tool' && segment.toolCall) {
|
||||||
|
// Add to current tool group
|
||||||
|
currentToolGroup.push(segment.toolCall);
|
||||||
|
} else {
|
||||||
|
// Flush any pending tool group before adding text
|
||||||
|
if (currentToolGroup.length > 0) {
|
||||||
|
grouped.push({ type: 'toolGroup', toolCalls: currentToolGroup });
|
||||||
|
currentToolGroup = [];
|
||||||
|
}
|
||||||
|
// Add text segment
|
||||||
|
if (segment.content) {
|
||||||
|
grouped.push({ type: 'text', content: segment.content });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any remaining tool group
|
||||||
|
if (currentToolGroup.length > 0) {
|
||||||
|
grouped.push({ type: 'toolGroup', toolCalls: currentToolGroup });
|
||||||
|
}
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook that tracks content changes and returns content with fade-in markers for new text
|
* Hook that tracks content changes and returns content with fade-in markers for new text
|
||||||
*/
|
*/
|
||||||
@@ -334,21 +375,15 @@ export function MarkdownMessage({ content, isStreaming = false }: MarkdownMessag
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When not streaming, parse and render tool calls
|
// When not streaming, parse and render tool calls (grouped)
|
||||||
const segments = parseContentWithToolCalls(content);
|
const segments = parseContentWithToolCalls(content);
|
||||||
|
const groupedSegments = groupConsecutiveToolCalls(segments);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.markdown}>
|
<div className={classes.markdown}>
|
||||||
{segments.map((segment, index) => {
|
{groupedSegments.map((segment, index) => {
|
||||||
if (segment.type === 'tool' && segment.toolCall) {
|
if (segment.type === 'toolGroup' && segment.toolCalls) {
|
||||||
return (
|
return <ToolCallGroup key={`toolgroup-${index}`} toolCalls={segment.toolCalls} />;
|
||||||
<ToolCallDisplay
|
|
||||||
key={`tool-${index}`}
|
|
||||||
toolName={segment.toolCall.toolName}
|
|
||||||
args={segment.toolCall.args}
|
|
||||||
result={segment.toolCall.result}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return <MarkdownContent key={`text-${index}`} content={segment.content || ''} />;
|
return <MarkdownContent key={`text-${index}`} content={segment.content || ''} />;
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -27,3 +27,45 @@
|
|||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Group container for multiple tool calls */
|
||||||
|
.groupContainer {
|
||||||
|
background-color: var(--mantine-color-default-hover);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupHeader {
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupHeader:hover {
|
||||||
|
background-color: var(--mantine-color-default-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupContent {
|
||||||
|
padding: var(--mantine-spacing-xs);
|
||||||
|
border-top: 1px solid var(--mantine-color-default-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nested tool call inside group */
|
||||||
|
.nestedContainer {
|
||||||
|
background-color: var(--mantine-color-body);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedContainer:not(:last-child) {
|
||||||
|
margin-bottom: var(--mantine-spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedHeader {
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedHeader:hover {
|
||||||
|
background-color: var(--mantine-color-default-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedContent {
|
||||||
|
padding: 0 var(--mantine-spacing-xs) var(--mantine-spacing-xs);
|
||||||
|
border-top: 1px solid var(--mantine-color-default-border);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,10 +6,21 @@ import { ActionIcon, Code, Collapse, Group, Paper, Text, useMantineTheme } from
|
|||||||
import { useThemeContext } from '@/components/DynamicThemeProvider';
|
import { useThemeContext } from '@/components/DynamicThemeProvider';
|
||||||
import classes from './ToolCallDisplay.module.css';
|
import classes from './ToolCallDisplay.module.css';
|
||||||
|
|
||||||
|
export interface ToolCall {
|
||||||
|
toolName: string;
|
||||||
|
args: Record<string, unknown>;
|
||||||
|
result: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface ToolCallDisplayProps {
|
interface ToolCallDisplayProps {
|
||||||
toolName: string;
|
toolName: string;
|
||||||
args: Record<string, unknown>;
|
args: Record<string, unknown>;
|
||||||
result: string;
|
result: string;
|
||||||
|
nested?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolCallGroupProps {
|
||||||
|
toolCalls: ToolCall[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Friendly tool names
|
// Friendly tool names
|
||||||
@@ -25,12 +36,19 @@ const toolDisplayNames: Record<string, string> = {
|
|||||||
generate_image: 'Image Generation',
|
generate_image: 'Image Generation',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ToolCallDisplay({ toolName, args, result }: ToolCallDisplayProps) {
|
function getDisplayName(toolName: string): string {
|
||||||
|
return toolDisplayNames[toolName] || toolName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a single tool call (can be standalone or nested inside a group)
|
||||||
|
*/
|
||||||
|
export function ToolCallDisplay({ toolName, args, result, nested = false }: ToolCallDisplayProps) {
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const { primaryColor } = useThemeContext();
|
const { primaryColor } = useThemeContext();
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
const displayName = toolDisplayNames[toolName] || toolName;
|
const displayName = getDisplayName(toolName);
|
||||||
const isError = result.startsWith('Error:');
|
const isError = result.startsWith('Error:');
|
||||||
|
|
||||||
// Format args for display
|
// Format args for display
|
||||||
@@ -38,10 +56,14 @@ export function ToolCallDisplay({ toolName, args, result }: ToolCallDisplayProps
|
|||||||
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
|
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
|
const containerClass = nested ? classes.nestedContainer : classes.container;
|
||||||
|
const headerClass = nested ? classes.nestedHeader : classes.header;
|
||||||
|
const contentClass = nested ? classes.nestedContent : classes.content;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className={classes.container} withBorder radius="sm" p={0} my="xs">
|
<Paper className={containerClass} withBorder radius="sm" p={0} my={nested ? 0 : 'xs'}>
|
||||||
<Group
|
<Group
|
||||||
className={classes.header}
|
className={headerClass}
|
||||||
onClick={() => setOpened(!opened)}
|
onClick={() => setOpened(!opened)}
|
||||||
gap="xs"
|
gap="xs"
|
||||||
wrap="nowrap"
|
wrap="nowrap"
|
||||||
@@ -63,7 +85,7 @@ export function ToolCallDisplay({ toolName, args, result }: ToolCallDisplayProps
|
|||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Collapse in={opened}>
|
<Collapse in={opened}>
|
||||||
<div className={classes.content}>
|
<div className={contentClass}>
|
||||||
{Object.keys(args).length > 0 && (
|
{Object.keys(args).length > 0 && (
|
||||||
<div className={classes.section}>
|
<div className={classes.section}>
|
||||||
<Text size="xs" c="dimmed" mb={4}>
|
<Text size="xs" c="dimmed" mb={4}>
|
||||||
@@ -87,3 +109,68 @@ export function ToolCallDisplay({ toolName, args, result }: ToolCallDisplayProps
|
|||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a group of consecutive tool calls in a single collapsible container
|
||||||
|
*/
|
||||||
|
export function ToolCallGroup({ toolCalls }: ToolCallGroupProps) {
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
const { primaryColor } = useThemeContext();
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
|
if (toolCalls.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only one tool call, render it directly without the group wrapper
|
||||||
|
if (toolCalls.length === 1) {
|
||||||
|
const tc = toolCalls[0];
|
||||||
|
return <ToolCallDisplay toolName={tc.toolName} args={tc.args} result={tc.result} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get summary of tool names
|
||||||
|
const toolNames = toolCalls.map((tc) => getDisplayName(tc.toolName));
|
||||||
|
const uniqueTools = Array.from(new Set(toolNames));
|
||||||
|
const summary =
|
||||||
|
uniqueTools.length <= 2
|
||||||
|
? uniqueTools.join(', ')
|
||||||
|
: `${uniqueTools.slice(0, 2).join(', ')} +${uniqueTools.length - 2} more`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className={classes.groupContainer} withBorder radius="sm" p={0} my="xs">
|
||||||
|
<Group
|
||||||
|
className={classes.groupHeader}
|
||||||
|
onClick={() => setOpened(!opened)}
|
||||||
|
gap="xs"
|
||||||
|
wrap="nowrap"
|
||||||
|
p="xs"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" color={primaryColor} size="xs">
|
||||||
|
{opened ? <IconChevronDown size={14} /> : <IconChevronRight size={14} />}
|
||||||
|
</ActionIcon>
|
||||||
|
<IconTool size={16} color={theme.colors[primaryColor][6]} />
|
||||||
|
<Text size="sm" fw={500} c={primaryColor}>
|
||||||
|
{toolCalls.length} Tool Calls
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed" truncate style={{ flex: 1 }}>
|
||||||
|
{summary}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Collapse in={opened}>
|
||||||
|
<div className={classes.groupContent}>
|
||||||
|
{toolCalls.map((tc, index) => (
|
||||||
|
<ToolCallDisplay
|
||||||
|
key={index}
|
||||||
|
toolName={tc.toolName}
|
||||||
|
args={tc.args}
|
||||||
|
result={tc.result}
|
||||||
|
nested
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user