diff --git a/web/src/components/chat/ToolCallsGroup.tsx b/web/src/components/chat/ToolCallsGroup.tsx index 72a700a580..7cb20a1adb 100644 --- a/web/src/components/chat/ToolCallsGroup.tsx +++ b/web/src/components/chat/ToolCallsGroup.tsx @@ -1,51 +1,98 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; -import { LuChevronDown, LuChevronRight } from "react-icons/lu"; -import { ToolCallBubble } from "@/components/chat/ToolCallBubble"; +import { LuChevronsUpDown } from "react-icons/lu"; import type { ToolCall } from "@/types/chat"; type ToolCallsGroupProps = { toolCalls: ToolCall[]; }; +function normalizeName(name: string): string { + return name + .replace(/_/g, " ") + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(" "); +} + export function ToolCallsGroup({ toolCalls }: ToolCallsGroupProps) { - const { t } = useTranslation(["views/chat"]); - const [open, setOpen] = useState(false); + const grouped = useMemo(() => { + const map = new Map(); + for (const tc of toolCalls) { + const existing = map.get(tc.name); + if (existing) { + existing.push(tc); + } else { + map.set(tc.name, [tc]); + } + } + return map; + }, [toolCalls]); if (toolCalls.length === 0) return null; + return ( +
+ {[...grouped.entries()].map(([name, calls]) => ( + + ))} +
+ ); +} + +type ToolCallRowProps = { + name: string; + calls: ToolCall[]; +}; + +function ToolCallRow({ name, calls }: ToolCallRowProps) { + const { t } = useTranslation(["views/chat"]); + const [open, setOpen] = useState(false); + const displayName = normalizeName(name); + const label = + calls.length > 1 ? `${displayName} (\u00d7${calls.length})` : displayName; + return ( - - {open ? ( - - ) : ( - - )} - - {open ? t("hideTools") : t("showTools", { count: toolCalls.length })} - + + {label} + -
- {toolCalls.map((tc, tcIdx) => ( -
- - {tc.response && ( - +
+ {calls.map((tc, idx) => ( +
1 + ? "space-y-1 border-l-2 border-border pl-3" + : "space-y-1" + } + > + {tc.arguments && Object.keys(tc.arguments).length > 0 && ( +
+
+ {t("arguments")} +
+
+                    {JSON.stringify(tc.arguments, null, 2)}
+                  
+
+ )} + {tc.response && tc.response !== "" && ( +
+
+ {t("response")} +
+
+                    {tc.response}
+                  
+
)}
))}