initial commit
This commit is contained in:
471
components/ai-elements/schema-display.tsx
Normal file
471
components/ai-elements/schema-display.tsx
Normal file
@@ -0,0 +1,471 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import type { ComponentProps, HTMLAttributes } from "react";
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
|
||||
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
|
||||
interface SchemaParameter {
|
||||
name: string;
|
||||
type: string;
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
location?: "path" | "query" | "header";
|
||||
}
|
||||
|
||||
interface SchemaProperty {
|
||||
name: string;
|
||||
type: string;
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
properties?: SchemaProperty[];
|
||||
items?: SchemaProperty;
|
||||
}
|
||||
|
||||
interface SchemaDisplayContextType {
|
||||
method: HttpMethod;
|
||||
path: string;
|
||||
description?: string;
|
||||
parameters?: SchemaParameter[];
|
||||
requestBody?: SchemaProperty[];
|
||||
responseBody?: SchemaProperty[];
|
||||
}
|
||||
|
||||
const SchemaDisplayContext = createContext<SchemaDisplayContextType>({
|
||||
method: "GET",
|
||||
path: "",
|
||||
});
|
||||
|
||||
const methodStyles: Record<HttpMethod, string> = {
|
||||
DELETE: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
|
||||
GET: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
|
||||
PATCH:
|
||||
"bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400",
|
||||
POST: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
|
||||
PUT: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400",
|
||||
};
|
||||
|
||||
export type SchemaDisplayHeaderProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const SchemaDisplayHeader = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SchemaDisplayHeaderProps) => (
|
||||
<div
|
||||
className={cn("flex items-center gap-3 border-b px-4 py-3", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export type SchemaDisplayMethodProps = ComponentProps<typeof Badge>;
|
||||
|
||||
export const SchemaDisplayMethod = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SchemaDisplayMethodProps) => {
|
||||
const { method } = useContext(SchemaDisplayContext);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={cn("font-mono text-xs", methodStyles[method], className)}
|
||||
variant="secondary"
|
||||
{...props}
|
||||
>
|
||||
{children ?? method}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
export type SchemaDisplayPathProps = HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
export const SchemaDisplayPath = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SchemaDisplayPathProps) => {
|
||||
const { path } = useContext(SchemaDisplayContext);
|
||||
|
||||
// Highlight path parameters
|
||||
const highlightedPath = path.replaceAll(
|
||||
/\{([^}]+)\}/g,
|
||||
'<span class="text-blue-600 dark:text-blue-400">{$1}</span>'
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn("font-mono text-sm", className)}
|
||||
// oxlint-disable-next-line eslint-plugin-react(no-danger)
|
||||
dangerouslySetInnerHTML={{ __html: children ?? highlightedPath }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export type SchemaDisplayDescriptionProps =
|
||||
HTMLAttributes<HTMLParagraphElement>;
|
||||
|
||||
export const SchemaDisplayDescription = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SchemaDisplayDescriptionProps) => {
|
||||
const { description } = useContext(SchemaDisplayContext);
|
||||
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
"border-b px-4 py-3 text-muted-foreground text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? description}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export type SchemaDisplayContentProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const SchemaDisplayContent = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SchemaDisplayContentProps) => (
|
||||
<div className={cn("divide-y", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export type SchemaDisplayParameterProps = HTMLAttributes<HTMLDivElement> &
|
||||
SchemaParameter;
|
||||
|
||||
export const SchemaDisplayParameter = ({
|
||||
name,
|
||||
type,
|
||||
required,
|
||||
description,
|
||||
location,
|
||||
className,
|
||||
...props
|
||||
}: SchemaDisplayParameterProps) => (
|
||||
<div className={cn("px-4 py-3 pl-10", className)} {...props}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm">{name}</span>
|
||||
<Badge className="text-xs" variant="outline">
|
||||
{type}
|
||||
</Badge>
|
||||
{location && (
|
||||
<Badge className="text-xs" variant="secondary">
|
||||
{location}
|
||||
</Badge>
|
||||
)}
|
||||
{required && (
|
||||
<Badge
|
||||
className="bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
|
||||
variant="secondary"
|
||||
>
|
||||
required
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{description && (
|
||||
<p className="mt-1 text-muted-foreground text-sm">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export type SchemaDisplayParametersProps = ComponentProps<typeof Collapsible>;
|
||||
|
||||
export const SchemaDisplayParameters = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SchemaDisplayParametersProps) => {
|
||||
const { parameters } = useContext(SchemaDisplayContext);
|
||||
|
||||
return (
|
||||
<Collapsible className={cn(className)} defaultOpen {...props}>
|
||||
<CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-muted/50">
|
||||
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
|
||||
<span className="font-medium text-sm">Parameters</span>
|
||||
<Badge className="ml-auto text-xs" variant="secondary">
|
||||
{parameters?.length}
|
||||
</Badge>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="divide-y border-t">
|
||||
{children ??
|
||||
parameters?.map((param) => (
|
||||
<SchemaDisplayParameter key={param.name} {...param} />
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
export type SchemaDisplayPropertyProps = HTMLAttributes<HTMLDivElement> &
|
||||
SchemaProperty & {
|
||||
depth?: number;
|
||||
};
|
||||
|
||||
export const SchemaDisplayProperty = ({
|
||||
name,
|
||||
type,
|
||||
required,
|
||||
description,
|
||||
properties,
|
||||
items,
|
||||
depth = 0,
|
||||
className,
|
||||
...props
|
||||
}: SchemaDisplayPropertyProps) => {
|
||||
const hasChildren = properties || items;
|
||||
const paddingLeft = 40 + depth * 16;
|
||||
|
||||
if (hasChildren) {
|
||||
return (
|
||||
<Collapsible defaultOpen={depth < 2}>
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
"group flex w-full items-center gap-2 py-3 text-left transition-colors hover:bg-muted/50",
|
||||
className
|
||||
)}
|
||||
style={{ paddingLeft }}
|
||||
>
|
||||
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
|
||||
<span className="font-mono text-sm">{name}</span>
|
||||
<Badge className="text-xs" variant="outline">
|
||||
{type}
|
||||
</Badge>
|
||||
{required && (
|
||||
<Badge
|
||||
className="bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
|
||||
variant="secondary"
|
||||
>
|
||||
required
|
||||
</Badge>
|
||||
)}
|
||||
</CollapsibleTrigger>
|
||||
{description && (
|
||||
<p
|
||||
className="pb-2 text-muted-foreground text-sm"
|
||||
style={{ paddingLeft: paddingLeft + 24 }}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
<CollapsibleContent>
|
||||
<div className="divide-y border-t">
|
||||
{properties?.map((prop) => (
|
||||
<SchemaDisplayProperty
|
||||
key={prop.name}
|
||||
{...prop}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
{items && (
|
||||
<SchemaDisplayProperty
|
||||
{...items}
|
||||
depth={depth + 1}
|
||||
name={`${name}[]`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("py-3 pr-4", className)}
|
||||
style={{ paddingLeft }}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Spacer for alignment */}
|
||||
<span className="size-4" />
|
||||
<span className="font-mono text-sm">{name}</span>
|
||||
<Badge className="text-xs" variant="outline">
|
||||
{type}
|
||||
</Badge>
|
||||
{required && (
|
||||
<Badge
|
||||
className="bg-red-100 text-red-700 text-xs dark:bg-red-900/30 dark:text-red-400"
|
||||
variant="secondary"
|
||||
>
|
||||
required
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{description && (
|
||||
<p className="mt-1 pl-6 text-muted-foreground text-sm">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type SchemaDisplayRequestProps = ComponentProps<typeof Collapsible>;
|
||||
|
||||
export const SchemaDisplayRequest = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SchemaDisplayRequestProps) => {
|
||||
const { requestBody } = useContext(SchemaDisplayContext);
|
||||
|
||||
return (
|
||||
<Collapsible className={cn(className)} defaultOpen {...props}>
|
||||
<CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-muted/50">
|
||||
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
|
||||
<span className="font-medium text-sm">Request Body</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="border-t">
|
||||
{children ??
|
||||
requestBody?.map((prop) => (
|
||||
<SchemaDisplayProperty key={prop.name} {...prop} depth={0} />
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
export type SchemaDisplayResponseProps = ComponentProps<typeof Collapsible>;
|
||||
|
||||
export const SchemaDisplayResponse = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SchemaDisplayResponseProps) => {
|
||||
const { responseBody } = useContext(SchemaDisplayContext);
|
||||
|
||||
return (
|
||||
<Collapsible className={cn(className)} defaultOpen {...props}>
|
||||
<CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-muted/50">
|
||||
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
|
||||
<span className="font-medium text-sm">Response</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="border-t">
|
||||
{children ??
|
||||
responseBody?.map((prop) => (
|
||||
<SchemaDisplayProperty key={prop.name} {...prop} depth={0} />
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
export type SchemaDisplayProps = HTMLAttributes<HTMLDivElement> & {
|
||||
method: HttpMethod;
|
||||
path: string;
|
||||
description?: string;
|
||||
parameters?: SchemaParameter[];
|
||||
requestBody?: SchemaProperty[];
|
||||
responseBody?: SchemaProperty[];
|
||||
};
|
||||
|
||||
export const SchemaDisplay = ({
|
||||
method,
|
||||
path,
|
||||
description,
|
||||
parameters,
|
||||
requestBody,
|
||||
responseBody,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SchemaDisplayProps) => {
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
description,
|
||||
method,
|
||||
parameters,
|
||||
path,
|
||||
requestBody,
|
||||
responseBody,
|
||||
}),
|
||||
[description, method, parameters, path, requestBody, responseBody]
|
||||
);
|
||||
|
||||
return (
|
||||
<SchemaDisplayContext.Provider value={contextValue}>
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden rounded-lg border bg-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? (
|
||||
<>
|
||||
<SchemaDisplayHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<SchemaDisplayMethod />
|
||||
<SchemaDisplayPath />
|
||||
</div>
|
||||
</SchemaDisplayHeader>
|
||||
{description && <SchemaDisplayDescription />}
|
||||
<SchemaDisplayContent>
|
||||
{parameters && parameters.length > 0 && (
|
||||
<SchemaDisplayParameters />
|
||||
)}
|
||||
{requestBody && requestBody.length > 0 && (
|
||||
<SchemaDisplayRequest />
|
||||
)}
|
||||
{responseBody && responseBody.length > 0 && (
|
||||
<SchemaDisplayResponse />
|
||||
)}
|
||||
</SchemaDisplayContent>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SchemaDisplayContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export type SchemaDisplayBodyProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const SchemaDisplayBody = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SchemaDisplayBodyProps) => (
|
||||
<div className={cn("divide-y", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export type SchemaDisplayExampleProps = HTMLAttributes<HTMLPreElement>;
|
||||
|
||||
export const SchemaDisplayExample = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SchemaDisplayExampleProps) => (
|
||||
<pre
|
||||
className={cn(
|
||||
"mx-4 mb-4 overflow-auto rounded-md bg-muted p-4 font-mono text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
);
|
||||
Reference in New Issue
Block a user