// SPDX-License-Identifier: MIT
// Copyright contributors to the openassistant project
import { ToolCallComponents, StreamMessagePart } from '@openassistant/core';
import { ToolInvocation } from 'ai';
import React, { ReactNode, memo } from 'react';
import remarkGfm from 'remark-gfm';
import Markdown from 'react-markdown';
import {
Accordion,
AccordionItem,
Table,
TableBody,
TableCell,
TableRow,
TableHeader,
TableColumn,
Card,
CardBody,
} from '@heroui/react';
import { Icon } from '@iconify/react';
export const MarkdownContent = ({
text,
showMarkdown = true,
}: {
text?: string;
showMarkdown?: boolean;
}) => {
if (!showMarkdown) {
return (
{text}
);
}
return (
(
),
ol: ({ children }) => (
{children}
),
li: ({ children }) => (
// Used Tailwind's CSS nesting syntax to target p tags directly inside li elements with [&>p]
// Added !mt-0 to force margin-top to 0
// Used -translate-y-5 to move the paragraph up by 1.25rem to align with the marker
// Added negative margin bottom to compensate for the translated paragraph
{children}
),
p: ({ children }) => (
{children}
),
}}
>
{text}
);
};
class ToolCallErrorBoundary extends React.Component<
{ children: ReactNode; onError?: () => void },
{ hasError: boolean }
> {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error) {
console.error('Tool call component error:', error);
this.props.onError?.();
}
render() {
if (this.state.hasError) {
return (
Failed to render tool component.
);
}
return this.props.children;
}
}
const ToolCallComponentRenderer = memo(
function ToolCallComponentRenderer({
Component,
additionalData,
}: {
Component:
| React.ComponentType>
| React.ReactElement
| null;
additionalData: unknown;
toolCallId: string;
}) {
if (!Component) return null;
return (
{typeof Component === 'function' ? (
)} />
) : (
Component
)}
);
},
(prevProps, nextProps) => {
// comparison of additionalData using toolCallId
return prevProps.toolCallId === nextProps.toolCallId;
}
);
export function PartComponent({
part,
components,
useMarkdown,
showTools,
}: {
part: StreamMessagePart;
components?: ToolCallComponents;
useMarkdown?: boolean;
showTools?: boolean;
}) {
if (part.type === 'text') {
return (
{useMarkdown ? : part.text}
);
} else if (part.type === 'tool-invocation' && showTools) {
return (
<>
>
);
} else {
return <>This part is not supported: {part.type}>;
}
}
export function ToolCallComponent({
toolInvocation,
additionalData,
components,
}: {
toolInvocation: ToolInvocation;
additionalData: unknown;
components?: ToolCallComponents;
}) {
const { toolName, args, state } = toolInvocation;
const isCompleted = state === 'result';
const llmResult = state === 'result' ? toolInvocation.result : {};
const Component = components?.find(
(component) => component.toolName === toolName
)?.component as React.ComponentType> | undefined;
const toolSuccess = Boolean(llmResult?.success);
const tableItems = llmResult
? Object.entries(llmResult).map(([key, value]) => ({
key,
value:
typeof value === 'object'
? JSON.stringify(value, (_, v) =>
typeof v === 'bigint' ? v.toString() : v
)
: String(value),
}))
: [];
return (
${toolName}`}
startContent={
!isCompleted && (
)
}
>
function call:
{toolName}
(
{Object.entries(args).map(([key, value], index, array) => (
{key}
:
{typeof value === 'object' && value !== null
? JSON.stringify(value, (_, v) =>
typeof v === 'bigint' ? v.toString() : v
)
: String(value)}
{index < array.length - 1 && (
,
)}
))}
)
{llmResult && (
result:
Key
Value
{(item) => (
{item.key}
{item.value}
)}
)}
{Component && isCompleted && toolSuccess && (
)}
);
}