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 (
+
+ );
+ }
+
+ 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 */