UNPKG

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
'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;