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
1. Setup
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
});| Property | Type | Required | Default | Description |
|---|---|---|---|---|
| baseUrl | string | Yes | — | Your Xpectrum Chat API endpoint. Always ends with something like /api/v1. |
| apiKey | string | Yes | — | Your API key from the Xpectrum admin panel. Sent as Bearer token. |
| user | string | No | 'sdk-user' | Unique ID for the current user. Tracks separate conversation histories per user. |
| inputs | Record<string, any> | No | {} | Default variables passed with every message. Useful if your AI agent expects variables (like user name). |
⚠ Where to create the instance
useEffect or useRef — never in the render body, or a new instance is created on every render.2. Send Your First Message
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
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
| text | string | Yes | — | The full accumulated response text so far. Each call has more text than the previous one. |
| messageId | string | Yes | — | Unique ID for this message (useful for feedback, suggested questions). |
| conversationId | string | Yes | — | The conversation this message belongs to. Save this to continue the conversation later. |
How text accumulates
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:
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
// 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
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)
// 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
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 sourcesRename / Pin / Delete
// 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
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
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
// 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
// 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
// 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
// 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:
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
// 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:
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:
await chat.sendMessage('Hello', {
onMessage: (text) => setAnswer(text),
onError: (err) => {
showError(err.message);
// The chat instance is still usable for the next message
},
});