Skip to main content

Tutorial - Support or AI Agent Console

Use this tutorial when MediaSFU is powering a support, agent, or operator workspace instead of a room-shaped UI.

Outcome

By the end, you will have:

  • a headless MediaSFU runtime mount
  • a support-console shell that owns the visible UI
  • a practical place to add transcription, translation, recording, and escalation controls

Best starting path

Start with:

  1. Choose your UI mode
  2. Secure backend proxy
  3. Headless mode
  4. Media lifecycle

Use mediasfu-shared only when the UI SDK helper surface is no longer enough.

Copy/paste starter: React headless support console

This starter mounts MediaSFU with returnUI={false} and uses sourceParameters to drive your own operator controls.

Create src/SupportAgentConsoleRoom.tsx, paste the component below, and keep your backend /api/mediasfu/create-room and /api/mediasfu/join-room routes forwarding to the same upstream MediaSFU Cloud rooms URL from Secure backend proxy.

import { useMemo, useState } from 'react';
import {
MediasfuGeneric,
type CreateMediaSFURoomOptions,
type JoinMediaSFURoomOptions,
} from 'mediasfu-reactjs';

type RoomResult = { data: Record<string, unknown> | null; success: boolean };
type RuntimeParameters = Record<string, any>;

async function createMediaSFURoom({ payload }: { payload: CreateMediaSFURoomOptions }): Promise<RoomResult> {
const response = await fetch('/api/mediasfu/create-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

return { data: await response.json(), success: response.ok };
}

async function joinMediaSFURoom({ payload }: { payload: JoinMediaSFURoomOptions }): Promise<RoomResult> {
const response = await fetch('/api/mediasfu/join-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

return { data: await response.json(), success: response.ok };
}

function OperatorConsole({ parameters }: { parameters: RuntimeParameters }) {
return (
<main style={{ minHeight: '100vh', display: 'grid', gridTemplateColumns: '360px 1fr 320px', background: '#f8fafc', color: '#0f172a' }}>
<aside style={{ padding: 20, borderRight: '1px solid #cbd5e1' }}>
<h2>Customer</h2>
<p>Room: {parameters.roomName ?? 'Connecting...'}</p>
<p>Participants: {(parameters.participants ?? []).length}</p>
</aside>

<section style={{ padding: 24, display: 'grid', gap: 16, alignContent: 'start' }}>
<h1>Live session</h1>
<div style={{ minHeight: 280, borderRadius: 8, background: '#e2e8f0', display: 'grid', placeItems: 'center' }}>
Media stream or transcript surface
</div>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<button onClick={() => parameters.clickAudio?.({ parameters })}>Mic</button>
<button onClick={() => parameters.clickVideo?.({ parameters })}>Camera</button>
<button onClick={() => parameters.launchRecording?.({ parameters })}>Record</button>
</div>
</section>

<aside style={{ padding: 20, borderLeft: '1px solid #cbd5e1' }}>
<h2>Agent actions</h2>
<button>Escalate</button>
<button>Send summary</button>
<button>Mark resolved</button>
</aside>
</main>
);
}

export function SupportAgentConsoleRoom() {
const [sourceParameters, setSourceParameters] = useState<RuntimeParameters>({});
const noUIPreJoinOptions = useMemo(
() => ({
action: 'create',
eventType: 'conference',
userName: 'Agent',
capacity: 3,
duration: 45,
}),
[]
);

return (
<>
<MediasfuGeneric
connectMediaSFU={true}
returnUI={false}
noUIPreJoinOptions={noUIPreJoinOptions}
sourceParameters={sourceParameters}
updateSourceParameters={setSourceParameters}
createMediaSFURoom={createMediaSFURoom}
joinMediaSFURoom={joinMediaSFURoom}
/>
<OperatorConsole parameters={sourceParameters} />
</>
);
}

Render it from your app entry:

import { SupportAgentConsoleRoom } from './SupportAgentConsoleRoom';

export default function App() {
return <SupportAgentConsoleRoom />;
}
About the operator controls

The parameters prop passed into OperatorConsole is just your live sourceParameters helper bundle forwarded into a custom shell. The button row is deliberately minimal: it proves that MediaSFU runtime actions are reachable from your own UI.

Use raw helper calls for simple toggles, but prefer shipped MediaSFU surfaces when one already exists. Recording is the clearest example: import RecordingModal or ModernRecordingModal, or restyle uiOverrides.recordingModal, instead of rebuilding the full recording UX from scratch. Read Media lifecycle and UI overrides before expanding the operator control area.

What to wire next

  1. Add your CRM, ticket, or customer profile data to the left panel.
  2. Route transcript, translation, or summary events into the main panel.
  3. Wire record, escalation, and handoff actions to your backend audit trail.
  4. Add role checks before enabling operator-only actions.