import type { ClientModel, LatencyMode, ReasoningEffort } from "@opengeni/sdk"; import { ArrowRightIcon, CheckIcon, SearchIcon, ZapIcon } from "lucide-react"; import { useRef, useState, type RefObject, type CSSProperties } from "react"; import { Popover, RadioGroup } from "radix-ui"; import { containedPortal } from "../ui/portal-container"; import { cn } from "../lib/cn"; import { MENU_CHECK_CLASS, MENU_LABEL_CLASS, MENU_NOTE_CLASS, MENU_SURFACE_CLASS, } from "../lib/menu-styles"; import { labelReasoningEffort, type PickerModelRow } from "../model-policy"; import { compactEffortLabel, groupPresentationFor, modelPickerChoice, modelPickerMenuView, } from "../model-picker-model"; import { BillingClassMark, defaultModelPolicyPickerMessages, PickerNavRow, type ModelPolicyPickerProps, } from "./model-policy-picker"; import { ModelMark } from "./model-mark"; import { useOpenGeniText, type OpenGeniText } from "../messages/runtime"; const PopoverContainedPortal = containedPortal(Popover.Portal); type ClientPickerModelRow = PickerModelRow; /** Model selection stays flat; reasoning never becomes a navigation destination. */ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) { const t = useOpenGeniText(); const [query, setQuery] = useState(""); const view = modelPickerMenuView(t, props, query); const { messages, rows, groups } = view; const selected = view.selected; const choose = (row: ClientPickerModelRow) => { if (props.disabled) return; const choice = modelPickerChoice(props, row); if (!choice) return; if (choice.model !== null) props.onModelChange(choice.model); // Hosts may commit the combined model/effort draft through this callback. props.onEffortChange(choice.effort); if (choice.latencyMode !== null) props.onLatencyModeChange(choice.latencyMode); }; const modelRow = (row: ClientPickerModelRow) => ( ) } label={row.label} hint={row.unavailableReason ?? row.fundingHint} disabled={props.disabled || !row.selectable} title={[row.label, row.unavailableReason ?? row.fundingHint].filter(Boolean).join(" · ")} active={row.selectable && row.id === props.model} showChevron={false} trailing={ row.catalog.cost === "free" || row.id === props.model ? ( {row.catalog.cost === "free" ? ( {messages.free} ) : null} {row.id === props.model ? ( ) : null} ) : null } testId={`model-picker-choice-${row.id}`} onClick={() => choose(row)} /> ); return (
{ if ( !(event.target instanceof HTMLButtonElement) || !["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key) ) return; const buttons = Array.from( event.currentTarget.querySelectorAll( '[data-testid^="model-picker-choice-"]:not(:disabled)', ), ); const index = buttons.indexOf(event.target); if (index < 0) return; event.preventDefault(); const next = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + buttons.length) % buttons.length; buttons[next]?.focus(); }} > {rows.some((row) => row.selectable) ? (
) : null} {props.error ? (

{props.error}

) : null}
{props.loading ? ( // Rows at the menu's row height, never a sentence, so nothing jumps.
{[58, 44, 66, 50].map((width) => ( ))}
) : rows.some((row) => row.selectable) ? ( <> {groups.map((group) => (
{group.label}
{group.description ? (

{group.description}

) : null} {group.rows.map(modelRow)}
))} {view.matchCount === 0 ? (

{view.searching ? messages.noMatches : messages.noModels}

) : null} ) : props.connectModelsHref || rows.length > 0 ? ( ) : (

{messages.noModels}

)}
{view.anySelectable && selected && (view.unsupportedAttachments || (props.allowModelOptions !== false && view.thinking)) ? (
{view.unsupportedAttachments ? (

{messages.unsupportedAttachments}

) : null} {props.allowModelOptions !== false ? : null}
) : null}
); } const CONNECT_MODEL_OPTIONS = ( t: OpenGeniText, ): Array<{ billingClass: "codex_subscription" | "supergrok_subscription" | "byok" | "external"; title: string; detail: string; }> => [ { billingClass: "codex_subscription", title: t("modelPolicyPickerMenu.codex"), detail: "ChatGPT", }, { billingClass: "supergrok_subscription", title: "SuperGrok", detail: "xAI" }, { billingClass: "byok", title: t("modelPolicyPickerMenu.aiGateway"), detail: t("modelPolicyPickerMenu.vercel"), }, { billingClass: "external", title: "OpenRouter", detail: t("modelPolicyPickerMenu.apiKey") }, ]; function ConnectModelsPanel(props: { href: string | undefined; messages: ReturnType; }) { const t = useOpenGeniText(); return (

{props.messages.connectTitle}

{props.messages.connectBody}

{CONNECT_MODEL_OPTIONS(t).map((option) => { const content = ( <> {option.title} {option.detail} ); const className = "flex min-w-0 items-center gap-2 rounded-og-md px-1.5 py-1.5 outline-hidden transition-colors hover:bg-og-hover focus-visible:ring-2 focus-visible:ring-og-accent/40"; return props.href ? ( {content} ) : (
{content}
); })}
{props.href ? ( {props.messages.connectAction} ) : (

{props.messages.noModels}

)}
); } function ModelThinkingControls(props: ModelPolicyPickerProps) { const t = useOpenGeniText(); const view = modelPickerMenuView(t, props, ""); const thinking = view.thinking; if (!view.selected || !thinking) return null; const messages = view.messages; const efforts = thinking.efforts; const showThinking = thinking.showThinking; const showFast = thinking.showFast; const speeds = thinking.speeds; const selectedSpeed = speeds?.find((speed) => speed.id === props.latencyMode) ?? speeds?.[0]; return (
{showThinking || showFast ? (
{showThinking ? {messages.thinking} : } {showFast ? ( ) : null}
) : null} {showThinking ? ( props.onEffortChange(value as ReasoningEffort)} orientation="horizontal" className="flex flex-wrap gap-0.5 rounded-og-md bg-og-surface-2 p-0.5" > {efforts.map((effort) => ( {compactEffortLabel(t, effort, labelReasoningEffort(t, effort))} ))} ) : null} {speeds && selectedSpeed ? (
{messages.speed} {selectedSpeed.hint ? ( {selectedSpeed.hint} ) : null}
props.onLatencyModeChange(value as LatencyMode)} orientation="horizontal" className="flex flex-wrap gap-0.5 rounded-og-md bg-og-surface-2 p-0.5" > {speeds.map((speed) => ( {speed.id === "standard" ? null : ( )} {speed.label} ))}
) : null}
); } export function ModelPolicyPickerPopover( props: ModelPolicyPickerProps & { anchor: RefObject; contentId: string; portalStyle: CSSProperties; }, ) { const t = useOpenGeniText(); const outside = useRef(false); const messages = { ...defaultModelPolicyPickerMessages(t), ...props.messages }; return ( props.onOpenChange?.(open)}> { if (props.anchor.current?.contains(event.target as Node)) event.preventDefault(); else outside.current = true; }} onCloseAutoFocus={(event) => { event.preventDefault(); if (!outside.current) props.anchor.current?.focus(); }} className={cn( MENU_SURFACE_CLASS, "og-root og-model-policy-menu z-50 flex max-h-[min(32rem,var(--radix-popover-content-available-height))] w-[22rem] max-w-[calc(100vw-16px)] flex-col overflow-hidden p-[var(--og-model-picker-menu-padding)]", props.contentClassName, )} style={{ ...props.portalStyle, ...props.contentStyle }} data-testid="model-picker-content" > ); }