langfuse
179 строк · 5.7 Кб
1import { JSONView } from "@/src/components/ui/code";
2import { z } from "zod";
3import { deepParseJson } from "@/src/utils/json";
4import { cn } from "@/src/utils/tailwind";
5import { useState } from "react";
6import { Button } from "@/src/components/ui/button";
7import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
8import { Fragment } from "react";
9
10export const IOPreview: React.FC<{
11input?: unknown;
12output?: unknown;
13isLoading?: boolean;
14hideIfNull?: boolean;
15}> = ({ isLoading = false, hideIfNull = false, ...props }) => {
16const [currentView, setCurrentView] = useState<"pretty" | "json">("pretty");
17
18const input = deepParseJson(props.input);
19const output = deepParseJson(props.output);
20
21// parse old completions: { completion: string } -> string
22const outLegacyCompletionSchema = z
23.object({
24completion: z.string(),
25})
26.refine((value) => Object.keys(value).length === 1);
27const outLegacyCompletionSchemaParsed =
28outLegacyCompletionSchema.safeParse(output);
29const outputClean = outLegacyCompletionSchemaParsed.success
30? outLegacyCompletionSchemaParsed.data
31: props.output ?? null;
32
33// OpenAI messages
34let inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(input);
35if (!inOpenAiMessageArray.success) {
36// check if input is an array of length 1 including an array of OpenAiMessageSchema
37// this is the case for some integrations
38// e.g. [[OpenAiMessageSchema, ...]]
39const inputArray = z.array(OpenAiMessageArraySchema).safeParse(input);
40if (inputArray.success && inputArray.data.length === 1) {
41inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(
42inputArray.data[0],
43);
44} else {
45// check if input is an object with a messages key
46// this is the case for some integrations
47// e.g. { messages: [OpenAiMessageSchema, ...] }
48const inputObject = z
49.object({
50messages: OpenAiMessageArraySchema,
51})
52.safeParse(input);
53
54if (inputObject.success) {
55inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(
56inputObject.data.messages,
57);
58}
59}
60}
61const outOpenAiMessage = OpenAiMessageSchema.safeParse(output);
62
63// Pretty view available
64const isPrettyViewAvailable = inOpenAiMessageArray.success;
65
66// default I/O
67return (
68<>
69{isPrettyViewAvailable ? (
70<Tabs
71value={currentView}
72onValueChange={(v) => setCurrentView(v as "pretty" | "json")}
73>
74<TabsList>
75<TabsTrigger value="pretty">Pretty ✨</TabsTrigger>
76<TabsTrigger value="json">JSON</TabsTrigger>
77</TabsList>
78</Tabs>
79) : null}
80{isPrettyViewAvailable && currentView === "pretty" ? (
81<OpenAiMessageView
82messages={inOpenAiMessageArray.data.concat(
83outOpenAiMessage.success
84? {
85...outOpenAiMessage.data,
86role: outOpenAiMessage.data.role ?? "assistant",
87}
88: {
89role: "assistant",
90content: outputClean ? JSON.stringify(outputClean) : null,
91},
92)}
93/>
94) : null}
95{currentView === "json" || !isPrettyViewAvailable ? (
96<>
97{!(hideIfNull && !input) ? (
98<JSONView
99title="Input"
100json={input ?? null}
101isLoading={isLoading}
102className="flex-1"
103/>
104) : null}
105{!(hideIfNull && !output) ? (
106<JSONView
107title="Output"
108json={outputClean}
109isLoading={isLoading}
110className="flex-1 bg-green-50"
111/>
112) : null}
113</>
114) : null}
115</>
116);
117};
118
119const OpenAiMessageSchema = z
120.object({
121role: z.enum(["system", "user", "assistant", "function"]).optional(),
122name: z.string().optional(),
123content: z
124.union([z.record(z.any()), z.record(z.any()).array(), z.string()])
125.nullable(),
126function_call: z
127.object({
128name: z.string(),
129arguments: z.record(z.any()),
130})
131.optional(),
132})
133.strict() // no additional properties
134.refine((value) => value.content !== null || value.role !== undefined);
135
136const OpenAiMessageArraySchema = z.array(OpenAiMessageSchema).min(1);
137
138const OpenAiMessageView: React.FC<{
139messages: z.infer<typeof OpenAiMessageArraySchema>;
140}> = ({ messages }) => {
141const COLLAPSE_THRESHOLD = 3;
142const [isCollapsed, setCollapsed] = useState(
143messages.length > COLLAPSE_THRESHOLD ? true : null,
144);
145
146return (
147<div className="flex flex-col gap-2 rounded-md border p-3">
148{messages
149.filter(
150(_, i) =>
151// show all if not collapsed or null; show first and last n if collapsed
152!isCollapsed || i == 0 || i > messages.length - COLLAPSE_THRESHOLD,
153)
154.map((message, index) => (
155<Fragment key={index}>
156<JSONView
157title={message.name ?? message.role}
158json={message.function_call ?? message.content}
159className={cn(
160message.role === "system" && "bg-gray-100",
161message.role === "assistant" && "bg-green-50",
162)}
163/>
164{isCollapsed !== null && index === 0 ? (
165<Button
166variant="ghost"
167size="xs"
168onClick={() => setCollapsed((v) => !v)}
169>
170{isCollapsed
171? `Show ${messages.length - COLLAPSE_THRESHOLD} more ...`
172: "Hide history"}
173</Button>
174) : null}
175</Fragment>
176))}
177</div>
178);
179};
180