UNPKG

@spectrumsense/spectrum-chat-dev

Version:

Embeddable AI Widget - Add trusted, evidence-based answers directly to your website. Simple installation, enterprise-grade security.

822 lines (684 loc) 23.1 kB
# Spectrum Chat v2 - Architecture Specification ## Overview Build `spectrum-chat-v2.js` from scratch with DeepChat as foundation, one-line installation via script tag data attributes, no custom element mode, and complete CSS isolation with fixed positioning. ## Core Requirements 1.**DeepChat Foundation** - Use DeepChat for heavy lifting (chat UI, messages, etc.) 2.**One-Line Installation** - Script tag with data-* attributes 3.**No Custom Element** - Single interface only 4.**Fixed Positioning** - Isolated from customer CSS (no iframe) 5.**Version Pinned DeepChat** - Prevent accidental breakage (v1.4.20) 6.**Mobile Responsive** - Center-bottom on mobile 7.**Minimal Wrapper** - Let DeepChat do the work ## Architecture ``` spectrum-chat-v2.js (~400 lines total) ├── Configuration Parser (~50 lines) │ └── Extract data-* from script tag or window.SpectrumChatConfig ├── DeepChat Wrapper (~150 lines) │ ├── Load DeepChat (pinned version) │ ├── Create container (fixed position) │ ├── Initialize DeepChat with config │ └── Setup API interceptors (site-key, JWT, etc.) ├── UI Layer (~100 lines) │ ├── FAB button (floating action button) │ ├── Container positioning (fixed, z-index: 9999) │ └── Open/close logic ├── Security Layer (~50 lines) │ ├── Session management (existing code) │ ├── Token handling (existing code) │ └── API interceptors (existing code) └── Public API (~50 lines) └── window.SpectrumChat methods ``` ## Design Decisions ### Why DeepChat? DeepChat provides out-of-the-box functionality that we don't want to reinvent: - Chat UI components (messages, input, bubbles) - Markdown rendering - Message history - Scroll management - Responsive layout - Accessibility features ### Why No Custom Element? - **Simplicity**: Single interface is easier for customers to understand - **No confusion**: One way to install = clearer documentation - **Consistency**: Same approach works for single-page and multi-page sites - **Flexibility**: Customers repeat on each page or use in templates ### Why No iframe? **Considered but rejected due to**: - Performance overhead - Accessibility issues (screen readers) - Mobile viewport complications - SEO impact - Can't integrate with parent page events **Alternative solution**: - High z-index (9999) - Position: fixed - Scoped CSS class names (spectrum-chat-*) - Provides 99% isolation without iframe drawbacks ### Attribute Naming Convention | Context | Format | Example | |---------|--------|---------| | Script tag | `data-kebab-case` | `data-site-key` | | Global config | `camelCase` | `siteKey` | | Internal | `camelCase` | `siteKey` | **Why `data-` prefix?** - Required by HTML5 specification for custom attributes - Prevents validator warnings - Standard best practice **Parser handles conversion**: `data-site-key` → `siteKey` ## Implementation Plan ### Phase 1: Setup & Configuration (~100 lines) **File**: `src/spectrum-chat-v2.js` ```javascript /** * Spectrum Chat v2 - AI Chat Widget * Built on DeepChat foundation * * One-line installation: * <script src="https://unpkg.com/@spectrumsense/spectrum-chat@2.0.0/dist/spectrum-chat.js" * data-site-key="pub_customer_xyz"></script> */ // Default configuration const defaultConfig = { apiUrl: '{{API_URL}}', siteKey: '{{SITE_KEY}}', useJWT: true, title: 'AI Assistant', introText: 'Hello! How can I help you today?', primaryColor: '#2c3e50', userColor: '#3498db', aiColor: '#2c3e50', fabIcon: '💬', position: 'bottom-right', width: '400px', height: '600px', enableCitations: true, debug: false }; // Global state (singleton) const state = { isInitialized: false, widget: null, deepChat: null, conversationId: null, tokenData: null, isOpen: false }; // Parse configuration from script tag data-* attributes function parseScriptConfig() { const script = document.currentScript; if (!script) return {}; const config = {}; for (const attr of script.attributes) { if (attr.name.startsWith('data-')) { const key = attr.name .replace('data-', '') .replace(/-([a-z])/g, (_, l) => l.toUpperCase()); config[key] = parseValue(attr.value); } } return config; } function parseValue(value) { if (value === 'true') return true; if (value === 'false') return false; if (!isNaN(value) && value !== '') return Number(value); return value; } // Merge configurations: defaults < window.SpectrumChatConfig < script data-* function getConfig() { return { ...defaultConfig, ...(window.SpectrumChatConfig || {}), ...parseScriptConfig() }; } ``` ### Phase 2: DeepChat Integration (~150 lines) ```javascript // Load DeepChat with pinned version function loadDeepChat() { return new Promise((resolve, reject) => { if (window.customElements.get('deep-chat')) { resolve(); return; } const script = document.createElement('script'); script.type = 'module'; // PINNED VERSION - prevents breaking changes script.src = 'https://unpkg.com/deep-chat@1.4.20/dist/deepChat.bundle.js'; script.onload = () => resolve(); script.onerror = () => reject(new Error('Failed to load DeepChat')); document.head.appendChild(script); }); } // Create widget container with fixed positioning function createWidgetContainer(config) { const container = document.createElement('div'); container.id = 'spectrum-chat-widget'; container.className = 'spectrum-chat-root'; container.innerHTML = ` <style> .spectrum-chat-root { position: fixed; ${config.position === 'bottom-right' ? 'bottom: 20px; right: 20px;' : ''} ${config.position === 'bottom-left' ? 'bottom: 20px; left: 20px;' : ''} z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .spectrum-chat-fab { width: 60px; height: 60px; border-radius: 50%; background: ${config.primaryColor}; color: white; border: none; font-size: 28px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; transition: transform 0.2s; } .spectrum-chat-fab:hover { transform: scale(1.1); } .spectrum-chat-panel { position: fixed; ${config.position === 'bottom-right' ? 'bottom: 20px; right: 20px;' : ''} width: ${config.width}; height: ${config.height}; background: white; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.2); display: none; flex-direction: column; overflow: hidden; } .spectrum-chat-panel.open { display: flex; } .spectrum-chat-header { background: ${config.primaryColor}; color: white; padding: 16px; display: flex; justify-content: space-between; align-items: center; } .spectrum-chat-title { margin: 0; font-size: 1rem; font-weight: 600; } .spectrum-chat-close { background: none; border: none; color: white; font-size: 24px; cursor: pointer; padding: 0; line-height: 1; } .spectrum-chat-close:hover { opacity: 0.8; } #spectrum-deepchat-container { flex: 1; overflow: hidden; } /* Mobile responsive - center bottom */ @media (max-width: 768px) { .spectrum-chat-root { left: 50% !important; right: auto !important; transform: translateX(-50%); bottom: 10px !important; } .spectrum-chat-panel { width: calc(100vw - 20px) !important; height: 70vh !important; left: 10px; right: 10px; } .spectrum-chat-fab { left: 50%; transform: translateX(-50%); } } </style> <button class="spectrum-chat-fab" aria-label="Open chat"> ${config.fabIcon} </button> <div class="spectrum-chat-panel"> <div class="spectrum-chat-header"> <h3 class="spectrum-chat-title">${config.title}</h3> <button class="spectrum-chat-close" aria-label="Close">&times;</button> </div> <div id="spectrum-deepchat-container"></div> </div> `; return container; } // Initialize DeepChat instance async function initializeDeepChat(container, config) { const deepChatContainer = container.querySelector('#spectrum-deepchat-container'); const deepChat = document.createElement('deep-chat'); // DeepChat configuration deepChat.style.width = '100%'; deepChat.style.height = '100%'; deepChat.style.border = 'none'; // Apply message bubble colors (user-color, ai-color) deepChat.messageStyles = { default: { shared: { bubble: { maxWidth: '85%', borderRadius: '12px', padding: '10px 14px', fontSize: '0.9rem', lineHeight: '1.4' } }, user: { bubble: { backgroundColor: config.userColor || config.primaryColor, color: 'white' } }, ai: { bubble: { backgroundColor: config.aiColor || config.primaryColor, color: 'white' } } } }; // Setup request/response interceptors for our API deepChat.requestInterceptor = createRequestInterceptor(config); deepChat.responseInterceptor = createResponseInterceptor(config); // Initial messages if (config.introText) { deepChat.initialMessages = [{ role: 'ai', text: config.introText }]; } // Enable HTML in messages (for citations, markdown) deepChat.htmlClassUtilities = { 'deep-chat-temporary-message': true }; deepChatContainer.appendChild(deepChat); return deepChat; } ``` ### Phase 3: Security Layer (Reuse Existing Code ~200 lines) Keep all existing security functions from current code: - `hashPageUrl()` - SHA256 hash for page URL telemetry - `generateNonce()` - UUID v4 generation - `isTokenValid()` - JWT token validation - `isTokenExpiredError()` - Error detection - `createSession()` - JWT session creation - `initializeSession()` - Session initialization/refresh - `handleApiError()` - User-friendly error messages - `loadTokenData()` / `saveTokenData()` - Token persistence - `loadConversationId()` / `saveConversationId()` - Conversation persistence Create interceptors for DeepChat: ```javascript function createRequestInterceptor(config) { return async (requestDetails) => { // Initialize JWT session if enabled await initializeSession(config); // Build API URL with conversation ID let apiUrl = config.apiUrl; if (state.conversationId) { apiUrl = apiUrl.replace(/\/conversations\/?$/, `/conversations/${state.conversationId}`); } // Extract message from DeepChat request const userMessage = requestDetails.body?.messages?.[0]?.text || ''; // Build request body const body = { message: userMessage, citations: config.enableCitations }; // Add site-key for new conversations (Phase 0 security) if (!state.conversationId) { body.siteKey = config.siteKey; body.pageUrlHash = await hashPageUrl(); body.nonce = generateNonce(); } // Backward compatibility if (config.tenantId) { body.tenant_id = config.tenantId; } // Build headers const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' }; // Add JWT token if enabled (Phase 1 security) if (config.useJWT && state.tokenData?.token) { headers['Authorization'] = `Bearer ${state.tokenData.token}`; } return { url: apiUrl, method: 'POST', headers, body }; }; } function createResponseInterceptor(config) { return (response) => { // Save conversation ID if (response.conversation_id) { state.conversationId = response.conversation_id; sessionStorage.setItem('spectrum-chat-conversation-id', response.conversation_id); } // Process citations let text = response.text || ''; if (config.enableCitations && response.sources) { response.sources.forEach(source => { const link = `<a href="${source.url}" target="_blank" rel="noopener noreferrer" style="color: #60a5fa; text-decoration: underline;">[${source.index}]</a>`; text = text.replace(new RegExp(`\\[${source.index}\\]`, 'g'), link); }); } return { text }; }; } ``` ### Phase 4: UI Logic & Public API (~100 lines) ```javascript // Initialize widget async function initialize() { if (state.isInitialized) return; const config = getConfig(); // Validate required config if (!config.siteKey) { console.error('Spectrum Chat: site-key is required'); return; } try { // Load stored data loadConversationId(); loadTokenData(); // Load DeepChat await loadDeepChat(); // Create widget container const container = createWidgetContainer(config); document.body.appendChild(container); // Setup event handlers const fab = container.querySelector('.spectrum-chat-fab'); const panel = container.querySelector('.spectrum-chat-panel'); const closeBtn = container.querySelector('.spectrum-chat-close'); fab.addEventListener('click', () => openChat()); closeBtn.addEventListener('click', () => closeChat()); // Click outside to close document.addEventListener('click', (e) => { if (state.isOpen && !container.contains(e.target)) { closeChat(); } }); // Initialize DeepChat const deepChat = await initializeDeepChat(container, config); state.widget = container; state.deepChat = deepChat; state.isInitialized = true; if (config.debug) { console.log('Spectrum Chat initialized', config); } } catch (error) { console.error('Failed to initialize Spectrum Chat:', error); } } function openChat() { if (!state.widget) return; const panel = state.widget.querySelector('.spectrum-chat-panel'); const fab = state.widget.querySelector('.spectrum-chat-fab'); panel.classList.add('open'); fab.style.display = 'none'; state.isOpen = true; document.dispatchEvent(new CustomEvent('spectrum-chat-opened')); } function closeChat() { if (!state.widget) return; const panel = state.widget.querySelector('.spectrum-chat-panel'); const fab = state.widget.querySelector('.spectrum-chat-fab'); panel.classList.remove('open'); fab.style.display = 'flex'; state.isOpen = false; document.dispatchEvent(new CustomEvent('spectrum-chat-closed')); } // Public API window.SpectrumChat = { open: openChat, close: closeChat, isOpen: () => state.isOpen, getConfig: getConfig, clearSession: () => { sessionStorage.removeItem('spectrum-chat-conversation-id'); sessionStorage.removeItem('spectrum-chat-token'); state.conversationId = null; state.tokenData = null; }, getConversationId: () => state.conversationId, getSessionId: () => state.tokenData?.session_id || null }; // Auto-initialize if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } ``` ## File Structure ``` src/ ├── spectrum-chat-v2.js (~400 lines - new clean version) └── spectrum-chat.js (keep old version for reference) dist/ ├── spectrum-chat.js (built v2) └── deep-chat/ (optional: bundled DeepChat) └── deepChat.bundle.js ``` ## Usage Examples ### One-Line Installation (Primary) ```html <script src="https://unpkg.com/@spectrumsense/spectrum-chat@2.0.0/dist/spectrum-chat.js" data-site-key="pub_customer_xyz" data-title="Help Center"></script> ``` ### Traditional (Alternative) ```html <script> window.SpectrumChatConfig = { siteKey: 'pub_customer_xyz', title: 'Help Center', primaryColor: '#007bff' }; </script> <script src="https://unpkg.com/@spectrumsense/spectrum-chat@2.0.0/dist/spectrum-chat.js"></script> ``` ### With All Options ```html <script src="https://unpkg.com/@spectrumsense/spectrum-chat@2.0.0/dist/spectrum-chat.js" data-site-key="pub_customer_xyz" data-title="Customer Support" data-intro-text="Hi! How can we help you today?" data-primary-color="#2c3e50" data-user-color="#3498db" data-ai-color="#2c3e50" data-fab-icon="💬" data-position="bottom-right" data-width="400px" data-height="600px" data-enable-citations="true" data-use-jwt="true"></script> ``` ### Programmatic Control ```javascript // Open chat window.SpectrumChat.open(); // Close chat window.SpectrumChat.close(); // Check state console.log(window.SpectrumChat.isOpen()); // Clear session window.SpectrumChat.clearSession(); // Get conversation ID console.log(window.SpectrumChat.getConversationId()); // Get session ID console.log(window.SpectrumChat.getSessionId()); ``` ## Key Improvements 1. **~1600 lines removed** - No custom element, no global mode duplication 2. **DeepChat does heavy lifting** - Chat UI, messages, markdown, etc. 3. **One interface** - Simple for customers 4. **Fixed positioning** - CSS isolated, high z-index (9999) 5. **Mobile responsive** - Auto-centers on mobile 6. **Version pinned** - DeepChat@1.4.20 (won't break) 7. **Clean architecture** - Easy to maintain 8. **Security intact** - All JWT and site-key features preserved ## Mobile Responsiveness The widget automatically adapts to mobile screens: - **Desktop**: Fixed bottom-right (or configured position) - **Mobile (<768px)**: Centered horizontally at bottom - **Panel**: Full-width on mobile (with 20px margins) - **Height**: 70vh on mobile for better UX ```css @media (max-width: 768px) { /* Widget centers horizontally */ .spectrum-chat-root { left: 50% !important; transform: translateX(-50%); } /* Panel takes most of screen */ .spectrum-chat-panel { width: calc(100vw - 20px) !important; height: 70vh !important; } } ``` ## Configuration Options All configuration options from v1 are retained: | Option | Type | Default | Description | |--------|------|---------|-------------| | `siteKey` | string | Required | Public site key for domain validation | | `apiUrl` | string | Build-time | API endpoint URL | | `useJWT` | boolean | `true` | Enable JWT session tokens | | `title` | string | `'AI Assistant'` | Chat widget title | | `introText` | string | Default message | Welcome message | | `primaryColor` | string | `'#2c3e50'` | Header and primary elements | | `userColor` | string | `'#3498db'` | User message bubble color | | `aiColor` | string | `'#2c3e50'` | AI message bubble color | | `fabIcon` | string | `'💬'` | FAB button icon (emoji) | | `position` | string | `'bottom-right'` | Widget position | | `width` | string | `'400px'` | Chat panel width | | `height` | string | `'600px'` | Chat panel height | | `enableCitations` | boolean | `true` | Show source citations | | `debug` | boolean | `false` | Enable debug logging | ## Security Features All existing security features are preserved: ### Phase 0: Site-Key + Origin Validation - Site key required for all conversations - Origin validated by API server - Page URL hash for telemetry - Request nonce for tracking ### Phase 1: JWT Session Tokens - Time-limited session tokens (1 hour) - Automatic token refresh - Token-bound to origin - Stored in sessionStorage ### Implementation via Interceptors DeepChat's interceptors provide perfect pass-through: 1. Request interceptor adds security headers 2. Response interceptor saves conversation state 3. DeepChat handles HTTP communication 4. No reinvention needed ## Future Enhancements (Not in v2) - [ ] Layout modes (floating/embedded/inline) - [ ] Custom themes - [ ] Widget size presets (small/medium/large) - [ ] Position variants (all corners) - [ ] Animation options - [ ] Sound notifications - [ ] Unread message badge - [ ] Multi-language support - [ ] Offline message queuing - [ ] Voice input ## Testing Checklist - [ ] One-line installation works - [ ] data-* attributes parsed correctly - [ ] window.SpectrumChatConfig works - [ ] DeepChat loads and initializes - [ ] Chat opens/closes correctly - [ ] Mobile layout centers properly - [ ] Multiple tabs don't conflict (same site-key) - [ ] JWT session management works - [ ] Conversation persistence works - [ ] Fixed positioning not affected by customer CSS - [ ] Message colors (user/AI) apply correctly - [ ] Header styling with primary color - [ ] Citations render as links - [ ] Works on all major browsers (Chrome, Firefox, Safari, Edge) - [ ] Works on customer sites (test on real domains) ## Success Criteria - [ ] spectrum-chat-v2.js is ~400 lines (vs 2000+ in v1) - [ ] No custom element code - [ ] No global mode duplication - [ ] DeepChat version pinned to 1.4.20 - [ ] One-line installation works - [ ] Mobile responsive (center-bottom) - [ ] Fixed positioning (z-index: 9999) - [ ] Custom header with primary color - [ ] User/AI message colors configurable - [ ] All security features work (JWT, site-key, etc.) - [ ] Conversation persistence across pages - [ ] Documentation updated - [ ] Examples updated ## Migration from v1 ### For customers using global mode: ```html <!-- v1 --> <script> window.SpectrumChatConfig = { siteKey: 'xyz' }; </script> <script src="..."></script> <!-- v2 (same!) --> <script> window.SpectrumChatConfig = { siteKey: 'xyz' }; </script> <script src="..."></script> ``` ### For customers using custom element: ```html <!-- v1 --> <script src="..."></script> <spectrum-chat site-key="xyz"></spectrum-chat> <!-- v2 (one-line) --> <script src="..." data-site-key="xyz"></script> ``` ### API Changes: - `window.SpectrumChatGlobal``window.SpectrumChat` - All methods remain the same - Events remain the same ## Build Process 1. Source: `src/spectrum-chat-v2.js` 2. Build replaces template variables: - `{{API_URL}}` → Environment-specific API URL - `{{SITE_KEY}}` → Default site key (optional) 3. Output: `dist/spectrum-chat.js` 4. Publish to npm: `@spectrumsense/spectrum-chat@2.0.0` ## Deployment Strategy 1. **Phase 1**: Build and test v2 locally 2. **Phase 2**: Deploy to dev package (`@spectrumsense/spectrum-chat-dev`) 3. **Phase 3**: Test on staging domains 4. **Phase 4**: Update documentation 5. **Phase 5**: Deploy to production package 6. **Phase 6**: Notify customers of migration path --- **Version**: 2.0.0 **Status**: Specification - Ready for Implementation **Last Updated**: October 20, 2025