@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
Markdown
# 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">×</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