chat-agent-sdk-csstacktr
Version:
Lightweight SDK for embedding chat agents powered by multiple LLMs and Contentstack
1,414 lines (1,377 loc) • 54.9 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var react = require('react');
/**
* Contentstack Adapter
* Handles dynamic connections to any Contentstack project
*/
class ContentstackAdapter {
constructor(config) {
this.schemaCache = new Map();
this.config = config;
this.baseUrl = this.getBaseUrl();
}
/**
* Get the base URL for Contentstack API based on region
*/
getBaseUrl() {
const region = this.config.region || 'us';
const regionUrls = {
'us': 'https://cdn.contentstack.io',
'eu': 'https://eu-cdn.contentstack.com',
'azure-na': 'https://azure-na-cdn.contentstack.com',
'azure-eu': 'https://azure-eu-cdn.contentstack.com',
'gcp-na': 'https://gcp-na-cdn.contentstack.com'
};
return regionUrls[region];
}
/**
* Get management API URL for schema discovery
*/
getManagementUrl() {
const region = this.config.region || 'us';
const regionUrls = {
'us': 'https://api.contentstack.io',
'eu': 'https://eu-api.contentstack.com',
'azure-na': 'https://azure-na-api.contentstack.com',
'azure-eu': 'https://azure-eu-api.contentstack.com',
'gcp-na': 'https://gcp-na-api.contentstack.com'
};
return regionUrls[region];
}
/**
* Discover content types from customer's Contentstack
*/
async discoverContentTypes(useCache = true) {
const cacheKey = `${this.config.apiKey}_${this.config.environment || 'production'}`;
// Check cache first
if (useCache && this.schemaCache.has(cacheKey)) {
const cached = this.schemaCache.get(cacheKey);
if (Date.now() < cached.expires) {
return cached.schema;
}
}
try {
// Use management API if token is provided, otherwise use delivery API
const contentTypes = this.config.managementToken
? await this.discoverViaManagementAPI()
: await this.discoverViaDeliveryAPI();
const schema = {
contentTypes,
lastUpdated: Date.now()
};
// Cache the schema
if (useCache) {
const expires = Date.now() + (24 * 60 * 60 * 1000); // 24 hours
this.schemaCache.set(cacheKey, { schema, expires });
}
return schema;
}
catch (error) {
console.error('Failed to discover content types:', error);
throw new Error(`Content type discovery failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Discover content types using Management API (more detailed)
*/
async discoverViaManagementAPI() {
if (!this.config.managementToken) {
throw new Error('Management token required for detailed schema discovery');
}
const response = await fetch(`${this.getManagementUrl()}/v3/content_types?include_count=true&include_global_field_schema=true`, {
headers: {
'api_key': this.config.apiKey,
'authorization': this.config.managementToken,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Management API error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return this.parseContentTypesFromManagement(data.content_types || []);
}
/**
* Discover content types using Delivery API (basic info)
*/
async discoverViaDeliveryAPI() {
const response = await fetch(`${this.baseUrl}/v3/content_types`, {
headers: {
'api_key': this.config.apiKey,
'access_token': this.config.deliveryToken
}
});
if (!response.ok) {
throw new Error(`Delivery API error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return this.parseContentTypesFromDelivery(data.content_types || []);
}
/**
* Parse content types from Management API response
*/
parseContentTypesFromManagement(contentTypes) {
return contentTypes.map(ct => {
const searchableFields = [];
const filterableFields = [];
const referenceFields = [];
// Parse schema to identify field types
if (ct.schema && Array.isArray(ct.schema)) {
ct.schema.forEach((field) => {
if (field.data_type === 'text' || field.data_type === 'isodate') {
searchableFields.push(field.uid);
}
if (['text', 'number', 'boolean', 'isodate'].indexOf(field.data_type) !== -1) {
filterableFields.push({
uid: field.uid,
dataType: this.mapDataType(field.data_type),
displayName: field.display_name
});
}
if (field.data_type === 'reference') {
referenceFields.push({
uid: field.uid,
referenceToContentType: field.reference_to
});
}
});
}
return {
uid: ct.uid,
title: ct.title,
searchableFields,
filterableFields,
referenceFields: referenceFields.length > 0 ? referenceFields : undefined
};
});
}
/**
* Parse content types from Delivery API response (basic)
*/
parseContentTypesFromDelivery(contentTypes) {
return contentTypes.map(ct => ({
uid: ct.uid,
title: ct.title,
searchableFields: ['title'], // Default searchable field
filterableFields: [
{ uid: 'title', dataType: 'text' },
{ uid: 'created_at', dataType: 'date' },
{ uid: 'updated_at', dataType: 'date' }
]
}));
}
/**
* Map Contentstack data types to our type system
*/
mapDataType(contentstackType) {
const mapping = {
'text': 'text',
'isodate': 'date',
'number': 'number',
'boolean': 'boolean',
'reference': 'reference',
'file': 'file',
'link': 'reference'
};
return mapping[contentstackType] || 'text';
}
/**
* Search content based on natural language query
*/
async searchContent(query, contentTypes, maxResultsPerType = 5) {
const results = [];
for (const contentType of contentTypes) {
try {
const searchResult = await this.searchContentType(contentType, query, maxResultsPerType);
if (searchResult.entries.length > 0) {
results.push(searchResult);
}
}
catch (error) {
console.warn(`Failed to search content type ${contentType}:`, error);
}
}
return results;
}
/**
* Search specific content type
*/
async searchContentType(contentType, query, limit = 5) {
// Process natural language query to Contentstack query
const processedQuery = this.processNaturalLanguageQuery(query);
const queryParams = new URLSearchParams({
include_count: 'true',
limit: limit.toString(),
environment: this.config.environment || 'production'
});
// Add processed query parameters
if (processedQuery.query && Object.keys(processedQuery.query).length > 0) {
for (const key in processedQuery.query) {
if (processedQuery.query.hasOwnProperty(key)) {
queryParams.append(`query[${key}]`, String(processedQuery.query[key]));
}
}
}
const response = await fetch(`${this.baseUrl}/v3/content_types/${contentType}/entries?${queryParams}`, {
headers: {
'api_key': this.config.apiKey,
'access_token': this.config.deliveryToken
}
});
if (!response.ok) {
throw new Error(`Search failed for ${contentType}: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return {
entries: data.entries || [],
contentType,
total: data.count || 0,
query: processedQuery
};
}
/**
* Process natural language query into Contentstack query parameters
* This is a basic implementation - can be enhanced with NLP
*/
processNaturalLanguageQuery(naturalQuery) {
const query = {};
const lowerQuery = naturalQuery.toLowerCase();
// Basic keyword extraction for common patterns
if (lowerQuery.includes('italy') || lowerQuery.includes('italian')) {
query['title'] = { '$regex': 'italy' };
}
if (lowerQuery.includes('tour') || lowerQuery.includes('tours')) {
query['title'] = { '$regex': 'tour' };
}
if (lowerQuery.includes('product') || lowerQuery.includes('products')) {
query['title'] = { '$regex': 'product' };
}
// Price patterns
const priceMatch = lowerQuery.match(/under\s*\$?(\d+)/);
if (priceMatch) {
query['price'] = { '$lt': parseInt(priceMatch[1]) };
}
// If no specific patterns found, do a general text search
if (Object.keys(query).length === 0) {
query['title'] = { '$regex': naturalQuery };
}
return {
query,
limit: 10,
include: []
};
}
/**
* Get entry by UID
*/
async getEntry(contentType, entryUid) {
const response = await fetch(`${this.baseUrl}/v3/content_types/${contentType}/entries/${entryUid}?environment=${this.config.environment || 'production'}`, {
headers: {
'api_key': this.config.apiKey,
'access_token': this.config.deliveryToken
}
});
if (!response.ok) {
throw new Error(`Failed to get entry: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data.entry;
}
/**
* Test connection to Contentstack
*/
async testConnection() {
try {
const response = await fetch(`${this.baseUrl}/v3/content_types?limit=1&environment=${this.config.environment || 'production'}`, {
headers: {
'api_key': this.config.apiKey,
'access_token': this.config.deliveryToken
}
});
return response.ok;
}
catch (_a) {
return false;
}
}
}
/**
* Core Chat Agent SDK Class
* Handles communication with your chat agent platform with dynamic Contentstack support
*/
class ChatAgent {
constructor(options) {
// Extract production API config
this.apiConfig = {
apiKey: options.apiKey || 'demo-key', // Fallback for development
baseUrl: options.baseUrl || options.apiEndpoint || this.detectEnvironmentUrl(),
stackId: options.stackId,
version: 'v1',
timeout: 30000
};
// Convert legacy config if needed
const isLegacy = 'domain' in options;
const dynamicConfig = isLegacy
? this.finishLegacyConversion(options)
: options;
this.config = {
streaming: true,
provider: 'groq',
temperature: 0.7,
maxTokens: 1000,
autoDiscoverContentTypes: true,
...dynamicConfig
};
this.handlers = options.handlers || {};
this.session = {
id: this.generateSessionId(),
messages: [],
config: this.config,
isConnected: false,
isLoading: false,
schema: undefined
};
// Initialize Contentstack adapter if config is provided
if (this.config.contentstack && this.config.contentstack.apiKey !== 'legacy_placeholder') {
this.contentstackAdapter = new ContentstackAdapter(this.config.contentstack);
}
if (options.autoConnect !== false) {
this.connect();
}
}
/**
* Convert legacy domain-based options to new dynamic config
*/
convertLegacyOptions(legacyOptions) {
console.warn('Using deprecated domain-based configuration. Please migrate to Contentstack-based configuration.');
return {
apiUrl: legacyOptions.apiUrl,
provider: legacyOptions.provider,
model: legacyOptions.model,
streaming: legacyOptions.streaming,
temperature: legacyOptions.temperature,
maxTokens: legacyOptions.maxTokens,
// Convert domain to legacy contentstack config
contentstack: {
apiKey: 'legacy_placeholder',
deliveryToken: 'legacy_placeholder',
environment: 'production'
}
};
}
/**
* Detect appropriate API URL based on environment
*/
detectEnvironmentUrl() {
var _a;
// Check if running in browser
if (typeof window !== 'undefined') {
const hostname = window.location.hostname;
// Local development
if (hostname === 'localhost' || hostname === '127.0.0.1') {
return 'http://localhost:3000';
}
}
// Node.js environment - check NODE_ENV
const nodeEnv = typeof process !== 'undefined' ? (_a = process.env) === null || _a === void 0 ? void 0 : _a.NODE_ENV : undefined;
if (nodeEnv === 'development' || nodeEnv === 'test') {
return 'http://localhost:3000';
}
// Default to your Vercel deployment for production
return 'https://chat-agent-platform-server.vercel.app';
}
/**
* Complete legacy options conversion method
*/
finishLegacyConversion(legacyOptions) {
return {
apiUrl: legacyOptions.apiUrl,
streaming: legacyOptions.streaming,
provider: legacyOptions.provider,
model: legacyOptions.model,
temperature: legacyOptions.temperature,
maxTokens: legacyOptions.maxTokens,
// Create a placeholder Contentstack config for legacy domains
contentstack: {
apiKey: 'legacy_placeholder',
deliveryToken: 'legacy_placeholder',
environment: 'production'
},
// Pass domain as metadata for backend processing
contentTypes: [{
uid: 'legacy_domain',
title: legacyOptions.domain || 'unknown',
searchableFields: ['title', 'description']
}]
};
}
/**
* Connect to the chat agent platform and discover content schema
*/
async connect() {
var _a, _b, _c, _d;
try {
// Test basic connection using production API config
const testUrl = this.config.apiUrl || `${this.apiConfig.baseUrl}/api/health`;
const testResponse = await fetch(testUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(this.apiConfig.apiKey !== 'demo-key' && {
Authorization: `Bearer ${this.apiConfig.apiKey}`
})
},
signal: AbortSignal.timeout(this.apiConfig.timeout || 10000)
});
// Health endpoint might not exist, so 404 is acceptable
if (!testResponse.ok && testResponse.status !== 404) {
console.warn(`Connection test failed: ${testResponse.status} ${testResponse.statusText}`);
// Don't throw error for connection test failures in production
}
// Discover content schema if adapter is available
if (this.contentstackAdapter && this.config.autoDiscoverContentTypes) {
try {
const schema = await this.contentstackAdapter.discoverContentTypes();
this.session.schema = schema;
}
catch (schemaError) {
console.warn('Failed to discover content schema:', schemaError);
// Don't fail connection just because schema discovery failed
}
}
this.session.isConnected = true;
(_b = (_a = this.handlers).onConnect) === null || _b === void 0 ? void 0 : _b.call(_a);
}
catch (error) {
this.session.isConnected = false;
const err = error instanceof Error ? error : new Error('Connection failed');
(_d = (_c = this.handlers).onError) === null || _d === void 0 ? void 0 : _d.call(_c, err);
throw err;
}
}
/**
* Disconnect from the chat agent platform
*/
disconnect() {
var _a, _b;
if (this.abortController) {
this.abortController.abort();
}
this.session.isConnected = false;
(_b = (_a = this.handlers).onDisconnect) === null || _b === void 0 ? void 0 : _b.call(_a);
}
/**
* Send a message to the chat agent
*/
async sendMessage(content, options = {}) {
var _a, _b, _c, _d, _e, _f, _g;
if (!this.session.isConnected) {
throw new Error('Not connected to chat agent. Call connect() first.');
}
// Create user message
const userMessage = {
id: this.generateMessageId(),
content: content.trim(),
role: 'user',
timestamp: Date.now(),
metadata: options.metadata
};
// Add to session
this.session.messages.push(userMessage);
(_b = (_a = this.handlers).onMessage) === null || _b === void 0 ? void 0 : _b.call(_a, userMessage);
this.session.isLoading = true;
this.abortController = new AbortController();
try {
const useStreaming = (_c = options.streaming) !== null && _c !== void 0 ? _c : this.config.streaming;
if (useStreaming) {
return await this.sendStreamingMessage(userMessage, options);
}
else {
return await this.sendNormalMessage(userMessage, options);
}
}
catch (error) {
const err = error instanceof Error ? error : new Error('Failed to send message');
(_e = (_d = this.handlers).onError) === null || _e === void 0 ? void 0 : _e.call(_d, err);
throw err;
}
finally {
this.session.isLoading = false;
(_g = (_f = this.handlers).onTyping) === null || _g === void 0 ? void 0 : _g.call(_f, false);
}
}
/**
* Send message with streaming response
*/
async sendStreamingMessage(userMessage, options) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
const assistantMessage = {
id: this.generateMessageId(),
content: '',
role: 'assistant',
timestamp: Date.now(),
metadata: {}
};
this.session.messages.push(assistantMessage);
(_b = (_a = this.handlers).onMessage) === null || _b === void 0 ? void 0 : _b.call(_a, assistantMessage);
(_d = (_c = this.handlers).onTyping) === null || _d === void 0 ? void 0 : _d.call(_c, true);
const requestBody = {
message: userMessage.content,
messages: this.session.messages.slice(0, -1), // Exclude the assistant message we just added
// Pass Contentstack configuration for dynamic content fetching
contentstack: this.config.contentstack,
contentTypes: this.config.contentTypes || [],
schema: this.session.schema,
provider: this.config.provider,
model: this.config.model,
temperature: this.config.temperature,
maxTokens: this.config.maxTokens,
systemPrompt: this.config.systemPrompt,
locale: this.config.locale,
// Multi-stack support
stackId: this.apiConfig.stackId,
userId: 'sdk-user', // TODO: Get from authentication
...options.context
};
// Use production API endpoint
const endpoint = this.apiConfig.stackId
? `${this.apiConfig.baseUrl}/api/stacks/${this.apiConfig.stackId}/stream`
: this.config.apiUrl
? `${this.config.apiUrl}/stream`
: `${this.apiConfig.baseUrl}/api/chat`;
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(this.apiConfig.apiKey !== 'demo-key' && {
Authorization: `Bearer ${this.apiConfig.apiKey}`
})
},
body: JSON.stringify(requestBody),
signal: (_e = this.abortController) === null || _e === void 0 ? void 0 : _e.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = (_f = response.body) === null || _f === void 0 ? void 0 : _f.getReader();
const decoder = new TextDecoder();
if (!reader) {
throw new Error('No response body');
}
while (true) {
const { done, value } = await reader.read();
if (done)
break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim());
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
const content = ((_j = (_h = (_g = data.choices) === null || _g === void 0 ? void 0 : _g[0]) === null || _h === void 0 ? void 0 : _h.delta) === null || _j === void 0 ? void 0 : _j.content) || data.content || '';
if (content) {
assistantMessage.content += content;
assistantMessage.timestamp = Date.now();
const streamChunk = {
content,
done: false,
metadata: data.metadata
};
(_l = (_k = this.handlers).onStreamChunk) === null || _l === void 0 ? void 0 : _l.call(_k, streamChunk);
(_o = (_m = this.handlers).onMessage) === null || _o === void 0 ? void 0 : _o.call(_m, assistantMessage);
}
}
catch (e) {
// Skip invalid JSON
}
}
}
}
(_q = (_p = this.handlers).onStreamChunk) === null || _q === void 0 ? void 0 : _q.call(_p, { content: '', done: true });
return assistantMessage;
}
/**
* Send message with normal (non-streaming) response
*/
async sendNormalMessage(userMessage, options) {
var _a, _b, _c, _d, _e;
(_b = (_a = this.handlers).onTyping) === null || _b === void 0 ? void 0 : _b.call(_a, true);
const requestBody = {
message: userMessage.content,
messages: this.session.messages,
// Pass Contentstack configuration for dynamic content fetching
contentstack: this.config.contentstack,
contentTypes: this.config.contentTypes || [],
schema: this.session.schema,
provider: this.config.provider,
model: this.config.model,
temperature: this.config.temperature,
maxTokens: this.config.maxTokens,
systemPrompt: this.config.systemPrompt,
locale: this.config.locale,
...options.context
};
const response = await fetch(this.config.apiUrl || `${this.apiConfig.baseUrl}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(this.apiConfig.apiKey !== 'demo-key' && {
Authorization: `Bearer ${this.apiConfig.apiKey}`
})
},
body: JSON.stringify(requestBody),
signal: (_c = this.abortController) === null || _c === void 0 ? void 0 : _c.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
const assistantMessage = {
id: this.generateMessageId(),
content: data.message.content,
role: 'assistant',
timestamp: Date.now(),
metadata: data.metadata
};
this.session.messages.push(assistantMessage);
(_e = (_d = this.handlers).onMessage) === null || _e === void 0 ? void 0 : _e.call(_d, assistantMessage);
return assistantMessage;
}
/**
* Clear all messages from the session
*/
clearMessages() {
this.session.messages = [];
}
/**
* Get current session
*/
getSession() {
return { ...this.session };
}
/**
* Get current messages
*/
getMessages() {
return [...this.session.messages];
}
/**
* Update configuration
*/
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
this.session.config = this.config;
}
/**
* Check if connected
*/
isConnected() {
return this.session.isConnected;
}
/**
* Check if loading
*/
isLoading() {
return this.session.isLoading;
}
/**
* Refresh Contentstack schema
*/
async refreshSchema() {
var _a, _b;
if (this.contentstackAdapter) {
try {
const schema = await this.contentstackAdapter.discoverContentTypes(false);
this.session.schema = schema;
}
catch (error) {
const err = error instanceof Error ? error : new Error('Failed to refresh schema');
(_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, err);
throw err;
}
}
}
/**
* Test Contentstack connection
*/
async testContentstackConnection() {
if (this.contentstackAdapter) {
return await this.contentstackAdapter.testConnection();
}
return false;
}
/**
* Generate unique session ID
*/
generateSessionId() {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Generate unique message ID
*/
generateMessageId() {
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
/**
* Vanilla JavaScript SDK for Chat Agent Platform with dynamic Contentstack support
* Use this in plain HTML/JS applications without React
*/
class ChatAgentSDK {
constructor(config) {
this.agent = new ChatAgent({
...config,
autoConnect: true,
handlers: {
onMessage: this.handleMessage.bind(this),
onError: this.handleError.bind(this),
onConnect: this.handleConnect.bind(this),
onDisconnect: this.handleDisconnect.bind(this),
onTyping: this.handleTyping.bind(this),
onStreamChunk: this.handleStreamChunk.bind(this)
}
});
// Auto-render if container specified
if (config.containerId) {
this.renderChat(config.containerId);
}
}
/**
* Render a complete chat interface in the specified container
*/
renderChat(containerId, options = {}) {
const container = document.getElementById(containerId);
if (!container) {
throw new Error(`Container with ID "${containerId}" not found`);
}
this.container = container;
container.innerHTML = this.getChatHTML(options);
this.attachEventListeners();
}
/**
* Send a message programmatically
*/
async sendMessage(content) {
await this.agent.sendMessage(content);
}
/**
* Get all messages
*/
getMessages() {
return this.agent.getMessages();
}
/**
* Clear all messages
*/
clearMessages() {
this.agent.clearMessages();
this.updateMessagesContainer([]);
}
/**
* Get current Contentstack schema
*/
getSchema() {
const session = this.agent.getSession();
return session.schema || null;
}
/**
* Refresh Contentstack schema
*/
async refreshSchema() {
await this.agent.refreshSchema();
this.updateConnectionStatus(this.agent.isConnected());
}
/**
* Test Contentstack connection
*/
async testContentstackConnection() {
return await this.agent.testContentstackConnection();
}
/**
* Get agent session info
*/
getSessionInfo() {
var _a;
const session = this.agent.getSession();
return {
isConnected: session.isConnected,
isLoading: session.isLoading,
messageCount: session.messages.length,
hasContentstack: !!session.config.contentstack && session.config.contentstack.apiKey !== 'legacy_placeholder',
contentTypes: ((_a = session.schema) === null || _a === void 0 ? void 0 : _a.contentTypes.length) || 0
};
}
/**
* Event handlers
*/
handleMessage(message) {
this.updateMessagesContainer(this.agent.getMessages());
// Emit custom event
this.dispatchEvent('chat:message', { message });
}
handleError(error) {
this.showError(error.message);
this.dispatchEvent('chat:error', { error });
}
handleConnect() {
this.updateConnectionStatus(true);
this.dispatchEvent('chat:connect', {});
}
handleDisconnect() {
this.updateConnectionStatus(false);
this.dispatchEvent('chat:disconnect', {});
}
handleTyping(isTyping) {
this.updateTypingIndicator(isTyping);
this.dispatchEvent('chat:typing', { isTyping });
}
handleStreamChunk(chunk) {
// Update the last message in real-time
this.updateMessagesContainer(this.agent.getMessages());
this.dispatchEvent('chat:stream', { chunk });
}
/**
* Generate chat HTML
*/
getChatHTML(options) {
const theme = options.theme || 'default';
return `
<div class="chat-agent-container ${theme}">
<div class="chat-agent-header">
<div class="header-title">
<h3>AI Assistant</h3>
<div class="schema-info" id="schema-info" style="font-size: 0.75rem; color: #6c757d;"></div>
</div>
<div class="connection-status" id="connection-status">
<span class="status-indicator"></span>
<span class="status-text">Connecting...</span>
</div>
</div>
<div class="chat-agent-messages" id="messages-container">
<!-- Messages will be populated here -->
</div>
<div class="chat-agent-typing" id="typing-indicator" style="display: none;">
<div class="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
AI is typing...
</div>
<div class="chat-agent-error" id="error-container" style="display: none;">
<span class="error-message" id="error-message"></span>
<button class="error-close" onclick="this.parentElement.style.display='none'">×</button>
</div>
<div class="chat-agent-input">
<div class="input-container">
<textarea
id="message-input"
placeholder="Type your message..."
rows="1"
style="resize: none; overflow-y: hidden;"
></textarea>
<button id="send-button">Send</button>
</div>
</div>
</div>
<style>
.chat-agent-container {
display: flex;
flex-direction: column;
height: 500px;
max-width: 600px;
border: 1px solid #e1e5e9;
border-radius: 8px;
background: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.chat-agent-header {
padding: 1rem;
background: #f8f9fa;
border-bottom: 1px solid #e1e5e9;
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.header-title h3 {
margin: 0 0 0.25rem 0;
color: #333;
font-size: 1.1rem;
}
.schema-info {
font-size: 0.75rem;
color: #6c757d;
margin: 0;
}
.connection-status {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #dc3545;
}
.status-indicator.connected {
background: #28a745;
}
.chat-agent-messages {
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.message {
max-width: 80%;
padding: 0.75rem 1rem;
border-radius: 18px;
word-wrap: break-word;
}
.message.user {
align-self: flex-end;
background: #007bff;
color: white;
}
.message.assistant {
align-self: flex-start;
background: #f1f3f5;
color: #333;
}
.message-time {
font-size: 0.75rem;
opacity: 0.7;
margin-top: 0.25rem;
}
.chat-agent-typing {
padding: 0.5rem 1rem;
color: #6c757d;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.typing-dots {
display: flex;
gap: 0.2rem;
}
.typing-dots span {
width: 4px;
height: 4px;
background: #6c757d;
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out;
}
.typing-dots span:nth-child(1) { animation-delay: -0.32s; }
.typing-dots span:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
.chat-agent-error {
background: #f8d7da;
color: #721c24;
padding: 0.75rem 1rem;
margin: 0.5rem 1rem;
border-radius: 6px;
border: 1px solid #f5c6cb;
display: flex;
justify-content: space-between;
align-items: center;
}
.error-close {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
color: #721c24;
}
.chat-agent-input {
padding: 1rem;
border-top: 1px solid #e1e5e9;
background: #f8f9fa;
border-radius: 0 0 8px 8px;
}
.input-container {
display: flex;
gap: 0.5rem;
align-items: flex-end;
}
#message-input {
flex: 1;
border: 1px solid #ced4da;
border-radius: 20px;
padding: 0.75rem 1rem;
font-size: 0.875rem;
min-height: 20px;
max-height: 120px;
outline: none;
}
#message-input:focus {
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.1);
}
#send-button {
background: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
#send-button:hover {
background: #0056b3;
}
#send-button:disabled {
background: #6c757d;
cursor: not-allowed;
}
</style>
`;
}
/**
* Attach event listeners to the chat interface
*/
attachEventListeners() {
const input = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
if (input && sendButton) {
// Auto-resize textarea
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = input.scrollHeight + 'px';
});
// Send message on Enter (but allow Shift+Enter for new line)
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.handleSendMessage();
}
});
// Send button click
sendButton.addEventListener('click', () => {
this.handleSendMessage();
});
}
}
/**
* Handle sending a message from the input
*/
async handleSendMessage() {
const input = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
if (!input || !sendButton)
return;
const content = input.value.trim();
if (!content)
return;
input.value = '';
input.style.height = 'auto';
sendButton.disabled = true;
try {
await this.sendMessage(content);
}
catch (error) {
console.error('Failed to send message:', error);
}
finally {
sendButton.disabled = false;
input.focus();
}
}
/**
* Update the messages container
*/
updateMessagesContainer(messages) {
const container = document.getElementById('messages-container');
if (!container)
return;
container.innerHTML = messages
.map(message => `
<div class="message ${message.role}">
<div class="message-content">${this.escapeHtml(message.content)}</div>
<div class="message-time">${new Date(message.timestamp).toLocaleTimeString()}</div>
</div>
`)
.join('');
// Scroll to bottom
container.scrollTop = container.scrollHeight;
}
/**
* Update connection status indicator
*/
updateConnectionStatus(connected) {
const statusIndicator = document.querySelector('.status-indicator');
const statusText = document.querySelector('.status-text');
const schemaInfo = document.getElementById('schema-info');
if (statusIndicator && statusText) {
if (connected) {
statusIndicator.classList.add('connected');
statusText.textContent = 'Connected';
}
else {
statusIndicator.classList.remove('connected');
statusText.textContent = 'Disconnected';
}
}
// Update schema info
if (schemaInfo) {
const sessionInfo = this.getSessionInfo();
if (sessionInfo.hasContentstack) {
schemaInfo.textContent = sessionInfo.contentTypes > 0
? `${sessionInfo.contentTypes} content types discovered`
: 'Discovering content types...';
}
else {
schemaInfo.textContent = 'Legacy domain mode';
}
}
}
/**
* Update typing indicator
*/
updateTypingIndicator(isTyping) {
const indicator = document.getElementById('typing-indicator');
if (indicator) {
indicator.style.display = isTyping ? 'flex' : 'none';
}
}
/**
* Show error message
*/
showError(message) {
const errorContainer = document.getElementById('error-container');
const errorMessage = document.getElementById('error-message');
if (errorContainer && errorMessage) {
errorMessage.textContent = message;
errorContainer.style.display = 'flex';
// Auto-hide after 5 seconds
setTimeout(() => {
errorContainer.style.display = 'none';
}, 5000);
}
}
/**
* Dispatch custom events
*/
dispatchEvent(eventName, detail) {
if (this.container) {
this.container.dispatchEvent(new CustomEvent(eventName, { detail }));
}
}
/**
* Escape HTML to prevent XSS
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Global function for easy initialization (browser only)
if (typeof window !== 'undefined') {
window.ChatAgentSDK = ChatAgentSDK;
}
/**
* Convert legacy domain-based config to new dynamic Contentstack config
*/
function convertLegacyConfig(legacyConfig) {
// This is a fallback for backward compatibility
// In practice, users should migrate to the new dynamic config
console.warn('Using deprecated domain-based configuration. Please migrate to Contentstack-based configuration.');
return {
apiUrl: legacyConfig.apiUrl,
provider: legacyConfig.provider,
model: legacyConfig.model,
apiKey: legacyConfig.apiKey,
systemPrompt: legacyConfig.systemPrompt,
maxTokens: legacyConfig.maxTokens,
temperature: legacyConfig.temperature,
streaming: legacyConfig.streaming,
// Create a placeholder Contentstack config for legacy domains
contentstack: {
apiKey: 'legacy_placeholder',
deliveryToken: 'legacy_placeholder',
environment: 'production'
},
// Pass domain as metadata for backend processing
contentTypes: [{
uid: 'legacy_domain',
title: legacyConfig.domain,
searchableFields: ['title', 'description']
}]
};
}
/**
* React Hook for basic chat functionality with dynamic Contentstack support
* Use this for simple chat implementations
*/
function useChat(config) {
const [messages, setMessages] = react.useState([]);
const [isLoading, setIsLoading] = react.useState(false);
const [isConnected, setIsConnected] = react.useState(false);
const [error, setError] = react.useState(null);
const [schema, setSchema] = react.useState(null);
const agentRef = react.useRef(null);
const adapterRef = react.useRef(null);
// Check if it's a legacy config or new dynamic config
const isLegacyConfig = 'domain' in config;
const dynamicConfig = isLegacyConfig ? convertLegacyConfig(config) : config;
// Initialize Contentstack adapter for dynamic configurations
react.useEffect(() => {
if (!isLegacyConfig && dynamicConfig.contentstack) {
adapterRef.current = new ContentstackAdapter(dynamicConfig.contentstack);
// Auto-discover content types if enabled
if (dynamicConfig.autoDiscoverContentTypes !== false) {
adapterRef.current.discoverContentTypes()
.then(setSchema)
.catch((err) => {
console.warn('Failed to discover content types:', err);
setError(err);
});
}
}
}, [isLegacyConfig, JSON.stringify(dynamicConfig.contentstack)]);
// Initialize ChatAgent
react.useEffect(() => {
const options = {
...dynamicConfig,
autoConnect: true,
handlers: {
onMessage: (message) => {
setMessages((prev) => {
// Update existing message or add new one
const existingIndex = prev.findIndex((m) => m.id === message.id);
if (existingIndex >= 0) {
const updated = [...prev];
updated[existingIndex] = message;
return updated;
}
return [...prev, message];
});
},
onError: (err) => {
setError(err);
setIsLoading(false);
},
onConnect: () => {
setIsConnected(true);
setError(null);
},
onDisconnect: () => {
setIsConnected(false);
},
onTyping: (typing) => {
setIsLoading(typing);
}
}
};
agentRef.current = new ChatAgent(options);
return () => {
var _a;
(_a = agentRef.current) === null || _a === void 0 ? void 0 : _a.disconnect();
};
}, [JSON.stringify(dynamicConfig)]);
const sendMessage = react.useCallback(async (content, options) => {
if (!agentRef.current) {
throw new Error('Chat agent not initialized');
}
setError(null);
setIsLoading(true);
try {
await agentRef.current.sendMessage(content, options);
}
catch (err) {
const error = err instanceof Error ? err : new Error('Failed to send message');
setError(error);
throw error;
}
finally {
setIsLoading(false);
}
}, []);
const clearMessages = react.useCallback(() => {
var _a;
(_a = agentRef.current) === null || _a === void 0 ? void 0 : _a.clearMessages();
setMessages([]);
}, []);
const reconnect = react.useCallback(async () => {
if (agentRef.current) {
try {
await agentRef.current.connect();
}
catch (err) {
const error = err instanceof Error ? err : new Error('Failed to reconnect');
setError(error);
}
}
}, []);
const disconnect = react.useCallback(() => {
var _a;
(_a = agentRef.current) === null || _a === void 0 ? void 0 : _a.disconnect();
}, []);
const refreshSchema = react.useCallback(async () => {
if (adapterRef.current) {
try {
const newSchema = await adapterRef.current.discoverContentTypes(false);
setSchema(newSchema);
}
catch (err) {
const error = err instanceof Error ? err : new Error('Failed to refresh schema');
setError(error);
throw error;
}
}
}, []);
return {
messages,
isLoading,
isConnected,
error,
schema,
sendMessage,
clearMessages,
reconnect,
disconnect,
refreshSchema
};
}
/**
* React Hook for advanced chat agent functionality with dynamic Contentstack support
* Use this for full-featured chat implementations
*/
function useChatAgent(initialConfig) {
const [config, setConfig] = react.useState(initialConfig);
const [session, setSession] = react.useState(null);
const agentRef = react.useRef(null);
// Use the basic useChat hook
const chatHook = useChat(config);
// Update session when messages change
react.useEffect(() => {
if (agentRef.current) {
setSession(agentRef.current.getSession());
}
}, [chatHook.messages, chatHook.isConnected, chatHook.isLoading]);
const updateConfig = react.useCallback((newConfig) => {
setConfig((prev) => {
const isLegacy = 'domain' in prev;
if (isLegacy) {
// Convert legacy config and merge
const converted = convertLegacyConfig(prev);
return { ...converted, ...newConfig };
}
return { ...prev, ...newConfig };
});
}, []);
const dynamicConfig = 'domain' in config ? convertLegacyConfig(config) : config;