Chat Module

Add AI-powered streaming chat to your app. This guide covers every feature from basic messaging to conversation management.

Overview

XpectrumChat is the main class for all chat features. It handles:

  • Sending messages to an AI agent
  • Receiving streamed responses in real-time (word by word)
  • Managing conversation history (list, rename, delete, pin)
  • File uploads, feedback, speech-to-text, and more

Key concept: Streaming

Messages stream back via Server-Sent Events (SSE). You get the AI's response word by word — not all at once. This creates a "typing" effect like ChatGPT.

1. Setup

typescript
import { XpectrumChat } from '@xpectrum/sdk';

const chat = new XpectrumChat({
  baseUrl: 'https://app.yourserver.com/api/v1',  // Your Xpectrum API URL
  apiKey: 'app-xxxxxxxxxxxx',                      // Your API key
  user: 'user-123',                                // Optional: unique user ID
  inputs: {},                                       // Optional: default variables
});
PropertyTypeRequiredDefaultDescription
baseUrlstringYesYour Xpectrum Chat API endpoint. Always ends with something like /api/v1.
apiKeystringYesYour API key from the Xpectrum admin panel. Sent as Bearer token.
userstringNo'sdk-user'Unique ID for the current user. Tracks separate conversation histories per user.
inputsRecord<string, any>No{}Default variables passed with every message. Useful if your AI agent expects variables (like user name).

Where to create the instance

React/Next.js: Inside a useEffect or useRef — never in the render body, or a new instance is created on every render.

2. Send Your First Message

typescript
await chat.sendMessage('Hello, what can you help me with?', {
  onMessage: (text, messageId, conversationId) => {
    console.log('AI says:', text);
  },
});

That's it! The onMessage callback fires every time a new text chunk arrives. The text parameter accumulates — it always contains the full response so far.

What onMessage receives

PropertyTypeRequiredDefaultDescription
textstringYesThe full accumulated response text so far. Each call has more text than the previous one.
messageIdstringYesUnique ID for this message (useful for feedback, suggested questions).
conversationIdstringYesThe conversation this message belongs to. Save this to continue the conversation later.

How text accumulates

text
Call 1: text = "Hello"
Call 2: text = "Hello!"
Call 3: text = "Hello! I"
Call 4: text = "Hello! I can"
Call 5: text = "Hello! I can help"
...
Call N: text = "Hello! I can help you with many things."

Since text always has the complete response, you can set it directly as your UI content — no need to concatenate.

3. All Message Callbacks

sendMessage accepts many optional callbacks. Here's every one:

typescript
const taskId = await chat.sendMessage('Your question', {
  // Resume an existing conversation (optional)
  conversationId: 'conv-abc-123',

  // Override default inputs for this message (optional)
  inputs: { userName: 'Alice' },

  // Attach files (optional — see "Sending Images" below)
  files: [],

  // ─── CALLBACKS (all optional) ───

  // Text chunks — text accumulates the full answer
  onMessage: (text, messageId, conversationId) => {
    updateUI(text);
  },

  // Agent reasoning steps (for agent-type apps)
  onThought: (thought) => {
    // thought.thought     — what the agent is thinking
    // thought.tool        — tool name being used (if any)
    // thought.tool_input  — input passed to the tool
    // thought.observation  — result from the tool
  },

  // File attachments from the agent
  onFile: (file) => {
    // file.url  — URL to download the file
    // file.type — MIME type
  },

  // Response complete — includes token usage, sources
  onMessageEnd: (metadata) => {
    // metadata.conversation_id
    // metadata.message_id
    // metadata.metadata.usage       — { prompt_tokens, completion_tokens, total_tokens }
    // metadata.metadata.retriever_resources — RAG source documents
  },

  // Content moderation replaced the answer
  onMessageReplace: (text, messageId) => {
    updateUI(text);
  },

  // TTS audio chunks (base64 encoded)
  onTTSChunk: (messageId, audioBase64) => {
    // Decode and queue for playback
  },

  // TTS complete
  onTTSEnd: (messageId, audioBase64) => {
    // Final audio chunk
  },

  // Error during streaming
  onError: (error) => {
    // error.message — human-readable error
    // error.code    — error code string
  },

  // Stream closed (fires regardless of success/failure)
  onCompleted: () => {
    setLoading(false);
  },

  // Get the AbortController for manual cancellation
  getAbortController: (controller) => {
    // Save controller to abort later if needed
  },
});

Which callbacks do I actually need?

For a basic chat app, just use onMessage. For a polished app, add onError and onMessageEnd.

4. Conversations

A conversation is a thread of messages between a user and the AI — like a chat thread in WhatsApp. Each has a unique conversationId returned in callbacks.

Resume a conversation

typescript
// First message — new conversation created automatically
let conversationId: string;

await chat.sendMessage('What are your business hours?', {
  onMessage: (text, msgId, convId) => {
    conversationId = convId;  // Save this!
    setAnswer(text);
  },
});

// Second message — pass conversationId so AI remembers context
await chat.sendMessage('Are you open on weekends?', {
  conversationId,
  onMessage: (text) => setAnswer(text),
});

List conversations

typescript
const { data: conversations, has_more } = await chat.getConversations({
  limit: 20,       // How many to fetch (default 20)
  pinned: false,    // Filter by pinned status
});

