fix(ui): make every playground model-settings help control a click popover

The playground Model Settings menu had two different help affordances. Stream
responses, Temperature and Max Tokens used a Tooltip, while Simulate failure to
test fallbacks used a Popover. Before the shadcn migration both were antd and
both opened on hover after 100ms, so the split was only cosmetic. Base UI's
Popover is click-triggered and its Tooltip waits for 600ms of motionless hover,
so the migration turned that cosmetic split into two interaction models: the
Simulate icon answered a click and the other three appeared inert.

Move all four onto Popover so each one opens on click, and share the content
class between them.
This commit is contained in:
Yuneng Jiang 2026-08-14 17:22:12 -07:00
parent d4d6bc2577
commit ec7405f616
No known key found for this signature in database
2 changed files with 51 additions and 26 deletions

View file

@ -129,6 +129,26 @@ describe("AdditionalModelSettings", () => {
});
});
it.each([
["Help: Stream responses", /Streams the answer token by token/],
["Help: Simulate failure to test fallbacks", /Causes the first request to fail/],
["Help: Temperature", /Controls randomness/],
["Help: Max Tokens", /Maximum number of tokens to generate/],
])("should open the %s help on click and not on hover", async (triggerLabel, helpText) => {
const user = userEvent.setup();
render(<AdditionalModelSettings onStreamingChange={vi.fn()} onMockTestFallbacksChange={vi.fn()} />);
const trigger = screen.getByLabelText(triggerLabel);
await user.hover(trigger);
await new Promise((resolve) => setTimeout(resolve, 900));
expect(screen.queryByText(helpText)).not.toBeInTheDocument();
await user.click(trigger);
await waitFor(() => expect(screen.getByText(helpText)).toBeInTheDocument());
});
it("should keep a half-typed decimal temperature instead of rewriting it", async () => {
const onTemperatureChange = vi.fn();

View file

@ -3,7 +3,6 @@ import React, { useEffect, useId, useState } from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/cva.config";
interface AdditionalModelSettingsProps {
@ -20,6 +19,8 @@ interface AdditionalModelSettingsProps {
showAdvancedParams?: boolean;
}
const helpPopoverClassName = "max-w-[340px] gap-2 p-3 text-sm";
function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
@ -116,15 +117,17 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
<label htmlFor={streamingId} className="cursor-pointer text-sm font-medium">
Stream responses
</label>
<Tooltip>
<TooltipTrigger aria-label="Help: Stream responses">
<Popover>
<PopoverTrigger aria-label="Help: Stream responses">
<Info className="size-3 shrink-0 cursor-pointer text-gray-400 hover:text-gray-600" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
Streams the answer token by token. Uncheck to send a non-streaming request and render the full response at
once.
</TooltipContent>
</Tooltip>
</PopoverTrigger>
<PopoverContent side="right" className={helpPopoverClassName}>
<p>
Streams the answer token by token. Uncheck to send a non-streaming request and render the full response
at once.
</p>
</PopoverContent>
</Popover>
</div>
)}
@ -157,7 +160,7 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
<PopoverTrigger aria-label="Help: Simulate failure to test fallbacks">
<Info className="size-3 shrink-0 cursor-pointer text-gray-400 hover:text-gray-600" />
</PopoverTrigger>
<PopoverContent side="right" className="max-w-[340px] gap-2 p-3 text-sm">
<PopoverContent side="right" className={helpPopoverClassName}>
<p>
Causes the first request to fail so the router tries fallbacks (if configured). Use this to verify your
fallback setup.
@ -188,14 +191,16 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
<label htmlFor={temperatureId} className={cn("text-sm", disabledTextColor)}>
Temperature
</label>
<Tooltip>
<TooltipTrigger aria-label="Help: Temperature">
<Info className={cn("size-3 cursor-help", disabledTextColor)} />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
Controls randomness. Lower values make output more deterministic, higher values more creative.
</TooltipContent>
</Tooltip>
<Popover>
<PopoverTrigger aria-label="Help: Temperature">
<Info className={cn("size-3 cursor-pointer", disabledTextColor)} />
</PopoverTrigger>
<PopoverContent side="right" className={helpPopoverClassName}>
<p>
Controls randomness. Lower values make output more deterministic, higher values more creative.
</p>
</PopoverContent>
</Popover>
</div>
<Input
id={`${temperatureId}-number`}
@ -234,14 +239,14 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
<label htmlFor={maxTokensId} className={cn("text-sm", disabledTextColor)}>
Max Tokens
</label>
<Tooltip>
<TooltipTrigger aria-label="Help: Max Tokens">
<Info className={cn("size-3 cursor-help", disabledTextColor)} />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
Maximum number of tokens to generate in the response.
</TooltipContent>
</Tooltip>
<Popover>
<PopoverTrigger aria-label="Help: Max Tokens">
<Info className={cn("size-3 cursor-pointer", disabledTextColor)} />
</PopoverTrigger>
<PopoverContent side="right" className={helpPopoverClassName}>
<p>Maximum number of tokens to generate in the response.</p>
</PopoverContent>
</Popover>
</div>
<Input
id={`${maxTokensId}-number`}