EffortSelector
Pick the reasoning effort: Low, Medium, High or Max by default, or your own levels. The selected level's description is shown under the control. It is a segmented control from breakpoint (380px of its own width) and a select when narrower; variant="inline" renders a compact menu button for a composer toolbar. Pass thinking together with onThinkingChange to add an extended thinking switch.
Getting Started
$pnpm add @sinups/ai-kit @mantine/core @mantine/hooks @tabler/icons-reactExamples
Wide and narrow
Inline in a toolbar
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.
model-settings/EffortSelector/EffortSelector.tsx
import React, { memo, useId } from 'react';
import {
Box,
Button,
Input,
Menu,
SegmentedControl,
Select,
Stack,
Switch,
Text,
} from '@mantine/core';
import { useElementSize } from '@mantine/hooks';
import { IconBrain, IconCheck, IconChevronDown } from '@tabler/icons-react';
import { DEFAULT_EFFORT_LEVELS, findEffortLevel } from '../effort';
import type { EffortLevel, EffortLevelValue } from '../types';
import classes from './EffortSelector.module.css';
export interface EffortSelectorLabels {
label: string;
thinking: string;
thinkingDescription: string;
}
export const DEFAULT_EFFORT_SELECTOR_LABELS: EffortSelectorLabels = {
label: 'Reasoning effort',
thinking: 'Extended thinking',
thinkingDescription: 'Show the reasoning before the answer',
};
export interface EffortSelectorProps {
/** Selected level value */
value: EffortLevelValue;
/** Called with the picked level value */
onChange: (value: EffortLevelValue) => void;
/** Levels in order from the least to the most effort, `low`, `medium`, `high`, `max` by default */
levels?: EffortLevel[];
/** State of the extended thinking switch; the switch is rendered only together with `onThinkingChange` */
thinking?: boolean;
/** Called when extended thinking is toggled */
onThinkingChange?: (thinking: boolean) => void;
/** `default` renders a labeled control, `inline` renders a compact menu button for a composer toolbar, `default` by default */
variant?: 'default' | 'inline';
/** Component width in px from which the levels are a segmented control instead of a select, `380` by default */
breakpoint?: number;
/** Disables the control */
disabled?: boolean;
/** Overrides of the default English labels */
labels?: Partial<EffortSelectorLabels>;
/** Class name added to the root element */
className?: string;
/** Inline styles added to the root element */
style?: React.CSSProperties;
}
/** Reasoning effort picker: segmented control when wide, select when narrow, or a menu button in a toolbar */
export const EffortSelector = memo(function EffortSelector({
value,
onChange,
levels = DEFAULT_EFFORT_LEVELS,
thinking = false,
onThinkingChange,
variant = 'default',
breakpoint = 380,
disabled = false,
labels: labelsProp,
className,
style,
}: EffortSelectorProps) {
const labels = { ...DEFAULT_EFFORT_SELECTOR_LABELS, ...labelsProp };
const { ref, width } = useElementSize();
const labelId = useId();
const active = findEffortLevel(levels, value);
if (variant === 'inline') {
return (
<Menu position="top-start" width={260} shadow="md" disabled={disabled}>
<Menu.Target>
<Button
size="compact-xs"
variant="subtle"
color="gray"
fw={500}
leftSection={<IconBrain size={14} />}
rightSection={<IconChevronDown size={12} />}
aria-label={`${labels.label}: ${active?.label ?? value}`}
disabled={disabled}
className={className}
style={style}
>
{active?.label ?? value}
</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>{labels.label}</Menu.Label>
{levels.map((level) => (
<Menu.Item
key={level.value}
onClick={() => onChange(level.value)}
rightSection={level.value === value ? <IconCheck size={14} /> : undefined}
aria-current={level.value === value || undefined}
>
<Text size="sm">{level.label}</Text>
{level.description && (
<Text size="xs" c="dimmed">
{level.description}
</Text>
)}
</Menu.Item>
))}
{onThinkingChange && (
<>
<Menu.Divider />
<Menu.Item
closeMenuOnClick={false}
onClick={() => onThinkingChange(!thinking)}
rightSection={
<Switch
size="xs"
checked={thinking}
readOnly
tabIndex={-1}
aria-label={labels.thinking}
/>
}
>
<Text size="sm">{labels.thinking}</Text>
</Menu.Item>
</>
)}
</Menu.Dropdown>
</Menu>
);
}
const measuring = width === 0;
const isWide = width >= breakpoint;
return (
<Stack ref={ref} gap="xs" className={className} style={style}>
<Box>
<Input.Label id={labelId}>{labels.label}</Input.Label>
<Box className={classes.measured} data-measuring={measuring || undefined}>
{isWide ? (
<SegmentedControl
fullWidth
aria-labelledby={labelId}
value={value}
onChange={onChange}
disabled={disabled}
data={levels.map((level) => ({ value: level.value, label: level.label }))}
/>
) : (
<Select
aria-labelledby={labelId}
value={value}
allowDeselect={false}
disabled={disabled}
onChange={(next) => next !== null && onChange(next)}
data={levels.map((level) => ({ value: level.value, label: level.label }))}
/>
)}
</Box>
{active?.description && (
<Text size="xs" c="dimmed" mt={6} aria-live="polite">
{active.description}
</Text>
)}
</Box>
{onThinkingChange && (
<Switch
label={labels.thinking}
description={labels.thinkingDescription}
checked={thinking}
disabled={disabled}
onChange={(event) => onThinkingChange(event.currentTarget.checked)}
/>
)}
</Stack>
);
});
EffortSelector.displayName = 'EffortSelector';
API reference
Prop
Type
Required
value
EffortLevelValue
Yes
Selected level value
onChange
(value: EffortLevelValue) => void
Yes
Called with the picked level value
levels
EffortLevel[]
No
Levels in order from the least to the most effort, `low`, `medium`, `high`, `max` by default
thinking
boolean
No
State of the extended thinking switch; the switch is rendered only together with `onThinkingChange`
onThinkingChange
(thinking: boolean) => void
No
Called when extended thinking is toggled
variant
'default' | 'inline'
No
`default` renders a labeled control, `inline` renders a compact menu button for a composer toolbar, `default` by default
breakpoint
number
No
Component width in px from which the levels are a segmented control instead of a select, `380` by default
disabled
boolean
No
Disables the control
labels
Partial<EffortSelectorLabels>
No
Overrides of the default English labels
className
string
No
Class name added to the root element
style
React.CSSProperties
No
Inline styles added to the root element