fix: recipe studio dialog combobox click-select + simplify model provider form

This commit is contained in:
Shine1i 2026-02-26 10:03:47 +01:00
commit 1de4b73244
5 changed files with 102 additions and 91 deletions

View file

@ -162,20 +162,20 @@ function ComboboxContent({
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="isolate z-50"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"
data-chips={!!anchor}
className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 shadow-border ring-1 ring-border *:data-[slot=input-group]:bg-input/30 max-h-72 min-w-36 overflow-hidden rounded-xl corner-squircle duration-100 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-9 *:data-[slot=input-group]:border-none *:data-[slot=input-group]:shadow-none group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) data-[chips=true]:min-w-(--anchor-width)",
className,
)}
{...props}
/>
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="isolate z-[120] pointer-events-auto"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"
data-chips={!!anchor}
className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 shadow-border ring-1 ring-border *:data-[slot=input-group]:bg-input/30 max-h-72 min-w-36 overflow-hidden rounded-xl corner-squircle duration-100 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-9 *:data-[slot=input-group]:border-none *:data-[slot=input-group]:shadow-none group/combobox-content relative pointer-events-auto max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) data-[chips=true]:min-w-(--anchor-width)",
className,
)}
{...props}
/>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
);

View file

@ -14,19 +14,6 @@ export function InlineModel(props: InlineModelProps): ReactElement {
if (props.config.kind === "model_provider") {
return (
<div className="grid gap-3 sm:grid-cols-2">
<InlineField label="Provider type">
<Input
className="nodrag h-8 w-full text-xs"
placeholder="openai-compatible"
value={props.config.provider_type}
onChange={(event) =>
props.onUpdate({
// biome-ignore lint/style/useNamingConvention: api schema
provider_type: event.target.value,
})
}
/>
</InlineField>
<InlineField label="Endpoint">
<Input
className="nodrag h-8 w-full text-xs"
@ -35,6 +22,19 @@ export function InlineModel(props: InlineModelProps): ReactElement {
onChange={(event) => props.onUpdate({ endpoint: event.target.value })}
/>
</InlineField>
<InlineField label="API key">
<Input
className="nodrag h-8 w-full text-xs"
placeholder="Optional"
value={props.config.api_key ?? ""}
onChange={(event) =>
props.onUpdate({
// biome-ignore lint/style/useNamingConvention: api schema
api_key: event.target.value,
})
}
/>
</InlineField>
</div>
);
}

View file

@ -147,7 +147,7 @@ export function LlmGeneralTab({
/>
<Textarea
id={promptId}
className="corner-squircle nodrag"
className="corner-squircle nodrag max-h-[600px] overflow-auto"
value={config.prompt}
onChange={(event) => onUpdate({ prompt: event.target.value })}
/>
@ -177,7 +177,7 @@ export function LlmGeneralTab({
/>
<Textarea
id={systemPromptId}
className="corner-squircle nodrag"
className="corner-squircle nodrag max-h-[600px] overflow-auto"
value={config.system_prompt}
onChange={(event) => onUpdate({ system_prompt: event.target.value })}
/>

View file

@ -1,6 +1,14 @@
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import type { ReactElement } from "react";
import { ArrowDown01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { type ReactElement, useState } from "react";
import type { ModelProviderConfig } from "../../types";
import { FieldLabel } from "../shared/field-label";
import { NameField } from "../shared/name-field";
@ -14,8 +22,8 @@ export function ModelProviderDialog({
config,
onUpdate,
}: ModelProviderDialogProps): ReactElement {
const [optionalOpen, setOptionalOpen] = useState(false);
const endpointId = `${config.id}-endpoint`;
const providerTypeId = `${config.id}-provider-type`;
const apiKeyEnvId = `${config.id}-api-key-env`;
const apiKeyId = `${config.id}-api-key`;
const extraHeadersId = `${config.id}-extra-headers`;
@ -33,22 +41,6 @@ export function ModelProviderDialog({
value={config.name}
onChange={(value) => onUpdate({ name: value })}
/>
<div className="grid gap-2">
<FieldLabel
label="Provider type"
htmlFor={providerTypeId}
hint="Provider adapter type, e.g. openai or openrouter."
/>
<Input
id={providerTypeId}
className="nodrag"
placeholder="openai"
value={config.provider_type}
onChange={(event) =>
updateField("provider_type", event.target.value)
}
/>
</div>
<div className="grid gap-2">
<FieldLabel
label="Endpoint"
@ -63,20 +55,6 @@ export function ModelProviderDialog({
onChange={(event) => updateField("endpoint", event.target.value)}
/>
</div>
<div className="grid gap-2">
<FieldLabel
label="API key env (optional)"
htmlFor={apiKeyEnvId}
hint="Env var name to read secret key from runtime."
/>
<Input
id={apiKeyEnvId}
className="nodrag"
placeholder="OPENAI_API_KEY"
value={config.api_key_env ?? ""}
onChange={(event) => updateField("api_key_env", event.target.value)}
/>
</div>
<div className="grid gap-2">
<FieldLabel
label="API key (optional)"
@ -90,36 +68,69 @@ export function ModelProviderDialog({
onChange={(event) => updateField("api_key", event.target.value)}
/>
</div>
<div className="grid gap-2">
<FieldLabel
label="Extra headers (JSON)"
htmlFor={extraHeadersId}
hint="Optional request headers merged into every call."
/>
<Textarea
id={extraHeadersId}
className="corner-squircle nodrag"
placeholder='{"X-Header": "value"}'
value={config.extra_headers ?? ""}
onChange={(event) =>
updateField("extra_headers", event.target.value)
}
/>
</div>
<div className="grid gap-2">
<FieldLabel
label="Extra body (JSON)"
htmlFor={extraBodyId}
hint="Optional payload fields merged into requests."
/>
<Textarea
id={extraBodyId}
className="corner-squircle nodrag"
placeholder='{"key": "value"}'
value={config.extra_body ?? ""}
onChange={(event) => updateField("extra_body", event.target.value)}
/>
</div>
<Collapsible open={optionalOpen} onOpenChange={setOptionalOpen}>
<CollapsibleTrigger asChild={true}>
<Button
type="button"
variant="outline"
size="sm"
className="w-full justify-between"
>
Optional
<HugeiconsIcon
icon={ArrowDown01Icon}
strokeWidth={2}
className={
optionalOpen ? "rotate-180 transition-transform" : "transition-transform"
}
/>
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-3 space-y-4">
<div className="grid gap-2">
<FieldLabel
label="API key env (optional)"
htmlFor={apiKeyEnvId}
hint="Env var name to read secret key from runtime."
/>
<Input
id={apiKeyEnvId}
className="nodrag"
placeholder="OPENAI_API_KEY"
value={config.api_key_env ?? ""}
onChange={(event) => updateField("api_key_env", event.target.value)}
/>
</div>
<div className="grid gap-2">
<FieldLabel
label="Extra headers (JSON)"
htmlFor={extraHeadersId}
hint="Optional request headers merged into every call."
/>
<Textarea
id={extraHeadersId}
className="corner-squircle nodrag"
placeholder='{"X-Header": "value"}'
value={config.extra_headers ?? ""}
onChange={(event) => updateField("extra_headers", event.target.value)}
/>
</div>
<div className="grid gap-2">
<FieldLabel
label="Extra body (JSON)"
htmlFor={extraBodyId}
hint="Optional payload fields merged into requests."
/>
<Textarea
id={extraBodyId}
className="corner-squircle nodrag"
placeholder='{"key": "value"}'
value={config.extra_body ?? ""}
onChange={(event) => updateField("extra_body", event.target.value)}
/>
</div>
</CollapsibleContent>
</Collapsible>
</div>
);
}

View file

@ -19,7 +19,7 @@ export function buildModelProvider(
name: config.name,
endpoint: config.endpoint,
// biome-ignore lint/style/useNamingConvention: api schema
provider_type: config.provider_type,
provider_type: "openai",
// biome-ignore lint/style/useNamingConvention: api schema
api_key_env: config.api_key_env?.trim() || undefined,
// biome-ignore lint/style/useNamingConvention: api schema