From 68ec69a9c5af4f7285342c9cfcea4b765c9a0c1b Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 28 May 2026 09:24:21 -0300 Subject: [PATCH] feat(playground): scaffold PlaygroundStudio + StudioTopBar + StudioConfigPane Adds the PlaygroundStudio orchestrator (tab routing via ?tab= deep-link, shared configState), StudioTopBar (4 tabs + export placeholder modal), and StudioConfigPane (collapsible, 10-endpoint select, model, system prompt, ParamSliders). F7 slots (SLOT_PRESETS / SLOT_IMPROVE) left as JSX comments. --- .../dashboard/playground/PlaygroundStudio.tsx | 120 ++++++++++++++ .../components/StudioConfigPane.tsx | 149 ++++++++++++++++++ .../playground/components/StudioTopBar.tsx | 108 +++++++++++++ 3 files changed, 377 insertions(+) create mode 100644 src/app/(dashboard)/dashboard/playground/PlaygroundStudio.tsx create mode 100644 src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx create mode 100644 src/app/(dashboard)/dashboard/playground/components/StudioTopBar.tsx diff --git a/src/app/(dashboard)/dashboard/playground/PlaygroundStudio.tsx b/src/app/(dashboard)/dashboard/playground/PlaygroundStudio.tsx new file mode 100644 index 0000000000..150e4632a4 --- /dev/null +++ b/src/app/(dashboard)/dashboard/playground/PlaygroundStudio.tsx @@ -0,0 +1,120 @@ +"use client"; + +// src/app/(dashboard)/dashboard/playground/PlaygroundStudio.tsx + +import { useState } from "react"; +import { useSearchParams } from "next/navigation"; +import StudioTopBar, { type StudioTab } from "./components/StudioTopBar"; +import StudioConfigPane, { type ConfigState } from "./components/StudioConfigPane"; +import { DEFAULT_PARAMS } from "./components/ParamSliders"; +import dynamic from "next/dynamic"; +import type { StreamMetrics } from "@/shared/schemas/playground"; + +// Lazy-load tabs to reduce initial bundle size +const ChatTab = dynamic(() => import("./components/tabs/ChatTab"), { ssr: false }); +const ApiTab = dynamic(() => import("./components/tabs/ApiTab"), { ssr: false }); + +const INITIAL_METRICS: StreamMetrics = { + ttftMs: null, + totalMs: null, + tokensIn: 0, + tokensOut: 0, + tps: null, + costUsd: null, +}; + +const INITIAL_CONFIG: ConfigState = { + endpoint: "chat.completions", + baseUrl: typeof window !== "undefined" ? window.location.origin : "http://localhost:20128", + model: "", + systemPrompt: "You are a helpful assistant.", + params: DEFAULT_PARAMS, +}; + +function resolveTab(value: string | null): StudioTab { + if (value === "chat" || value === "compare" || value === "api" || value === "build") { + return value; + } + return "chat"; +} + +/** + * PlaygroundStudio — orchestrator component for the Playground Studio. + * - Manages active tab state (supports ?tab=chat|compare|api|build deep-link) + * - Manages shared configState for all tabs + * - Renders StudioTopBar + content + StudioConfigPane + * + * F7 SLOTS: + * - SLOT_PRESETS: F7 will replace the comment in StudioConfigPane with PresetPicker + * - SLOT_IMPROVE: F7 will replace the comment in StudioConfigPane with ImprovePromptButton + */ +export function PlaygroundStudio() { + const searchParams = useSearchParams(); + // Derive active tab from URL — no effect needed, avoids setState-in-effect lint violation. + // When URL changes (client nav), searchParams updates, component re-renders with new tab. + const activeTab: StudioTab = resolveTab(searchParams.get("tab")); + const [manualTab, setManualTab] = useState(null); + const effectiveTab = manualTab ?? activeTab; + + const [configState, setConfigState] = useState(INITIAL_CONFIG); + const [metrics, setMetrics] = useState(INITIAL_METRICS); + + function handleTabChange(tab: StudioTab) { + setManualTab(tab); + } + + function handleMetricsUpdate(m: StreamMetrics) { + setMetrics(m); + } + + return ( +
+ {/* Top bar with tabs + token/cost counter + export button */} + + + {/* Main content area: tab content + config pane */} +
+ {/* Tab content */} +
+ {effectiveTab === "chat" && ( + + )} + {effectiveTab === "compare" && ( +
+
+ + compare + +

Compare tab — F7 implementation pending

+
+
+ )} + {effectiveTab === "api" && ( + + )} + {effectiveTab === "build" && ( +
+
+ + build + +

Build tab — F7 implementation pending

+
+
+ )} +
+ + {/* Config pane — always visible, collapsible */} + {/* SLOT_PRESETS and SLOT_IMPROVE are inside StudioConfigPane */} + +
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx b/src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx new file mode 100644 index 0000000000..a4c6cc5ec7 --- /dev/null +++ b/src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx @@ -0,0 +1,149 @@ +"use client"; + +// src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx + +import { useState } from "react"; +import ParamSliders, { type PlaygroundParams } from "./ParamSliders"; +import type { PlaygroundEndpoint } from "@/lib/playground/codeExport"; +import { endpointToPath } from "@/lib/playground/codeExport"; + +export interface ConfigState { + endpoint: PlaygroundEndpoint; + baseUrl: string; + model: string; + systemPrompt: string; + params: PlaygroundParams; +} + +interface StudioConfigPaneProps { + configState: ConfigState; + setConfigState: (s: ConfigState) => void; +} + +const ENDPOINT_OPTIONS: Array<{ value: PlaygroundEndpoint; label: string }> = [ + { value: "chat.completions", label: "Chat completions" }, + { value: "completions", label: "Completions" }, + { value: "embeddings", label: "Embeddings" }, + { value: "images", label: "Images" }, + { value: "audio.transcriptions", label: "Audio transcriptions" }, + { value: "audio.speech", label: "Audio speech" }, + { value: "moderations", label: "Moderations" }, + { value: "rerank", label: "Rerank" }, + { value: "search", label: "Search" }, + { value: "web.fetch", label: "Web fetch" }, +]; + +/** + * Right-side collapsible config pane for PlaygroundStudio. + * Slots for F7: + * - SLOT_PRESETS: PresetPicker will be injected here + * - SLOT_IMPROVE: ImprovePromptButton will be injected here + */ +export default function StudioConfigPane({ configState, setConfigState }: StudioConfigPaneProps) { + const [collapsed, setCollapsed] = useState(false); + + function update(key: K, value: ConfigState[K]) { + setConfigState({ ...configState, [key]: value }); + } + + if (collapsed) { + return ( +
+ +
+ ); + } + + return ( +