Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/content/components/(chatbot)/tool.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ Shows a tool that encountered an error during execution. Opens by default to dis
type={{
input: {
description:
"The input parameters passed to the tool, displayed as formatted JSON.",
"The input parameters passed to the tool, displayed as formatted JSON. Renders nothing when input is undefined, such as before parameters arrive during input-streaming.",
type: 'ToolUIPart["input"]',
},
"...props": {
Expand Down
68 changes: 68 additions & 0 deletions packages/elements/__tests__/tool.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ import {

const QUERY_REGEX = /"query"/;

const renderTool = (
input: unknown,
state: "input-streaming" | "input-available"
) => (
<Tool defaultOpen>
<ToolHeader state={state} toolName="search" type="dynamic-tool" />
<ToolContent>
<ToolInput input={input} />
<ToolOutput errorText={undefined} output={undefined} />
</ToolContent>
</Tool>
);

describe("tool", () => {
it("renders children", () => {
render(<Tool>Content</Tool>);
Expand Down Expand Up @@ -182,6 +195,61 @@ describe("toolContent", () => {
});

describe("toolInput", () => {
it("renders nothing when input is undefined", () => {
const { container } = render(<ToolInput input={undefined} />);

expect(container).toBeEmptyDOMElement();
});

it("keeps the tool visible as input starts streaming and becomes available", () => {
const { container, rerender } = render(
renderTool(undefined, "input-streaming")
);

expect(
screen.getByRole("button", { name: "searchPending" })
).toBeInTheDocument();
expect(screen.queryByText("Parameters")).not.toBeInTheDocument();

rerender(renderTool({ query: "test" }, "input-streaming"));

expect(container.querySelector("code")).toHaveTextContent('"test"');

rerender(renderTool({ query: "test search" }, "input-available"));

expect(
screen.getByRole("button", { name: "searchRunning" })
).toBeInTheDocument();
expect(container.querySelector("code")).toHaveTextContent('"test search"');
});

it("removes previous parameters when a new input starts streaming", () => {
const { container, rerender } = render(
renderTool({ query: "test search" }, "input-available")
);

rerender(renderTool(undefined, "input-streaming"));

expect(
screen.getByRole("button", { name: "searchPending" })
).toBeInTheDocument();
expect(screen.queryByText("Parameters")).not.toBeInTheDocument();
expect(container.querySelector("code")).not.toBeInTheDocument();
});

it.each([
{ input: null, json: "null" },
{ input: false, json: "false" },
{ input: 0, json: "0" },
{ input: "", json: '""' },
{ input: {}, json: "{}" },
])("renders defined input $json", ({ input, json }) => {
const { container } = render(<ToolInput input={input} />);

expect(screen.getByText("Parameters")).toBeInTheDocument();
expect(container.querySelector("code")).toHaveTextContent(json);
});

it("renders input parameters", async () => {
const input = { query: "test search" };
render(
Expand Down
24 changes: 15 additions & 9 deletions packages/elements/src/tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,22 @@ export type ToolInputProps = ComponentProps<"div"> & {
input: ToolPart["input"];
};

export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
<div className={cn("space-y-2 overflow-hidden", className)} {...props}>
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
Parameters
</h4>
<div className="rounded-md bg-muted/50">
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
export const ToolInput = ({ className, input, ...props }: ToolInputProps) => {
if (input === undefined) {
return null;
}

return (
<div className={cn("space-y-2 overflow-hidden", className)} {...props}>
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
Parameters
</h4>
<div className="rounded-md bg-muted/50">
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
</div>
</div>
</div>
);
);
};

export type ToolOutputProps = ComponentProps<"div"> & {
output: ToolPart["output"];
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/src/tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const Example = () => (
type="tool-database_query"
/>
<ToolContent>
<ToolInput input={{}} />
<ToolInput input={undefined} />
</ToolContent>
</Tool>

Expand Down