diff --git a/webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx b/webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx new file mode 100644 index 00000000..5a1481b7 --- /dev/null +++ b/webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx @@ -0,0 +1,73 @@ +import { cn } from "@heroui/react"; +import { JsonRpcResult } from "./utils/common"; +import MarkdownComponent from "../../markdown"; +import { LoadingIndicator } from "../../loading-indicator"; +import { useState } from "react"; + +type JsonRpcProps = { + functionName: string; + jsonRpcResult: JsonRpcResult; + preparing: boolean; + animated: boolean; +}; + +export const JsonRpc = ({ functionName, jsonRpcResult, preparing, animated }: JsonRpcProps) => { + const [isCollapsed, setIsCollapsed] = useState(false); + + if (preparing) { + return ( +
+
+

{functionName}

+
+ +
+ ); + } + + const toggleCollapse = () => { + setIsCollapsed(!isCollapsed); + }; + + return ( +
+
+

{functionName}

+ +
+ +
+ {jsonRpcResult.result && ( +
+ + {jsonRpcResult.result.content?.map((content) => content.text).join("\n") || ""} + +
+ )} + + {jsonRpcResult.error && ( +
+ {jsonRpcResult.error.message} +
+ )} +
+
+ ); +}; diff --git a/webapp/_webapp/src/components/message-entry-container/tools/tools.tsx b/webapp/_webapp/src/components/message-entry-container/tools/tools.tsx index 044b20ef..62688ab9 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/tools.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/tools.tsx @@ -4,6 +4,8 @@ import { UnknownToolCard } from "./unknown"; import { GreetingCard } from "./greeting"; import { ErrorToolCard } from "./error"; import { AlwaysExceptionCard } from "./always-exception"; +import { JsonRpc } from "./jsonrpc"; +import { parseJsonRpcResult, UNKNOWN_JSONRPC_RESULT } from "./utils/common"; type ToolsProps = { messageId: string; @@ -14,11 +16,26 @@ type ToolsProps = { animated: boolean; }; +// define a const string list. +const XTRA_MCP_TOOL_NAMES = [ + "enhance_academic_writing", + "search_relevant_papers", + "search_user", + "identify_improvements", + "get_user_papers", + "deep_research", + "get_conference_papers", + "search_papers_on_openreview", + "search_papers", +]; + export default function Tools({ messageId, functionName, message, error, preparing, animated }: ToolsProps) { if (error && error !== "") { return ; } + const jsonRpcResult = parseJsonRpcResult(message); + if (functionName === "paper_score") { return ; } else if (functionName === "paper_score_comment") { @@ -27,7 +44,14 @@ export default function Tools({ messageId, functionName, message, error, prepari return ; } else if (functionName === "always_exception") { return ; + } else if (XTRA_MCP_TOOL_NAMES.includes(functionName)) { + return ; } - return ; + // fallback to unknown tool card if the json rpc result is not defined + if (jsonRpcResult) { + return ; + } else { + return ; + } } diff --git a/webapp/_webapp/src/components/message-entry-container/tools/unknown-jsonrpc.tsx b/webapp/_webapp/src/components/message-entry-container/tools/unknown-jsonrpc.tsx new file mode 100644 index 00000000..f4a280ae --- /dev/null +++ b/webapp/_webapp/src/components/message-entry-container/tools/unknown-jsonrpc.tsx @@ -0,0 +1,18 @@ +import { cn } from "@heroui/react"; + +type UnknownJsonRpcProps = { + functionName: string; + message: string; + animated: boolean; +}; + +export const UnknownJsonRpc = ({ functionName, message, animated }: UnknownJsonRpcProps) => { + return ( +
+

+ Unknown JsonRPC "{functionName}" +

+ {message} +
+ ); +}; diff --git a/webapp/_webapp/src/components/message-entry-container/tools/utils/common.tsx b/webapp/_webapp/src/components/message-entry-container/tools/utils/common.tsx new file mode 100644 index 00000000..4a455dc5 --- /dev/null +++ b/webapp/_webapp/src/components/message-entry-container/tools/utils/common.tsx @@ -0,0 +1,83 @@ +export type JsonRpcResult = { + jsonrpc: string; + id: number; + result?: { + content: Array<{ + type: string; + text: string; + }>; + }; + error?: { + code: number; + message: string; + } +} + +export const UNKNOWN_JSONRPC_RESULT: JsonRpcResult = { + jsonrpc: "2.0", + id: -1, + error: { + code: -1, + message: "Unknown JSONRPC result", + }, +} + +const isValidJsonRpcResult = (obj: any): obj is JsonRpcResult => { + // Check if obj is an object and not null + if (typeof obj !== 'object' || obj === null) { + return false; + } + + // Check required properties + if (typeof obj.jsonrpc !== 'string' || typeof obj.id !== 'number') { + return false; + } + + // Check that either result or error is present (but not both required) + const hasResult = obj.result !== undefined; + const hasError = obj.error !== undefined; + + // Validate result structure if present + if (hasResult) { + if (typeof obj.result !== 'object' || obj.result === null) { + return false; + } + if (obj.result.content !== undefined) { + if (!Array.isArray(obj.result.content)) { + return false; + } + // Validate each content item + for (const item of obj.result.content) { + if (typeof item !== 'object' || item === null || + typeof item.type !== 'string' || typeof item.text !== 'string') { + return false; + } + } + } + } + + // Validate error structure if present + if (hasError) { + if (typeof obj.error !== 'object' || obj.error === null || + typeof obj.error.code !== 'number' || typeof obj.error.message !== 'string') { + return false; + } + } + + return true; +}; + +export const parseJsonRpcResult = (message: string): JsonRpcResult | undefined => { + try { + const json = JSON.parse(message); + + // Validate the structure before casting + if (isValidJsonRpcResult(json)) { + return json; + } + + return undefined; + } catch (error) { + return undefined; + } +} \ No newline at end of file diff --git a/webapp/_webapp/src/index.css b/webapp/_webapp/src/index.css index 949b7a3b..0ad3db66 100644 --- a/webapp/_webapp/src/index.css +++ b/webapp/_webapp/src/index.css @@ -91,6 +91,10 @@ body { border-color: var(--pd-border-color); } +.tool-card.narrow { + @apply px-2 py-0 my-1 bg-transparent; +} + .tool-card.animated { opacity: 0; animation: toolCardAppear 0.5s ease-out 200ms forwards; @@ -98,9 +102,14 @@ body { } .tool-card-title { - @apply text-xs font-semibold font-sans text-primary-700 uppercase tracking-wider mb-1 noselect; + @apply text-xs font-semibold font-sans text-primary-700 uppercase tracking-wider noselect; } +.tool-card-title.tool-card-jsonrpc { + @apply font-medium text-gray-500; +} + + /* 相邻 tool-card 的样式处理 */ .tool-card + .tool-card { /* 相邻的第二个卡片:移除上边框,调整上圆角,减少上边距,减少上 padding */