merge main
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { Copy } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import JsonView from "./JsonView";
|
||||
|
||||
@@ -25,10 +24,6 @@ const HistoryAndNotifications = ({
|
||||
setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] }));
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-card overflow-hidden flex h-full">
|
||||
<div className="flex-1 overflow-y-auto p-4 border-r">
|
||||
@@ -68,16 +63,12 @@ const HistoryAndNotifications = ({
|
||||
<span className="font-semibold text-blue-600">
|
||||
Request:
|
||||
</span>
|
||||
<button
|
||||
onClick={() => copyToClipboard(request.request)}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Copy size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-background p-2 rounded">
|
||||
<JsonView data={request.request} />
|
||||
</div>
|
||||
|
||||
<JsonView
|
||||
data={request.request}
|
||||
className="bg-background"
|
||||
/>
|
||||
</div>
|
||||
{request.response && (
|
||||
<div className="mt-2">
|
||||
@@ -85,16 +76,11 @@ const HistoryAndNotifications = ({
|
||||
<span className="font-semibold text-green-600">
|
||||
Response:
|
||||
</span>
|
||||
<button
|
||||
onClick={() => copyToClipboard(request.response!)}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Copy size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-background p-2 rounded">
|
||||
<JsonView data={request.response} />
|
||||
</div>
|
||||
<JsonView
|
||||
data={request.response}
|
||||
className="bg-background"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -134,20 +120,11 @@ const HistoryAndNotifications = ({
|
||||
<span className="font-semibold text-purple-600">
|
||||
Details:
|
||||
</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
copyToClipboard(JSON.stringify(notification))
|
||||
}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Copy size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-background p-2 rounded">
|
||||
<JsonView
|
||||
data={JSON.stringify(notification, null, 2)}
|
||||
/>
|
||||
</div>
|
||||
<JsonView
|
||||
data={JSON.stringify(notification, null, 2)}
|
||||
className="bg-background"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { useState, memo } from "react";
|
||||
import { useState, memo, useMemo, useCallback, useEffect } from "react";
|
||||
import { JsonValue } from "./DynamicJsonForm";
|
||||
import clsx from "clsx";
|
||||
import { Copy, CheckCheck } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface JsonViewProps {
|
||||
data: unknown;
|
||||
name?: string;
|
||||
initialExpandDepth?: number;
|
||||
className?: string;
|
||||
withCopyButton?: boolean;
|
||||
}
|
||||
|
||||
function tryParseJson(str: string): { success: boolean; data: JsonValue } {
|
||||
@@ -24,22 +29,79 @@ function tryParseJson(str: string): { success: boolean; data: JsonValue } {
|
||||
}
|
||||
|
||||
const JsonView = memo(
|
||||
({ data, name, initialExpandDepth = 3 }: JsonViewProps) => {
|
||||
const normalizedData =
|
||||
typeof data === "string"
|
||||
({
|
||||
data,
|
||||
name,
|
||||
initialExpandDepth = 3,
|
||||
className,
|
||||
withCopyButton = true,
|
||||
}: JsonViewProps) => {
|
||||
const { toast } = useToast();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
if (copied) {
|
||||
timeoutId = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 500);
|
||||
}
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [copied]);
|
||||
|
||||
const normalizedData = useMemo(() => {
|
||||
return typeof data === "string"
|
||||
? tryParseJson(data).success
|
||||
? tryParseJson(data).data
|
||||
: data
|
||||
: data;
|
||||
}, [data]);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
try {
|
||||
navigator.clipboard.writeText(
|
||||
typeof normalizedData === "string"
|
||||
? normalizedData
|
||||
: JSON.stringify(normalizedData, null, 2),
|
||||
);
|
||||
setCopied(true);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}, [toast, normalizedData]);
|
||||
|
||||
return (
|
||||
<div className="font-mono text-sm transition-all duration-300 ">
|
||||
<JsonNode
|
||||
data={normalizedData as JsonValue}
|
||||
name={name}
|
||||
depth={0}
|
||||
initialExpandDepth={initialExpandDepth}
|
||||
/>
|
||||
<div className={clsx("p-4 border rounded relative", className)}>
|
||||
{withCopyButton && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
|
||||
) : (
|
||||
<Copy className="size-4 text-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<div className="font-mono text-sm transition-all duration-300">
|
||||
<JsonNode
|
||||
data={normalizedData as JsonValue}
|
||||
name={name}
|
||||
depth={0}
|
||||
initialExpandDepth={initialExpandDepth}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -152,9 +152,7 @@ const PromptsTab = ({
|
||||
Get Prompt
|
||||
</Button>
|
||||
{promptContent && (
|
||||
<div className="p-4 border rounded">
|
||||
<JsonView data={promptContent} />
|
||||
</div>
|
||||
<JsonView data={promptContent} withCopyButton={false} />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -215,9 +215,10 @@ const ResourcesTab = ({
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
) : selectedResource ? (
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 text-gray-900 dark:text-gray-100">
|
||||
<JsonView data={resourceContent} />
|
||||
</div>
|
||||
<JsonView
|
||||
data={resourceContent}
|
||||
className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 text-gray-900 dark:text-gray-100"
|
||||
/>
|
||||
) : selectedTemplate ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
|
||||
@@ -44,9 +44,11 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
|
||||
<h3 className="text-lg font-semibold">Recent Requests</h3>
|
||||
{pendingRequests.map((request) => (
|
||||
<div key={request.id} className="p-4 border rounded-lg space-y-4">
|
||||
<div className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-2 rounded">
|
||||
<JsonView data={JSON.stringify(request.request)} />
|
||||
</div>
|
||||
<JsonView
|
||||
className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 rounded"
|
||||
data={JSON.stringify(request.request)}
|
||||
/>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button onClick={() => handleApprove(request.id)}>Approve</Button>
|
||||
<Button variant="outline" onClick={() => onReject(request.id)}>
|
||||
|
||||
@@ -52,14 +52,10 @@ const ToolsTab = ({
|
||||
return (
|
||||
<>
|
||||
<h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
|
||||
<div className="p-4 border rounded">
|
||||
<JsonView data={toolResult} />
|
||||
</div>
|
||||
<JsonView data={toolResult} />
|
||||
<h4 className="font-semibold mb-2">Errors:</h4>
|
||||
{parsedResult.error.errors.map((error, idx) => (
|
||||
<div key={idx} className="p-4 border rounded">
|
||||
<JsonView data={error} />
|
||||
</div>
|
||||
<JsonView data={error} key={idx} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
@@ -74,11 +70,7 @@ const ToolsTab = ({
|
||||
</h4>
|
||||
{structuredResult.content.map((item, index) => (
|
||||
<div key={index} className="mb-2">
|
||||
{item.type === "text" && (
|
||||
<div className="p-4 border rounded">
|
||||
<JsonView data={item.text} />
|
||||
</div>
|
||||
)}
|
||||
{item.type === "text" && <JsonView data={item.text} />}
|
||||
{item.type === "image" && (
|
||||
<img
|
||||
src={`data:${item.mimeType};base64,${item.data}`}
|
||||
@@ -96,9 +88,7 @@ const ToolsTab = ({
|
||||
<p>Your browser does not support audio playback</p>
|
||||
</audio>
|
||||
) : (
|
||||
<div className="p-4 border rounded">
|
||||
<JsonView data={item.resource} />
|
||||
</div>
|
||||
<JsonView data={item.resource} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
@@ -108,9 +98,8 @@ const ToolsTab = ({
|
||||
return (
|
||||
<>
|
||||
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
|
||||
<div className="p-4 border rounded">
|
||||
<JsonView data={toolResult.toolResult} />
|
||||
</div>
|
||||
|
||||
<JsonView data={toolResult.toolResult} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 hover:border-[#646cff] hover:border-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
126
client/src/components/ui/toast.tsx
Normal file
126
client/src/components/ui/toast.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import * as React from "react";
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className,
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
));
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
};
|
||||
33
client/src/components/ui/toaster.tsx
Normal file
33
client/src/components/ui/toaster.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast";
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast();
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user