import { useMemo, useState } from "react"; import { Button } from "@/src/components/ui/button"; import { Check, ChevronsDownUp, ChevronsUpDown, Copy, FoldVertical, UnfoldVertical, } from "lucide-react"; import { cn } from "@/src/utils/tailwind"; import { default as React18JsonView } from "react18-json-view"; import "react18-json-view/src/dark.css"; import { deepParseJson } from "@langfuse/shared"; import { Skeleton } from "@/src/components/ui/skeleton"; import { useTheme } from "next-themes"; import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture"; import { useMarkdownContext } from "@/src/features/theming/useMarkdownContext"; import { type MediaReturnType } from "@/src/features/media/validation"; import { LangfuseMediaView } from "@/src/components/ui/LangfuseMediaView"; import { MarkdownJsonViewHeader } from "@/src/components/ui/MarkdownJsonView"; import { renderRichPromptContent } from "@/src/features/prompts/components/prompt-content-utils"; import { copyTextToClipboard } from "@/src/utils/clipboard"; export const IO_TABLE_CHAR_LIMIT = 10000; export function JSONView(props: { canEnableMarkdown?: boolean; json?: unknown; title?: string; hideTitle?: boolean; className?: string; isLoading?: boolean; codeClassName?: string; collapseStringsAfterLength?: number | null; media?: MediaReturnType[]; scrollable?: boolean; borderless?: boolean; projectIdForPromptButtons?: string; controlButtons?: React.ReactNode; externalJsonCollapsed?: boolean; onToggleCollapse?: () => void; }) { // some users ingest stringified json nested in json, parse it const parsedJson = useMemo(() => deepParseJson(props.json), [props.json]); const { resolvedTheme } = useTheme(); const { setIsMarkdownEnabled } = useMarkdownContext(); const capture = usePostHogClientCapture(); const [internalCollapsed, setInternalCollapsed] = useState(false); const collapseStringsAfterLength = props.collapseStringsAfterLength === null ? 100_000_000 // if null, show all (100M chars) : (props.collapseStringsAfterLength ?? 500); const isCollapsed = props.externalJsonCollapsed ?? internalCollapsed; const handleOnCopy = (event?: React.MouseEvent) => { if (event) { event.preventDefault(); } const textToCopy = stringifyJsonNode(parsedJson); void copyTextToClipboard(textToCopy); // Keep focus on the copy button to prevent focus shifting if (event) { event.currentTarget.focus(); } }; const handleOnValueChange = () => { setIsMarkdownEnabled(true); capture("trace_detail:io_pretty_format_toggle_group", { renderMarkdown: true, }); }; const handleToggleCollapse = () => { if (props.onToggleCollapse) { props.onToggleCollapse(); } else { setInternalCollapsed(!internalCollapsed); } }; const body = ( <>
{props.isLoading ? ( ) : props.projectIdForPromptButtons ? ( {renderRichPromptContent( props.projectIdForPromptButtons, String(parsedJson), )} ) : (
{ // If externally collapsed and user clicks to expand, sync the state if (props.externalJsonCollapsed && props.onToggleCollapse) { props.onToggleCollapse(); } }} > truncated ? (
{`\n...expand (${Math.max(fullSTring.length - collapseStringsAfterLength, 0)} more characters)`}
) : ( "" ) } displaySize={isCollapsed ? "collapsed" : "expanded"} matchesURL={true} customizeCopy={(node) => stringifyJsonNode(node)} className="w-full" />
)}
{props.media && props.media.length > 0 && ( <>
Media
{props.media.map((m) => ( ))}
)} ); return (
{props.title && !props.hideTitle ? ( {props.controlButtons} } /> ) : null} {props.scrollable ? (
{body}
) : ( body )}
); } export function CodeView(props: { content: string | React.ReactNode[] | undefined | null; originalContent?: string; className?: string; defaultCollapsed?: boolean; title?: string; scrollable?: boolean; }) { const [isCopied, setIsCopied] = useState(false); const [isCollapsed, setCollapsed] = useState(props.defaultCollapsed); const handleCopy = (event: React.MouseEvent) => { event.preventDefault(); setIsCopied(true); const content = props.originalContent ?? (typeof props.content === "string" ? props.content : (props.content?.join("\n") ?? "")); void copyTextToClipboard(content); setTimeout(() => setIsCopied(false), 1000); // Keep focus on the copy button to prevent focus shifting event.currentTarget.focus(); }; const handleShowAll = () => setCollapsed(!isCollapsed); return (
<> {props.title ? (
{props.title}
) : undefined}
{!props.title && ( )} {props.content} {props.defaultCollapsed ? (
) : undefined}
); } export const JsonSkeleton = ({ numRows = 10, borderless = false, className, }: { numRows?: number; borderless?: boolean; className?: string; }) => { return (
{[...Array(numRows)].map((_, i) => ( ))}
); }; // TODO: deduplicate with PrettyJsonView.tsx export function stringifyJsonNode(node: unknown) { // return single string nodes without quotes if (typeof node === "string") { return node; } try { return JSON.stringify( node, (_key, value) => { switch (typeof value) { case "bigint": return String(value) + "n"; case "number": case "boolean": case "object": case "string": return value as string; default: return String(value); } }, 4, ); } catch (error) { console.error("JSON stringify error", error); return "Error: JSON.stringify failed"; } }