File size: 4,942 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"use client";

// src/app/(dashboard)/dashboard/playground/components/StructuredOutputEditor.tsx

import { useState } from "react";
import { useStructuredOutput } from "../hooks/useStructuredOutput";
import type { StructuredOutputSchemaInput } from "../hooks/useStructuredOutput";

interface StructuredOutputEditorProps {
  structuredOutput: ReturnType<typeof useStructuredOutput>;
}

const DEFAULT_SCHEMA: StructuredOutputSchemaInput = {
  name: "my_schema",
  schema: {
    type: "object",
    properties: {
      result: { type: "string" },
    },
    required: ["result"],
  },
  strict: true,
};

/**
 * StructuredOutputEditor — toggle JSON mode on/off + edit JSON schema.
 *
 * When enabled, the Build tab sends response_format: { type: "json_schema", json_schema: {...} }.
 * Schema validation is client-side via Zod StructuredOutputSchema (D9).
 */
export default function StructuredOutputEditor({ structuredOutput }: StructuredOutputEditorProps) {
  const { enabled, schema, error, setEnabled, setSchema } = structuredOutput;

  const [schemaRaw, setSchemaRaw] = useState(
    JSON.stringify(schema ?? DEFAULT_SCHEMA, null, 2),
  );
  const [nameField, setNameField] = useState(schema?.name ?? "my_schema");
  const [parseError, setParseError] = useState<string | null>(null);

  function handleValidate() {
    let parsed: unknown;
    try {
      parsed = JSON.parse(schemaRaw);
    } catch {
      setParseError("Invalid JSON");
      return;
    }
    setParseError(null);

    // The parsed JSON should be the inner schema object (not the full response_format wrapper)
    const input: StructuredOutputSchemaInput = {
      name: nameField.trim() || "my_schema",
      schema: parsed as Record<string, unknown>,
      strict: true,
    };

    setSchema(input);
  }

  return (
    <div className="flex flex-col gap-3">
      {/* Toggle */}
      <div className="flex items-center justify-between">
        <div className="flex flex-col gap-0.5">
          <span className="text-xs font-medium text-text-main">JSON mode</span>
          <span className="text-[11px] text-text-muted">
            Forces response_format: json_schema
          </span>
        </div>
        <button
          role="switch"
          aria-checked={enabled}
          onClick={() => setEnabled(!enabled)}
          className={`relative inline-flex w-10 h-5 rounded-full transition-colors ${
            enabled ? "bg-primary" : "bg-text-muted/30"
          }`}
          aria-label={enabled ? "Disable JSON mode" : "Enable JSON mode"}
        >
          <span
            className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${
              enabled ? "translate-x-5" : "translate-x-0"
            }`}
          />
        </button>
      </div>

      {/* Schema editor (shown only when enabled) */}
      {enabled && (
        <div className="flex flex-col gap-2 border border-border rounded-lg p-3 bg-surface">
          {/* Schema name */}
          <div className="flex flex-col gap-1">
            <label className="text-[10px] text-text-muted uppercase tracking-wider">
              Schema name
            </label>
            <input
              type="text"
              value={nameField}
              onChange={(e) => setNameField(e.target.value)}
              placeholder="my_schema"
              className="text-xs bg-bg-alt border border-border rounded px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary text-text-main"
            />
          </div>

          {/* Schema JSON textarea */}
          <div className="flex flex-col gap-1">
            <label className="text-[10px] text-text-muted uppercase tracking-wider">
              JSON schema
            </label>
            <textarea
              value={schemaRaw}
              onChange={(e) => setSchemaRaw(e.target.value)}
              rows={8}
              className="text-xs font-mono bg-bg-alt border border-border rounded px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary text-text-main resize-y"
              aria-label="JSON schema editor"
            />
          </div>

          {/* Errors */}
          {(parseError ?? error) && (
            <p className="text-xs text-destructive">
              {parseError ?? error}
            </p>
          )}

          {/* Status — show when schema is set and no errors */}
          {schema != null && !parseError && !error && (
            <p className="text-xs text-green-600 dark:text-green-400">
              ✓ Schema validated
            </p>
          )}

          {/* Validate button */}
          <button
            onClick={handleValidate}
            className="text-xs px-3 py-1.5 rounded border border-border text-text-muted hover:text-text-main hover:bg-black/5 dark:hover:bg-white/5 transition-colors self-start"
          >
            Validate schema
          </button>
        </div>
      )}
    </div>
  );
}