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

bash
npx create-xpectrum-app my-app
cd my-app
# Add your API keys to .env.local
npm run dev

That'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:

text
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
IncludedVersion
Next.js15.5.7
React19
@xpectrum/sdklatest
Tailwind CSS4
TypeScript5

Usage

With a custom name

bash
npx create-xpectrum-app my-cool-app

With the default name

bash
npx create-xpectrum-app
# Creates a folder called "my-xpectrum-app"

What happens under the hood

  1. Creates the project directory
  2. Copies the template files
  3. Renames _env.local to .env.local and _gitignore to .gitignore
  4. Updates package.json name to your project name
  5. Runs npm install

Configure Your API Keys

Open .env.local and replace the placeholder values with your real keys:

.env.local
# 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

The .gitignore already excludes .env* files, so your API keys won't be committed to git.

Available Routes

RouteDescription
/Welcome page with quick start instructions
/chatChat demo — streaming AI conversation
/voiceVoice 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.

components/xpectrum-chat.tsx
'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.

components/xpectrum-voice.tsx
'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

The browser will prompt for microphone access when the user clicks "Start Call". This must be triggered by a user click — it can't happen automatically on page load.

Customizing

The starter app is meant to be modified. Here are some common next steps:

  • Change the themeedit CSS variables in app/globals.css
  • Add a chat widgetsee the Widgets docs to add a floating chat bubble
  • Extract hooksas your app grows, move the SDK logic into custom hooks. See the Next.js examples for patterns
  • Add conversation historyuse chatRef.current.getConversations() and chatRef.current.getMessages(). See the Chat docs