create-xpectrum-app
The fastest way to start building with @xpectrum/sdk. One command gives you a fully configured Next.js app with chat and voice demos ready to go.
Quick Start
npx create-xpectrum-app my-app
cd my-app
# Add your API keys to .env.local
npm run devThat's it. Open http://localhost:3000 and you'll see your app.
What You Get
The scaffolded app includes everything you need to start building:
my-app/
├── app/
│ ├── layout.tsx # Root layout with Geist fonts
│ ├── page.tsx # Welcome page with links to demos
│ ├── globals.css # Tailwind CSS + light/dark theme
│ ├── chat/
│ │ └── page.tsx # Chat demo route
│ └── voice/
│ └── page.tsx # Voice demo route
├── components/
│ ├── xpectrum-chat.tsx # Fully working chat component
│ └── xpectrum-voice.tsx # Fully working voice component
├── .env.local # Placeholder API keys (edit this!)
├── package.json # Next.js 15, React 19, @xpectrum/sdk
├── tsconfig.json
├── next.config.ts
├── postcss.config.mjs
└── eslint.config.mjs| Included | Version |
|---|---|
| Next.js | 15.5.7 |
| React | 19 |
| @xpectrum/sdk | latest |
| Tailwind CSS | 4 |
| TypeScript | 5 |
Usage
With a custom name
npx create-xpectrum-app my-cool-appWith the default name
npx create-xpectrum-app
# Creates a folder called "my-xpectrum-app"ℹ What happens under the hood
- Creates the project directory
- Copies the template files
- Renames
_env.localto.env.localand_gitignoreto.gitignore - Updates
package.jsonname to your project name - Runs
npm install
Configure Your API Keys
Open .env.local and replace the placeholder values with your real keys:
# Chat Configuration
NEXT_PUBLIC_CHAT_BASE_URL=https://app.yourserver.com/api/v1
NEXT_PUBLIC_CHAT_API_KEY=app-xxxxxxxxxxxx
# Voice Configuration
NEXT_PUBLIC_VOICE_BASE_URL=https://api.xpectrum.co/
NEXT_PUBLIC_VOICE_API_KEY=xpectrum_ai_sk_xxxxxxxxxxxx
NEXT_PUBLIC_VOICE_AGENT_NAME=your-agent-uuid-here⚠ Don't commit .env.local
.gitignore already excludes .env* files, so your API keys won't be committed to git.Available Routes
| Route | Description |
|---|---|
| / | Welcome page with quick start instructions |
| /chat | Chat demo — streaming AI conversation |
| /voice | Voice demo — real-time voice call with transcription |
Chat Component
The starter app includes a self-contained chat component at components/xpectrum-chat.tsx. Everything is in one file — no custom hooks or abstractions.
'use client';
import { useState, useRef, useEffect } from 'react';
import { XpectrumChat } from '@xpectrum/sdk';
interface Message {
id: string;
role: 'user' | 'assistant';
text: string;
}
export default function XpectrumChatDemo() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [conversationId, setConversationId] = useState<string>();
const chatRef = useRef<XpectrumChat | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
// Initialize the SDK once
useEffect(() => {
chatRef.current = new XpectrumChat({
baseUrl: process.env.NEXT_PUBLIC_CHAT_BASE_URL!,
apiKey: process.env.NEXT_PUBLIC_CHAT_API_KEY!,
});
return () => { chatRef.current?.destroy(); };
}, []);
// Auto-scroll to latest message
useEffect(() => {
scrollRef.current?.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: 'smooth',
});
}, [messages]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!input.trim() || isLoading || !chatRef.current) return;
const userMessage: Message = {
id: `user-${Date.now()}`,
role: 'user',
text: input,
};
const assistantMessage: Message = {
id: `asst-${Date.now()}`,
role: 'assistant',
text: '',
};
setMessages((prev) => [...prev, userMessage, assistantMessage]);
setInput('');
setError(null);
setIsLoading(true);
try {
await chatRef.current.sendMessage(input, {
conversationId,
onMessage: (responseText, messageId, convId) => {
setConversationId(convId);
setMessages((prev) => {
const updated = [...prev];
updated[updated.length - 1] = {
id: messageId,
role: 'assistant',
text: responseText,
};
return updated;
});
},
onError: (err) => setError(err.message),
onCompleted: () => setIsLoading(false),
});
} catch (err) {
setError(err instanceof Error ? err.message : 'Something went wrong');
setIsLoading(false);
}
}
function handleNewChat() {
setMessages([]);
setConversationId(undefined);
setError(null);
}
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
{/* Header, Messages, Input ... */}
</div>
);
}Voice Component
The voice component at components/xpectrum-voice.tsx gives you a working voice call interface with live transcription.
'use client';
import { useState, useRef, useEffect } from 'react';
import { XpectrumVoice } from '@xpectrum/sdk';
import type { TranscriptionSegment } from '@xpectrum/sdk';
export default function XpectrumVoiceDemo() {
const [connectionState, setConnectionState] = useState('disconnected');
const [micEnabled, setMicEnabled] = useState(true);
const [transcripts, setTranscripts] = useState<TranscriptionSegment[]>([]);
const [agentSpeaking, setAgentSpeaking] = useState(false);
const [error, setError] = useState<string | null>(null);
const voiceRef = useRef<XpectrumVoice | null>(null);
const isConnected = connectionState === 'connected';
const isConnecting = connectionState === 'connecting';
// Initialize the SDK once
useEffect(() => {
voiceRef.current = new XpectrumVoice({
baseUrl: process.env.NEXT_PUBLIC_VOICE_BASE_URL!,
apiKey: process.env.NEXT_PUBLIC_VOICE_API_KEY!,
agentName: process.env.NEXT_PUBLIC_VOICE_AGENT_NAME!,
});
return () => { voiceRef.current?.destroy(); };
}, []);
async function handleConnect() {
if (!voiceRef.current) return;
setError(null);
setTranscripts([]);
try {
await voiceRef.current.connect({
onConnected: () => setConnectionState('connected'),
onDisconnected: () => {
setConnectionState('disconnected');
setAgentSpeaking(false);
},
onConnectionStateChanged: (state) => setConnectionState(state),
onAgentSpeaking: (isSpeaking) => setAgentSpeaking(isSpeaking),
onTranscription: (segment) => {
setTranscripts((prev) => {
const idx = prev.findIndex((t) => t.id === segment.id);
if (idx >= 0) {
const updated = [...prev];
updated[idx] = segment;
return updated;
}
return [...prev, segment];
});
},
onError: (err) => setError(err.message),
});
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to connect');
setConnectionState('failed');
}
}
async function handleDisconnect() {
await voiceRef.current?.disconnect();
}
async function handleToggleMic() {
if (!voiceRef.current) return;
const next = !micEnabled;
await voiceRef.current.setMicEnabled(next);
setMicEnabled(next);
}
return (
<div className="flex flex-col h-screen max-w-xl mx-auto p-4">
{/* Status, Controls, Transcription ... */}
</div>
);
}⚠ Browser permission required
Customizing
The starter app is meant to be modified. Here are some common next steps:
- Change the theme — edit CSS variables in
app/globals.css - Add a chat widget — see the Widgets docs to add a floating chat bubble
- Extract hooks — as your app grows, move the SDK logic into custom hooks. See the Next.js examples for patterns
- Add conversation history — use
chatRef.current.getConversations()andchatRef.current.getMessages(). See the Chat docs