ModelSettingsPanel

Assemble a model settings screen in SettingsLayout: Model (model select and EffortSelector), Output style (OutputStylePicker), Usage (UsagePanel) and Status (StatusPanel). A section appears only when its prop is passed, so the same panel works for a model picker alone or a full settings page. The active section can be controlled with activeSection and onActiveSectionChange. Navigation sits beside the content when wide and turns into a section picker in a narrow widget.

Getting Started
$pnpm add @sinups/ai-kit @mantine/core @mantine/hooks @tabler/icons-react
Examples
Wide
Narrow
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/ModelSettingsPanel/ModelSettingsPanel.tsx
import React, { memo, useId, useState } from 'react';
import { Select, Stack } from '@mantine/core';
import { IconActivity, IconBrain, IconChartBar, IconMessageChatbot } from '@tabler/icons-react';
import type { SettingsNavItem } from '../../primitives/SettingsLayout/settings-nav';
import { SettingRow } from '../../primitives/SettingsLayout/SettingRow';
import { SettingsLayout } from '../../primitives/SettingsLayout/SettingsLayout';
import { SettingsSection } from '../../primitives/SettingsLayout/SettingsSection';
import type { ModelOption } from '../../types';
import { EffortSelector, type EffortSelectorProps } from '../EffortSelector/EffortSelector';
import {
  OutputStylePicker,
  type OutputStylePickerProps,
} from '../OutputStylePicker/OutputStylePicker';
import { StatusPanel, type StatusPanelProps } from '../StatusPanel/StatusPanel';
import { UsagePanel, type UsagePanelProps } from '../UsagePanel/UsagePanel';

export type ModelSettingsSection = 'model' | 'output-style' | 'usage' | 'status';

export interface ModelSettingsPanelLabels {
  title: string;
  model: string;
  modelDescription: string;
  modelLabel: string;
  modelRowDescription: string;
  effort: string;
  outputStyle: string;
  outputStyleDescription: string;
  usage: string;
  status: string;
}

export const DEFAULT_MODEL_SETTINGS_PANEL_LABELS: ModelSettingsPanelLabels = {
  title: 'Model settings',
  model: 'Model',
  modelDescription: 'Model and reasoning',
  modelLabel: 'Default model',
  modelRowDescription: 'Used for new conversations',
  effort: 'Reasoning',
  outputStyle: 'Output style',
  outputStyleDescription: 'How answers are written',
  usage: 'Usage',
  status: 'Status',
};

export interface ModelSettingsPanelProps {
  /** Models offered in the model select */
  models?: ModelOption[];
  /** Id of the selected model */
  model?: string | null;
  /** Called with the id of the picked model */
  onModelChange?: (modelId: string) => void;
  /** Reasoning effort control, rendered in the model section */
  effort?: Omit<EffortSelectorProps, 'variant'>;
  /** Output style section */
  outputStyle?: Omit<OutputStylePickerProps, 'label' | 'description' | 'labelledBy'>;
  /** Usage section */
  usage?: UsagePanelProps;
  /** Status section */
  status?: StatusPanelProps;
  /** Controlled active section */
  activeSection?: ModelSettingsSection;
  /** Called with the picked section */
  onActiveSectionChange?: (section: ModelSettingsSection) => void;
  /** Section shown first in uncontrolled mode, the first available one by default */
  defaultActiveSection?: ModelSettingsSection;
  /** Overrides of the default English labels */
  labels?: Partial<ModelSettingsPanelLabels>;
  /** Class name added to the root element */
  className?: string;
  /** Inline styles added to the root element */
  style?: React.CSSProperties;
}

