Examples
This page provides complete, runnable examples for integrating with the Blue Oyster Personal Companion API.Basic Chat Application
HTML Structure
Copy
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blue Oyster Chat</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; margin: 0; padding: 20px; }
.chat-container { max-width: 800px; margin: 0 auto; }
.messages { height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; margin-bottom: 10px; }
.message { margin-bottom: 10px; padding: 8px; border-radius: 8px; }
.user-message { background: #007bff; color: white; margin-left: 100px; }
.assistant-message { background: #f8f9fa; margin-right: 100px; }
.input-area { display: flex; gap: 10px; }
input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
button { padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; }
.loading { opacity: 0.6; }
.personality-selector { margin-bottom: 10px; }
.personality-btn { padding: 5px 10px; margin: 0 5px; border: 1px solid #ddd; background: white; cursor: pointer; }
.personality-btn.active { background: #007bff; color: white; }
</style>
</head>
<body>
<div class="chat-container">
<h1>Blue Oyster Personal Companion</h1>
<div class="personality-selector">
<button class="personality-btn active" data-personality="friend">🤝 Friend</button>
<button class="personality-btn" data-personality="guru">🧘 Guru</button>
<button class="personality-btn" data-personality="wanderer">🗺️ Wanderer</button>
<button class="personality-btn" data-personality="philosopher">🤔 Philosopher</button>
</div>
<div id="messages" class="messages"></div>
<div class="input-area">
<input type="text" id="messageInput" placeholder="Ask me anything about spirituality..." />
<button onclick="sendMessage()">Send</button>
</div>
</div>
<script src="chat.js"></script>
</body>
</html>
JavaScript Implementation
Copy
// chat.js
class BlueOysterChat {
constructor() {
this.baseUrl = 'https://oyester.metaphy.live';
this.currentPersonality = 'friend';
this.threadId = null;
this.isLoading = false;
this.initializeUI();
}
initializeUI() {
// Personality selector
document.querySelectorAll('.personality-btn').forEach(btn => {
btn.addEventListener('click', () => {
this.setPersonality(btn.dataset.personality);
});
});
// Message input
document.getElementById('messageInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
}
});
}
setPersonality(personality) {
this.currentPersonality = personality;
// Update UI
document.querySelectorAll('.personality-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.personality === personality);
});
this.addMessage(`Switched to ${personality} personality`, 'system');
}
async sendMessage() {
if (this.isLoading) return;
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (!message) return;
input.value = '';
this.addMessage(message, 'user');
await this.processMessage(message);
}
async processMessage(message) {
this.isLoading = true;
this.showTypingIndicator();
try {
const response = await this.callAPI(message);
this.hideTypingIndicator();
this.addMessage(response.text, 'assistant');
// Store thread ID for continuity
if (response.threadId) {
this.threadId = response.threadId;
}
} catch (error) {
this.hideTypingIndicator();
this.addMessage(`Error: ${error.message}`, 'error');
} finally {
this.isLoading = false;
}
}
async callAPI(message) {
const response = await fetch(`${this.baseUrl}/api/agents/companionAgent/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
messages: [{ role: 'user', content: message }],
runtimeContext: {
metadata: {
personality: this.currentPersonality
}
},
threadId: this.threadId,
resourceId: 'demo-user'
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || `HTTP ${response.status}`);
}
return await response.json();
}
addMessage(text, type) {
const messages = document.getElementById('messages');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}-message`;
if (type === 'system') {
messageDiv.style.fontStyle = 'italic';
messageDiv.style.color = '#666';
} else if (type === 'error') {
messageDiv.style.color = '#dc3545';
}
messageDiv.textContent = text;
messages.appendChild(messageDiv);
messages.scrollTop = messages.scrollHeight;
}
showTypingIndicator() {
const messages = document.getElementById('messages');
const indicator = document.createElement('div');
indicator.className = 'message assistant-message typing-indicator';
indicator.innerHTML = '<em>Thinking...</em>';
indicator.id = 'typing-indicator';
messages.appendChild(indicator);
messages.scrollTop = messages.scrollHeight;
}
hideTypingIndicator() {
const indicator = document.getElementById('typing-indicator');
if (indicator) {
indicator.remove();
}
}
}
// Initialize chat when page loads
const chat = new BlueOysterChat();
// Global function for button
function sendMessage() {
chat.sendMessage();
}
Streaming Chat Application
Enhanced Streaming Implementation
Copy
class StreamingChat extends BlueOysterChat {
constructor() {
super();
this.useStreaming = true;
}
async processMessage(message) {
this.isLoading = true;
this.showTypingIndicator();
try {
const fullResponse = await this.callStreamingAPI(message);
this.hideTypingIndicator();
// Response is built progressively, so just finalize
} catch (error) {
this.hideTypingIndicator();
this.addMessage(`Error: ${error.message}`, 'error');
} finally {
this.isLoading = false;
}
}
async callStreamingAPI(message) {
const response = await fetch(`${this.baseUrl}/api/agents/companionAgent/stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
messages: [{ role: 'user', content: message }],
runtimeContext: {
metadata: {
personality: this.currentPersonality
}
},
threadId: this.threadId,
resourceId: 'demo-user'
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || `HTTP ${response.status}`);
}
return await this.processStream(response);
}
async processStream(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullText = '';
let currentMessageDiv = null;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
await this.handleStreamEvent(data, fullText, currentMessageDiv);
} catch (e) {
console.warn('Failed to parse stream event:', e);
}
}
}
}
return fullText;
} catch (error) {
throw error;
} finally {
reader.releaseLock();
}
}
async handleStreamEvent(event, fullText, currentMessageDiv) {
switch (event.type) {
case 'text':
if (!currentMessageDiv) {
this.hideTypingIndicator();
currentMessageDiv = this.createStreamingMessage();
}
currentMessageDiv.textContent += event.text;
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
break;
case 'tool-call':
this.addMessage(`🔧 Using ${event.payload.toolName}...`, 'system');
break;
case 'tool-result':
const toolName = event.payload.toolName;
const result = event.payload.result;
if (toolName === 'weatherTool') {
this.addMessage(`🌤️ Weather: ${result.temperature}°C, ${result.conditions}`, 'system');
} else if (toolName === 'search-destinations') {
this.addMessage(`📍 Found ${result.totalFound} spiritual destinations`, 'system');
}
break;
case 'error':
this.addMessage(`❌ Error: ${event.error.message}`, 'error');
break;
}
}
createStreamingMessage() {
const messages = document.getElementById('messages');
const messageDiv = document.createElement('div');
messageDiv.className = 'message assistant-message';
messages.appendChild(messageDiv);
return messageDiv;
}
}
// Use streaming version
const chat = new StreamingChat();
Meditation Timer with Audio
Complete Meditation App
Copy
// meditation-app.js
class MeditationApp {
constructor() {
this.chat = new BlueOysterChat();
this.audioPlayer = new MeditationAudioPlayer();
this.timer = new MeditationTimer();
this.currentSession = null;
this.initializeUI();
}
initializeUI() {
// Timer controls
document.getElementById('start-timer').addEventListener('click', () => {
this.startMeditationSession();
});
document.getElementById('stop-timer').addEventListener('click', () => {
this.stopMeditationSession();
});
// Duration selector
document.getElementById('duration').addEventListener('change', (e) => {
this.timer.setDuration(parseInt(e.target.value));
});
}
async startMeditationSession() {
const mood = document.getElementById('meditation-mood').value;
const duration = parseInt(document.getElementById('duration').value);
// Get guidance for the session
const guidance = await this.getMeditationGuidance(mood, duration);
// Find and play appropriate audio
await this.loadMeditationAudio(mood);
// Start the timer
this.timer.start(duration * 60, (remaining) => {
this.updateTimerDisplay(remaining);
}, () => {
this.onSessionComplete();
});
this.currentSession = { mood, duration, guidance };
this.updateUI('active');
}
async getMeditationGuidance(mood, duration) {
const prompt = `Guide me through a ${duration}-minute ${mood} meditation session. Provide step-by-step instructions.`;
const response = await this.chat.callAPI(prompt);
return response.text;
}
async loadMeditationAudio(mood) {
// Search for appropriate meditation audio
const searchResponse = await this.chat.callAPI(
`Find ${mood} meditation audio tracks for a ${duration}-minute session`
);
// Extract audio URLs from tool results
const audioResults = searchResponse.toolResults?.find(
r => r.toolName === 'search-meditation-audio'
);
if (audioResults?.result?.tracks?.length > 0) {
const track = audioResults.result.tracks[0];
this.audioPlayer.playTrack(track);
}
}
stopMeditationSession() {
this.timer.stop();
this.audioPlayer.pause();
this.updateUI('stopped');
}
onSessionComplete() {
this.audioPlayer.fadeOut();
this.updateUI('completed');
// Get completion message
setTimeout(async () => {
const completionMessage = await this.getCompletionMessage();
this.chat.addMessage(completionMessage, 'assistant');
}, 2000);
}
async getCompletionMessage() {
const response = await this.chat.callAPI(
'I just completed my meditation session. What are your thoughts?'
);
return response.text;
}
updateTimerDisplay(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
document.getElementById('timer-display').textContent =
`${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
updateUI(state) {
const startBtn = document.getElementById('start-timer');
const stopBtn = document.getElementById('stop-timer');
switch (state) {
case 'active':
startBtn.disabled = true;
stopBtn.disabled = false;
break;
case 'stopped':
case 'completed':
startBtn.disabled = false;
stopBtn.disabled = true;
break;
}
}
}
class MeditationTimer {
constructor() {
this.interval = null;
this.duration = 5; // minutes
this.remaining = 0;
}
setDuration(minutes) {
this.duration = minutes;
}
start(durationSeconds, onTick, onComplete) {
this.remaining = durationSeconds;
this.interval = setInterval(() => {
this.remaining--;
if (this.remaining <= 0) {
this.stop();
onComplete();
} else {
onTick(this.remaining);
}
}, 1000);
}
stop() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
}
class MeditationAudioPlayer {
constructor() {
this.audio = new Audio();
this.currentTrack = null;
}
async playTrack(track) {
this.currentTrack = track;
this.audio.src = track.audioUrl;
this.audio.volume = 0.3; // Gentle volume
try {
await this.audio.play();
} catch (error) {
console.warn('Audio playback failed:', error);
}
}
pause() {
this.audio.pause();
}
fadeOut() {
const fadeStep = 0.05;
const fadeInterval = setInterval(() => {
if (this.audio.volume > fadeStep) {
this.audio.volume -= fadeStep;
} else {
this.audio.volume = 0;
this.audio.pause();
clearInterval(fadeInterval);
}
}, 100);
}
}
// HTML for meditation app
const meditationHTML = `
<div class="meditation-app">
<h2>Meditation Session</h2>
<div class="session-controls">
<select id="meditation-mood">
<option value="calm">Calm</option>
<option value="peaceful">Peaceful</option>
<option value="focused">Focused</option>
<option value="relaxing">Relaxing</option>
</select>
<select id="duration">
<option value="5">5 minutes</option>
<option value="10">10 minutes</option>
<option value="15">15 minutes</option>
<option value="20">20 minutes</option>
</select>
<button id="start-timer">Start Session</button>
<button id="stop-timer" disabled>Stop</button>
</div>
<div id="timer-display" class="timer">5:00</div>
<div id="guidance" class="guidance-area"></div>
</div>
`;
// Initialize the app
const meditationApp = new MeditationApp();
Weather-Aware Spiritual Planning
Travel Planning Integration
Copy
class SpiritualTravelPlanner {
constructor(chat) {
this.chat = chat;
this.weatherCache = new Map();
}
async planSpiritualTrip(destination, interests) {
// Get weather for the destination
const weather = await this.getWeather(destination);
// Find spiritual destinations
const destinations = await this.findDestinations(destination, interests);
// Create personalized itinerary
const itinerary = await this.createItinerary(destinations, weather, interests);
return {
destination,
weather,
destinations,
itinerary,
recommendations: this.generateRecommendations(weather, destinations)
};
}
async getWeather(location) {
if (this.weatherCache.has(location)) {
return this.weatherCache.get(location);
}
const response = await this.chat.callAPI(`What's the weather like in ${location}?`);
const weatherData = this.extractWeatherFromResponse(response);
if (weatherData) {
this.weatherCache.set(location, weatherData);
}
return weatherData;
}
async findDestinations(location, interests) {
const query = `Find spiritual destinations in ${location} related to ${interests.join(', ')}`;
const response = await this.chat.callAPI(query);
const toolResults = response.toolResults || [];
const destinationResult = toolResults.find(r => r.toolName === 'search-destinations');
return destinationResult?.result?.destinations || [];
}
async createItinerary(destinations, weather, interests) {
const destinationNames = destinations.slice(0, 3).map(d => d.name).join(', ');
const query = `Create a spiritual itinerary for ${destinationNames} considering ${weather.conditions} weather. Focus on ${interests.join(', ')} activities.`;
const response = await this.chat.callAPI(query);
return response.text;
}
generateRecommendations(weather, destinations) {
const recommendations = [];
if (weather.temperature < 15) {
recommendations.push('Bring warm clothing for outdoor activities');
}
if (weather.humidity > 70) {
recommendations.push('Stay hydrated, especially during meditation');
}
if (destinations.some(d => d.category === 'temple')) {
recommendations.push('Visit temples early morning for peaceful atmosphere');
}
return recommendations;
}
extractWeatherFromResponse(response) {
const toolResults = response.toolResults || [];
const weatherResult = toolResults.find(r => r.toolName === 'weatherTool');
return weatherResult?.result || null;
}
}
// Usage
const travelPlanner = new SpiritualTravelPlanner(chat);
async function planTrip() {
const trip = await travelPlanner.planSpiritualTrip('Kyoto, Japan', [
'Zen meditation',
'temple visits',
'traditional tea ceremony'
]);
console.log('Spiritual Trip Plan:', trip);
}
Voice Integration with Speech-to-Text
Voice-Enabled Chat
Copy
class VoiceEnabledChat extends StreamingChat {
constructor() {
super();
this.recognition = null;
this.synthesis = new SpeechSynthesisUtterance();
this.isListening = false;
this.initializeVoice();
}
initializeVoice() {
// Check for Web Speech API support
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
this.recognition = new SpeechRecognition();
this.recognition.continuous = false;
this.recognition.interimResults = false;
this.recognition.lang = 'en-US';
this.recognition.onresult = (event) => {
const transcript = event.results[0][0].transcript;
document.getElementById('messageInput').value = transcript;
this.sendMessage();
};
this.recognition.onerror = (event) => {
console.error('Speech recognition error:', event.error);
this.stopListening();
};
this.recognition.onend = () => {
this.stopListening();
};
}
// Voice synthesis
this.synthesis.rate = 0.9;
this.synthesis.pitch = 1;
}
startListening() {
if (!this.recognition) {
alert('Speech recognition not supported in this browser');
return;
}
if (this.isListening) return;
this.isListening = true;
this.recognition.start();
// Update UI
document.getElementById('voice-btn').classList.add('listening');
document.getElementById('voice-btn').textContent = '🎤 Listening...';
}
stopListening() {
if (this.recognition && this.isListening) {
this.recognition.stop();
}
this.isListening = false;
// Update UI
document.getElementById('voice-btn').classList.remove('listening');
document.getElementById('voice-btn').textContent = '🎤';
}
speakText(text) {
if ('speechSynthesis' in window) {
this.synthesis.text = text;
window.speechSynthesis.speak(this.synthesis);
}
}
async processVoiceMessage(audioBlob) {
try {
// Convert blob to base64
const base64Audio = await this.blobToBase64(audioBlob);
// Send to speech-to-text endpoint
const transcript = await this.transcribeAudio(base64Audio);
// Process the transcribed text
document.getElementById('messageInput').value = transcript;
await this.sendMessage();
} catch (error) {
console.error('Voice processing error:', error);
this.addMessage('Sorry, I couldn\'t understand that audio. Please try again.', 'error');
}
}
async transcribeAudio(base64Audio) {
const formData = new FormData();
// Convert base64 back to blob for upload
const audioBlob = this.base64ToBlob(base64Audio);
formData.append('audio', audioBlob);
const response = await fetch(`${this.baseUrl}/stt`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Speech-to-text failed');
}
const result = await response.json();
return result.transcript;
}
blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
base64ToBlob(base64) {
const byteCharacters = atob(base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
return new Blob([byteArray], { type: 'audio/wav' });
}
async handleStreamEvent(event, fullText, currentMessageDiv) {
// Call parent method
await super.handleStreamEvent(event, fullText, currentMessageDiv);
// Auto-speak completed responses
if (event.type === 'run-finish' && fullText) {
setTimeout(() => this.speakText(fullText), 500);
}
}
}
// Enhanced HTML with voice controls
const voiceChatHTML = `
<div class="voice-controls">
<button id="voice-btn" onclick="toggleVoice()">🎤</button>
<button onclick="speakLastResponse()">🔊</button>
</div>
`;
// Global functions
function toggleVoice() {
if (chat.isListening) {
chat.stopListening();
} else {
chat.startListening();
}
}
function speakLastResponse() {
const messages = document.querySelectorAll('.assistant-message');
const lastMessage = messages[messages.length - 1];
if (lastMessage) {
chat.speakText(lastMessage.textContent);
}
}
