From 3ac00598ff76a13290584aed8c78f8e5a8db41ac Mon Sep 17 00:00:00 2001 From: yusheng chen Date: Sun, 23 Mar 2025 21:29:46 +0800 Subject: [PATCH 01/13] perf: add `useMemo` to the return value of `useTheme` --- client/src/lib/useTheme.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/client/src/lib/useTheme.ts b/client/src/lib/useTheme.ts index c73159b..8e598e1 100644 --- a/client/src/lib/useTheme.ts +++ b/client/src/lib/useTheme.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; type Theme = "light" | "dark" | "system"; @@ -35,17 +35,18 @@ const useTheme = (): [Theme, (mode: Theme) => void] => { darkModeMediaQuery.removeEventListener("change", handleDarkModeChange); }; }, [theme]); - - return [ + + const setThemeWithSideEffect = useCallback((newTheme: Theme) => { + setTheme(newTheme); + localStorage.setItem("theme", newTheme); + if (newTheme !== "system") { + document.documentElement.classList.toggle("dark", newTheme === "dark"); + } + }, []); + return useMemo(() => [ theme, - useCallback((newTheme: Theme) => { - setTheme(newTheme); - localStorage.setItem("theme", newTheme); - if (newTheme !== "system") { - document.documentElement.classList.toggle("dark", newTheme === "dark"); - } - }, []), - ]; + setThemeWithSideEffect, + ], [theme]); }; export default useTheme; From f0b28d476040f4fa14fdb7bc07cdecd8faaa2d07 Mon Sep 17 00:00:00 2001 From: cgoing Date: Tue, 25 Mar 2025 01:48:29 +0900 Subject: [PATCH 02/13] feat: json view component --- client/src/components/History.tsx | 13 +- client/src/components/JsonView.tsx | 210 +++++++++++++++++++++++++ client/src/components/ResourcesTab.tsx | 3 +- client/src/components/SamplingTab.tsx | 3 +- client/src/components/ToolsTab.tsx | 11 +- 5 files changed, 226 insertions(+), 14 deletions(-) create mode 100644 client/src/components/JsonView.tsx diff --git a/client/src/components/History.tsx b/client/src/components/History.tsx index 532d3b1..7ce7dcf 100644 --- a/client/src/components/History.tsx +++ b/client/src/components/History.tsx @@ -1,6 +1,7 @@ import { ServerNotification } from "@modelcontextprotocol/sdk/types.js"; import { Copy } from "lucide-react"; import { useState } from "react"; +import JsonView from "./JsonView"; const HistoryAndNotifications = ({ requestHistory, @@ -75,7 +76,7 @@ const HistoryAndNotifications = ({
-                          {JSON.stringify(JSON.parse(request.request), null, 2)}
+                          
                         
{request.response && ( @@ -92,11 +93,7 @@ const HistoryAndNotifications = ({
-                            {JSON.stringify(
-                              JSON.parse(request.response),
-                              null,
-                              2,
-                            )}
+                            
                           
)} @@ -147,7 +144,9 @@ const HistoryAndNotifications = ({
-                        {JSON.stringify(notification, null, 2)}
+                        
                       
)} diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx new file mode 100644 index 0000000..21d65d8 --- /dev/null +++ b/client/src/components/JsonView.tsx @@ -0,0 +1,210 @@ +import { useState, memo } from "react"; +import { JsonValue } from "./DynamicJsonForm"; + +interface JsonViewProps { + data: JsonValue; + name?: string; + initialExpandDepth?: number; +} + +function tryParseJson(str: string): { success: boolean; data: JsonValue } { + const trimmed = str.trim(); + if ( + !(trimmed.startsWith("{") && trimmed.endsWith("}")) && + !(trimmed.startsWith("[") && trimmed.endsWith("]")) + ) { + return { success: false, data: str }; + } + try { + return { success: true, data: JSON.parse(str) }; + } catch { + return { success: false, data: str }; + } +} + +const JsonView = memo( + ({ data, name, initialExpandDepth = 2 }: JsonViewProps) => { + const normalizedData = + typeof data === "string" + ? tryParseJson(data).success + ? tryParseJson(data).data + : data + : data; + + return ( +
+ +
+ ); + }, +); + +JsonView.displayName = "JsonView"; + +interface JsonNodeProps { + data: JsonValue; + name?: string; + depth: number; + initialExpandDepth: number; +} + +const JsonNode = memo( + ({ data, name, depth = 0, initialExpandDepth }: JsonNodeProps) => { + const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth); + + const getDataType = (value: JsonValue): string => { + if (Array.isArray(value)) return "array"; + if (value === null) return "null"; + return typeof value; + }; + + const dataType = getDataType(data); + + const typeStyleMap: Record = { + number: "text-blue-600", + boolean: "text-amber-600", + null: "text-purple-600", + undefined: "text-gray-600", + string: "text-green-600", + default: "text-gray-700", + }; + + const renderCollapsible = (isArray: boolean) => { + const items = isArray + ? (data as JsonValue[]) + : Object.entries(data as Record); + const itemCount = items.length; + const isEmpty = itemCount === 0; + + const symbolMap = { + open: isArray ? "[" : "{", + close: isArray ? "]" : "}", + collapsed: isArray ? "[ ... ]" : "{ ... }", + empty: isArray ? "[]" : "{}", + }; + + if (isEmpty) { + return ( +
+ {name && {name}:} + {symbolMap.empty} +
+ ); + } + + return ( +
+
setIsExpanded(!isExpanded)} + > + {name && ( + + {name}: + + )} + {isExpanded ? ( + + {symbolMap.open} + + ) : ( + <> + + {symbolMap.collapsed} + + + {itemCount} {itemCount === 1 ? "item" : "items"} + + + )} +
+ {isExpanded && ( + <> +
+ {isArray + ? (items as JsonValue[]).map((item, index) => ( +
+ +
+ )) + : (items as [string, JsonValue][]).map(([key, value]) => ( +
+ +
+ ))} +
+
{symbolMap.close}
+ + )} +
+ ); + }; + + const renderString = (value: string) => { + const maxLength = 100; + const isTooLong = value.length > maxLength; + + if (!isTooLong) { + return ( +
+ {name && {name}:} + "{value}" +
+ ); + } + + return ( +
+ {name && ( + + {name}: + + )} + setIsExpanded(!isExpanded)} + title={isExpanded ? "클릭하여 축소" : "클릭하여 전체 보기"} + > + {isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`} + +
+ ); + }; + + switch (dataType) { + case "object": + case "array": + return renderCollapsible(dataType === "array"); + case "string": + return renderString(data as string); + default: + return ( +
+ {name && {name}:} + + {data === null ? "null" : String(data)} + +
+ ); + } + }, +); + +JsonNode.displayName = "JsonNode"; + +export default JsonView; diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index f000840..bc3e39a 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -15,6 +15,7 @@ import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react"; import ListPane from "./ListPane"; import { useEffect, useState } from "react"; import { useCompletionState } from "@/lib/hooks/useCompletionState"; +import JsonView from "./JsonView"; const ResourcesTab = ({ resources, @@ -215,7 +216,7 @@ const ResourcesTab = ({ ) : selectedResource ? (
-              {resourceContent}
+              
             
) : selectedTemplate ? (
diff --git a/client/src/components/SamplingTab.tsx b/client/src/components/SamplingTab.tsx index 5c45400..d14d4c8 100644 --- a/client/src/components/SamplingTab.tsx +++ b/client/src/components/SamplingTab.tsx @@ -5,6 +5,7 @@ import { CreateMessageRequest, CreateMessageResult, } from "@modelcontextprotocol/sdk/types.js"; +import JsonView from "./JsonView"; export type PendingRequest = { id: number; @@ -44,7 +45,7 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => { {pendingRequests.map((request) => (
-              {JSON.stringify(request.request, null, 2)}
+              
             
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 82ebdb0..345f7bd 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -17,6 +17,7 @@ import { AlertCircle, Send } from "lucide-react"; import { useEffect, useState } from "react"; import ListPane from "./ListPane"; import { escapeUnicode } from "@/utils/escapeUnicode"; +import JsonView from "./JsonView"; const ToolsTab = ({ tools, @@ -54,7 +55,7 @@ const ToolsTab = ({ <>

Invalid Tool Result:

-              {escapeUnicode(toolResult)}
+              
             

Errors:

{parsedResult.error.errors.map((error, idx) => ( @@ -62,7 +63,7 @@ const ToolsTab = ({ key={idx} className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64" > - {escapeUnicode(error)} + ))} @@ -80,7 +81,7 @@ const ToolsTab = ({
{item.type === "text" && (
-                  {item.text}
+                  
                 
)} {item.type === "image" && ( @@ -101,7 +102,7 @@ const ToolsTab = ({ ) : (
-                    {escapeUnicode(item.resource)}
+                    
                   
))}
@@ -113,7 +114,7 @@ const ToolsTab = ({ <>

Tool Result (Legacy):

-            {escapeUnicode(toolResult.toolResult)}
+            
           
); From d204dd6e7e9b5b8a72232b14a350eccb1e8ceb2a Mon Sep 17 00:00:00 2001 From: cgoing Date: Tue, 25 Mar 2025 01:56:53 +0900 Subject: [PATCH 03/13] feat: json view component - dark color --- client/src/components/JsonView.tsx | 36 +++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index 21d65d8..7d3c586 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -91,7 +91,11 @@ const JsonNode = memo( if (isEmpty) { return (
- {name && {name}:} + {name && ( + + {name}: + + )} {symbolMap.empty}
); @@ -100,24 +104,24 @@ const JsonNode = memo( return (
setIsExpanded(!isExpanded)} > {name && ( - + {name}: )} {isExpanded ? ( - + {symbolMap.open} ) : ( <> - + {symbolMap.collapsed} - + {itemCount} {itemCount === 1 ? "item" : "items"} @@ -125,7 +129,7 @@ const JsonNode = memo(
{isExpanded && ( <> -
+
{isArray ? (items as JsonValue[]).map((item, index) => (
@@ -148,7 +152,9 @@ const JsonNode = memo(
))}
-
{symbolMap.close}
+
+ {symbolMap.close} +
)}
@@ -162,7 +168,11 @@ const JsonNode = memo( if (!isTooLong) { return (
- {name && {name}:} + {name && ( + + {name}: + + )} "{value}"
); @@ -171,7 +181,7 @@ const JsonNode = memo( return (
{name && ( - + {name}: )} @@ -195,7 +205,11 @@ const JsonNode = memo( default: return (
- {name && {name}:} + {name && ( + + {name}: + + )} {data === null ? "null" : String(data)} From 2588f3aeb3c7518d7a3e87e2529692e4bc86841b Mon Sep 17 00:00:00 2001 From: cgoing Date: Wed, 26 Mar 2025 00:02:10 +0900 Subject: [PATCH 04/13] Change tooltip title from Korean to English --- client/src/components/JsonView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index 7d3c586..9add405 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -188,7 +188,7 @@ const JsonNode = memo( setIsExpanded(!isExpanded)} - title={isExpanded ? "클릭하여 축소" : "클릭하여 전체 보기"} + title={isExpanded ? "Click to collapse" : "Click to expand"} > {isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`} From 03c1ba3092d748f60beeb1ef9b5e78be35b98c84 Mon Sep 17 00:00:00 2001 From: cgoing Date: Wed, 26 Mar 2025 00:11:07 +0900 Subject: [PATCH 05/13] Change JsonView default initialExpandDepth from 2 to 3 --- client/src/components/JsonView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index 9add405..6c348fe 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -23,7 +23,7 @@ function tryParseJson(str: string): { success: boolean; data: JsonValue } { } const JsonView = memo( - ({ data, name, initialExpandDepth = 2 }: JsonViewProps) => { + ({ data, name, initialExpandDepth = 3 }: JsonViewProps) => { const normalizedData = typeof data === "string" ? tryParseJson(data).success From e6f5da838301d7dcc6a263aa97f586ccae56d068 Mon Sep 17 00:00:00 2001 From: cgoing Date: Thu, 27 Mar 2025 10:49:37 +0900 Subject: [PATCH 06/13] Improve JsonView component styling and change to use JsonView in PromptsTab --- client/src/components/JsonView.tsx | 10 +++++++--- client/src/components/PromptsTab.tsx | 11 +++++------ client/src/components/ToolsTab.tsx | 23 ++++++++++------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index 6c348fe..e20dd5c 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -1,5 +1,6 @@ import { useState, memo } from "react"; import { JsonValue } from "./DynamicJsonForm"; +import clsx from "clsx"; interface JsonViewProps { data: JsonValue; @@ -70,7 +71,7 @@ const JsonNode = memo( boolean: "text-amber-600", null: "text-purple-600", undefined: "text-gray-600", - string: "text-green-600", + string: "text-green-600 break-all", default: "text-gray-700", }; @@ -173,7 +174,7 @@ const JsonNode = memo( {name}: )} - "{value}" +

"{value}"

); } @@ -186,7 +187,10 @@ const JsonNode = memo(
)} setIsExpanded(!isExpanded)} title={isExpanded ? "Click to collapse" : "Click to expand"} > diff --git a/client/src/components/PromptsTab.tsx b/client/src/components/PromptsTab.tsx index f88b16f..b42cf77 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button"; import { Combobox } from "@/components/ui/combobox"; import { Label } from "@/components/ui/label"; import { TabsContent } from "@/components/ui/tabs"; -import { Textarea } from "@/components/ui/textarea"; + import { ListPromptsResult, PromptReference, @@ -13,6 +13,7 @@ import { AlertCircle } from "lucide-react"; import { useEffect, useState } from "react"; import ListPane from "./ListPane"; import { useCompletionState } from "@/lib/hooks/useCompletionState"; +import JsonView from "./JsonView"; export type Prompt = { name: string; @@ -151,11 +152,9 @@ const PromptsTab = ({ Get Prompt {promptContent && ( -