Skip to content
+

Chat - Model selector

Let users switch between AI models from the conversation header.

The demo below shows how to place a Material UI Select inside the conversation header so users can switch between AI models. No new component is needed—drop any Material UI control into slots.conversationHeaderActions.

The demo below shows a Select mounted in the conversationHeaderActions slot, with model state owned by the selector component:

Material UI chat

Styled with your active MUI theme

MUI Assistant
MUI Assistant

Hello! I am styled using your active Material UI theme. Try sending a message.

You
You

Great — the bubble colors come from palette.primary and the typography from the theme.

'use client';
import * as React from 'react';
import MenuItem from '@mui/material/MenuItem';
import type { SelectChangeEvent } from '@mui/material/Select';
import Select from '@mui/material/Select';
import { ChatBox } from '@mui/x-chat';
import { createEchoAdapter } from 'docs/data/chat/core/examples/shared/demoUtils';
import {
  minimalConversation,
  minimalMessages,
} from 'docs/data/chat/core/examples/shared/demoData';

const MODELS = [
  { id: 'gpt-4o', label: 'GPT-4o' },
  { id: 'claude-3-5-sonnet', label: 'Claude 3.5 Sonnet' },
  { id: 'gemini-1-5-pro', label: 'Gemini 1.5 Pro' },
];

// Defined outside the render function so the slot reference stays stable
// and ChatBox does not remount the header on every render.
function ModelSelectControl() {
  const [model, setModel] = React.useState('gpt-4o');
  return (
    <Select
      value={model}
      onChange={(event: SelectChangeEvent) => setModel(event.target.value)}
      size="small"
      sx={{ minWidth: 160 }}
    >
      {MODELS.map((m) => (
        <MenuItem key={m.id} value={m.id}>
          {m.label}
        </MenuItem>
      ))}
    </Select>
  );
}

const adapter = createEchoAdapter();

export default function ModelSelector() {
  return (
    <ChatBox
      adapter={adapter}
      initialActiveConversationId={minimalConversation.id}
      initialConversations={[minimalConversation]}
      initialMessages={minimalMessages}
      slots={{
        conversationHeaderActions: ModelSelectControl,
      }}
      sx={{
        height: 500,
        border: '1px solid',
        borderColor: 'divider',
        borderRadius: 1,
      }}
    />
  );
}

Mounting the model selector

function MyModelSelector() {
  const [model, setModel] = React.useState('gpt-4o');
  return (
    <Select value={model} onChange={(e) => setModel(e.target.value)} size="small">
      <MenuItem value="gpt-4o">GPT-4o</MenuItem>
      <MenuItem value="claude-3-5-sonnet">Claude 3.5 Sonnet</MenuItem>
    </Select>
  );
}

<ChatBox slots={{ conversationHeaderActions: MyModelSelector }} />;

Passing the model to the adapter

The demo above keeps model state inside the selector. To pass the selected model to adapter.sendMessage, hoist state to the parent and construct the adapter with React.useMemo:

export default function App() {
  const [model, setModel] = React.useState('gpt-4o');

  const adapter = React.useMemo(
    () => ({
      async sendMessage({ message, signal }) {
        const res = await fetch('/api/chat', {
          method: 'POST',
          body: JSON.stringify({ message, model }),
          signal,
        });
        return res.body;
      },
    }),
    [model],
  );

  // Stable reference—defined outside or memoized to avoid remounts
  const HeaderActions = React.useMemo(
    () =>
      function ModelSelector() {
        return (
          <Select
            value={model}
            onChange={(e) => setModel(e.target.value)}
            size="small"
          >
            <MenuItem value="gpt-4o">GPT-4o</MenuItem>
            <MenuItem value="claude-3-5-sonnet">Claude 3.5 Sonnet</MenuItem>
          </Select>
        );
      },
    [model],
  );

  return (
    <ChatBox
      adapter={adapter}
      slots={{ conversationHeaderActions: HeaderActions }}
    />
  );
}

Implementation notes

  • ChatConversationHeaderActions applies marginInlineStart: 'auto' so the selector aligns to the end of the header.
  • Define the slot component outside the render function, or stabilize it with React.useMemo, to avoid remounting the header on every render.
  • Use size="small" on Select to match the default header height.

See also

API