diff --git a/src/app/(dashboard)/dashboard/playground/hooks/useStructuredOutput.ts b/src/app/(dashboard)/dashboard/playground/hooks/useStructuredOutput.ts new file mode 100644 index 0000000000..7eb621600a --- /dev/null +++ b/src/app/(dashboard)/dashboard/playground/hooks/useStructuredOutput.ts @@ -0,0 +1,124 @@ +// src/app/(dashboard)/dashboard/playground/hooks/useStructuredOutput.ts +"use client"; + +import { useState, useCallback } from "react"; +import { StructuredOutputSchema } from "@/shared/schemas/playground"; + +export interface StructuredOutputSchemaInput { + name: string; + schema: Record; + strict?: boolean; +} + +export interface UseStructuredOutputState { + enabled: boolean; + schema: StructuredOutputSchemaInput | null; + error: string | null; +} + +export interface ValidateResponseResult { + valid: boolean; + error?: string; +} + +export interface UseStructuredOutput extends UseStructuredOutputState { + setEnabled: (enabled: boolean) => void; + /** + * Set and validate the JSON schema via Zod StructuredOutputSchema. + * Clears error on success; sets error on validation failure. + */ + setSchema: (s: StructuredOutputSchemaInput) => void; + /** + * Validate a response body against the current schema. + * - First tries JSON.parse if content is a string. + * - Then checks that the parsed value is a non-null object (basic shape validation). + * Returns { valid: true } on success; { valid: false, error } on failure. + */ + validateResponse: (content: unknown) => ValidateResponseResult; +} + +/** + * Hook for the Structured Output (JSON mode) toggle in the Build tab. + * Manages toggle state + JSON schema + client-side response validation. + */ +export function useStructuredOutput(): UseStructuredOutput { + const [enabled, setEnabledState] = useState(false); + const [schema, setSchemaState] = useState(null); + const [error, setError] = useState(null); + + const setEnabled = useCallback((value: boolean): void => { + setEnabledState(value); + }, []); + + const setSchema = useCallback((s: StructuredOutputSchemaInput): void => { + // Validate via StructuredOutputSchema (wraps the input as if it were a full response_format object). + const parsed = StructuredOutputSchema.safeParse({ + type: "json_schema", + json_schema: s, + }); + if (!parsed.success) { + const message = parsed.error.issues.map((i) => i.message).join("; "); + setError(message); + return; + } + setSchemaState(s); + setError(null); + }, []); + + const validateResponse = useCallback( + (content: unknown): ValidateResponseResult => { + if (schema == null) { + return { valid: false, error: "No schema set" }; + } + + // Step 1: parse JSON if the content is a string. + let parsed: unknown = content; + if (typeof content === "string") { + try { + parsed = JSON.parse(content); + } catch { + return { valid: false, error: "Response is not valid JSON" }; + } + } + + // Step 2: basic shape validation — must be a non-null object. + // Full JSON Schema validation would require ajv (not present in this project). + // We do a structural check: verify it's an object, and for each key in the + // schema's `properties` (if present), check that the key exists in the response. + if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) { + return { valid: false, error: "Response is not a JSON object" }; + } + + const responseObj = parsed as Record; + const schemaObj = schema.schema; + + // If the schema has a "properties" field, validate that required keys are present. + const properties = schemaObj["properties"]; + if (properties != null && typeof properties === "object" && !Array.isArray(properties)) { + const required = schemaObj["required"]; + if (Array.isArray(required)) { + for (const key of required) { + if (typeof key === "string" && !(key in responseObj)) { + return { + valid: false, + error: `Response is missing required field: "${key}"`, + }; + } + } + } + } + + return { valid: true }; + }, + [schema], + ); + + return { + enabled, + schema, + error, + setEnabled, + setSchema, + validateResponse, + }; +} diff --git a/src/app/(dashboard)/dashboard/playground/hooks/useToolsBuilder.ts b/src/app/(dashboard)/dashboard/playground/hooks/useToolsBuilder.ts new file mode 100644 index 0000000000..a41950a3bf --- /dev/null +++ b/src/app/(dashboard)/dashboard/playground/hooks/useToolsBuilder.ts @@ -0,0 +1,119 @@ +// src/app/(dashboard)/dashboard/playground/hooks/useToolsBuilder.ts +"use client"; + +import { useState, useCallback } from "react"; +import { ToolDefinitionSchema } from "@/shared/schemas/playground"; +import type { ToolDefinition } from "@/lib/playground/codeExport"; + +export interface ToolsBuildResult { + ok: true; +} + +export interface ToolsBuildError { + ok: false; + error: string; +} + +export type ToolsBuildOutcome = ToolsBuildResult | ToolsBuildError; + +export interface UseToolsBuilderState { + tools: ToolDefinition[]; + errors: Map; +} + +export interface UseToolsBuilder extends UseToolsBuilderState { + /** + * Add a tool after Zod validation. + * Returns { ok: true } on success; { ok: false, error } if validation fails. + */ + add: (tool: ToolDefinition) => ToolsBuildOutcome; + /** Remove tool at given index. No-op if index out of bounds. */ + remove: (index: number) => void; + /** + * Replace tool at index after Zod validation. + * Returns { ok: true } on success; { ok: false, error } if validation fails. + * Clears previous error for that index on success. + */ + update: (index: number, tool: ToolDefinition) => ToolsBuildOutcome; + /** Remove all tools and clear all errors. */ + clear: () => void; +} + +/** + * Client-side manager for the `tools[]` array (Function Calling / Build tab). + * + * Validates each tool via Zod `ToolDefinitionSchema` before adding/updating. + * Stores per-index validation errors so the UI can show inline hints. + */ +export function useToolsBuilder(): UseToolsBuilder { + const [tools, setTools] = useState([]); + const [errors, setErrors] = useState>(new Map()); + + const add = useCallback((tool: ToolDefinition): ToolsBuildOutcome => { + const parsed = ToolDefinitionSchema.safeParse(tool); + if (!parsed.success) { + const message = parsed.error.issues.map((i) => i.message).join("; "); + return { ok: false, error: message }; + } + setTools((prev) => [...prev, parsed.data as ToolDefinition]); + return { ok: true }; + }, []); + + const remove = useCallback((index: number): void => { + setTools((prev) => { + if (index < 0 || index >= prev.length) return prev; + const next = [...prev]; + next.splice(index, 1); + return next; + }); + setErrors((prev) => { + const next = new Map(prev); + next.delete(index); + // Re-index errors for items after the removed one. + const reindexed = new Map(); + next.forEach((v, k) => { + reindexed.set(k > index ? k - 1 : k, v); + }); + return reindexed; + }); + }, []); + + const update = useCallback((index: number, tool: ToolDefinition): ToolsBuildOutcome => { + const parsed = ToolDefinitionSchema.safeParse(tool); + if (!parsed.success) { + const message = parsed.error.issues.map((i) => i.message).join("; "); + setErrors((prev) => { + const next = new Map(prev); + next.set(index, message); + return next; + }); + return { ok: false, error: message }; + } + setTools((prev) => { + if (index < 0 || index >= prev.length) return prev; + const next = [...prev]; + next[index] = parsed.data as ToolDefinition; + return next; + }); + setErrors((prev) => { + const next = new Map(prev); + next.delete(index); + return next; + }); + return { ok: true }; + }, []); + + const clear = useCallback((): void => { + setTools([]); + setErrors(new Map()); + }, []); + + return { + tools, + errors, + add, + remove, + update, + clear, + }; +}