@schoolexl/mentor
v0.0.2
Published
Drop-in React UI for real-time AI tutors — chat, live voice, and lip-synced avatar video over LiveKit. Fully themeable, with a typed client and hook for managing avatars.
Downloads
322
Maintainers
Readme
@schoolexl/mentor
Drop-in React UI for real-time AI tutors — chat, live voice, and lip-synced avatar video over LiveKit. Fully themeable, with a typed client and hook for managing avatars.
npm install @schoolexl/mentorreact and react-dom (18.3+ or 19+) are peer dependencies.
Quick start
"use client";
import { MentorPage } from "@schoolexl/mentor";
import "@schoolexl/mentor/mentor.css";
export default function Tutor() {
return (
<MentorPage
accessKey={process.env.NEXT_PUBLIC_ACCESS_KEY!}
serverUrl="https://your-token-server.com"
agentName="Coach Sarah"
systemPrompt="You are a warm, encouraging tutor."
language="en-US"
gender="female"
/>
);
}<MentorPage> never holds your model/TTS/avatar credentials. It calls your token server with accessKey, gets a LiveKit token, and joins the room; the agent (server-side) runs the model, voice, and avatar.
Styling
Import the self-contained stylesheet once. It ships the utilities + theme tokens the components use, with no CSS reset, so it won't restyle the rest of your app:
import "@schoolexl/mentor/mentor.css";Already on Tailwind v4? You can skip the import and let your own build pick up the classes instead, with @source "../node_modules/@schoolexl/mentor/dist/**/*.{js,mjs,cjs}";.
In Next.js, <MentorPage> is a client component — render it from a client component, or load it with next/dynamic and ssr: false.
Modes
<MentorPage accessKey="..." mode="chat" /> // text chat
<MentorPage accessKey="..." mode="avatar" videoMode avatarId="imogen" avatarSrc="/imogen.jpg" /> // talking-head avatarCommon props: systemPrompt, agentName, language (BCP-47), gender, voiceName, ttsProvider ("google" | "chatterbox"), chatterboxVoice, cameraEnabled, micEnabled, showFps, showTranscript, overlay, tools, onToolCall, onConnect / onDisconnect. Everything is fully typed via MentorPageProps.
To drive connect/disconnect from your own UI, pass a controls ref and read onConnectionChange:
const controls = useRef<MentorControls | null>(null);
const [connected, setConnected] = useState(false);
<MentorPage accessKey="..." mode="avatar" videoMode controls={controls} onConnectionChange={setConnected} />
{connected && <button onClick={() => controls.current?.disconnect()}>End</button>}There's also <AvatarPanel> for a standalone avatar frame (no chat).
Theming
Override CSS variables scoped to .mentor-root — nothing leaks to the rest of your app:
.mentor-root {
--mentor-primary: #775ff2; /* accent */
--mentor-text: #1e1b2e;
--mentor-bg: #ffffff;
--mentor-radius: 0.625rem;
--mentor-border: rgba(119, 95, 242, 0.15);
}Also available: --mentor-panel-bg, --mentor-panel-border, --mentor-panel-shadow, --mentor-font. Top-level components accept className / style too.
Avatar management
A typed client and useAvatars hook to create, list, delete, and watch avatars on a scxl-avtr server — separate from <MentorPage>, which only drives an existing avatar.
import { useAvatars } from "@schoolexl/mentor";
const { avatars, status, create, remove } = useAvatars({
baseUrl: "https://avatar.example.com",
apiKey: process.env.NEXT_PUBLIC_AVATAR_KEY, // or omit and use a server-side proxy baseUrl
liveStatus: "ws", // "ws" | "poll" | "off"
});
const id = await create({ imageFile: file, aspect: "3:4" }); // watch status[id].stateAuth — supply the avatar server's shared key either way:
apiKey— sent asX-API-Keyon REST and?key=on the status WebSocket. Fine server-side or in trusted/internal tools.- Proxy — point
baseUrlat your own route (e.g./api/avatars) that injects the key, and omitapiKey. Recommended for public web apps so the key isn't shipped to the browser.
The same operations are exported as plain functions: listAvatars, createAvatar, deleteAvatar, getAvatarStatus, subscribeAvatarStatus, fileToBase64 — each takes an AvatarApiConfig ({ baseUrl, apiKey?, fetchImpl? }). Types: AvatarStatus, AvatarSummary, AvatarState, AvatarAspect, CreateAvatarInput.
TTS
Google Chirp3 HD (default) — neural voices in 50+ languages, auto-selected from language + gender, or pin one with voiceName.
Chatterbox Turbo — GPU voice cloning; needs a Chatterbox server configured on your backend. Supports inline tags like [laughs], [sighs], [excited].
<MentorPage ttsProvider="chatterbox" chatterboxVoice="jamaican_female" />Tool calling
Forward OpenAI-format tools and handle calls in the browser:
<MentorPage
tools={[{ type: "function", function: { name: "show_hint",
parameters: { type: "object", properties: { hint: { type: "string" } }, required: ["hint"] } } }]}
onToolCall={async (call) => {
if (call.name === "show_hint") { setHint(call.arguments.hint as string); return { success: true }; }
}}
/>Environment variables
NEXT_PUBLIC_COREEXL_MENTOR_SERVER— default token-server URL (used ifserverUrlis omitted).NEXT_PUBLIC_ACCESS_KEY— default access key (used ifaccessKeyis omitted).NEXT_PUBLIC_AVATAR_KEY— avatar-server key; not read automatically, pass it asapiKey(or use a proxy).
Backend
<MentorPage> POSTs the session config to POST {serverUrl}/api/start and expects { token, url } back. Your server validates accessKey, configures the LiveKit room, dispatches the agent, and returns the token.
Troubleshooting
- Unstyled layout — import
@schoolexl/mentor/mentor.css(or add the@sourcedirective); do one, not both. - Avatar greets twice — the agent must run with room audio output disabled in avatar mode (the avatar republishes the agent's audio). Backend config, not a package issue.
useAvatarsstatus never updates withliveStatus: "ws"— ifbaseUrlis a relative proxy, pass an absolutewsBaseUrl, or useliveStatus: "poll".
License
MIT
