|
| 1 | +import { TooltipPortal } from "@radix-ui/react-tooltip"; |
| 2 | +import { BracesIcon, X } from "lucide-react"; |
| 3 | +import { PropsWithChildren, useCallback } from "react"; |
| 4 | +import { Controller, useFormContext } from "react-hook-form"; |
| 5 | + |
| 6 | +import { Button } from "@/components/ui/button"; |
| 7 | +import CodeHighlighter from "@/components/ui/code-highlighter/index"; |
| 8 | +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; |
| 9 | +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; |
| 10 | +import { PlaygroundForm } from "@/lib/playground/types"; |
| 11 | +import { cn } from "@/lib/utils"; |
| 12 | + |
| 13 | +const exampleStructuredOutput = { |
| 14 | + type: "object", |
| 15 | + properties: { |
| 16 | + name: { |
| 17 | + type: "string", |
| 18 | + description: "The name of the person", |
| 19 | + }, |
| 20 | + age: { |
| 21 | + type: "number", |
| 22 | + description: "The age of the person", |
| 23 | + }, |
| 24 | + }, |
| 25 | + required: ["name", "age"], |
| 26 | + additionalProperties: false, |
| 27 | +}; |
| 28 | + |
| 29 | +export default function StructuredOutputSheet({ |
| 30 | + children, |
| 31 | + className, |
| 32 | +}: PropsWithChildren<{ |
| 33 | + className?: string; |
| 34 | +}>) { |
| 35 | + const { |
| 36 | + control, |
| 37 | + watch, |
| 38 | + setValue, |
| 39 | + formState: { errors }, |
| 40 | + } = useFormContext<PlaygroundForm>(); |
| 41 | + |
| 42 | + const structuredOutput = watch("structuredOutput"); |
| 43 | + const model = watch("model"); |
| 44 | + |
| 45 | + const renderTrigger = useCallback(() => { |
| 46 | + if (!structuredOutput) { |
| 47 | + return ( |
| 48 | + <SheetTrigger asChild> |
| 49 | + <Button |
| 50 | + disabled={!model} |
| 51 | + variant="outline" |
| 52 | + size="icon" |
| 53 | + className={cn("focus-visible:ring-0 w-8 h-8 p-2", className)} |
| 54 | + > |
| 55 | + <BracesIcon className="w-3.5 h-3.5" /> |
| 56 | + </Button> |
| 57 | + </SheetTrigger> |
| 58 | + ); |
| 59 | + } |
| 60 | + |
| 61 | + return ( |
| 62 | + <div className="flex flex-row [&>*:first-child]:border-r-0 [&>*:first-child]:rounded-l [&>*:first-child]:rounded-r-none [&>*:last-child]:rounded-r [&>*:last-child]:rounded-l-none"> |
| 63 | + <SheetTrigger asChild> |
| 64 | + <Button |
| 65 | + disabled={!model} |
| 66 | + variant="outlinePrimary" |
| 67 | + size="icon" |
| 68 | + className={cn("focus-visible:ring-0 w-8 h-8 p-2", className)} |
| 69 | + > |
| 70 | + <BracesIcon className="w-3.5 h-3.5" /> |
| 71 | + </Button> |
| 72 | + </SheetTrigger> |
| 73 | + <Button |
| 74 | + onClick={() => setValue("structuredOutput", undefined)} |
| 75 | + className="w-8 h-8" |
| 76 | + variant="outlinePrimary" |
| 77 | + size="icon" |
| 78 | + > |
| 79 | + <X className="h-3.5 w-3.5" /> |
| 80 | + </Button> |
| 81 | + </div> |
| 82 | + ); |
| 83 | + }, [className, model, setValue, structuredOutput]); |
| 84 | + |
| 85 | + return ( |
| 86 | + <Sheet> |
| 87 | + <Tooltip> |
| 88 | + <TooltipTrigger asChild>{children || renderTrigger()}</TooltipTrigger> |
| 89 | + <TooltipPortal> |
| 90 | + <TooltipContent>Structured Output</TooltipContent> |
| 91 | + </TooltipPortal> |
| 92 | + </Tooltip> |
| 93 | + <SheetContent side="right" className="min-w-[50vw] w-full flex flex-col gap-4 p-4"> |
| 94 | + <SheetHeader> |
| 95 | + <SheetTitle>Structured Output</SheetTitle> |
| 96 | + </SheetHeader> |
| 97 | + <div className="flex flex-col gap-2 flex-1 min-h-0"> |
| 98 | + <div className="flex items-center gap-2 justify-between"> |
| 99 | + <span className="text-sm">JSON Schema</span> |
| 100 | + <Button |
| 101 | + onClick={() => setValue("structuredOutput", JSON.stringify(exampleStructuredOutput, null, 2))} |
| 102 | + className="text-primary text-sm p-0" |
| 103 | + variant="ghost" |
| 104 | + > |
| 105 | + <span className="text-sm">Insert example</span> |
| 106 | + </Button> |
| 107 | + </div> |
| 108 | + <div className="p-1 flex flex-1 overflow-hidden"> |
| 109 | + <Controller |
| 110 | + render={({ field: { onChange } }) => ( |
| 111 | + <CodeHighlighter |
| 112 | + onChange={(v) => onChange(v)} |
| 113 | + codeEditorClassName="rounded-b" |
| 114 | + className={cn("rounded h-full", { |
| 115 | + "border border-destructive/75": errors.structuredOutput?.message, |
| 116 | + })} |
| 117 | + defaultMode="json" |
| 118 | + modes={["JSON"]} |
| 119 | + value={structuredOutput ?? ""} |
| 120 | + /> |
| 121 | + )} |
| 122 | + name="structuredOutput" |
| 123 | + control={control} |
| 124 | + rules={{ |
| 125 | + validate: (value) => { |
| 126 | + try { |
| 127 | + if (!value) { |
| 128 | + return true; |
| 129 | + } |
| 130 | + JSON.parse(value); |
| 131 | + return true; |
| 132 | + } catch (e) { |
| 133 | + return "Invalid JSON structure"; |
| 134 | + } |
| 135 | + }, |
| 136 | + }} |
| 137 | + /> |
| 138 | + </div> |
| 139 | + <span className="text-xs text-secondary-foreground"> |
| 140 | + Define a JSON Schema to structure the model's output. |
| 141 | + </span> |
| 142 | + </div> |
| 143 | + </SheetContent> |
| 144 | + </Sheet> |
| 145 | + ); |
| 146 | +} |
0 commit comments