/** Settings screen for model, reasoning effort, output style, usage and status; sections appear for the props given */
export const ModelSettingsPanel = memo(function ModelSettingsPanel({
  models,
  model,
  onModelChange,
  effort,
  outputStyle,
  usage,
  status,
  activeSection,
  onActiveSectionChange,
  defaultActiveSection,
  labels: labelsProp,
  className,
  style,
}: ModelSettingsPanelProps) {
  const labels = { ...DEFAULT_MODEL_SETTINGS_PANEL_LABELS, ...labelsProp };
  const modelSelectId = useId();
  const outputStyleTitleId = useId();
  const hasModel = Boolean((models && models.length > 0) || effort);

  const sections: SettingsNavItem[] = [];
  if (hasModel) {
    sections.push({
      id: 'model',
      label: labels.model,
      description: labels.modelDescription,
      icon: <IconBrain size={16} />,
    });
  }
  if (outputStyle) {
    sections.push({
      id: 'output-style',
      label: labels.outputStyle,
      description: labels.outputStyleDescription,
      icon: <IconMessageChatbot size={16} />,
    });
  }
  if (usage) {
    sections.push({ id: 'usage', label: labels.usage, icon: <IconChartBar size={16} /> });
  }
  if (status) {
    sections.push({ id: 'status', label: labels.status, icon: <IconActivity size={16} /> });
  }

  const [internalSection, setInternalSection] = useState<string | undefined>(defaultActiveSection);
  const requested = activeSection ?? internalSection;
  const active = sections.some((section) => section.id === requested)
    ? requested!
    : (sections[0]?.id ?? 'model');

  const handleChange = (id: string) => {
    setInternalSection(id);
    onActiveSectionChange?.(id as ModelSettingsSection);
  };

  let content: React.ReactNode = null;
  if (active === 'model' && hasModel) {
    content = (
      <SettingsSection title={labels.model} description={labels.modelDescription}>
        {models && models.length > 0 && (
          <SettingRow
            label={labels.modelLabel}
            description={labels.modelRowDescription}
            htmlFor={modelSelectId}
            control={
              <Select
                id={modelSelectId}
                data={models.map((item) => ({
                  value: item.id,
                  label: item.version ? `${item.name} ${item.version}` : item.name,
                }))}
                value={model ?? null}
                allowDeselect={false}
                onChange={(value) => value !== null && onModelChange?.(value)}
              />
            }
          />
        )}
        {effort && <EffortSelector {...effort} />}
      </SettingsSection>
    );
  } else if (active === 'output-style' && outputStyle) {
    content = (
      <SettingsSection
        title={<span id={outputStyleTitleId}>{labels.outputStyle}</span>}
        description={labels.outputStyleDescription}
      >
        <OutputStylePicker {...outputStyle} labelledBy={outputStyleTitleId} />
      </SettingsSection>
    );
  } else if (active === 'usage' && usage) {
    content = <UsagePanel {...usage} />;
  } else if (active === 'status' && status) {
    content = <StatusPanel {...status} />;
  }

  return (
    <SettingsLayout
      title={labels.title}
      sections={sections}
      activeId={active}
      onActiveIdChange={handleChange}
      className={className}
      style={style}
    >
      <Stack gap="lg">{content}</Stack>
    </SettingsLayout>
  );
});

ModelSettingsPanel.displayName = 'ModelSettingsPanel';
API reference
Prop
Type
Required
models
ModelOption[]
No
Models offered in the model select
model
string | null
No
Id of the selected model
onModelChange
(modelId: string) => void
No
Called with the id of the picked model
effort
Omit<EffortSelectorProps, 'variant'>
No
Reasoning effort control, rendered in the model section
outputStyle
Omit<OutputStylePickerProps, 'label' | 'description' | 'labelledBy'>
No
Output style section
usage
UsagePanelProps
No
Usage section
status
StatusPanelProps
No
Status section
activeSection
ModelSettingsSection
No
Controlled active section
onActiveSectionChange
(section: ModelSettingsSection) => void
No
Called with the picked section
defaultActiveSection
ModelSettingsSection
No
Section shown first in uncontrolled mode, the first available one by default
labels
Partial<ModelSettingsPanelLabels>
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