ModelPicker
Standalone model picker. Drop it into InputBar via leftActions/rightActions, a header, a settings sheet, or anywhere else. It does not depend on InputBar. Supports controlled and uncontrolled modes. Use ModelBadge for a read-only variant.
Getting Started
$pnpm add @sinups/ai-kit @mantine/core @mantine/hooks @tabler/icons-reactExamples
Uncontrolled
Inside InputBar
Read-only badge
Source
The component source as it ships in the package. Useful as a reference, or as a starting point if you want to fork a piece.
input/ModelPicker.tsx
import React, { memo, useCallback, useState } from 'react';
import { Box, UnstyledButton } from '@mantine/core';
import { IconCheck, IconChevronDown } from '@tabler/icons-react';
import type { ModelOption } from '../types';
import { cx } from '../utils/cx';
import { InputPopover } from './InputPopover';
import classes from './ModelPicker.module.css';
export interface ModelPickerProps {
models: ModelOption[];
/** Controlled selected model id */
value?: string;
/** Initial selected model id in uncontrolled mode */
defaultValue?: string;
onChange?: (modelId: string) => void;
/** Label shown when no model matches, `'Auto'` by default */
placeholder?: string;
className?: string;
style?: React.CSSProperties;
}
/** Toolbar dropdown for choosing the active model */
export const ModelPicker = memo(function ModelPicker({
models,
value,
defaultValue,
onChange,
placeholder = 'Auto',
className,
style,
}: ModelPickerProps) {
const isControlled = value !== undefined;
const [internalValue, setInternalValue] = useState(defaultValue);
const activeId = isControlled ? value : internalValue;
const activeModel = models.find((m) => m.id === activeId) ?? models[0];
const [open, setOpen] = useState(false);
const handleSelect = useCallback(
(id: string) => {
if (!isControlled) {
setInternalValue(id);
}
onChange?.(id);
setOpen(false);
},
[isControlled, onChange]
);
return (
<InputPopover
open={open}
onOpenChange={setOpen}
side="top"
align="start"
trigger={
<UnstyledButton
className={cx(classes.trigger, className)}
style={style}
aria-label="Select model"
>
<span className={classes.triggerName}>{activeModel?.name ?? placeholder}</span>
{activeModel?.version && (
<span className={classes.triggerVersion}>{activeModel.version}</span>
)}
<IconChevronDown size={12} className={classes.chevron} />
</UnstyledButton>
}
>
{models.map((model) => {
const isActive = model.id === activeModel?.id;
return (
<UnstyledButton
key={model.id}
onClick={() => handleSelect(model.id)}
className={classes.option}
data-active={isActive || undefined}
>
<span className={classes.optionLabel}>
{model.name}
{model.version && <span className={classes.optionVersion}>{model.version}</span>}
</span>
{isActive && <IconCheck size={14} className={classes.check} />}
</UnstyledButton>
);
})}
</InputPopover>
);
});
ModelPicker.displayName = 'ModelPicker';
export interface ModelBadgeProps {
models: ModelOption[];
value?: string;
/** Label shown when no model matches, `'Auto'` by default */
placeholder?: string;
className?: string;
style?: React.CSSProperties;
}
/** Read-only label of the active model, for toolbars without model switching */
export const ModelBadge = memo(function ModelBadge({
models,
value,
placeholder = 'Auto',
className,
style,
}: ModelBadgeProps) {
const activeModel = models.find((m) => m.id === value) ?? models[0];
return (
<Box className={cx(classes.badge, className)} style={style}>
<span className={classes.triggerName}>{activeModel?.name ?? placeholder}</span>
{activeModel?.version && <span className={classes.badgeVersion}>{activeModel.version}</span>}
</Box>
);
});
ModelBadge.displayName = 'ModelBadge';
API reference
Prop
Type
Required
models
ModelOption[]
Yes
value
string
No
Controlled selected model id
defaultValue
string
No
Initial selected model id in uncontrolled mode
onChange
(modelId: string) => void
No
placeholder
string
No
Label shown when no model matches, `'Auto'` by default
className
string
No
style
React.CSSProperties
No