From 9625b9ad9d3a628e324602f3a01bf3ea49806e6b Mon Sep 17 00:00:00 2001 From: Junyi Hou Date: Tue, 28 Oct 2025 21:47:35 +0800 Subject: [PATCH 1/6] add jsonrpc tool call card --- .../message-entry-container/tools/jsonrpc.tsx | 74 +++++++++++++++++++ .../message-entry-container/tools/tools.tsx | 31 +++++++- .../tools/unknown-jsonrpc.tsx | 18 +++++ .../tools/utils/common.tsx | 32 ++++++++ webapp/_webapp/src/index.css | 11 ++- 5 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx create mode 100644 webapp/_webapp/src/components/message-entry-container/tools/unknown-jsonrpc.tsx create mode 100644 webapp/_webapp/src/components/message-entry-container/tools/utils/common.tsx 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..8ad1ea8f --- /dev/null +++ b/webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx @@ -0,0 +1,74 @@ +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..d22a9f02 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,10 +16,30 @@ 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 ; } +console.log("message: ", message); +console.log("functionName: ", functionName); +console.log("error: ", error); +console.log("preparing: ", preparing); +console.log("animated: ", animated); + const jsonRpcResult = parseJsonRpcResult(message); + console.log("jsonRpcResult: ", jsonRpcResult); if (functionName === "paper_score") { return ; @@ -27,7 +49,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..4c7c0ba0 --- /dev/null +++ b/webapp/_webapp/src/components/message-entry-container/tools/utils/common.tsx @@ -0,0 +1,32 @@ +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", + }, +} + +export const parseJsonRpcResult = (message: string): JsonRpcResult | undefined => { + try { + const json = JSON.parse(message); + return json as JsonRpcResult; + } 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..54ee1f1e 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 */ From 26c8c598758d5a5c2bde25cfea4ce74cffab3af1 Mon Sep 17 00:00:00 2001 From: Junyi Date: Tue, 28 Oct 2025 21:50:16 +0800 Subject: [PATCH 2/6] Update webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/components/message-entry-container/tools/jsonrpc.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx b/webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx index 8ad1ea8f..a0d50c48 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx @@ -58,7 +58,7 @@ export const JsonRpc = ({ functionName, jsonRpcResult, preparing, animated }: Js {jsonRpcResult.result && (
- {jsonRpcResult.result.content?.map((content) => content.text).join("\\n") || ""} + {jsonRpcResult.result.content?.map((content) => content.text).join("\n") || ""}
)} From 4fdd38f5b9a1582c5e1cc8a5de15e4b208b3314c Mon Sep 17 00:00:00 2001 From: Junyi Date: Tue, 28 Oct 2025 21:50:26 +0800 Subject: [PATCH 3/6] Update webapp/_webapp/src/components/message-entry-container/tools/tools.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/components/message-entry-container/tools/tools.tsx | 1 - 1 file changed, 1 deletion(-) 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 d22a9f02..4965b049 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/tools.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/tools.tsx @@ -39,7 +39,6 @@ console.log("error: ", error); console.log("preparing: ", preparing); console.log("animated: ", animated); const jsonRpcResult = parseJsonRpcResult(message); - console.log("jsonRpcResult: ", jsonRpcResult); if (functionName === "paper_score") { return ; From f0f43426d8818d8424bad9ff54081a77745aaef7 Mon Sep 17 00:00:00 2001 From: Junyi Date: Tue, 28 Oct 2025 21:50:38 +0800 Subject: [PATCH 4/6] Update webapp/_webapp/src/index.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- webapp/_webapp/src/index.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/_webapp/src/index.css b/webapp/_webapp/src/index.css index 54ee1f1e..0ad3db66 100644 --- a/webapp/_webapp/src/index.css +++ b/webapp/_webapp/src/index.css @@ -106,7 +106,7 @@ body { } .tool-card-title.tool-card-jsonrpc { - @apply font-medium text-gray-500 ; + @apply font-medium text-gray-500; } From 7208e152f1ff9d2f7137c24e119e750c195c7d7d Mon Sep 17 00:00:00 2001 From: Junyi Hou Date: Tue, 28 Oct 2025 21:53:32 +0800 Subject: [PATCH 5/6] fix: type validator --- .../tools/utils/common.tsx | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) 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 index 4c7c0ba0..4a455dc5 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/utils/common.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/utils/common.tsx @@ -22,10 +22,61 @@ export const UNKNOWN_JSONRPC_RESULT: JsonRpcResult = { }, } +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); - return json as JsonRpcResult; + + // Validate the structure before casting + if (isValidJsonRpcResult(json)) { + return json; + } + + return undefined; } catch (error) { return undefined; } From cab0003d9c4372bd78ebc09adae45a9a6c5089ab Mon Sep 17 00:00:00 2001 From: Junyi Hou Date: Tue, 28 Oct 2025 22:40:00 +0800 Subject: [PATCH 6/6] chore: remove console.log --- .../components/message-entry-container/tools/jsonrpc.tsx | 1 - .../src/components/message-entry-container/tools/tools.tsx | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx b/webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx index a0d50c48..5a1481b7 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/jsonrpc.tsx @@ -34,7 +34,6 @@ export const JsonRpc = ({ functionName, jsonRpcResult, preparing, animated }: Js

{functionName}