import { ClaudeMark } from "./claude-mark"; import { GrokMark } from "./grok-mark"; import { ChatGptMark, ModelMark, modelHasMark } from "./model-mark"; import { modelDisplayName, modelLogoUrl } from "@opengeni/sdk/model-display"; import type { ClientModel, LatencyMode, ReasoningEffort } from "@opengeni/sdk"; import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, Globe2Icon, KeyRoundIcon, SparklesIcon, ZapIcon, } from "lucide-react"; import { motion, useReducedMotion } from "motion/react"; import { Suspense, lazy, useMemo, useId, useState, type CSSProperties, type ReactNode, } from "react"; import { cn } from "../lib/cn"; import { billingClassForMissingSelection, defaultModelPolicyPickerMessages, effectiveRows as sharedEffectiveRows, groupPresentationFor, SCOPED_BILLING_HINTS, type ModelPickerGroupPresentation, type ModelPolicyPickerMessages, } from "../model-picker-model"; import { MENU_CHEVRON_CLASS } from "../lib/menu-styles"; import { usePortalTokenSource, usePortalTokenStyle } from "../lib/use-portal-token-style"; import { findPickerRow, labelReasoningEffort, modelOffersEffortChoice, type PickerBillingClass, type PickerModelRow, } from "../model-policy"; import { useOpenGeniText, type OpenGeniText } from "../messages/runtime"; const LazyModelPolicyPickerMenu = lazy(() => import("./model-policy-picker-menu").then(({ ModelPolicyPickerPopover }) => ({ default: ModelPolicyPickerPopover, })), ); type ClientPickerModelRow = PickerModelRow; /** Presentation only; never changes model identity, billing or availability. */ export type ModelPolicyPickerGroupPresentation = ModelPickerGroupPresentation; export { defaultModelPolicyPickerMessages, groupPresentationFor, SCOPED_BILLING_HINTS }; export type { ModelPolicyPickerMessages }; export type ModelPolicyPickerProps = { /** Host branding for payment-source groups, shared by menu and trigger. */ groupPresentation?: ModelPolicyPickerGroupPresentation | undefined; /** Lightweight deployment models. Catalog rows take precedence when supplied. */ models?: ClientModel[] | undefined; /** Warn only when the draft actually contains images this model cannot view. */ hasImageAttachments?: boolean | undefined; /** Catalog-backed rows with availability and billing-class truth. */ rows?: PickerModelRow[] | undefined; model: string; effort: ReasoningEffort; latencyMode: LatencyMode; disabled?: boolean | undefined; loading?: boolean | undefined; error?: string | null | undefined; /** Controlled menu state. Omit for the built-in uncontrolled behavior. */ open?: boolean | undefined; /** Initial menu state when `open` is uncontrolled. */ defaultOpen?: boolean | undefined; onOpenChange?: ((open: boolean) => void) | undefined; /** Non-Codex models stay visible but cannot be selected. */ codexOnly?: boolean | undefined; /** Identity of the current session scope. */ sessionKey?: string | undefined; /** Prefer bottom on new-chat surfaces and top for bottom-docked composers. */ menuSide?: "top" | "bottom" | undefined; /** * Default true: show models the way people choose them, with clean names, * maker marks, one "API keys" group for organization- and workspace-connected * keys, and one row for identical copies. Models settings passes false to * keep its scope-aware presentation (raw catalog labels, scoped groups). */ collapseScopes?: boolean | undefined; /** Hide latency controls on surfaces whose saved policy does not include latency. */ allowLatencyMode?: boolean | undefined; /** Hide effort and latency controls when the saved policy chooses only a model identity. */ allowModelOptions?: boolean | undefined; /** Settings → Models. Shown when the catalog has no model that can run. */ connectModelsHref?: string | undefined; /** Classes for the portalled menu surface. Prefer --og-* tokens for theming. */ contentClassName?: string | undefined; /** Inline styles for the portalled menu, applied after inherited --og-* tokens. */ contentStyle?: CSSProperties | undefined; className?: string | undefined; /** * "pill" (default) is the composer's quiet rounded trigger with the effort. * "field" is a settings control: a bordered rectangle with the model name * and `triggerMeta` (for example the payer) in muted text. The effort stays * in the menu. */ triggerStyle?: "pill" | "field" | undefined; /** Muted text after the model name in the "field" trigger. */ triggerMeta?: ReactNode; messages?: Partial | undefined; onModelChange: (modelId: string) => void; onEffortChange: (effort: ReasoningEffort) => void; onLatencyModeChange: (latencyMode: LatencyMode) => void; }; const SLIDE_EASE = [0.22, 1, 0.36, 1] as const; export function BillingClassMark(props: { billingClass: PickerBillingClass; presentation?: ModelPolicyPickerGroupPresentation[PickerBillingClass] | undefined; className?: string | undefined; "aria-label"?: string | undefined; }) { const t = useOpenGeniText(); const labels: Record = { opengeni_credits: t("modelPolicyPicker.models"), external: t("modelPolicyPicker.externalProvider"), codex_subscription: t("modelPolicyPicker.codex"), supergrok_subscription: "SuperGrok", claude_subscription: t("modelPolicyPicker.claude"), byok: t("modelPolicyPicker.apiKey"), organization_byok: t("modelPolicyPicker.apiKey"), }; if (props.presentation?.icon === null) return null; const label = props["aria-label"] ?? props.presentation?.label ?? labels[props.billingClass]; const accessibility = label.length === 0 ? { "aria-hidden": true as const } : { role: "img" as const, "aria-label": label }; const shell = cn( "inline-flex size-3.5 shrink-0 items-center justify-center overflow-hidden text-og-fg-subtle [&>svg]:size-full [&>img]:size-full", props.className, ); const mark = "size-3.5"; return ( {props.presentation?.icon !== undefined ? ( props.presentation.icon ) : props.billingClass === "opengeni_credits" ? ( ) : props.billingClass === "external" ? ( ) : props.billingClass === "codex_subscription" ? ( ) : props.billingClass === "claude_subscription" ? ( ) : props.billingClass === "supergrok_subscription" ? ( ) : ( )} ); } /** * The trigger shows who makes the chosen model. Subscription rails already * carry their maker's mark (ChatGPT, Claude, Grok). Configured catalog logos win * in collapsed model presentation; otherwise explicit host branding wins and models show * the maker's logo instead of a generic key, falling back to the group mark. */ function SelectedModelMark(input: { props: ModelPolicyPickerProps; selected: ClientPickerModelRow | null; }) { const t = useOpenGeniText(); const billingClass = input.selected?.billingClass ?? billingClassForMissingSelection(input.props.model); const presentation = groupPresentationFor(input.props, billingClass); const model = input.selected?.catalog ?? input.props.model; if (input.props.collapseScopes === false) { return ( ); } if (modelLogoUrl(model)) { return ; } if ( presentation?.icon !== undefined || BRANDED_BILLING_CLASSES.has(billingClass) || !modelHasMark(model) ) { return ( ); } return ; } const SCOPED_MARK_LABELS = (t: OpenGeniText): Partial> => ({ byok: t("modelPolicyPicker.workspaceProviderAccount"), organization_byok: t("modelPolicyPicker.organizationProviderAccount"), }); /** A selection the catalog no longer lists: its clean name (settings keep the id). */ function fallbackName(props: ModelPolicyPickerProps): string { return props.collapseScopes === false ? props.model : modelDisplayName(props.model); } const BRANDED_BILLING_CLASSES: ReadonlySet = new Set([ "codex_subscription", "claude_subscription", "supergrok_subscription", ]); export function effectiveRows( t: OpenGeniText, props: ModelPolicyPickerProps, ): ClientPickerModelRow[] { return sharedEffectiveRows(t, props); } export function PickerNavRow(props: { label: string; hint?: string | undefined; icon?: ReactNode; trailing?: ReactNode; showChevron?: boolean | undefined; disabled?: boolean | undefined; title?: string | undefined; description?: string | undefined; active?: boolean | undefined; testId?: string | undefined; onClick: () => void; }) { return ( ); } export function PickerBackHeader(props: { label: string; icon?: ReactNode; onBack: () => void; trailing?: ReactNode; }) { return (
{props.trailing ?
{props.trailing}
: null}
); } export function PickerAnimatedPage(props: { pageKey: string; direction: 1 | -1; children: ReactNode; }) { const reduceMotion = useReducedMotion(); return ( {props.children} ); } /** @internal Shared controlled/uncontrolled state seam. */ export function useModelPolicyPickerState(props: ModelPolicyPickerProps) { const t = useOpenGeniText(); const [uncontrolledOpen, setUncontrolledOpen] = useState(props.defaultOpen ?? false); const open = props.open ?? uncontrolledOpen; const setOpen = (next: boolean) => { if (props.open === undefined) setUncontrolledOpen(next); props.onOpenChange?.(next); }; return { open, setOpen, rows: effectiveRows(t, props) }; } export function ModelPolicyPicker(props: ModelPolicyPickerProps) { const t = useOpenGeniText(); const contentId = useId(); const trigger = usePortalTokenSource(); const portalStyle = usePortalTokenStyle(trigger.source); const messages = useMemo( () => ({ ...defaultModelPolicyPickerMessages(t), ...props.messages }), [props.messages, t], ); const { open, setOpen, rows } = useModelPolicyPickerState(props); const selected = findPickerRow(rows, props.model); const needsModel = !rows.some((row) => row.selectable) && (rows.length > 0 || Boolean(props.connectModelsHref)); const selectedDescription = needsModel ? messages.connectTitle : [selected?.label ?? fallbackName(props), labelReasoningEffort(t, props.effort)].join(" · "); if (props.loading) { return ( ); } if (props.triggerStyle === "field") { return ( <> {open ? ( ) : null} ); } return ( <> {open ? ( ) : null} ); }