Merge pull request #1211 from modelcontextprotocol/v2/phase-1-elements
Phase 1: Refactor element component interfaces to MCP SDK types
This commit is contained in:
commit
9082e02e65
69 changed files with 1070 additions and 712 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.claude/
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
.env
|
.env
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Preview } from '@storybook/react-vite'
|
import type { Preview } from '@storybook/react-vite'
|
||||||
import { MantineProvider, useMantineColorScheme } from '@mantine/core'
|
import { MantineProvider, useMantineColorScheme, type CSSVariablesResolver } from '@mantine/core'
|
||||||
import { Notifications } from '@mantine/notifications'
|
import { Notifications } from '@mantine/notifications'
|
||||||
import '@mantine/core/styles.css'
|
import '@mantine/core/styles.css'
|
||||||
import '@mantine/notifications/styles.css'
|
import '@mantine/notifications/styles.css'
|
||||||
|
|
@ -24,6 +24,14 @@ function ColorSchemeWrapper({
|
||||||
return <>{children}</>
|
return <>{children}</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolver: CSSVariablesResolver = () => ({
|
||||||
|
variables: {},
|
||||||
|
light: {},
|
||||||
|
dark: {
|
||||||
|
'--mantine-color-body': 'var(--mantine-color-dark-9)',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
globalTypes: {
|
globalTypes: {
|
||||||
colorScheme: {
|
colorScheme: {
|
||||||
|
|
@ -46,7 +54,7 @@ const preview: Preview = {
|
||||||
(Story, context) => {
|
(Story, context) => {
|
||||||
const isFullscreen = context.parameters?.layout === 'fullscreen'
|
const isFullscreen = context.parameters?.layout === 'fullscreen'
|
||||||
return (
|
return (
|
||||||
<MantineProvider theme={theme} defaultColorScheme="light">
|
<MantineProvider theme={theme} defaultColorScheme="light" cssVariablesResolver={resolver}>
|
||||||
<ColorSchemeWrapper colorScheme={context.globals.colorScheme ?? 'light'}>
|
<ColorSchemeWrapper colorScheme={context.globals.colorScheme ?? 'light'}>
|
||||||
<Notifications position="top-right" />
|
<Notifications position="top-right" />
|
||||||
{isFullscreen ? (
|
{isFullscreen ? (
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@
|
||||||
--inspector-surface-card: var(--mantine-color-gray-0);
|
--inspector-surface-card: var(--mantine-color-gray-0);
|
||||||
--inspector-surface-code: var(--mantine-color-white);
|
--inspector-surface-code: var(--mantine-color-white);
|
||||||
|
|
||||||
|
/* ── Inputs ────────────────────────────────────────────── */
|
||||||
|
--inspector-input-background: var(--mantine-color-white);
|
||||||
|
|
||||||
/* ── Borders ───────────────────────────────────────────── */
|
/* ── Borders ───────────────────────────────────────────── */
|
||||||
--inspector-border-default: var(--mantine-color-default-border);
|
--inspector-border-default: var(--mantine-color-default-border);
|
||||||
--inspector-border-subtle: var(--mantine-color-gray-3);
|
--inspector-border-subtle: var(--mantine-color-gray-3);
|
||||||
|
|
@ -34,7 +37,7 @@
|
||||||
--inspector-status-connected: var(--mantine-color-green-6);
|
--inspector-status-connected: var(--mantine-color-green-6);
|
||||||
--inspector-status-connecting: var(--mantine-color-yellow-6);
|
--inspector-status-connecting: var(--mantine-color-yellow-6);
|
||||||
--inspector-status-disconnected: var(--mantine-color-gray-5);
|
--inspector-status-disconnected: var(--mantine-color-gray-5);
|
||||||
--inspector-status-failed: var(--mantine-color-red-6);
|
--inspector-status-error: var(--mantine-color-red-6);
|
||||||
|
|
||||||
/* ── Log levels (RFC 5424) ─────────────────────────────── */
|
/* ── Log levels (RFC 5424) ─────────────────────────────── */
|
||||||
--inspector-log-debug: var(--mantine-color-gray-6);
|
--inspector-log-debug: var(--mantine-color-gray-6);
|
||||||
|
|
@ -46,15 +49,6 @@
|
||||||
--inspector-log-alert: var(--mantine-color-red-9);
|
--inspector-log-alert: var(--mantine-color-red-9);
|
||||||
--inspector-log-emergency: var(--mantine-color-red-9);
|
--inspector-log-emergency: var(--mantine-color-red-9);
|
||||||
|
|
||||||
/* ── Annotations ───────────────────────────────────────── */
|
|
||||||
--inspector-annotation-destructive: var(--mantine-color-red-6);
|
|
||||||
--inspector-annotation-long-run: var(--mantine-color-yellow-6);
|
|
||||||
--inspector-annotation-read-only: var(--mantine-color-dark-6);
|
|
||||||
--inspector-annotation-audience: var(--mantine-color-gray-6);
|
|
||||||
--inspector-annotation-priority-high: var(--mantine-color-orange-6);
|
|
||||||
--inspector-annotation-priority-medium: var(--mantine-color-yellow-6);
|
|
||||||
--inspector-annotation-priority-low: var(--mantine-color-green-6);
|
|
||||||
|
|
||||||
/* ── Task status ───────────────────────────────────────── */
|
/* ── Task status ───────────────────────────────────────── */
|
||||||
--inspector-task-waiting: var(--mantine-color-gray-5);
|
--inspector-task-waiting: var(--mantine-color-gray-5);
|
||||||
--inspector-task-running: var(--mantine-color-blue-6);
|
--inspector-task-running: var(--mantine-color-blue-6);
|
||||||
|
|
@ -71,7 +65,8 @@
|
||||||
[data-mantine-color-scheme="dark"] {
|
[data-mantine-color-scheme="dark"] {
|
||||||
--inspector-surface-card: var(--mantine-color-gray-9);
|
--inspector-surface-card: var(--mantine-color-gray-9);
|
||||||
--inspector-surface-code: var(--mantine-color-dark-9);
|
--inspector-surface-code: var(--mantine-color-dark-9);
|
||||||
--inspector-border-subtle: var(--mantine-color-dark-4);
|
--inspector-border-subtle: var(--mantine-color-gray-4);
|
||||||
|
--inspector-input-background: var(--mantine-color-dark-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Animations ──────────────────────────────────────────── */
|
/* ── Animations ──────────────────────────────────────────── */
|
||||||
|
|
@ -108,11 +103,43 @@
|
||||||
/* ── List item hover ────────────────────────────────────── */
|
/* ── List item hover ────────────────────────────────────── */
|
||||||
|
|
||||||
.list-item:hover {
|
.list-item:hover {
|
||||||
background-color: var(--mantine-color-gray-1);
|
background-color: var(--mantine-primary-color-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-mantine-color-scheme="dark"] .list-item:hover {
|
[data-mantine-color-scheme="dark"] .list-item:hover {
|
||||||
background-color: var(--mantine-color-dark-5);
|
background-color: var(--mantine-primary-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Select dropdown / option (dark mode) ──────────────────────── */
|
||||||
|
|
||||||
|
[data-mantine-color-scheme="dark"] .mantine-Select-dropdown {
|
||||||
|
background-color: var(--mantine-color-gray-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-mantine-color-scheme="dark"] .mantine-Select-option:hover {
|
||||||
|
background-color: var(--mantine-primary-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Menu dropdown / option (dark mode) ──────────────────────── */
|
||||||
|
|
||||||
|
[data-mantine-color-scheme="dark"] .mantine-Menu-dropdown {
|
||||||
|
background-color: var(--mantine-color-gray-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-mantine-color-scheme="dark"] .mantine-Menu-item:hover {
|
||||||
|
background-color: var(--mantine-primary-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Segmented control label (dark mode) ──────────────────────── */
|
||||||
|
|
||||||
|
[data-mantine-color-scheme="dark"] .mantine-SegmentedControl-indicator {
|
||||||
|
background-color: var(--mantine-primary-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Accordion button hover ─────────────────────────────── */
|
||||||
|
|
||||||
|
.mantine-Accordion-control:hover {
|
||||||
|
background-color: var(--mantine-primary-color-light-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Grid alignment ─────────────────────────────────────── */
|
/* ── Grid alignment ─────────────────────────────────────── */
|
||||||
|
|
|
||||||
|
|
@ -11,42 +11,63 @@ type Story = StoryObj<typeof AnnotationBadge>;
|
||||||
|
|
||||||
export const Audience: Story = {
|
export const Audience: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: "user",
|
facet: "audience",
|
||||||
variant: "audience",
|
value: ["user"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AudienceMultiple: Story = {
|
||||||
|
args: {
|
||||||
|
facet: "audience",
|
||||||
|
value: ["user", "assistant"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReadOnly: Story = {
|
export const ReadOnly: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: "read-only",
|
facet: "readOnlyHint",
|
||||||
variant: "readOnly",
|
value: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Destructive: Story = {
|
export const Destructive: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: "destructive",
|
facet: "destructiveHint",
|
||||||
variant: "destructive",
|
value: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LongRun: Story = {
|
export const Idempotent: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: "long-run",
|
facet: "idempotentHint",
|
||||||
variant: "longRun",
|
value: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OpenWorld: Story = {
|
||||||
|
args: {
|
||||||
|
facet: "openWorldHint",
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LongRunning: Story = {
|
||||||
|
args: {
|
||||||
|
facet: "longRunHint",
|
||||||
|
value: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PriorityHigh: Story = {
|
export const PriorityHigh: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: "priority: high",
|
facet: "priority",
|
||||||
variant: "priority",
|
value: 0.9,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Default: Story = {
|
export const PriorityLow: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: "custom",
|
facet: "priority",
|
||||||
variant: "default",
|
value: 0.2,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,63 @@
|
||||||
import { Badge, useComputedColorScheme } from "@mantine/core";
|
import { Badge, useComputedColorScheme } from "@mantine/core";
|
||||||
|
import type { Role } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
export type AnnotationVariant =
|
export type AnnotationFacet =
|
||||||
| "audience"
|
| "audience"
|
||||||
| "readOnly"
|
|
||||||
| "destructive"
|
|
||||||
| "longRun"
|
|
||||||
| "priority"
|
| "priority"
|
||||||
| "default";
|
| "readOnlyHint"
|
||||||
|
| "destructiveHint"
|
||||||
|
| "idempotentHint"
|
||||||
|
| "openWorldHint"
|
||||||
|
| "longRunHint";
|
||||||
|
|
||||||
export interface AnnotationBadgeProps {
|
export interface AnnotationBadgeProps {
|
||||||
label: string;
|
facet: AnnotationFacet;
|
||||||
variant?: AnnotationVariant;
|
value: Role[] | number | boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorMap: Record<AnnotationVariant, string> = {
|
const colorMap: Record<AnnotationFacet, string> = {
|
||||||
audience: "blue",
|
audience: "blue",
|
||||||
readOnly: "green",
|
|
||||||
destructive: "red",
|
|
||||||
longRun: "yellow",
|
|
||||||
priority: "orange",
|
priority: "orange",
|
||||||
default: "gray",
|
readOnlyHint: "green",
|
||||||
|
destructiveHint: "red",
|
||||||
|
idempotentHint: "teal",
|
||||||
|
openWorldHint: "grape",
|
||||||
|
longRunHint: "yellow",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AnnotationBadge({
|
function formatLabel(
|
||||||
label,
|
facet: AnnotationFacet,
|
||||||
variant = "default",
|
value: Role[] | number | boolean,
|
||||||
}: AnnotationBadgeProps) {
|
): string {
|
||||||
|
switch (facet) {
|
||||||
|
case "audience":
|
||||||
|
return `audience: ${(value as Role[]).join(", ")}`;
|
||||||
|
case "priority": {
|
||||||
|
const n = value as number;
|
||||||
|
if (n >= 0.7) return "priority: high";
|
||||||
|
if (n >= 0.4) return "priority: medium";
|
||||||
|
return "priority: low";
|
||||||
|
}
|
||||||
|
case "readOnlyHint":
|
||||||
|
return "read-only";
|
||||||
|
case "destructiveHint":
|
||||||
|
return "destructive";
|
||||||
|
case "idempotentHint":
|
||||||
|
return "idempotent";
|
||||||
|
case "openWorldHint":
|
||||||
|
return "open-world";
|
||||||
|
case "longRunHint":
|
||||||
|
return "long-running";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnnotationBadge({ facet, value }: AnnotationBadgeProps) {
|
||||||
const colorScheme = useComputedColorScheme();
|
const colorScheme = useComputedColorScheme();
|
||||||
const color = colorMap[variant];
|
const color = colorMap[facet];
|
||||||
const textColor = colorScheme === "dark" ? "black" : "white";
|
const textColor = colorScheme === "dark" ? "black" : "white";
|
||||||
return (
|
return (
|
||||||
<Badge color={color} variant="filled" fw={500} c={textColor}>
|
<Badge color={color} variant="filled" fw={500} c={textColor}>
|
||||||
{label}
|
{formatLabel(facet, value)}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@ type Story = StoryObj<typeof CapabilityItem>;
|
||||||
|
|
||||||
export const Supported: Story = {
|
export const Supported: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: "Tools",
|
capability: "tools",
|
||||||
supported: true,
|
supported: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SupportedWithCount: Story = {
|
export const SupportedWithCount: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: "Tools",
|
capability: "tools",
|
||||||
supported: true,
|
supported: true,
|
||||||
count: 4,
|
count: 4,
|
||||||
},
|
},
|
||||||
|
|
@ -26,7 +26,7 @@ export const SupportedWithCount: Story = {
|
||||||
|
|
||||||
export const NotSupported: Story = {
|
export const NotSupported: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: "Completions",
|
capability: "completions",
|
||||||
supported: false,
|
supported: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,36 @@
|
||||||
import { Group, Text } from "@mantine/core";
|
import { Group, Text } from "@mantine/core";
|
||||||
|
import type {
|
||||||
|
ClientCapabilities,
|
||||||
|
ServerCapabilities,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
|
export type CapabilityKey = keyof ServerCapabilities | keyof ClientCapabilities;
|
||||||
|
|
||||||
export interface CapabilityItemProps {
|
export interface CapabilityItemProps {
|
||||||
name: string;
|
capability: CapabilityKey;
|
||||||
supported: boolean;
|
supported: boolean;
|
||||||
count?: number;
|
count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const displayLabel: Record<string, string> = {
|
||||||
|
tools: "Tools",
|
||||||
|
resources: "Resources",
|
||||||
|
prompts: "Prompts",
|
||||||
|
logging: "Logging",
|
||||||
|
completions: "Completions",
|
||||||
|
tasks: "Tasks",
|
||||||
|
experimental: "Experimental",
|
||||||
|
roots: "Roots",
|
||||||
|
sampling: "Sampling",
|
||||||
|
elicitation: "Elicitation",
|
||||||
|
};
|
||||||
|
|
||||||
export function CapabilityItem({
|
export function CapabilityItem({
|
||||||
name,
|
capability,
|
||||||
supported,
|
supported,
|
||||||
count,
|
count,
|
||||||
}: CapabilityItemProps) {
|
}: CapabilityItemProps) {
|
||||||
|
const name = displayLabel[capability] ?? String(capability);
|
||||||
const label = count != null ? `${name} (${count})` : name;
|
const label = count != null ? `${name} (${count})` : name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ const meta: Meta<typeof ConnectionToggle> = {
|
||||||
title: "Elements/ConnectionToggle",
|
title: "Elements/ConnectionToggle",
|
||||||
component: ConnectionToggle,
|
component: ConnectionToggle,
|
||||||
args: {
|
args: {
|
||||||
onChange: fn(),
|
onConnect: fn(),
|
||||||
|
onDisconnect: fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -15,32 +16,31 @@ type Story = StoryObj<typeof ConnectionToggle>;
|
||||||
|
|
||||||
export const Connected: Story = {
|
export const Connected: Story = {
|
||||||
args: {
|
args: {
|
||||||
checked: true,
|
status: "connected",
|
||||||
loading: false,
|
|
||||||
disabled: false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Disconnected: Story = {
|
export const Disconnected: Story = {
|
||||||
args: {
|
args: {
|
||||||
checked: false,
|
status: "disconnected",
|
||||||
loading: false,
|
|
||||||
disabled: false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Loading: Story = {
|
export const Connecting: Story = {
|
||||||
args: {
|
args: {
|
||||||
checked: false,
|
status: "connecting",
|
||||||
loading: true,
|
|
||||||
disabled: false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Disabled: Story = {
|
export const Disabled: Story = {
|
||||||
args: {
|
args: {
|
||||||
checked: false,
|
status: "disconnected",
|
||||||
loading: false,
|
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Error: Story = {
|
||||||
|
args: {
|
||||||
|
status: "error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,34 @@
|
||||||
import { Switch } from "@mantine/core";
|
import { Switch } from "@mantine/core";
|
||||||
|
import type { ConnectionStatus } from "@inspector/core/mcp/types.js";
|
||||||
|
|
||||||
export interface ConnectionToggleProps {
|
export interface ConnectionToggleProps {
|
||||||
checked: boolean;
|
status: ConnectionStatus;
|
||||||
loading: boolean;
|
disabled?: boolean;
|
||||||
disabled: boolean;
|
onConnect: () => void;
|
||||||
onChange: (checked: boolean) => void;
|
onDisconnect: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConnectionToggle({
|
export function ConnectionToggle({
|
||||||
checked,
|
status,
|
||||||
loading,
|
disabled = false,
|
||||||
disabled,
|
onConnect,
|
||||||
onChange,
|
onDisconnect,
|
||||||
}: ConnectionToggleProps) {
|
}: ConnectionToggleProps) {
|
||||||
|
const isConnected = status === "connected";
|
||||||
|
const isConnecting = status === "connecting";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
size="lg"
|
size="lg"
|
||||||
checked={checked || loading}
|
checked={isConnected || isConnecting}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || isConnecting}
|
||||||
onChange={(event) => onChange(event.currentTarget.checked)}
|
onChange={(event) => {
|
||||||
|
if (event.currentTarget.checked) {
|
||||||
|
onConnect();
|
||||||
|
} else {
|
||||||
|
onDisconnect();
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,77 +11,66 @@ type Story = StoryObj<typeof ContentViewer>;
|
||||||
|
|
||||||
export const PlainText: Story = {
|
export const PlainText: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: "text",
|
block: { type: "text", text: "Hello, world!\nThis is plain text." },
|
||||||
content: "Hello, world!\nThis is plain text.",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const JsonContent: Story = {
|
export const JsonContent: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: "json",
|
block: {
|
||||||
content:
|
type: "text",
|
||||||
'{"name":"my-app","version":"1.0.0","description":"Sample configuration file"}',
|
text: '{"name":"my-app","version":"1.0.0","description":"Sample configuration file"}',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImagePreview: Story = {
|
export const ImagePreview: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: "image",
|
block: {
|
||||||
content:
|
type: "image",
|
||||||
"iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAABWUlEQVR4nO3YsQnAMAwAQe+/dLKCRZpD+UA6cVh8p/Oc81z9t1/eJ+/oD/ybVxDMKwjmFQTzCoJ5BcG8gmBeQTCvIJg3mLQX2eIVBPMKgnkFwbyCYF5BMK8gmFcQzCsI5hUE8zqdYF5BMK8gmFcQzCsI5hUE8wqCeQXBvIJgXkEwbzBpL7LFKwjmFQTzCoJ5BcG8gmBeQTCvIJhXEMwrCOZ1OsG8gmBeQTCvIJhXEMwrCOYVBPMKgnkFwbyCYN5g0l5ki1cQzCsI5hUE8wqCeQXBvIJgXkEwryCYVxDM63SCeQXBvIJgXkEwryCYVxDMKwjmFQTzCoJ5BcG8waS9yBavIJhXEMwrCOYVBPMKgnkFwbyCYF5BMK8gmNfpBPMKgnkFwbyCYF5BMK8gmFcQzCsI5hUE8wqCeYNJe5EtXkEwryCYVxDMKwjmFQTzCoJ5BcG8gmBeQTDvBaFNwZ0T5gfxAAAAAElFTkSuQmCC",
|
data: "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAABWUlEQVR4nO3YsQnAMAwAQe+/dLKCRZpD+UA6cVh8p/Oc81z9t1/eJ+/oD/ybVxDMKwjmFQTzCoJ5BcG8gmBeQTCvIJg3mLQX2eIVBPMKgnkFwbyCYF5BMK8gmFcQzCsI5hUE8zqdYF5BMK8gmFcQzCsI5hUE8wqCeQXBvIJgXkEwbzBpL7LFKwjmFQTzCoJ5BcG8gmBeQTCvIJhXEMwrCOZ1OsG8gmBeQTCvIJhXEMwrCOYVBPMKgnkFwbyCYN5g0l5ki1cQzCsI5hUE8wqCeQXBvIJgXkEwryCYVxDM63SCeQXBvIJgXkEwryCYVxDMKwjmFQTzCoJ5BcG8waS9yBavIJhXEMwrCOYVBPMKgnkFwbyCYF5BMK8gmNfpBPMKgnkFwbyCYF5BMK8gmFcQzCsI5hUE8wqCeYNJe5EtXkEwryCYVxDMKwjmFQTzCoJ5BcG8gmBeQTDvBaFNwZ0T5gfxAAAAAElFTkSuQmCC",
|
||||||
mimeType: "image/png",
|
mimeType: "image/png",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LongContent: Story = {
|
export const LongJson: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: "json",
|
block: {
|
||||||
content: JSON.stringify({
|
type: "text",
|
||||||
name: "my-app",
|
text: JSON.stringify({
|
||||||
version: "2.5.0",
|
name: "my-app",
|
||||||
description: "A comprehensive application configuration",
|
version: "2.5.0",
|
||||||
author: "Inspector Team",
|
description: "A comprehensive application configuration",
|
||||||
license: "MIT",
|
dependencies: {
|
||||||
repository: "https://github.com/example/my-app",
|
react: "^18.0.0",
|
||||||
dependencies: {
|
typescript: "^5.0.0",
|
||||||
react: "^18.0.0",
|
},
|
||||||
typescript: "^5.0.0",
|
scripts: {
|
||||||
mantine: "^7.0.0",
|
dev: "vite",
|
||||||
},
|
build: "tsc && vite build",
|
||||||
devDependencies: {
|
},
|
||||||
vitest: "^1.0.0",
|
}),
|
||||||
storybook: "^8.0.0",
|
},
|
||||||
eslint: "^9.0.0",
|
|
||||||
},
|
|
||||||
scripts: {
|
|
||||||
dev: "vite",
|
|
||||||
build: "tsc && vite build",
|
|
||||||
test: "vitest",
|
|
||||||
lint: "eslint .",
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
theme: "dark",
|
|
||||||
language: "en",
|
|
||||||
notifications: true,
|
|
||||||
maxRetries: 3,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithCopyButton: Story = {
|
export const WithCopyButton: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: "text",
|
block: {
|
||||||
content: "Hello, world!\nThis text can be copied.",
|
type: "text",
|
||||||
|
text: "Hello, world!\nThis text can be copied.",
|
||||||
|
},
|
||||||
copyable: true,
|
copyable: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const JsonWithCopy: Story = {
|
export const JsonWithCopy: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: "json",
|
block: {
|
||||||
content:
|
type: "text",
|
||||||
'{"name":"my-app","version":"1.0.0","description":"Sample configuration file"}',
|
text: '{"name":"my-app","version":"1.0.0","description":"Sample configuration file"}',
|
||||||
|
},
|
||||||
copyable: true,
|
copyable: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { Code, Flex, Image, Stack } from "@mantine/core";
|
import { Code, Flex, Image, Stack, Text } from "@mantine/core";
|
||||||
|
import type { ContentBlock } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { CopyButton } from "../CopyButton/CopyButton";
|
import { CopyButton } from "../CopyButton/CopyButton";
|
||||||
|
|
||||||
export interface ContentViewerProps {
|
export interface ContentViewerProps {
|
||||||
type: "text" | "json" | "image" | "audio";
|
block: ContentBlock;
|
||||||
content: string;
|
|
||||||
mimeType?: string;
|
|
||||||
copyable?: boolean;
|
copyable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,12 +15,14 @@ function formatJson(content: string): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDataUri(mimeType: string, content: string): string {
|
function isJsonText(block: ContentBlock): boolean {
|
||||||
return `data:${mimeType};base64,${content}`;
|
if (block.type !== "text") return false;
|
||||||
|
const trimmed = block.text.trimStart();
|
||||||
|
return trimmed.startsWith("{") || trimmed.startsWith("[");
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatContent(type: "text" | "json", content: string): string {
|
function buildDataUri(mimeType: string, data: string): string {
|
||||||
return type === "json" ? formatJson(content) : content;
|
return `data:${mimeType};base64,${data}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContentWrapper = Flex.withProps({
|
const ContentWrapper = Flex.withProps({
|
||||||
|
|
@ -41,36 +42,61 @@ const PreviewImage = Image.withProps({
|
||||||
radius: "md",
|
radius: "md",
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ContentViewer({
|
export function ContentViewer({ block, copyable = false }: ContentViewerProps) {
|
||||||
type,
|
switch (block.type) {
|
||||||
content,
|
case "text": {
|
||||||
mimeType,
|
const isJson = isJsonText(block);
|
||||||
copyable = false,
|
const displayText = isJson ? formatJson(block.text) : block.text;
|
||||||
}: ContentViewerProps) {
|
return (
|
||||||
const showCopy = copyable && (type === "text" || type === "json");
|
<Stack gap="xs">
|
||||||
|
<ContentWrapper>
|
||||||
return (
|
<Code block p={36}>
|
||||||
<Stack gap="xs">
|
{displayText}
|
||||||
{(type === "json" || type === "text") && (
|
</Code>
|
||||||
<ContentWrapper>
|
{copyable && (
|
||||||
<Code block p={36}>
|
<CopyOverlay>
|
||||||
{formatContent(type, content)}
|
<CopyButton value={block.text} />
|
||||||
</Code>
|
</CopyOverlay>
|
||||||
{showCopy && (
|
)}
|
||||||
<CopyOverlay>
|
</ContentWrapper>
|
||||||
<CopyButton value={content} />
|
</Stack>
|
||||||
</CopyOverlay>
|
);
|
||||||
)}
|
}
|
||||||
</ContentWrapper>
|
case "image":
|
||||||
)}
|
return (
|
||||||
{type === "image" && (
|
<Stack gap="xs">
|
||||||
<PreviewImage src={buildDataUri(mimeType || "image/png", content)} />
|
<PreviewImage src={buildDataUri(block.mimeType, block.data)} />
|
||||||
)}
|
</Stack>
|
||||||
{type === "audio" && (
|
);
|
||||||
<audio controls>
|
case "audio":
|
||||||
<source src={buildDataUri(mimeType || "audio/wav", content)} />
|
return (
|
||||||
</audio>
|
<Stack gap="xs">
|
||||||
)}
|
<audio controls>
|
||||||
</Stack>
|
<source src={buildDataUri(block.mimeType, block.data)} />
|
||||||
);
|
</audio>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
case "resource":
|
||||||
|
return (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<ContentWrapper>
|
||||||
|
<Code block p={36}>
|
||||||
|
{"text" in block.resource
|
||||||
|
? block.resource.text
|
||||||
|
: `[blob: ${block.resource.uri}]`}
|
||||||
|
</Code>
|
||||||
|
</ContentWrapper>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
case "resource_link":
|
||||||
|
return (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="sm" c="blue">
|
||||||
|
{block.name ?? block.uri}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@ export function CopyButton({ value }: CopyButtonProps) {
|
||||||
<Tooltip label={copied ? "Copied" : "Copy"}>
|
<Tooltip label={copied ? "Copied" : "Copy"}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color={copied ? "green" : "gray"}
|
color={copied ? "green" : "var(--inspector-text-primary)"}
|
||||||
onClick={copy}
|
onClick={copy}
|
||||||
|
fz={24}
|
||||||
>
|
>
|
||||||
{copied ? "\u2713" : "\u2398"}
|
{copied ? "\u2713" : "\u2398"}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,13 @@ type Story = StoryObj<typeof InlineError>;
|
||||||
|
|
||||||
export const ShortError: Story = {
|
export const ShortError: Story = {
|
||||||
args: {
|
args: {
|
||||||
message: "Connection timeout after 20s",
|
error: { message: "Connection timeout after 20s" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithRetryCount: Story = {
|
export const WithRetryCount: Story = {
|
||||||
args: {
|
args: {
|
||||||
message: "Connection failed",
|
error: { message: "Connection failed" },
|
||||||
retryCount: 3,
|
retryCount: 3,
|
||||||
maxRetries: 5,
|
maxRetries: 5,
|
||||||
},
|
},
|
||||||
|
|
@ -25,24 +25,26 @@ export const WithRetryCount: Story = {
|
||||||
|
|
||||||
export const WithDetails: Story = {
|
export const WithDetails: Story = {
|
||||||
args: {
|
args: {
|
||||||
message: "Connection refused",
|
error: {
|
||||||
details:
|
message: "Connection refused",
|
||||||
"Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)",
|
data: "Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithDocLink: Story = {
|
export const WithDocLink: Story = {
|
||||||
args: {
|
args: {
|
||||||
message: "Connection failed",
|
error: { message: "Connection failed" },
|
||||||
docLink: "https://example.com/troubleshooting",
|
docLink: "https://example.com/troubleshooting",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FullError: Story = {
|
export const FullError: Story = {
|
||||||
args: {
|
args: {
|
||||||
message: "Connection refused",
|
error: {
|
||||||
details:
|
message: "Connection refused",
|
||||||
"Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)",
|
data: "Error: ECONNREFUSED 127.0.0.1:3000\n at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)",
|
||||||
|
},
|
||||||
retryCount: 3,
|
retryCount: 3,
|
||||||
maxRetries: 5,
|
maxRetries: 5,
|
||||||
docLink: "https://example.com/troubleshooting",
|
docLink: "https://example.com/troubleshooting",
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@ import { Alert, Button, Collapse, Group, Stack, Text } from "@mantine/core";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export interface InlineErrorProps {
|
export interface InlineErrorProps {
|
||||||
message: string;
|
error: { message: string; data?: unknown };
|
||||||
details?: string;
|
|
||||||
retryCount?: number;
|
retryCount?: number;
|
||||||
maxRetries?: number;
|
maxRetries?: number;
|
||||||
docLink?: string;
|
docLink?: string;
|
||||||
|
|
@ -49,14 +48,20 @@ function formatRetryLabel(retryCount: number, maxRetries?: number): string {
|
||||||
: `Retry attempt ${retryCount}`;
|
: `Retry attempt ${retryCount}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDetails(data: unknown): string {
|
||||||
|
if (typeof data === "string") return data;
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
export function InlineError({
|
export function InlineError({
|
||||||
message,
|
error,
|
||||||
details,
|
|
||||||
retryCount,
|
retryCount,
|
||||||
maxRetries,
|
maxRetries,
|
||||||
docLink,
|
docLink,
|
||||||
}: InlineErrorProps) {
|
}: InlineErrorProps) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const details =
|
||||||
|
error.data !== undefined ? formatDetails(error.data) : undefined;
|
||||||
const hasExpandable = details !== undefined || docLink !== undefined;
|
const hasExpandable = details !== undefined || docLink !== undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -64,7 +69,7 @@ export function InlineError({
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<HeaderRow>
|
<HeaderRow>
|
||||||
<MessageGroup>
|
<MessageGroup>
|
||||||
<ErrorMessage>{message}</ErrorMessage>
|
<ErrorMessage>{error.message}</ErrorMessage>
|
||||||
{retryCount !== undefined && (
|
{retryCount !== undefined && (
|
||||||
<RetryText>{formatRetryLabel(retryCount, maxRetries)}</RetryText>
|
<RetryText>{formatRetryLabel(retryCount, maxRetries)}</RetryText>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const UpdateLabel = Text.withProps({
|
||||||
|
|
||||||
const RefreshButton = Button.withProps({
|
const RefreshButton = Button.withProps({
|
||||||
size: "sm",
|
size: "sm",
|
||||||
variant: "light",
|
variant: "subtle",
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ListChangedIndicator({
|
export function ListChangedIndicator({
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export function ListToggle({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button size="sm" onClick={onToggle}>
|
<Button size="sm" variant="subtle" onClick={onToggle}>
|
||||||
<Icon size={20} />
|
<Icon size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,73 +11,70 @@ type Story = StoryObj<typeof LogEntry>;
|
||||||
|
|
||||||
export const Debug: Story = {
|
export const Debug: Story = {
|
||||||
args: {
|
args: {
|
||||||
timestamp: "2026-03-17T10:00:00Z",
|
entry: {
|
||||||
level: "debug",
|
receivedAt: new Date("2026-03-29T20:18:20.000Z"),
|
||||||
message: "Initializing connection pool",
|
params: { level: "debug", data: "Initializing connection pool" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Info: Story = {
|
export const Info: Story = {
|
||||||
args: {
|
args: {
|
||||||
timestamp: "2026-03-17T10:00:01Z",
|
entry: {
|
||||||
level: "info",
|
receivedAt: new Date("2026-03-29T20:18:21.000Z"),
|
||||||
message: "Server started on port 3000",
|
params: { level: "info", data: "Server started on port 3000" },
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
|
||||||
export const Notice: Story = {
|
|
||||||
args: {
|
|
||||||
timestamp: "2026-03-17T10:00:02Z",
|
|
||||||
level: "notice",
|
|
||||||
message: "Configuration reloaded successfully",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Warning: Story = {
|
export const Warning: Story = {
|
||||||
args: {
|
args: {
|
||||||
timestamp: "2026-03-17T10:00:03Z",
|
entry: {
|
||||||
level: "warning",
|
receivedAt: new Date("2026-03-29T20:18:22.000Z"),
|
||||||
message: "Memory usage above 80%",
|
params: {
|
||||||
|
level: "warning",
|
||||||
|
data: "Connection pool nearing capacity",
|
||||||
|
logger: "pool",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Error: Story = {
|
export const Error: Story = {
|
||||||
args: {
|
args: {
|
||||||
timestamp: "2026-03-17T10:00:04Z",
|
entry: {
|
||||||
level: "error",
|
receivedAt: new Date("2026-03-29T20:18:23.000Z"),
|
||||||
message: "Failed to connect to database",
|
params: {
|
||||||
},
|
level: "error",
|
||||||
};
|
data: "Failed to connect to database",
|
||||||
|
logger: "db",
|
||||||
export const Critical: Story = {
|
},
|
||||||
args: {
|
},
|
||||||
timestamp: "2026-03-17T10:00:05Z",
|
|
||||||
level: "critical",
|
|
||||||
message: "Disk space critically low",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Alert: Story = {
|
|
||||||
args: {
|
|
||||||
timestamp: "2026-03-17T10:00:06Z",
|
|
||||||
level: "alert",
|
|
||||||
message: "Service degradation detected",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Emergency: Story = {
|
export const Emergency: Story = {
|
||||||
args: {
|
args: {
|
||||||
timestamp: "2026-03-17T10:00:07Z",
|
entry: {
|
||||||
level: "emergency",
|
receivedAt: new Date("2026-03-29T20:18:24.000Z"),
|
||||||
message: "System is unusable",
|
params: {
|
||||||
|
level: "emergency",
|
||||||
|
data: "System is unusable",
|
||||||
|
logger: "kernel",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithLogger: Story = {
|
export const WithJsonData: Story = {
|
||||||
args: {
|
args: {
|
||||||
timestamp: "2026-03-17T10:00:08Z",
|
entry: {
|
||||||
level: "info",
|
receivedAt: new Date("2026-03-29T20:18:25.000Z"),
|
||||||
message: "Tool execution completed",
|
params: {
|
||||||
logger: "mcp-server",
|
level: "info",
|
||||||
|
data: { key: "value", count: 42 },
|
||||||
|
logger: "api",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,20 @@
|
||||||
import { Group, Text } from "@mantine/core";
|
import { Group, Text } from "@mantine/core";
|
||||||
|
import type {
|
||||||
|
LoggingLevel,
|
||||||
|
LoggingMessageNotification,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { LogLevelBadge } from "../LogLevelBadge/LogLevelBadge";
|
import { LogLevelBadge } from "../LogLevelBadge/LogLevelBadge";
|
||||||
|
|
||||||
export type LogLevel =
|
export interface LogEntryData {
|
||||||
| "debug"
|
receivedAt: Date;
|
||||||
| "info"
|
params: LoggingMessageNotification["params"];
|
||||||
| "notice"
|
|
||||||
| "warning"
|
|
||||||
| "error"
|
|
||||||
| "critical"
|
|
||||||
| "alert"
|
|
||||||
| "emergency";
|
|
||||||
|
|
||||||
export interface LogEntryProps {
|
|
||||||
timestamp: string;
|
|
||||||
level: LogLevel;
|
|
||||||
message: string;
|
|
||||||
logger?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const levelMessageColor: Record<LogLevel, string | undefined> = {
|
export interface LogEntryProps {
|
||||||
|
entry: LogEntryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelMessageColor: Record<LoggingLevel, string | undefined> = {
|
||||||
debug: "dimmed",
|
debug: "dimmed",
|
||||||
info: "blue",
|
info: "blue",
|
||||||
notice: undefined,
|
notice: undefined,
|
||||||
|
|
@ -29,10 +25,20 @@ const levelMessageColor: Record<LogLevel, string | undefined> = {
|
||||||
emergency: "red",
|
emergency: "red",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function formatTimestamp(date: Date): string {
|
||||||
|
return date.toLocaleTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
function formatLogger(logger: string): string {
|
function formatLogger(logger: string): string {
|
||||||
return `[${logger}]`;
|
return `[${logger}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatData(data: unknown): string {
|
||||||
|
if (data === undefined || data === null) return "";
|
||||||
|
if (typeof data === "string") return data;
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
const TimestampText = Text.withProps({
|
const TimestampText = Text.withProps({
|
||||||
size: "sm",
|
size: "sm",
|
||||||
ff: "monospace",
|
ff: "monospace",
|
||||||
|
|
@ -45,13 +51,16 @@ const LoggerText = Text.withProps({
|
||||||
c: "dimmed",
|
c: "dimmed",
|
||||||
});
|
});
|
||||||
|
|
||||||
export function LogEntry({ timestamp, level, message, logger }: LogEntryProps) {
|
export function LogEntry({ entry }: LogEntryProps) {
|
||||||
|
const { receivedAt, params } = entry;
|
||||||
|
const message = formatData(params.data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<TimestampText>{timestamp}</TimestampText>
|
<TimestampText>{formatTimestamp(receivedAt)}</TimestampText>
|
||||||
<LogLevelBadge level={level} />
|
<LogLevelBadge level={params.level} />
|
||||||
{logger && <LoggerText>{formatLogger(logger)}</LoggerText>}
|
{params.logger && <LoggerText>{formatLogger(params.logger)}</LoggerText>}
|
||||||
<Text size="sm" ff="monospace" c={levelMessageColor[level]}>
|
<Text size="sm" ff="monospace" c={levelMessageColor[params.level]}>
|
||||||
{message}
|
{message}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Badge, useComputedColorScheme } from "@mantine/core";
|
import { Badge, useComputedColorScheme } from "@mantine/core";
|
||||||
import type { LogLevel } from "../LogEntry/LogEntry";
|
import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
export interface LogLevelBadgeProps {
|
export interface LogLevelBadgeProps {
|
||||||
level: LogLevel;
|
level: LoggingLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const levelColor: Record<LogLevel, string> = {
|
const levelColor: Record<LoggingLevel, string> = {
|
||||||
debug: "gray",
|
debug: "gray",
|
||||||
info: "blue",
|
info: "blue",
|
||||||
notice: "teal",
|
notice: "teal",
|
||||||
|
|
@ -16,7 +16,7 @@ const levelColor: Record<LogLevel, string> = {
|
||||||
emergency: "red",
|
emergency: "red",
|
||||||
};
|
};
|
||||||
|
|
||||||
const boldLevels: Set<LogLevel> = new Set(["alert", "emergency"]);
|
const boldLevels: Set<LoggingLevel> = new Set(["alert", "emergency"]);
|
||||||
|
|
||||||
export function LogLevelBadge({ level }: LogLevelBadgeProps) {
|
export function LogLevelBadge({ level }: LogLevelBadgeProps) {
|
||||||
const colorScheme = useComputedColorScheme();
|
const colorScheme = useComputedColorScheme();
|
||||||
|
|
|
||||||
|
|
@ -12,28 +12,39 @@ type Story = StoryObj<typeof MessageBubble>;
|
||||||
export const UserText: Story = {
|
export const UserText: Story = {
|
||||||
args: {
|
args: {
|
||||||
index: 0,
|
index: 0,
|
||||||
role: "user",
|
message: {
|
||||||
content: "Hello, my name is John and I like cats",
|
role: "user",
|
||||||
|
content: { type: "text", text: "Hello, my name is John and I like cats" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AssistantText: Story = {
|
export const AssistantText: Story = {
|
||||||
args: {
|
args: {
|
||||||
index: 1,
|
index: 1,
|
||||||
role: "assistant",
|
message: {
|
||||||
content:
|
role: "assistant",
|
||||||
"Nice to meet you, John! It's wonderful that you enjoy cats. They're such fascinating and independent creatures. Do you have any cats of your own?",
|
content: {
|
||||||
|
type: "text",
|
||||||
|
text: "Nice to meet you, John! It's wonderful that you enjoy cats. They're such fascinating and independent creatures. Do you have any cats of your own?",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserWithImage: Story = {
|
export const UserWithImage: Story = {
|
||||||
args: {
|
args: {
|
||||||
index: 0,
|
index: 0,
|
||||||
role: "user",
|
message: {
|
||||||
content: "Check this out",
|
role: "user",
|
||||||
imageContent: {
|
content: [
|
||||||
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
{ type: "text", text: "Check this out" },
|
||||||
mimeType: "image/png",
|
{
|
||||||
|
type: "image",
|
||||||
|
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||||
|
mimeType: "image/png",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -41,8 +52,12 @@ export const UserWithImage: Story = {
|
||||||
export const LongMessage: Story = {
|
export const LongMessage: Story = {
|
||||||
args: {
|
args: {
|
||||||
index: 0,
|
index: 0,
|
||||||
role: "user",
|
message: {
|
||||||
content:
|
role: "user",
|
||||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.",
|
content: {
|
||||||
|
type: "text",
|
||||||
|
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue