Development Guide
Embedding AI in Static Pages
Techniques for adding AI features to static websites using embedded code and client-side implementations
Overview
Static websites can leverage AI capabilities through client-side JavaScript, embedded widgets, and serverless functions. This approach enables dynamic AI features while maintaining the simplicity and performance benefits of static hosting.
Client-Side Integration
Run AI features directly in the browser using JavaScript
Embedded Widgets
Pre-built AI components that can be easily added to any site
Serverless Backend
Use cloud functions for AI processing without managing servers
Approaches to AI Integration
Client-Side Only
- Browser-based Models: Use TensorFlow.js or ONNX runtime
- WebAssembly: Run optimized AI models in the browser
- Pre-trained Models: Load models from CDN for immediate use
- Limited Scope: Best for classification, sentiment analysis
Hybrid Approach
- Client Pre-processing: Handle simple tasks in browser
- API Calls: Send complex requests to AI services
- Caching: Store results locally to reduce API calls
- Progressive Enhancement: Basic functionality without JavaScript
Client-Side AI Implementation
TensorFlow.js
- Use Cases: Image recognition, NLP
- Models: Pre-trained or custom models
- Performance: WebGL acceleration
- Limitations: Model size constraints
Hugging Face Transformers.js
- Use Cases: Text generation, classification
- Models: Transformer-based models
- Performance: WebAssembly optimized
- Limitations: Limited model selection
Custom WebAssembly
- Use Cases: High-performance inference
- Models: Any ONNX compatible model
- Performance: Near-native speed
- Limitations: Complex setup
Embedded Chat Widget
HTML Implementation
<!-- Simple AI Chat Widget -->
<div id="ai-chat-widget">
<div class="chat-header">
<h3>AI Assistant</h3>
<button class="close-btn">×</button>
</div>
<div class="chat-messages" id="chat-messages">
<div class="message bot-message">
Hello! How can I help you today?
</div>
</div>
<div class="chat-input">
<input type="text" id="user-input" placeholder="Type your message...">
<button id="send-btn">Send</button>
</div>
</div>
<style>
#ai-chat-widget {
position: fixed;
bottom: 20px;
right: 20px;
width: 350px;
height: 500px;
background: white;
border: 1px solid #ddd;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
z-index: 1000;
}
.chat-header {
background: #007bff;
color: white;
padding: 15px;
border-radius: 10px 10px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-messages {
flex: 1;
padding: 15px;
overflow-y: auto;
background: #f8f9fa;
}
.message {
margin: 10px 0;
padding: 10px 15px;
border-radius: 15px;
max-width: 80%;
}
.user-message {
background: #007bff;
color: white;
margin-left: auto;
}
.bot-message {
background: white;
border: 1px solid #ddd;
}
.chat-input {
padding: 15px;
border-top: 1px solid #ddd;
display: flex;
gap: 10px;
}
.chat-input input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
.chat-input button {
background: #007bff;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
}
</style>
JavaScript Integration
// AI Chat Widget JavaScript
class AIChatWidget {
constructor(apiKey, model = 'gpt-3.5-turbo') {
this.apiKey = apiKey;
this.model = model;
this.conversationHistory = [];
this.init();
}
init() {
this.bindEvents();
this.loadFromStorage();
}
bindEvents() {
document.getElementById('send-btn').addEventListener('click', () => this.sendMessage());
document.getElementById('user-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.sendMessage();
});
document.querySelector('.close-btn').addEventListener('click', () => {
this.hideWidget();
});
}
async sendMessage() {
const input = document.getElementById('user-input');
const message = input.value.trim();
if (!message) return;
// Add user message to UI
this.addMessage(message, 'user');
input.value = '';
// Show loading indicator
this.showLoading();
try {
const response = await this.callAIAPI(message);
this.addMessage(response, 'bot');
this.saveToStorage();
} catch (error) {
this.addMessage('Sorry, I encountered an error. Please try again.', 'bot');
console.error('AI API Error:', error);
} finally {
this.hideLoading();
}
}
async callAIAPI(userMessage) {
// Add user message to conversation history
this.conversationHistory.push({ role: 'user', content: userMessage });
// Keep only last 10 messages to manage context
if (this.conversationHistory.length > 10) {
this.conversationHistory = this.conversationHistory.slice(-10);
}
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
model: this.model,
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
...this.conversationHistory
],
max_tokens: 500,
temperature: 0.7
})
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
const aiResponse = data.choices[0].message.content;
// Add AI response to conversation history
this.conversationHistory.push({ role: 'assistant', content: aiResponse });
return aiResponse;
}
addMessage(content, sender) {
const messagesContainer = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}-message`;
messageDiv.textContent = content;
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
showLoading() {
const messagesContainer = document.getElementById('chat-messages');
const loadingDiv = document.createElement('div');
loadingDiv.id = 'loading-message';
loadingDiv.className = 'message bot-message';
loadingDiv.innerHTML = '<div class="loading-dots"><span></span><span></span><span></span></div>';
messagesContainer.appendChild(loadingDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
hideLoading() {
const loadingMessage = document.getElementById('loading-message');
if (loadingMessage) {
loadingMessage.remove();
}
}
saveToStorage() {
localStorage.setItem('aiChatHistory', JSON.stringify(this.conversationHistory));
}
loadFromStorage() {
const saved = localStorage.getItem('aiChatHistory');
if (saved) {
this.conversationHistory = JSON.parse(saved);
// Display last message if exists
if (this.conversationHistory.length > 0) {
const lastMessage = this.conversationHistory[this.conversationHistory.length - 1];
if (lastMessage.role === 'assistant') {
this.addMessage(lastMessage.content, 'bot');
}
}
}
}
hideWidget() {
document.getElementById('ai-chat-widget').style.display = 'none';
}
showWidget() {
document.getElementById('ai-chat-widget').style.display = 'flex';
}
}
// Initialize the chat widget
// Note: In production, use environment variables for API key
const chatWidget = new AIChatWidget('your-openai-api-key-here');
Serverless AI Integration
Netlify Function Example
// netlify/functions/ai-chat.js
const OpenAI = require('openai');
exports.handler = async function(event, context) {
// Handle CORS
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS'
};
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: ''
};
}
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: 'Method not allowed' })
};
}
try {
const { message, conversationHistory = [] } = JSON.parse(event.body);
if (!message) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: 'Message is required' })
};
}
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
});
const messages = [
{
role: 'system',
content: 'You are a helpful assistant. Provide concise and helpful responses.'
},
...conversationHistory.slice(-8), // Keep last 8 messages for context
{ role: 'user', content: message }
];
const completion = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: messages,
max_tokens: 500,
temperature: 0.7
});
const response = completion.choices[0].message.content;
return {
statusCode: 200,
headers,
body: JSON.stringify({
response: response,
usage: completion.usage
})
};
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: 'Internal server error',
message: error.message
})
};
}
};
Client-Side Integration with Serverless
// Enhanced chat widget with serverless backend
class ServerlessAIChatWidget extends AIChatWidget {
constructor() {
super(); // No API key needed for client
this.endpoint = '/.netlify/functions/ai-chat';
}
async callAIAPI(userMessage) {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: userMessage,
conversationHistory: this.conversationHistory
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'API request failed');
}
const data = await response.json();
const aiResponse = data.response;
// Update conversation history
this.conversationHistory.push(
{ role: 'user', content: userMessage },
{ role: 'assistant', content: aiResponse }
);
return aiResponse;
}
}
// Usage - no API key exposed in client code
const chatWidget = new ServerlessAIChatWidget();
Pre-built AI Widgets
Embeddable Chat Interface
<!-- One-line embed code -->
<script>
(function() {
var script = document.createElement('script');
script.src = 'https://cdn.yourdomain.com/ai-widget.js';
script.setAttribute('data-api-key', 'YOUR_PUBLIC_KEY');
script.setAttribute('data-theme', 'light');
script.setAttribute('data-position', 'bottom-right');
document.head.appendChild(script);
})();
</script>
// ai-widget.js - Self-contained widget
(function() {
const config = {
apiKey: document.currentScript.getAttribute('data-api-key'),
theme: document.currentScript.getAttribute('data-theme') || 'light',
position: document.currentScript.getAttribute('data-position') || 'bottom-right'
};
// Create widget HTML
const widgetHTML = `
<div id="ai-widget-container" class="ai-widget ${config.theme} ${config.position}">
<button id="ai-widget-toggle" class="ai-widget-toggle">
🤖
</button>
<div id="ai-widget-content" class="ai-widget-content hidden">
<div class="ai-widget-header">
<h3>AI Assistant</h3>
<button class="ai-widget-close">×</button>
</div>
<div class="ai-widget-messages"></div>
<div class="ai-widget-input">
<input type="text" placeholder="Ask me anything...">
<button>Send</button>
</div>
</div>
</div>
`;
// Inject styles
const styles = `
.ai-widget {
position: fixed;
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.ai-widget.bottom-right {
bottom: 20px;
right: 20px;
}
.ai-widget-toggle {
width: 60px;
height: 60px;
border-radius: 50%;
background: #007bff;
color: white;
border: none;
font-size: 24px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.ai-widget-content {
position: absolute;
bottom: 70px;
right: 0;
width: 350px;
height: 500px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
display: flex;
flex-direction: column;
}
.ai-widget.light .ai-widget-content {
background: white;
color: #333;
}
.ai-widget.dark .ai-widget-content {
background: #2d3748;
color: white;
}
.hidden {
display: none !important;
}
/* Additional styles for chat interface */
`;
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
// Inject widget HTML
document.body.insertAdjacentHTML('beforeend', widgetHTML);
// Initialize widget functionality
const toggleBtn = document.getElementById('ai-widget-toggle');
const widgetContent = document.getElementById('ai-widget-content');
const closeBtn = document.querySelector('.ai-widget-close');
toggleBtn.addEventListener('click', () => {
widgetContent.classList.toggle('hidden');
});
closeBtn.addEventListener('click', () => {
widgetContent.classList.add('hidden');
});
// Add chat functionality
// ... similar to previous chat implementation
})();
</script>
AI-Powered Search
// Semantic search for static sites
class AISiteSearch {
constructor(apiKey, searchIndex) {
this.apiKey = apiKey;
this.searchIndex = searchIndex; // Pre-computed embeddings
this.embeddings = {};
this.init();
}
async init() {
await this.loadEmbeddings();
this.setupSearchUI();
}
async loadEmbeddings() {
// Load pre-computed embeddings for site content
const response = await fetch('/search-index.json');
this.embeddings = await response.json();
}
setupSearchUI() {
const searchHTML = `
<div class="ai-search-container">
<input type="text" id="ai-search-input" placeholder="Search with AI...">
<div id="ai-search-results" class="ai-search-results"></div>
</div>
`;
// Inject search UI into page
const searchContainer = document.querySelector('.search-area') || document.body;
searchContainer.insertAdjacentHTML('beforeend', searchHTML);
// Add event listeners
const searchInput = document.getElementById('ai-search-input');
searchInput.addEventListener('input', this.debounce(() => {
this.performSearch(searchInput.value);
}, 300));
}
async performSearch(query) {
if (!query.trim()) {
this.clearResults();
return;
}
// Get query embedding
const queryEmbedding = await this.getEmbedding(query);
// Find similar content using cosine similarity
const results = this.findSimilarContent(queryEmbedding);
this.displayResults(results, query);
}
async getEmbedding(text) {
// Use OpenAI embeddings API
const response = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
model: 'text-embedding-ada-002',
input: text
})
});
const data = await response.json();
return data.data[0].embedding;
}
findSimilarContent(queryEmbedding, topK = 5) {
const similarities = [];
for (const [url, pageData] of Object.entries(this.embeddings)) {
const similarity = this.cosineSimilarity(queryEmbedding, pageData.embedding);
similarities.push({
url: url,
title: pageData.title,
content: pageData.content,
similarity: similarity
});
}
// Sort by similarity and return top results
return similarities
.sort((a, b) => b.similarity - a.similarity)
.slice(0, topK);
}
cosineSimilarity(a, b) {
const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0);
const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
return dotProduct / (magnitudeA * magnitudeB);
}
displayResults(results, query) {
const resultsContainer = document.getElementById('ai-search-results');
if (results.length === 0) {
resultsContainer.innerHTML = '<div class="no-results">No results found</div>';
return;
}
const resultsHTML = results.map(result => `
<div class="search-result">
<a href="${result.url}" class="result-title">${result.title}</a>
<div class="result-snippet">${this.generateSnippet(result.content, query)}</div>
<div class="result-confidence">Relevance: ${(result.similarity * 100).toFixed(1)}%</div>
</div>
`).join('');
resultsContainer.innerHTML = resultsHTML;
}
generateSnippet(content, query) {
// Simple snippet generation - highlight query terms
const words = query.toLowerCase().split(' ');
let snippet = content.substring(0, 200);
words.forEach(word => {
const regex = new RegExp(word, 'gi');
snippet = snippet.replace(regex, '<mark>$&</mark>');
});
return snippet + '...';
}
clearResults() {
document.getElementById('ai-search-results').innerHTML = '';
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
}
// Initialize AI search
// const aiSearch = new AISiteSearch('your-api-key', 'search-index.json');
Performance Optimization
Lazy Loading AI Features
// Lazy load AI widgets when needed
class LazyAILoader {
constructor() {
this.observers = new Map();
this.init();
}
init() {
// Observe elements with data-ai-widget attribute
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadWidget(entry.target);
observer.unobserve(entry.target);
}
});
}, {
rootMargin: '50px', // Load 50px before element is visible
threshold: 0.1
});
// Find all lazy AI widgets
document.querySelectorAll('[data-ai-widget]').forEach(element => {
observer.observe(element);
});
}
async loadWidget(element) {
const widgetType = element.getAttribute('data-ai-widget');
switch(widgetType) {
case 'chat':
await this.loadChatWidget(element);
break;
case 'search':
await this.loadSearchWidget(element);
break;
case 'summary':
await this.loadSummaryWidget(element);
break;
}
}
async loadChatWidget(container) {
// Dynamically import chat widget
const { AIChatWidget } = await import('./ai-chat-widget.js');
new AIChatWidget(container);
}
async loadSearchWidget(container) {
const { AISiteSearch } = await import('./ai-search.js');
new AISiteSearch(container);
}
async loadSummaryWidget(container) {
// Generate summary for long content
const content = container.getAttribute('data-content') || container.textContent;
const summary = await this.generateSummary(content);
container.innerHTML = summary;
}
async generateSummary(text) {
// Use AI to generate concise summary
// Implementation depends on your AI provider
return text.substring(0, 200) + '...'; // Fallback
}
}
// Initialize lazy loader when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new LazyAILoader();
});
Caching Strategies
// Cache AI responses for better performance
class AICache {
constructor() {
this.cache = new Map();
this.maxSize = 100; // Maximum cache entries
}
getKey(messages, model) {
// Create a unique key for the request
return JSON.stringify({ messages, model });
}
get(messages, model) {
const key = this.getKey(messages, model);
const entry = this.cache.get(key);
if (entry && Date.now() < entry.expiry) {
return entry.response;
}
// Remove expired entry
this.cache.delete(key);
return null;
}
set(messages, model, response, ttl = 5 * 60 * 1000) { // 5 minutes default
const key = this.getKey(messages, model);
// Remove oldest entry if cache is full
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
response: response,
expiry: Date.now() + ttl
});
}
clear() {
this.cache.clear();
}
}
// Enhanced AI client with caching
class CachedAIClient extends AIChatWidget {
constructor(apiKey, model = 'gpt-3.5-turbo') {
super(apiKey, model);
this.cache = new AICache();
}
async callAIAPI(userMessage) {
const messages = [
{ role: 'system', content: 'You are a helpful assistant.' },
...this.conversationHistory,
{ role: 'user', content: userMessage }
];
// Check cache first
const cachedResponse = this.cache.get(messages, this.model);
if (cachedResponse) {
console.log('Using cached response');
return cachedResponse;
}
// Not in cache, make API call
const response = await super.callAIAPI(userMessage);
// Cache the response
this.cache.set(messages, this.model, response);
return response;
}
}
Security Considerations
| Risk | Mitigation | Implementation |
|---|---|---|
| API Key Exposure | Use serverless functions | Netlify/Vercel functions |
| Rate Limiting | Client-side throttling | Request queuing and delays |
| Content Security | Input sanitization | HTML escaping, content filtering |
| Data Privacy | Minimal data collection | Local storage only, no PII |
Secure Implementation Checklist
- Never expose API keys in client-side code
- Implement rate limiting and request throttling
- Sanitize all user inputs before processing
- Use HTTPS for all API communications
- Implement proper CORS policies
- Regularly update dependencies and libraries
- Monitor for unusual usage patterns
- Provide clear privacy policies to users