// Speaker management for multi-speaker support export interface Speaker { id: string; name: string; language: string; color: string; avatar?: string; isActive: boolean; lastActiveTime?: number; } export interface SpeakerTranscription { speakerId: string; text: string; language: string; timestamp: number; } export interface ConversationEntry { id: string; speakerId: string; originalText: string; originalLanguage: string; translations: Map; // languageCode -> translatedText timestamp: number; audioUrl?: string; } export class SpeakerManager { private static instance: SpeakerManager; private speakers: Map = new Map(); private conversation: ConversationEntry[] = []; private activeSpeakerId: string | null = null; private maxConversationLength = 100; // Predefined colors for speakers private speakerColors = [ '#007bff', '#28a745', '#dc3545', '#ffc107', '#17a2b8', '#6f42c1', '#e83e8c', '#fd7e14' ]; private constructor() { this.loadFromLocalStorage(); } static getInstance(): SpeakerManager { if (!SpeakerManager.instance) { SpeakerManager.instance = new SpeakerManager(); } return SpeakerManager.instance; } // Add a new speaker addSpeaker(name: string, language: string): Speaker { const id = this.generateSpeakerId(); const colorIndex = this.speakers.size % this.speakerColors.length; const speaker: Speaker = { id, name, language, color: this.speakerColors[colorIndex], isActive: false, avatar: this.generateAvatar(name) }; this.speakers.set(id, speaker); this.saveToLocalStorage(); return speaker; } // Update speaker updateSpeaker(id: string, updates: Partial): void { const speaker = this.speakers.get(id); if (speaker) { Object.assign(speaker, updates); this.saveToLocalStorage(); } } // Remove speaker removeSpeaker(id: string): void { this.speakers.delete(id); if (this.activeSpeakerId === id) { this.activeSpeakerId = null; } this.saveToLocalStorage(); } // Get all speakers getAllSpeakers(): Speaker[] { return Array.from(this.speakers.values()); } // Get speaker by ID getSpeaker(id: string): Speaker | undefined { return this.speakers.get(id); } // Set active speaker setActiveSpeaker(id: string | null): void { // Deactivate all speakers this.speakers.forEach(speaker => { speaker.isActive = false; }); // Activate selected speaker if (id && this.speakers.has(id)) { const speaker = this.speakers.get(id)!; speaker.isActive = true; speaker.lastActiveTime = Date.now(); this.activeSpeakerId = id; } else { this.activeSpeakerId = null; } this.saveToLocalStorage(); } // Get active speaker getActiveSpeaker(): Speaker | null { return this.activeSpeakerId ? this.speakers.get(this.activeSpeakerId) || null : null; } // Add conversation entry addConversationEntry( speakerId: string, originalText: string, originalLanguage: string ): ConversationEntry { const entry: ConversationEntry = { id: this.generateEntryId(), speakerId, originalText, originalLanguage, translations: new Map(), timestamp: Date.now() }; this.conversation.push(entry); // Limit conversation length if (this.conversation.length > this.maxConversationLength) { this.conversation.shift(); } this.saveToLocalStorage(); return entry; } // Add translation to conversation entry addTranslation(entryId: string, language: string, translatedText: string): void { const entry = this.conversation.find(e => e.id === entryId); if (entry) { entry.translations.set(language, translatedText); this.saveToLocalStorage(); } } // Get conversation for a specific language getConversationInLanguage(language: string): Array<{ speakerId: string; speakerName: string; speakerColor: string; text: string; timestamp: number; isOriginal: boolean; }> { return this.conversation.map(entry => { const speaker = this.speakers.get(entry.speakerId); const isOriginal = entry.originalLanguage === language; const text = isOriginal ? entry.originalText : entry.translations.get(language) || `[Translating from ${entry.originalLanguage}...]`; return { speakerId: entry.speakerId, speakerName: speaker?.name || 'Unknown', speakerColor: speaker?.color || '#666', text, timestamp: entry.timestamp, isOriginal }; }); } // Get full conversation history getFullConversation(): ConversationEntry[] { return [...this.conversation]; } // Clear conversation clearConversation(): void { this.conversation = []; this.saveToLocalStorage(); } // Generate unique speaker ID private generateSpeakerId(): string { return `speaker_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } // Generate unique entry ID private generateEntryId(): string { return `entry_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } // Generate avatar initials private generateAvatar(name: string): string { const parts = name.trim().split(' '); if (parts.length >= 2) { return parts[0][0].toUpperCase() + parts[1][0].toUpperCase(); } return name.substr(0, 2).toUpperCase(); } // Save to localStorage private saveToLocalStorage(): void { try { const data = { speakers: Array.from(this.speakers.entries()), conversation: this.conversation.map(entry => ({ ...entry, translations: Array.from(entry.translations.entries()) })), activeSpeakerId: this.activeSpeakerId }; localStorage.setItem('speakerData', JSON.stringify(data)); } catch (error) { console.error('Failed to save speaker data:', error); } } // Load from localStorage private loadFromLocalStorage(): void { try { const saved = localStorage.getItem('speakerData'); if (saved) { const data = JSON.parse(saved); // Restore speakers if (data.speakers) { this.speakers = new Map(data.speakers); } // Restore conversation with Map translations if (data.conversation) { this.conversation = data.conversation.map((entry: any) => ({ ...entry, translations: new Map(entry.translations || []) })); } // Restore active speaker this.activeSpeakerId = data.activeSpeakerId || null; } } catch (error) { console.error('Failed to load speaker data:', error); } } // Export conversation as text exportConversation(language: string): string { const entries = this.getConversationInLanguage(language); return entries.map(entry => `[${new Date(entry.timestamp).toLocaleTimeString()}] ${entry.speakerName}: ${entry.text}` ).join('\n'); } }