import { PrettyJsonView } from "@/src/components/ui/PrettyJsonView"; import { z } from "zod/v4"; import { type Prisma, deepParseJson } from "@langfuse/shared"; import { cn } from "@/src/utils/tailwind"; import { useEffect, useMemo, useState } from "react"; import { Button } from "@/src/components/ui/button"; import { Fragment } from "react"; import { ChatMlArraySchema, type ChatMlMessageSchema, } from "@/src/components/schemas/ChatMlSchema"; import { type MediaReturnType } from "@/src/features/media/validation"; import { LangfuseMediaView } from "@/src/components/ui/LangfuseMediaView"; import { MarkdownJsonView } from "@/src/components/ui/MarkdownJsonView"; import { SubHeaderLabel } from "@/src/components/layouts/header"; import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs"; import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture"; import useLocalStorage from "@/src/components/useLocalStorage"; import usePreserveRelativeScroll from "@/src/hooks/usePreserveRelativeScroll"; import { MARKDOWN_RENDER_CHARACTER_LIMIT } from "@/src/utils/constants"; export const IOPreview: React.FC<{ input?: Prisma.JsonValue; output?: Prisma.JsonValue; isLoading?: boolean; hideIfNull?: boolean; media?: MediaReturnType[]; hideOutput?: boolean; hideInput?: boolean; currentView?: "pretty" | "json"; setIsPrettyViewAvailable?: (value: boolean) => void; inputExpansionState?: Record | boolean; outputExpansionState?: Record | boolean; onInputExpansionChange?: ( expansion: Record | boolean, ) => void; onOutputExpansionChange?: ( expansion: Record | boolean, ) => void; }> = ({ isLoading = false, hideIfNull = false, hideOutput = false, hideInput = false, media, currentView, inputExpansionState, outputExpansionState, onInputExpansionChange, onOutputExpansionChange, ...props }) => { const [localCurrentView, setLocalCurrentView] = useLocalStorage< "pretty" | "json" >("jsonViewPreference", "pretty"); const selectedView = currentView ?? localCurrentView; const capture = usePostHogClientCapture(); const input = deepParseJson(props.input); const output = deepParseJson(props.output); const [compensateScrollRef, startPreserveScroll] = usePreserveRelativeScroll([selectedView]); // parse old completions: { completion: string } -> string const outLegacyCompletionSchema = z .object({ completion: z.string(), }) .refine((value) => Object.keys(value).length === 1); const outLegacyCompletionSchemaParsed = outLegacyCompletionSchema.safeParse(output); const outputClean = outLegacyCompletionSchemaParsed.success ? outLegacyCompletionSchemaParsed.data : (props.output ?? null); // ChatML format let inChatMlArray = ChatMlArraySchema.safeParse(input); if (!inChatMlArray.success) { // check if input is an array of length 1 including an array of ChatMlMessageSchema // this is the case for some integrations // e.g. [[ChatMlMessageSchema, ...]] const inputArray = z.array(ChatMlArraySchema).safeParse(input); if (inputArray.success && inputArray.data.length === 1) { inChatMlArray = ChatMlArraySchema.safeParse(inputArray.data[0]); } else { // check if input is an object with a messages key // this is the case for some integrations // e.g. { messages: [ChatMlMessageSchema, ...] } const inputObject = z .object({ messages: ChatMlArraySchema, }) .safeParse(input); if (inputObject.success) { inChatMlArray = ChatMlArraySchema.safeParse(inputObject.data.messages); } } } const outChatMlArray = ChatMlArraySchema.safeParse( Array.isArray(output) ? output : [output], ); // Pretty view is available for ChatML content OR any JSON content const isPrettyViewAvailable = true; // Always show the toggle, let individual components decide how to render useEffect(() => { props.setIsPrettyViewAvailable?.(isPrettyViewAvailable); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPrettyViewAvailable]); // If there are additional input fields beyond the messages, render them const additionalInput = typeof input === "object" && input !== null && !Array.isArray(input) ? Object.fromEntries( Object.entries(input as object).filter(([key]) => key !== "messages"), ) : undefined; // Don't render markdown if total content size exceeds limit const inputSize = JSON.stringify(input || {}).length; const outputSize = JSON.stringify(outputClean || {}).length; const messagesSize = inChatMlArray.success ? JSON.stringify(inChatMlArray.data).length : 0; const totalContentSize = inputSize + outputSize + messagesSize; const shouldRenderMarkdownSafely = totalContentSize <= MARKDOWN_RENDER_CHARACTER_LIMIT; // default I/O return ( <> {isPrettyViewAvailable && !currentView ? (
{ startPreserveScroll(); capture("trace_detail:io_mode_switch", { view: value }); setLocalCurrentView(value as "pretty" | "json"); }} > Formatted JSON
) : null} {/* Always render components to preserve state, just hide via CSS*/} {isPrettyViewAvailable ? ( <> {/* Pretty view content */}
{inChatMlArray.success ? ( ({ ...m, role: m.role ?? "assistant", })) : [ { role: "assistant", ...(typeof outputClean === "string" ? { content: outputClean } : { json: outputClean }), } as ChatMlMessageSchema, ]), ]} shouldRenderMarkdown={shouldRenderMarkdownSafely} additionalInput={ Object.keys(additionalInput ?? {}).length > 0 ? additionalInput : undefined } media={media ?? []} currentView={selectedView} /> ) : ( <> {!(hideIfNull && !input) && !hideInput ? ( m.field === "input") ?? []} currentView={selectedView} externalExpansionState={inputExpansionState} onExternalExpansionChange={onInputExpansionChange} /> ) : null} {!(hideIfNull && !output) && !hideOutput ? ( m.field === "output") ?? []} currentView={selectedView} externalExpansionState={outputExpansionState} onExternalExpansionChange={onOutputExpansionChange} /> ) : null} )}
{/* JSON view content */}
{!(hideIfNull && !input) && !hideInput ? ( m.field === "input") ?? []} currentView={selectedView} externalExpansionState={inputExpansionState} onExternalExpansionChange={onInputExpansionChange} /> ) : null} {!(hideIfNull && !output) && !hideOutput ? ( m.field === "output") ?? []} currentView={selectedView} externalExpansionState={outputExpansionState} onExternalExpansionChange={onOutputExpansionChange} /> ) : null}
) : ( <> {!(hideIfNull && !input) && !hideInput ? ( m.field === "input") ?? []} currentView={selectedView} externalExpansionState={inputExpansionState} onExternalExpansionChange={onInputExpansionChange} /> ) : null} {!(hideIfNull && !output) && !hideOutput ? ( m.field === "output") ?? []} currentView={selectedView} externalExpansionState={outputExpansionState} onExternalExpansionChange={onOutputExpansionChange} /> ) : null} )} ); }; export const OpenAiMessageView: React.FC<{ messages: z.infer; title?: string; shouldRenderMarkdown?: boolean; collapseLongHistory?: boolean; media?: MediaReturnType[]; additionalInput?: Record; projectIdForPromptButtons?: string; currentView?: "pretty" | "json"; }> = ({ title, messages, shouldRenderMarkdown = false, media, collapseLongHistory = true, additionalInput, projectIdForPromptButtons, currentView = "json", }) => { const COLLAPSE_THRESHOLD = 3; const [isCollapsed, setCollapsed] = useState( collapseLongHistory && messages.length > COLLAPSE_THRESHOLD ? true : null, ); const shouldRenderContent = (message: ChatMlMessageSchema) => { return message.content != null || !!message.audio; }; const shouldRenderJson = (message: ChatMlMessageSchema) => { return !!message.json; }; const isPlaceholderMessage = (message: ChatMlMessageSchema) => { return message.type === "placeholder"; }; const messagesToRender = useMemo( () => messages.filter( (message) => shouldRenderContent(message) || shouldRenderJson(message) || isPlaceholderMessage(message), ), [messages], ); return (
{title && }
{messagesToRender .filter( (_, i) => // show all if not collapsed or null; show first and last n if collapsed !isCollapsed || i == 0 || i > messagesToRender.length - COLLAPSE_THRESHOLD, ) .map((message, index) => ( {isPlaceholderMessage(message) ? ( <>
) : ( <> {shouldRenderContent(message) && ( <>
)} {shouldRenderJson(message) && !isPlaceholderMessage(message) && ( )} )} {isCollapsed !== null && index === 0 ? ( ) : null}
))}
{additionalInput && ( )} {media && media.length > 0 && ( <>
Media
{media.map((m) => ( ))}
)}
); };