refactor: group assistant runtime controls

This commit is contained in:
ajnart committed 2026-07-31 06:00:49 +02:00
1 parent e6981a7f44
commit 1d15b614f3
3 files changed
+192 -64

No files matched your search

@@ -740,30 +740,70 @@
flex: 1 1 auto;
}
.modelSelect {
width: min(15rem, 52%);
min-width: 8rem;
.runtimeButtonGroup {
min-width: 0;
max-width: 100%;
}
.reasoningSelect {
width: 8.75rem;
flex: 0 0 auto;
}
.runtimeSelectInput {
border-color: transparent;
border-radius: var(--mantine-radius-md);
.runtimeButton {
min-width: 0;
border-color: var(--mantine-color-default-border);
color: var(--mantine-color-text);
background: var(--mantine-color-body);
font-weight: 600;
}
.runtimeSelectInput:hover {
.runtimeButton:hover {
background: var(--mantine-color-default-hover);
}
.runtimeSelectInput:focus,
.runtimeSelectInput:focus-within {
border-color: var(--mantine-primary-color-filled);
.runtimeButtonLabel {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.modelButton {
min-width: 8rem;
max-width: 15rem;
flex: 1 1 auto;
}
.reasoningButton {
min-width: 7.5rem;
max-width: 10.5rem;
flex: 0 1 auto;
}
.modelDropdown {
max-width: calc(100vw - 2rem);
}
.modelOptions {
max-height: min(18rem, 42dvh);
overflow-y: auto;
overscroll-behavior: contain;
}
.modelOption {
padding-block: 0.45rem;
}
.modelOptionText {
min-width: 0;
flex: 1;
}
.runtimeOptionCheck {
flex: 0 0 auto;
color: var(--mantine-primary-color-filled);
}
.runtimeMenuItem[data-selected] {
color: var(--mantine-primary-color-filled);
background: var(--mantine-primary-color-light);
font-weight: 600;
}
.triggerPopover {
@@ -918,10 +958,17 @@
width: 100%;
}
.modelSelect {
width: auto;
.runtimeButtonGroup {
width: 100%;
}
.modelButton {
min-width: 0;
flex: 1 1 auto;
max-width: none;
}
.reasoningButton {
max-width: 9rem;
}
.suggestions {
@@ -970,8 +1017,9 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.reasoningSelect {
width: 7.75rem;
.reasoningButton {
min-width: 2.5rem;
max-width: 7.75rem;
}
.approvalActions {
@@ -36,19 +36,21 @@ import {
Box,
Button,
Collapse,
Combobox,
Divider,
FocusTrap,
Group,
Loader,
Menu,
Popover,
RingProgress,
ScrollArea,
Select,
Stack,
Text,
ThemeIcon,
Tooltip,
UnstyledButton,
useCombobox,
} from "@mantine/core";
import { useWindowEvent } from "@mantine/hooks";
import {
@@ -1212,45 +1214,123 @@ const RuntimeControls = ({
onReasoningChange,
}: ComposerProps) => {
const t = useScopedI18n("common.assistant");
const [modelSearch, setModelSearch] = useState("");
const selectedModel = models.find((model) => model.id === modelId);
const normalizedModelSearch = modelSearch.trim().toLocaleLowerCase();
const visibleModels =
normalizedModelSearch.length === 0
? models
: models.filter(
(model) =>
model.name.toLocaleLowerCase().includes(normalizedModelSearch) ||
model.id.toLocaleLowerCase().includes(normalizedModelSearch),
);
const modelCombobox = useCombobox({
onDropdownOpen: () => {
modelCombobox.selectActiveOption();
requestAnimationFrame(() => modelCombobox.focusSearchInput());
},
onDropdownClose: () => {
modelCombobox.resetSelectedOption();
setModelSearch("");
},
});
const selectModel = (value: string) => {
if (!models.some((model) => model.id === value)) return;
onModelChange(value);
modelCombobox.closeDropdown();
};
return (
<Group className={classes.runtimeControls} gap={6} wrap="nowrap">
<Select
className={classes.modelSelect}
classNames={{ input: classes.runtimeSelectInput }}
size="xs"
value={modelId}
onChange={(value) => {
if (value) onModelChange(value);
}}
data={models.map((model) => ({ value: model.id, label: model.name }))}
searchable
allowDeselect={false}
disabled={modelOptionsLoading || models.length === 0}
leftSection={modelOptionsLoading ? <Loader size={13} /> : <IconSparkles size={14} />}
aria-label={t("runtime.model")}
placeholder={t("runtime.model")}
nothingFoundMessage={t("runtime.noModels")}
comboboxProps={{ withinPortal: true, position: "top-start" }}
/>
<Select
className={classes.reasoningSelect}
classNames={{ input: classes.runtimeSelectInput }}
size="xs"
value={reasoning}
onChange={(value) => {
if (value && assistantReasoningModes.includes(value as AssistantReasoningMode)) {
onReasoningChange(value as AssistantReasoningMode);
}
}}
data={assistantReasoningModes.map((mode) => ({
value: mode,
label: t(`runtime.reasoning.${mode}`),
}))}
allowDeselect={false}
leftSection={<IconBrain size={14} />}
aria-label={t("runtime.thinking")}
comboboxProps={{ withinPortal: true, position: "top-end" }}
/>
<Group className={classes.runtimeControls} gap={0} wrap="nowrap">
<Button.Group
className={classes.runtimeButtonGroup}
aria-label={`${t("runtime.model")}, ${t("runtime.thinking")}`}
>
<Combobox store={modelCombobox} onOptionSubmit={selectModel} withinPortal position="top-start" width={320}>
<Combobox.Target>
<Button
className={classes.modelButton}
classNames={{ root: classes.runtimeButton, label: classes.runtimeButtonLabel }}
variant="default"
size="compact-sm"
disabled={modelOptionsLoading || models.length === 0}
leftSection={modelOptionsLoading ? <Loader size={13} /> : <IconSparkles size={14} />}
rightSection={<Combobox.Chevron size="xs" />}
onClick={() => modelCombobox.toggleDropdown()}
aria-label={`${t("runtime.model")}: ${selectedModel?.name ?? t("runtime.noModels")}`}
title={selectedModel?.name}
>
{selectedModel?.name ?? t("runtime.model")}
</Button>
</Combobox.Target>
<Combobox.Dropdown className={classes.modelDropdown}>
<Combobox.Search
value={modelSearch}
onChange={(event) => {
setModelSearch(event.currentTarget.value);
modelCombobox.updateSelectedOptionIndex();
}}
placeholder={t("runtime.model")}
aria-label={t("runtime.model")}
/>
<Combobox.Options className={classes.modelOptions}>
{visibleModels.map((model) => (
<Combobox.Option
className={classes.modelOption}
key={model.id}
value={model.id}
active={model.id === modelId}
>
<Group gap="xs" wrap="nowrap">
<Stack gap={0} className={classes.modelOptionText}>
<Text size="sm" fw={model.id === modelId ? 650 : 500} lineClamp={1}>
{model.name}
</Text>
<Text size="xs" c="dimmed" lineClamp={1}>
{model.id}
</Text>
</Stack>
{model.id === modelId && <IconCheck size={15} className={classes.runtimeOptionCheck} />}
</Group>
</Combobox.Option>
))}
{visibleModels.length === 0 && <Combobox.Empty>{t("runtime.noModels")}</Combobox.Empty>}
</Combobox.Options>
</Combobox.Dropdown>
</Combobox>
<Menu position="top-end" width={210} withinPortal>
<Menu.Target>
<Button
className={classes.reasoningButton}
classNames={{ root: classes.runtimeButton, label: classes.runtimeButtonLabel }}
variant="default"
size="compact-sm"
leftSection={<IconBrain size={14} />}
rightSection={<IconChevronUp size={14} />}
aria-label={`${t("runtime.thinking")}: ${t(`runtime.reasoning.${reasoning}`)}`}
title={t(`runtime.reasoning.${reasoning}`)}
>
{t(`runtime.reasoning.${reasoning}`)}
</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>{t("runtime.thinking")}</Menu.Label>
{assistantReasoningModes.map((mode) => (
<Menu.Item
key={mode}
className={classes.runtimeMenuItem}
data-selected={mode === reasoning || undefined}
rightSection={mode === reasoning ? <IconCheck size={15} /> : null}
onClick={() => onReasoningChange(mode)}
>
{t(`runtime.reasoning.${mode}`)}
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
</Button.Group>
</Group>
);
};