feat(playground): add ParamSliders + TokenCostCounter

ParamSliders: temperature/max_tokens/top_p/penalties/seed/stop/JSON-mode
sliders and inputs. TokenCostCounter: D13-compliant "(estimated)" cost label
with ↑/↓ arrow notation (e.g. 142↑ 38↓ · $0.002 estimated).
This commit is contained in:
diegosouzapw
2026-05-28 09:24:34 -03:00
parent 68ec69a9c5
commit e2a22c57b0
2 changed files with 205 additions and 0 deletions

View File

@@ -0,0 +1,176 @@
"use client";
// src/app/(dashboard)/dashboard/playground/components/ParamSliders.tsx
export interface PlaygroundParams {
temperature: number;
max_tokens: number;
top_p: number;
presence_penalty: number;
frequency_penalty: number;
seed: number | null;
stop: string;
jsonMode: boolean;
}
export const DEFAULT_PARAMS: PlaygroundParams = {
temperature: 1.0,
max_tokens: 1024,
top_p: 1.0,
presence_penalty: 0,
frequency_penalty: 0,
seed: null,
stop: "",
jsonMode: false,
};
interface ParamSlidersProps {
params: PlaygroundParams;
setParams: (params: PlaygroundParams) => void;
}
interface SliderRowProps {
label: string;
value: number;
min: number;
max: number;
step: number;
onChange: (v: number) => void;
}
function SliderRow({ label, value, min, max, step, onChange }: SliderRowProps) {
return (
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<label className="text-xs text-text-muted font-medium">{label}</label>
<input
type="number"
value={value}
min={min}
max={max}
step={step}
onChange={(e) => {
const v = parseFloat(e.target.value);
if (!isNaN(v)) onChange(Math.min(max, Math.max(min, v)));
}}
className="w-16 text-xs text-right bg-surface border border-border rounded px-1.5 py-0.5 focus:outline-none focus:ring-1 focus:ring-primary"
/>
</div>
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => onChange(parseFloat(e.target.value))}
className="w-full accent-primary h-1.5"
/>
</div>
);
}
/**
* Sliders for temperature, max_tokens, top_p, penalties, seed, stop, JSON mode.
* Props: { params, setParams }
*/
export default function ParamSliders({ params, setParams }: ParamSlidersProps) {
function update<K extends keyof PlaygroundParams>(key: K, value: PlaygroundParams[K]) {
setParams({ ...params, [key]: value });
}
return (
<div className="space-y-3">
<SliderRow
label="Temperature"
value={params.temperature}
min={0}
max={2}
step={0.01}
onChange={(v) => update("temperature", v)}
/>
<SliderRow
label="Max tokens"
value={params.max_tokens}
min={1}
max={32768}
step={1}
onChange={(v) => update("max_tokens", Math.round(v))}
/>
<SliderRow
label="Top-p"
value={params.top_p}
min={0}
max={1}
step={0.01}
onChange={(v) => update("top_p", v)}
/>
<SliderRow
label="Presence penalty"
value={params.presence_penalty}
min={-2}
max={2}
step={0.01}
onChange={(v) => update("presence_penalty", v)}
/>
<SliderRow
label="Frequency penalty"
value={params.frequency_penalty}
min={-2}
max={2}
step={0.01}
onChange={(v) => update("frequency_penalty", v)}
/>
{/* Seed input */}
<div className="flex flex-col gap-1">
<label className="text-xs text-text-muted font-medium">Seed</label>
<input
type="number"
value={params.seed ?? ""}
placeholder="Random (leave empty)"
onChange={(e) => {
const raw = e.target.value;
update("seed", raw === "" ? null : parseInt(raw, 10));
}}
className="w-full text-xs bg-surface border border-border rounded px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
/>
</div>
{/* Stop sequences */}
<div className="flex flex-col gap-1">
<label className="text-xs text-text-muted font-medium">Stop sequences</label>
<input
type="text"
value={params.stop}
placeholder='e.g. "\n\n" or "END"'
onChange={(e) => update("stop", e.target.value)}
className="w-full text-xs bg-surface border border-border rounded px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
/>
</div>
{/* JSON mode toggle */}
<div className="flex items-center justify-between">
<label className="text-xs text-text-muted font-medium">JSON mode</label>
<button
type="button"
role="switch"
aria-checked={params.jsonMode}
onClick={() => update("jsonMode", !params.jsonMode)}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary/30 ${
params.jsonMode ? "bg-primary" : "bg-neutral-300 dark:bg-neutral-600"
}`}
>
<span
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow transition-transform ${
params.jsonMode ? "translate-x-4" : "translate-x-0.5"
}`}
/>
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
"use client";
// src/app/(dashboard)/dashboard/playground/components/TokenCostCounter.tsx
interface TokenCostCounterProps {
tokensIn: number;
tokensOut: number;
costUsd: number | null;
}
/**
* Displays token counts and estimated cost.
* D13: Cost is labeled "(estimated)" to reflect client-side calculation.
*/
export default function TokenCostCounter({ tokensIn, tokensOut, costUsd }: TokenCostCounterProps) {
if (tokensIn === 0 && tokensOut === 0 && costUsd === null) {
return null;
}
return (
<span className="text-xs text-text-muted flex items-center gap-1 font-mono">
{tokensIn > 0 && <span>{tokensIn}</span>}
{tokensOut > 0 && <span>{tokensOut}</span>}
{costUsd !== null && costUsd > 0 && (
<span>· ${costUsd.toFixed(4)} (estimated)</span>
)}
</span>
);
}