From 331cc68e4508e5e6a138f3f82e2b800d0d0b5f6e Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 28 May 2026 11:20:10 -0300 Subject: [PATCH] feat(playground): add BuildTab with tools + structured output --- .../playground/components/tabs/BuildTab.tsx | 399 ++++++++++++++++++ 1 file changed, 399 insertions(+) create mode 100644 src/app/(dashboard)/dashboard/playground/components/tabs/BuildTab.tsx diff --git a/src/app/(dashboard)/dashboard/playground/components/tabs/BuildTab.tsx b/src/app/(dashboard)/dashboard/playground/components/tabs/BuildTab.tsx new file mode 100644 index 0000000000..bf67889bf5 --- /dev/null +++ b/src/app/(dashboard)/dashboard/playground/components/tabs/BuildTab.tsx @@ -0,0 +1,399 @@ +"use client"; + +// src/app/(dashboard)/dashboard/playground/components/tabs/BuildTab.tsx + +import { useRef, useState } from "react"; +import { useToolsBuilder } from "../../hooks/useToolsBuilder"; +import { useStructuredOutput } from "../../hooks/useStructuredOutput"; +import ToolsBuilder from "../ToolsBuilder"; +import StructuredOutputEditor from "../StructuredOutputEditor"; +import MarkdownMessage from "../MarkdownMessage"; +import type { ConfigState } from "../StudioConfigPane"; + +interface BuildTabProps { + configState: ConfigState; +} + +interface ToolCall { + id: string; + function: { + name: string; + arguments: string; + }; +} + +interface Message { + role: "user" | "assistant" | "tool"; + content: string; + toolCallId?: string; +} + +interface ToolResultDraft { + toolCallId: string; + functionName: string; + draft: string; +} + +/** + * BuildTab — tools / function calling UI + structured output (D9). + * + * Runs /v1/chat/completions with: + * - tools[] if any are defined + * - response_format: json_schema if structured output is enabled + * + * When tool_calls appear in the response, shows each tool call with an input + * for the tool_result + "Send result" button to continue the conversation. + */ +export default function BuildTab({ configState }: BuildTabProps) { + const toolsBuilder = useToolsBuilder(); + const structuredOutput = useStructuredOutput(); + + const [prompt, setPrompt] = useState(""); + const [messages, setMessages] = useState([]); + const [running, setRunning] = useState(false); + const [toolCalls, setToolCalls] = useState([]); + const [toolResultDrafts, setToolResultDrafts] = useState([]); + const [validationResult, setValidationResult] = useState<{ + valid: boolean; + error?: string; + } | null>(null); + const abortRef = useRef(null); + + function buildRequestBody(msgs: Message[]) { + const body: Record = { + model: configState.model, + stream: false, + messages: [ + ...(configState.systemPrompt + ? [{ role: "system", content: configState.systemPrompt }] + : []), + ...msgs.map((m) => { + if (m.role === "tool") { + return { + role: "tool", + content: m.content, + tool_call_id: m.toolCallId ?? "", + }; + } + return { role: m.role, content: m.content }; + }), + ], + }; + + // Attach tools if any defined + if (toolsBuilder.tools.length > 0) { + body["tools"] = toolsBuilder.tools; + } + + // Attach response_format if JSON mode enabled and schema set + if (structuredOutput.enabled && structuredOutput.schema != null) { + body["response_format"] = { + type: "json_schema", + json_schema: structuredOutput.schema, + }; + } + + const { params } = configState; + if (params.temperature != null) body["temperature"] = params.temperature; + if (params.max_tokens != null) body["max_tokens"] = params.max_tokens; + if (params.top_p != null) body["top_p"] = params.top_p; + if (params.presence_penalty != null) body["presence_penalty"] = params.presence_penalty; + if (params.frequency_penalty != null) body["frequency_penalty"] = params.frequency_penalty; + if (params.seed != null) body["seed"] = params.seed; + if (params.stop != null) body["stop"] = params.stop; + + return body; + } + + async function runRequest(msgs: Message[]) { + abortRef.current?.abort(); + const controller = new AbortController(); + abortRef.current = controller; + setRunning(true); + setToolCalls([]); + setToolResultDrafts([]); + setValidationResult(null); + + try { + const res = await fetch(`${configState.baseUrl}/v1/chat/completions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(buildRequestBody(msgs)), + signal: controller.signal, + }); + + if (!res.ok) { + const text = await res.text().catch(() => "Unknown error"); + const errMsg = text.slice(0, 300); + setMessages((prev) => [...prev, { role: "assistant", content: `Error: ${errMsg}` }]); + return; + } + + const data = (await res.json()) as { + choices?: Array<{ + message?: { + content?: string | null; + tool_calls?: ToolCall[]; + }; + }>; + }; + + const choice = data.choices?.[0]; + const assistantMsg = choice?.message; + + if (assistantMsg == null) { + setMessages((prev) => [ + ...prev, + { role: "assistant", content: "(empty response)" }, + ]); + return; + } + + if (assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0) { + // Tool call response — show tool call UI + setToolCalls(assistantMsg.tool_calls); + setMessages((prev) => [ + ...prev, + { + role: "assistant", + content: assistantMsg.content ?? "(tool call)", + }, + ]); + // Initialize drafts + setToolResultDrafts( + assistantMsg.tool_calls.map((tc) => ({ + toolCallId: tc.id, + functionName: tc.function.name, + draft: "", + })), + ); + } else { + const content = assistantMsg.content ?? ""; + setMessages((prev) => [ + ...prev, + { role: "assistant", content }, + ]); + + // Validate structured output response if enabled + if (structuredOutput.enabled && structuredOutput.schema != null) { + const validation = structuredOutput.validateResponse(content); + setValidationResult(validation); + } + } + } catch (err) { + if (controller.signal.aborted) return; + const msg = err instanceof Error ? err.message : String(err); + setMessages((prev) => [...prev, { role: "assistant", content: `Error: ${msg}` }]); + } finally { + setRunning(false); + } + } + + async function handleRun() { + if (!prompt.trim() && messages.length === 0) return; + + const newMessages: Message[] = [ + ...messages, + ...(prompt.trim() ? [{ role: "user" as const, content: prompt }] : []), + ]; + + if (prompt.trim()) { + setMessages(newMessages); + setPrompt(""); + } + + await runRequest(newMessages); + } + + async function sendToolResult(toolCallId: string) { + const draft = toolResultDrafts.find((d) => d.toolCallId === toolCallId); + if (draft == null) return; + + const toolResultMsg: Message = { + role: "tool", + content: draft.draft, + toolCallId, + }; + + const newMessages = [...messages, toolResultMsg]; + setMessages(newMessages); + setToolResultDrafts([]); + setToolCalls([]); + + await runRequest(newMessages); + } + + function clearConversation() { + setMessages([]); + setToolCalls([]); + setToolResultDrafts([]); + setValidationResult(null); + setPrompt(""); + } + + return ( +
+ {/* Left panel: conversation + run */} +
+ {/* Toolbar */} +
+ + + {messages.length > 0 && ( + + )} + +
+ {toolsBuilder.tools.length > 0 && ( + + {toolsBuilder.tools.length} tool{toolsBuilder.tools.length !== 1 ? "s" : ""} + + )} + {structuredOutput.enabled && ( + + JSON mode + + )} +
+
+ + {/* Conversation history */} +
+ {messages.map((msg, idx) => ( +
+
+ {msg.role === "user" ? ( + {msg.content} + ) : ( + + )} +
+
+ ))} + + {/* Tool call UI */} + {toolCalls.length > 0 && ( +
+ {toolCalls.map((tc) => { + const draft = toolResultDrafts.find((d) => d.toolCallId === tc.id); + return ( +
+
+ + function + + + {tc.function.name} + +
+
+                      {tc.function.arguments}
+                    
+
+ +