// conversations = [
//   { id: 'conv-abc', name: 'Business Hours', created_at: 1234567890, ... },
//   { id: 'conv-def', name: 'Pricing Questions', created_at: 1234567891, ... },
// ]

Pagination (load more)

typescript
// First page
const page1 = await chat.getConversations({ limit: 20 });

// Next page
if (page1.has_more) {
  const page2 = await chat.getConversations({
    limit: 20,
    lastId: page1.data[page1.data.length - 1].id,
  });
}

Get messages in a conversation

typescript
const { data: messages } = await chat.getMessages('conv-abc-123', {
  limit: 20,
});

// Each message has:
//   .query    — user's question
//   .answer   — AI's response
//   .feedback — { rating: 'like' | 'dislike' | null }
//   .agent_thoughts — reasoning steps
//   .retriever_resources — RAG sources

Rename / Pin / Delete

typescript
// Manual rename
await chat.renameConversation('conv-abc-123', 'My Custom Name');

// Auto-generate name from content
const updated = await chat.generateConversationName('conv-abc-123');
console.log(updated.name); // "Business Hours Discussion"

// Pin / Unpin / Delete
await chat.pinConversation('conv-abc-123');
await chat.unpinConversation('conv-abc-123');
await chat.deleteConversation('conv-abc-123');

5. Sending Images

Remote URL

typescript
await chat.sendMessage('What is in this image?', {
  files: [{
    type: 'image',
    transfer_method: 'remote_url',
    url: 'https://example.com/photo.jpg',
  }],
  onMessage: (text) => console.log(text),
});

Uploaded file ID

typescript
await chat.sendMessage('Describe this photo', {
  files: [{
    type: 'image',
    transfer_method: 'local_file',
    upload_file_id: 'file-xyz-123',
  }],
  onMessage: (text) => console.log(text),
});

6. Stop a Response Mid-Stream

typescript
// sendMessage returns a taskId
const taskId = await chat.sendMessage('Write a very long essay...', {
  onMessage: (text) => setAnswer(text),
});

// Later, stop the generation:
if (taskId) {
  await chat.stopResponse(taskId);
  // The stream stops and onCompleted fires
}

7. Get App Info & Settings

typescript
// App display info
const info = await chat.getAppInfo();
// { app_id, title, description, icon, icon_type, icon_background, icon_url }

// App configuration (features, opening message, etc.)
const params = await chat.getAppParams();
// {
//   opening_statement: 'Hi! How can I help?',
//   suggested_questions: ['What are your hours?', 'Tell me about pricing'],
//   speech_to_text: { enabled: true },
//   text_to_speech: { enabled: true, voice: 'alloy', language: 'en-US' },
//   file_upload: { image: { enabled: true, number_limits: 3 } },
//   ...
// }

Use getAppParams() to configure your UI — show suggested questions, enable voice input, etc.

8. Feedback & Suggested Questions

typescript
// Like / Dislike / Remove feedback
await chat.submitFeedback('msg-xyz-123', 'like');
await chat.submitFeedback('msg-xyz-123', 'dislike');
await chat.submitFeedback('msg-xyz-123', null);     // Remove

// Get suggested follow-up questions
const suggestions = await chat.getSuggestedQuestions('msg-xyz-123');
// ['What about pricing?', 'Can I schedule a demo?']

9. Speech-to-Text

typescript
// From a file input
const fileInput = document.getElementById('audioInput') as HTMLInputElement;
const audioFile = fileInput.files![0];

const transcription = await chat.speechToText(audioFile);
console.log(transcription); // "What are your business hours?"

10. Text-to-Speech (TTS)

If TTS is enabled on your app, audio chunks arrive via callbacks:

typescript
const audioChunks: string[] = [];

await chat.sendMessage('Tell me a story', {
  onMessage: (text) => setAnswer(text),

  onTTSChunk: (messageId, audioBase64) => {
    audioChunks.push(audioBase64);
  },

  onTTSEnd: (messageId, audioBase64) => {
    audioChunks.push(audioBase64);
    const fullAudio = audioChunks.join('');
    const audio = new Audio(`data:audio/mp3;base64,${fullAudio}`);
    audio.play();
  },
});

11. Cleanup

typescript
// Always destroy when done (aborts all active streams)
chat.destroy();

// In React / Next.js:
useEffect(() => {
  const chat = new XpectrumChat({ ... });
  chatRef.current = chat;
  return () => chat.destroy();  // Cleanup on unmount
}, []);

12. Error Handling

REST API errors

Methods like getConversations(), getAppInfo(), etc. throw on failure:

typescript
import { XpectrumApiError } from '@xpectrum/sdk';

try {
  const conversations = await chat.getConversations();
} catch (error) {
  if (error instanceof XpectrumApiError) {
    console.error(error.message); // Human-readable
    console.error(error.status);  // 401, 403, 404, 429, 500
    console.error(error.code);    // 'unauthorized', etc.
  }
}

Streaming errors

Streaming errors come through the onError callback — they don't throw:

typescript
await chat.sendMessage('Hello', {
  onMessage: (text) => setAnswer(text),
  onError: (err) => {
    showError(err.message);
    // The chat instance is still usable for the next message
  },
});