mirror of
https://github.com/ONLYOFFICE/desktop-sdk.git
synced 2026-03-31 10:23:12 +08:00
287 lines
11 KiB
TypeScript
287 lines
11 KiB
TypeScript
import { useEffect, useState, useCallback } from "react";
|
|
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
|
|
import { ReactSVG } from "react-svg";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import ToolCalledIconUrl from "@/assets/tool.called.svg?url";
|
|
import CodeIconUrl from "@/assets/code.svg?url";
|
|
import ArrowBottomIconUrl from "@/assets/arrow.bottom.svg?url";
|
|
import ArrowRightIconUrl from "@/assets/arrow.right.svg?url";
|
|
import CopyIconUrl from "@/assets/btn-copy.svg?url";
|
|
import CheckedIconUrl from "@/assets/checked.svg?url";
|
|
import SearchIconUrl from "@/assets/btn-web-search.svg?url";
|
|
import ExternalIconUrl from "@/assets/btn-external.svg?url";
|
|
|
|
import server from "@/servers";
|
|
|
|
import { Loader } from "../loader";
|
|
import { IconButton } from "../icon-button";
|
|
|
|
export const ToolFallback: ToolCallMessagePartComponent = ({
|
|
toolName,
|
|
argsText,
|
|
result,
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
|
const [isArgsCopied, setIsArgsCopied] = useState(false);
|
|
const [isResultCopied, setIsResultCopied] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (isArgsCopied) {
|
|
setTimeout(() => {
|
|
setIsArgsCopied(false);
|
|
}, 2000);
|
|
}
|
|
}, [isArgsCopied]);
|
|
|
|
useEffect(() => {
|
|
if (isResultCopied) {
|
|
setTimeout(() => {
|
|
setIsResultCopied(false);
|
|
}, 2000);
|
|
}
|
|
}, [isResultCopied]);
|
|
|
|
const type = server.getServerType(toolName);
|
|
const name = toolName.replace(type + "_", "");
|
|
|
|
const isLoading = result === undefined;
|
|
|
|
const isWebSearch = name === "web_search";
|
|
const isWebCrawling = name === "web_crawling";
|
|
|
|
let webSearchName = "";
|
|
|
|
let argsTextFinal = argsText;
|
|
|
|
try {
|
|
const parsedArgs = JSON.parse(argsTextFinal);
|
|
|
|
if (parsedArgs.args) {
|
|
argsTextFinal = JSON.stringify(parsedArgs.args);
|
|
}
|
|
|
|
webSearchName = argsTextFinal
|
|
? isWebSearch
|
|
? JSON.parse(argsTextFinal).query
|
|
: isWebCrawling
|
|
? JSON.parse(argsTextFinal).urls[0]
|
|
: ""
|
|
: "";
|
|
} catch {
|
|
//ignore
|
|
}
|
|
|
|
const handleBeforeInjectionFill = useCallback((svg: SVGSVGElement) => {
|
|
const paths = svg.querySelectorAll("path");
|
|
paths.forEach((path) => {
|
|
path.setAttribute("fill", "var(--chat-message-tool-call-name-color)");
|
|
});
|
|
const circles = svg.querySelectorAll("circle");
|
|
circles.forEach((circle) => {
|
|
circle.setAttribute("fill", "var(--chat-message-tool-call-name-color)");
|
|
});
|
|
}, []);
|
|
|
|
const handleBeforeInjection = useCallback((svg: SVGSVGElement) => {
|
|
const paths = svg.querySelectorAll("path");
|
|
paths.forEach((path) => {
|
|
path.setAttribute("stroke", "var(--chat-message-tool-call-name-color)");
|
|
});
|
|
const circles = svg.querySelectorAll("circle");
|
|
circles.forEach((circle) => {
|
|
circle.setAttribute("stroke", "var(--chat-message-tool-call-name-color)");
|
|
});
|
|
}, []);
|
|
|
|
return (
|
|
<div className="my-[16px] flex w-full flex-col gap-3">
|
|
<div
|
|
className="flex items-center gap-[10px] cursor-pointer"
|
|
onClick={() => {
|
|
if (isWebCrawling) {
|
|
window.open(webSearchName, "_blank");
|
|
return;
|
|
}
|
|
|
|
if (isWebSearch && result === undefined) {
|
|
return;
|
|
}
|
|
|
|
setIsCollapsed(!isCollapsed);
|
|
}}
|
|
>
|
|
{!isLoading ? (
|
|
<ReactSVG src={ToolCalledIconUrl} />
|
|
) : (
|
|
<Loader size={16} />
|
|
)}
|
|
{isLoading && !isWebSearch && !isWebCrawling ? (
|
|
<p className="text-[var(--chat-message-tool-call-header-color)] text-[14px] font-normal leading-[16px]">
|
|
{t("ToolExecuted")}
|
|
</p>
|
|
) : null}
|
|
<span className="flex items-center gap-[8px] rounded-[4px] ps-[4px] pe-[8px] text-[14px] leading-[20px] font-normal text-[var(--chat-message-tool-call-name-color)] bg-[var(--chat-message-tool-call-name-background-color)] min-w-0 w-fit">
|
|
{isWebSearch ? (
|
|
<ReactSVG
|
|
src={SearchIconUrl}
|
|
beforeInjection={handleBeforeInjectionFill}
|
|
/>
|
|
) : !isWebCrawling ? (
|
|
<ReactSVG
|
|
src={CodeIconUrl}
|
|
beforeInjection={handleBeforeInjection}
|
|
/>
|
|
) : null}
|
|
<span className="truncate">
|
|
{isWebSearch
|
|
? webSearchName
|
|
: isWebCrawling
|
|
? `${name} | ${webSearchName}`
|
|
: name}
|
|
</span>
|
|
</span>
|
|
{isWebCrawling ? (
|
|
<ReactSVG
|
|
src={ExternalIconUrl}
|
|
beforeInjection={handleBeforeInjection}
|
|
/>
|
|
) : isWebSearch && result === undefined ? null : (
|
|
<ReactSVG
|
|
src={!isCollapsed ? ArrowBottomIconUrl : ArrowRightIconUrl}
|
|
beforeInjection={handleBeforeInjection}
|
|
/>
|
|
)}
|
|
</div>
|
|
{!isCollapsed ? (
|
|
<div className="flex flex-col gap-[24px] mt-[8px] p-[12px] bg-[var(--chat-message-tool-call-body-background-color)] rounded-[12px]">
|
|
{isWebSearch ? null : (
|
|
<div className="">
|
|
<p className="flex flex-row items-center justify-between text-[var(--chat-message-tool-call-header-color)] text-[14px] font-bold leading-[20px]">
|
|
{t("ToolCallArguments")}
|
|
<ReactSVG
|
|
src={isArgsCopied ? CheckedIconUrl : CopyIconUrl}
|
|
onClick={() => setIsArgsCopied(true)}
|
|
beforeInjection={
|
|
!isArgsCopied
|
|
? handleBeforeInjectionFill
|
|
: handleBeforeInjection
|
|
}
|
|
/>
|
|
</p>
|
|
<pre className="max-h-[200px] overflow-y-auto whitespace-pre-wrap text-[var(--chat-message-tool-call-pre-color)] border border-[var(--chat-message-tool-call-pre-border-color)] bg-[var(--chat-message-tool-call-pre-background-color)] px-[8px] py-[2px] rounded-[4px]">
|
|
{argsTextFinal ? argsTextFinal : "{}"}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
{result !== undefined && (
|
|
<div className="">
|
|
{isWebSearch ? (
|
|
<div>
|
|
{(() => {
|
|
try {
|
|
const parsedResult =
|
|
typeof result === "string"
|
|
? JSON.parse(result)
|
|
: result;
|
|
|
|
// Check if there's an error in the result
|
|
if (parsedResult?.error) {
|
|
return (
|
|
<pre className="max-h-[200px] overflow-y-auto whitespace-pre-wrap text-[var(--chat-message-tool-call-pre-color)] border border-[var(--chat-message-tool-call-pre-border-color)] bg-[var(--chat-message-tool-call-pre-background-color)] px-[8px] py-[2px] rounded-[4px]">
|
|
{typeof result === "string"
|
|
? result
|
|
: JSON.stringify(result, null, 2)}
|
|
</pre>
|
|
);
|
|
}
|
|
|
|
const searchResults = parsedResult?.data || [];
|
|
|
|
return searchResults.length > 0 ? (
|
|
<div className="flex flex-col gap-[10px]">
|
|
{searchResults.map(
|
|
(
|
|
item: {
|
|
id: string;
|
|
title: string;
|
|
url: string;
|
|
publishedDate?: string;
|
|
author?: string;
|
|
},
|
|
index: number
|
|
) => (
|
|
<div
|
|
key={index}
|
|
className="group h-[36px] px-[8px] rounded-[4px] flex flex-row items-center justify-between cursor-pointer hover:bg-[var(--drop-down-menu-item-hover-color)] transition-colors"
|
|
onClick={() => window.open(item.url, "_blank")}
|
|
>
|
|
<div className="flex flex-row items-center gap-[8px] min-w-0 flex-1">
|
|
<IconButton
|
|
iconName={SearchIconUrl}
|
|
size={24}
|
|
disableHover
|
|
/>
|
|
<h4 className="text-[14px] font-normal text-[var(--chat-message-tool-call-pre-color)] truncate">
|
|
{item.title}
|
|
</h4>
|
|
</div>
|
|
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<IconButton
|
|
iconName={ExternalIconUrl}
|
|
size={24}
|
|
disableHover
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
) : (
|
|
<pre className="max-h-[200px] overflow-y-auto whitespace-pre-wrap text-[var(--chat-message-tool-call-pre-color)] border border-[var(--chat-message-tool-call-pre-border-color)] bg-[var(--chat-message-tool-call-pre-background-color)] px-[8px] py-[2px] rounded-[4px]">
|
|
{typeof result === "string"
|
|
? result
|
|
: JSON.stringify(result, null, 2)}
|
|
</pre>
|
|
);
|
|
} catch {
|
|
return (
|
|
<pre className="max-h-[200px] overflow-y-auto whitespace-pre-wrap text-[var(--chat-message-tool-call-pre-color)] border border-[var(--chat-message-tool-call-pre-border-color)] bg-[var(--chat-message-tool-call-pre-background-color)] px-[8px] py-[2px] rounded-[4px]">
|
|
{typeof result === "string"
|
|
? result
|
|
: JSON.stringify(result, null, 2)}
|
|
</pre>
|
|
);
|
|
}
|
|
})()}
|
|
</div>
|
|
) : (
|
|
<>
|
|
<p className="flex flex-row items-center justify-between text-[var(--chat-message-tool-call-header-color)] text-[14px] font-bold leading-[20px]">
|
|
{t("ToolCallResult")}
|
|
<ReactSVG
|
|
src={isResultCopied ? CheckedIconUrl : CopyIconUrl}
|
|
onClick={() => setIsResultCopied(true)}
|
|
beforeInjection={
|
|
!isResultCopied
|
|
? handleBeforeInjectionFill
|
|
: handleBeforeInjection
|
|
}
|
|
/>
|
|
</p>
|
|
<pre className="max-h-[200px] overflow-y-auto whitespace-pre-wrap text-[var(--chat-message-tool-call-pre-color)] border border-[var(--chat-message-tool-call-pre-border-color)] bg-[var(--chat-message-tool-call-pre-background-color)] px-[8px] py-[2px] rounded-[4px]">
|
|
{typeof result === "string"
|
|
? result
|
|
: JSON.stringify(result, null, 2)}
|
|
</pre>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
};
|