1import { Auth } from './auth.js'; 2// phase8-voicecontrol-migrated: removed direct firestore import 3import { agentsApi } from './agentsApi.js'; 4import { phoneNumbersApi } from './phoneNumbersApi.js'; 5import { usersApi } from './usersApi.js'; 6import UI from './ui.js?v=20260820-dd1-imperr1-bann1-impauth1'; 7import { MultiCall } from './multiCall.js'; 8import { Language } from './language.js'; 9 10 11// ============================================================ 12// phase8-voicecontrol-helpers: API replacements for FS reads 13// ============================================================ 14async function _vc_authedFetch(url, options = {}) { 15 const token = await Auth.getIdToken(); 16 const headers = { 17 'Authorization': `Bearer ${token}`, 18 'Content-Type': 'application/json', 19 ...(options.headers || {}), 20 }; 21 const r = await fetch(url, { ...options, headers }); 22 if (!r.ok) { 23 let msg = `HTTP ${r.status}`; 24 try { const j = await r.json(); if (j && j.error) msg += `: ${j.error}`; } catch (e) {} 25 const err = new Error(msg); err.status = r.status; throw err; 26 } 27 if (r.status === 204) return null; 28 return r.json(); 29} 30async function _vc_getUserMe() { 31 try { return await usersApi.me(); } 32 catch (e) { console.error('phase8 _vc_getUserMe failed:', e); return null; } 33} 34async function _vc_listContacts() { 35 try { 36 const j = await _vc_authedFetch('/api/contacts'); 37 return Array.isArray(j) ? j : (j.contacts || j.items || j.data || []); 38 } catch (e) { console.error('phase8 _vc_listContacts failed:', e); return []; } 39} 40async function _vc_findOutboundAgentId(fromNumber) { 41 if (!fromNumber) return null; 42 try { 43 const list = await phoneNumbersApi.list(); 44 const arr = Array.isArray(list) ? list : (list.phoneNumbers || list.items || []); 45 const hit = arr.find(p => (p.phoneNumber === fromNumber || p.phone_number === fromNumber) && (p.outboundEnabled === true || p.outbound_enabled === true)); 46 return hit ? (hit.assignedAgentId || hit.assigned_agent_id || null) : null; 47 } catch (e) { console.error('phase8 _vc_findOutboundAgentId failed:', e); return null; } 48} 49async function _vc_findAgentForUser(uid) { 50 try { 51 const list = await agentsApi.list(); 52 const byUser = list.find(a => (a.userID === uid) || (a.userId === uid)); 53 if (byUser) return byUser; 54 const byCreator = list.find(a => (a.createdBy === uid)); 55 return byCreator || null; 56 } catch (e) { console.error('phase8 _vc_findAgentForUser failed:', e); return null; } 57} 58async function _vc_getContact(contactId) { 59 try { return await _vc_authedFetch(`/api/contacts/${encodeURIComponent(contactId)}`); } 60 catch (e) { console.error('phase8 _vc_getContact failed:', e); return null; } 61} 62async function _vc_createContactTask(contactId, taskBody) { 63 return _vc_authedFetch(`/api/contacts/${encodeURIComponent(contactId)}/tasks`, { 64 method: 'POST', 65 body: JSON.stringify(taskBody || {}), 66 }); 67} 68async function _vc_createMulticallEntry(payload) { 69 return _vc_authedFetch('/api/multicall', { 70 method: 'POST', 71 body: JSON.stringify(payload || {}), 72 }); 73} 74 75// Kurze Ãbersetzungsfunktion für einfacheren Zugriff 76function t(text) { 77 return Language.translate(text); 78} 79 80export class VoiceControl { 81 constructor() { 82 this.recognition = null; 83 this.isListening = false; 84 this.manualStop = false; 85 this.currentTranscript = ''; 86 this.contacts = []; 87 this.selectedContact = null; 88 this.commands = { 89 call: { 90 patterns: [
91 // Flexiblere Patterns - Kontext ist optional 92 /rufe?\s+(?:bitte\s+)?(.+?)\s+an(?:\s+(?:um|und|wegen|für|bezüglich)\s+(.+))?/i, // "rufe Michael Horn an" oder "rufe Michael Horn an um/und/wegen..." 93 /(?:bitte\s+)?(.+?)\s+anrufen(?:\s+(?:um|und|wegen|für|bezüglich)\s+(.+))?/i, // "Michael Horn anrufen" 94 /kontaktiere\s+(?:bitte\s+)?(.+?)(?:\s+(?:und|für|wegen|bezüglich|um)\s+(.+))?/i, // "kontaktiere Michael Horn" 95 /anruf\s+(?:an\s+|bei\s+)?(.+?)(?:\s+(?:wegen|für|bezüglich|um)\s+(.+))?/i, // "anruf bei Michael Horn" 96 /frage?\s+(.+?)\s+(?:nach\s+|wegen\s+|bezüglich\s+)(.+)/i, // "frage Michael Horn nach..." 97 /call\s+(.+?)(?:\s+(?:about|regarding|for)\s+(.+))?/i, // English: "call Michael Horn" 98 // Neue flexible Patterns 99 /telefoniere?\s+(?:mit\s+)?(.+?)(?:\s+(?:wegen|für|bezüglich|um)\s+(.+))?/i, // "telefoniere mit Michael Horn" 100 /(?:ich\s+)?(?:möchte|will)\s+(.+?)\s+(?:anrufen|kontaktieren)(?:\s+(?:um|und|wegen|für)\s+(.+))?/i, // "ich möchte Michael Horn anrufen" 101 /(?:ich\s+)?(?:möchte|will)\s+(?:gerne\s+)?mit\s+(.+?)\s+sprechen(?:\s+(?:über|wegen|bezüglich)\s+(.+))?/i, // "ich möchte mit Michael Horn sprechen" 102 /(?:ich\s+)?(?:muss|sollte|würde\s+gerne?)\s+(?:mal\s+)?mit\s+(.+?)\s+(?:sprechen|reden|telefonieren)(?:\s+(?:über|wegen|bezüglich)\s+(.+))?/i, // "ich muss mit Michael Horn sprechen" 103 /spreche?\s+(?:mal\s+)?(?:mit\s+)?(.+?)(?:\s+(?:über|wegen|bezüglich)\s+(.+))?/i, // "sprich mit Michael Horn" 104 /rede?\s+(?:mal\s+)?mit\s+(.+?)(?:\s+(?:über|wegen|bezüglich)\s+(.+))?/i, // "rede mit Michael Horn" 105 /(?:kannst\s+du\s+)?(?:bitte\s+)?(.+?)\s+(?:für\s+mich\s+)?(?:anrufen|kontaktieren)(?:\s+(?:und|wegen)\s+(.+))?/i // "kannst du Michael Horn anrufen" 106 ], 107 action: 'initiateCall' 108 }, 109 appointment: { 110 patterns: [ 111 /vereinbare\s+(?:einen\s+)?termin\s+(?:mit\s+|bei\s+)?(.+?)\s+(?:für\s+|wegen\s+|bezüglich\s+)(.+)/i, 112 /termin\s+(?:bei|mit)\s+(.+?)\s+(?:für\s+|wegen\s+)(.+)/i, 113 /schedule\s+(?:an?\s+)?appointment\s+(?:with\s+)?(.+?)(?:\s+for\s+(.+))?/i 114 ], 115 action: 'scheduleAppointment' 116 } 117 }; 118 119 this.init(); 120 } 121 122 init() { 123 // Setup UI Event Handlers first 124 this.setupUIHandlers(); 125 126 // Load contacts for autocomplete 127 this.loadContacts(); 128 129 // Initialize Speech Recognition will be done when panel opens 130 this.setupSpeechRecognition(); 131 } 132 133 setupSpeechRecognition() { 134 console.log('Setting up speech recognition'); 135 136 if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { 137 console.error('Speech recognition not supported'); 138 const statusText = document.querySelector('.voice-status-text'); 139 if (statusText) { 140 statusText.textContent = t('Spracherkennung nicht unterstützt'); 141 } 142 return; 143 } 144 145 // Cleanup existing recognition instance 146 if (this.recognition) { 147 try { 148 if (this.isListening) { 149 console.log('Stopping existing recognition'); 150 this.recognition.stop(); 151 } 152 } catch (e) { 153 console.error('Error stopping recognition:', e); 154 } 155 this.recognition.onresult = null; 156 this.recognition.onend = null; 157 this.recognition.onerror = null; 158 this.recognition.onstart = null; 159 this.isListening = false; 160 } 161 162 const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; 163 this.recognition = new SpeechRecognition(); 164 165 this.recognition.continuous = true; 166 this.recognition.interimResults = true; 167 // Always use German for voice commands 168 this.recognition.lang = 'de-DE'; 169 this.recognition.maxAlternatives = 1; 170 171 this.setupRecognitionHandlers(); 172 } 173 174 setupRecognitionHandlers() { 175 this.recognition.onresult = (event) => { 176 let finalTranscript = ''; 177 let interimTranscript = ''; 178 179 for (let i = event.resultIndex; i < event.results.length; i++) { 180 const transcript = event.results[i][0].transcript; 181 if (event.results[i].isFinal) { 182 finalTranscript += transcript; 183 } else { 184 interimTranscript += transcript; 185 } 186 } 187 188 // Update input field 189 const input = document.getElementById('voiceCommandInput'); 190 if (input) { 191 input.value = finalTranscript || interimTranscript; 192 this.currentTranscript = input.value; 193 this.showSuggestions(input.value); 194 } 195 }; 196 197 this.recognition.onstart = () => { 198 console.log('Speech recognition started'); 199 this.isListening = true; 200 this.updateUI('listening'); 201 }; 202 203 this.recognition.onend = () => { 204 console.log('Speech recognition ended'); 205 this.isListening = false;
206 this.updateUI('stopped'); 207 208 // Auto-restart if panel is still open 209 const panel = document.getElementById('voiceInputPanel'); 210 if (panel && panel.classList.contains('active') && !this.manualStop) { 211 console.log('Auto-restarting recognition'); 212 setTimeout(() => this.startListening(), 500); 213 } 214 }; 215 216 this.recognition.onerror = (event) => { 217 console.error('Speech recognition error:', event.error, event); 218 this.isListening = false; 219 220 if (event.error === 'aborted') { 221 console.log('Recognition aborted'); 222 return; 223 } 224 225 this.updateUI('error'); 226 227 if (event.error === 'no-speech') { 228 // Don't show error for no-speech, just restart 229 const panel = document.getElementById('voiceInputPanel'); 230 if (panel && panel.classList.contains('active')) { 231 setTimeout(() => this.startListening(), 1000); 232 } 233 } else if (event.error === 'not-allowed') { 234 UI.showError(t('Mikrofonzugriff wurde verweigert. Bitte erlauben Sie den Zugriff in den Browsereinstellungen.')); 235 } else if (event.error === 'network') { 236 UI.showError(t('Netzwerkfehler. Bitte überprüfen Sie Ihre Internetverbindung.')); 237 } 238 }; 239 } 240 241 setupUIHandlers() { 242 // Mic button click 243 const micButton = document.getElementById('voiceMicButton'); 244 if (micButton) { 245 micButton.addEventListener('click', () => this.togglePanel()); 246 } 247 248 // Input field events 249 const input = document.getElementById('voiceCommandInput'); 250 if (input) { 251 input.addEventListener('input', (e) => { 252 this.currentTranscript = e.target.value; 253 console.log('Input changed:', e.target.value); 254 this.showSuggestions(e.target.value); 255 }); 256 257 input.addEventListener('keydown', (e) => { 258 // For textarea: Ctrl+Enter or Cmd+Enter to execute 259 if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { 260 e.preventDefault(); 261 console.log('Ctrl/Cmd+Enter pressed, executing command'); 262 this.execute(); 263 } 264 }); 265 } 266 } 267 268 togglePanel() { 269 const panel = document.getElementById('voiceInputPanel'); 270 const micButton = document.getElementById('voiceMicButton'); 271 272 if (!panel) return; 273 274 if (panel.classList.contains('active')) { 275 this.close(); 276 } else { 277 panel.classList.add('active'); 278 const input = document.getElementById('voiceCommandInput'); 279 if (input) { 280 // Set translated placeholder 281 input.placeholder = t('Sprechen Sie oder tippen Sie Ihren Befehl...'); 282 input.focus(); 283 // Clear previous input 284 input.value = ''; 285 this.currentTranscript = ''; 286 } 287 288 // Reload contacts when opening panel 289 this.loadContacts().then(() => { 290 // Show initial suggestions after contacts are loaded 291 this.showSuggestions(''); 292 }); 293 294 // Start listening automatically if recognition is available 295 if (this.recognition) { 296 setTimeout(() => { 297 // Re-setup recognition to ensure clean state 298 this.setupSpeechRecognition(); 299 this.startListening(); 300 }, 300); 301 } 302 } 303 } 304 305 startListening() { 306 if (!this.recognition) { 307 console.error('Recognition not initialized'); 308 return; 309 } 310 311 if (this.isListening) { 312 console.log('Already listening'); 313 return; 314 } 315 316 this.manualStop = false;
317 318 try { 319 console.log('Starting speech recognition'); 320 this.recognition.start(); 321 } catch (error) { 322 console.error('Error starting recognition:', error); 323 324 if (error instanceof DOMException && error.name === 'InvalidStateError') { 325 console.log('Recognition already started, stopping first'); 326 try { 327 this.recognition.stop(); 328 } catch (stopError) { 329 console.error('Error stopping recognition:', stopError); 330 } 331 setTimeout(() => this.startListening(), 200); 332 } 333 } 334 } 335 336 stopListening() { 337 console.log('Stopping speech recognition'); 338 this.manualStop = true; 339 340 if (this.recognition) { 341 try { 342 this.recognition.stop(); 343 } catch (error) { 344 console.error('Error stopping recognition:', error); 345 } 346 } 347 348 this.isListening = false; 349 this.updateUI('stopped'); 350 } 351 352 updateUI(status) { 353 const micButton = document.getElementById('voiceMicButton'); 354 const statusIcon = document.querySelector('.voice-status-icon'); 355 const statusText = document.querySelector('.voice-status-text'); 356 357 switch (status) { 358 case 'listening': 359 micButton?.classList.add('listening'); 360 statusIcon?.classList.add('recording'); 361 if (statusText) statusText.textContent = t('Zuhören...'); 362 break; 363 case 'stopped': 364 micButton?.classList.remove('listening'); 365 statusIcon?.classList.remove('recording'); 366 if (statusText) statusText.textContent = t('Befehl eingeben'); 367 break; 368 case 'error': 369 micButton?.classList.remove('listening'); 370 if (statusText) statusText.textContent = t('Fehler'); 371 break; 372 } 373 } 374 375 async loadContacts() { 376 try { 377 const user = Auth.getCurrentUser(); 378 if (!user) { console.log('No user found, cannot load contacts'); return; } 379 // phase8-voicecontrol-migrated: API-only 380 const all = await _vc_listContacts(); 381 const withPhone = all.filter(c => c && c.phone_number && String(c.phone_number).trim() !== ''); 382 this.contacts = withPhone.length > 0 ? withPhone : all; 383 console.log(`Loaded ${this.contacts.length} contacts via API`); 384 if (this.contacts.length > 0) { 385 console.log('Sample contacts:', this.contacts.slice(0, 5).map(c => ({ 386 name: c.name, 387 phone: c.phone_number, 388 tenantID: c.tenantID 389 }))); 390 391 // Search for "Michael" in contacts 392 const michaelContacts = this.contacts.filter(c => 393 c.name && c.name.toLowerCase().includes('michael') 394 ); 395 console.log(`Found ${michaelContacts.length} contacts with "Michael" in name:`); 396 if (michaelContacts.length > 0) { 397 console.log('All Michael contacts:', michaelContacts.map(c => c.name)); 398 } 399 400 // Check exact match 401 const exactMatch = this.contacts.find(c => 402 c.name && c.name.toLowerCase() === 'michael horn' 403 ); 404 if (exactMatch) { 405 console.log('Found exact match for "Michael Horn":', { 406 name: exactMatch.name, 407 phone: exactMatch.phone_number, 408 hasPhone: !!exactMatch.phone_number, 409 id: exactMatch.id 410 }); 411 412 // Check if in filtered list 413 const inFilteredList = this.contacts.some(c => c.id === exactMatch.id); 414 console.log('Michael Horn is in filtered contacts list:', inFilteredList); 415 } else { 416 console.log('No exact match found for "Michael Horn"'); 417 } 418 } 419 } catch (error) { 420 console.error('Error loading contacts:', error); 421 console.error('Error details:', error.message, error.code); 422 } 423 } 424 425 showSuggestions(input) { 426 const suggestionsContainer = document.getElementById('voiceSuggestions'); 427 if (!suggestionsContainer) return; 428 429 suggestionsContainer.innerHTML = ''; 430 431 // Extract contact name from command if present 432 let searchInput = input; 433 if (input && input.length > 2) { 434 // Try to parse the command to extract just the contact name
435 const parsedCommand = this.parseCommand(input); 436 if (parsedCommand && parsedCommand.contactName) { 437 searchInput = parsedCommand.contactName; 438 console.log('Extracted contact name from command:', searchInput); 439 } 440 } 441 442 if (!input || input.length < 2) { 443 // Show example commands 444 const examples = [ 445 { icon: 'fa-phone', text: t('Rufe Max Mustermann an') }, 446 { icon: 'fa-calendar', text: t('Vereinbare einen Termin mit Anna Schmidt') }, 447 { icon: 'fa-redo', text: t('Kontaktiere Peter Müller für Follow-up') } 448 ]; 449 450 examples.forEach(example => { 451 const item = document.createElement('div'); 452 item.className = 'voice-suggestion-item'; 453 item.innerHTML = `<i class="fas ${example.icon}"></i>${example.text}`; 454 item.onclick = () => { 455 const input = document.getElementById('voiceCommandInput'); 456 if (input) { 457 input.value = example.text; 458 this.currentTranscript = example.text; 459 } 460 }; 461 suggestionsContainer.appendChild(item); 462 }); 463 } else { 464 // Show matching contacts based on extracted name or full input 465 const matchingContacts = this.findMatchingContacts(searchInput); 466 467 if (matchingContacts.length > 0) {
468 matchingContacts.forEach(contact => { 469 const item = document.createElement('div'); 470 item.className = 'voice-suggestion-item'; 471 item.innerHTML = ` 472 <i class="fas fa-user"></i> 473 ${contact.name} 474 ${contact.company ? `<small>(${contact.company})</small>` : ''} 475 `; 476 item.onclick = () => { 477 const currentInput = document.getElementById('voiceCommandInput').value; 478 const updatedInput = this.replaceContactName(currentInput, contact.name); 479 const input = document.getElementById('voiceCommandInput'); 480 if (input) { 481 input.value = updatedInput; 482 this.currentTranscript = updatedInput; 483 } 484 this.selectedContact = contact; 485 console.log('Contact selected from suggestions:', contact.name, contact.phone_number); 486 }; 487 suggestionsContainer.appendChild(item); 488 }); 489 } else { 490 // No matching contacts 491 const item = document.createElement('div'); 492 item.className = 'voice-suggestion-item'; 493 item.style.color = '#999'; 494 item.innerHTML = `<i class="fas fa-info-circle"></i>${t('Keine passenden Kontakte gefunden')}`; 495 suggestionsContainer.appendChild(item); 496 } 497 } 498 } 499 500 findMatchingContacts(input) { 501 const searchTerms = input.toLowerCase().split(' ').filter(t => t.length > 0); 502 503 console.log('Finding matching contacts for:', input, 'Terms:', searchTerms); 504 505 const matches = this.contacts.filter(contact => { 506 const contactName = contact.name?.toLowerCase() || ''; 507 const contactCompany = contact.company?.toLowerCase() || ''; 508 const contactPhone = contact.phone_number || ''; 509 510 // Check if ALL search terms appear in the contact (not just some)
511 const allTermsMatch = searchTerms.every(term => 512 contactName.includes(term) || 513 contactCompany.includes(term) || 514 contactPhone.includes(term) 515 ); 516 517 return allTermsMatch; 518 }); 519 520 // Sort by relevance - exact matches first 521 matches.sort((a, b) => { 522 const aName = a.name?.toLowerCase() || ''; 523 const bName = b.name?.toLowerCase() || ''; 524 const searchLower = input.toLowerCase(); 525 526 // Exact match gets highest priority 527 if (aName === searchLower) return -1; 528 if (bName === searchLower) return 1; 529 530 // Then prioritize matches that start with the search term 531 if (aName.startsWith(searchLower)) return -1; 532 if (bName.startsWith(searchLower)) return 1; 533 534 return 0; 535 }); 536 537 const results = matches.slice(0, 5); 538 console.log('Found matches:', results.map(c => c.name)); 539 540 return results; 541 } 542 543 replaceContactName(input, contactName) { 544 // Intelligent replacement of partial names with full names 545 const words = input.split(' '); 546 let bestMatch = -1; 547 let bestScore = 0; 548 549 // Find best matching position 550 for (let i = 0; i < words.length; i++) { 551 const score = this.calculateMatchScore(words.slice(i).join(' '), contactName); 552 if (score > bestScore) { 553 bestScore = score; 554 bestMatch = i; 555 } 556 } 557 558 if (bestMatch >= 0) { 559 // Calculate how many words to replace 560 const contactWords = contactName.split(' ').length; 561 const before = words.slice(0, bestMatch); 562 const after = words.slice(bestMatch + contactWords); 563 return [...before, contactName, ...after].join(' ').trim(); 564 } 565 566 return input; 567 } 568 569 calculateMatchScore(partial, full) { 570 const partialLower = partial.toLowerCase(); 571 const fullLower = full.toLowerCase(); 572 573 if (fullLower.startsWith(partialLower)) { 574 return partialLower.length / fullLower.length; 575 } 576 577 // Check if any word in partial matches any word in full 578 const partialWords = partialLower.split(' '); 579 const fullWords = fullLower.split(' '); 580 581 let matchCount = 0; 582 partialWords.forEach(pWord => { 583 if (fullWords.some(fWord => fWord.startsWith(pWord))) { 584 matchCount++; 585 } 586 }); 587 588 return matchCount / fullWords.length; 589 } 590 591 async execute() { 592 // Get input from text field (may be different from currentTranscript if manually edited) 593 const inputField = document.getElementById('voiceCommandInput'); 594 const input = (inputField ? inputField.value : this.currentTranscript).trim(); 595 596 // Update currentTranscript with actual input 597 this.currentTranscript = input; 598 599 console.log('Executing command with input:', input); 600 console.log('Current transcript was:', this.currentTranscript); 601 602 if (!input) { 603 UI.showError(t('Bitte geben Sie einen Befehl ein.')); 604 return; 605 } 606 607 // Stop listening 608 this.stopListening(); 609 610 // Parse command 611 const parsedCommand = this.parseCommand(input); 612 613 if (parsedCommand) { 614 console.log('Command recognized, executing:', parsedCommand); 615 try { 616 await this.executeCommand(parsedCommand); 617 this.close(); 618 } catch (error) { 619 console.error('Error executing command:', error); 620 UI.showError(t('Fehler beim Ausführen des Befehls.')); 621 } 622 } else {
623 console.log('Command not recognized for input:', input); 624 UI.showError(t('Befehl nicht erkannt. Bitte versuchen Sie es erneut.')); 625 } 626 } 627 628 parseCommand(input) { 629 console.log('Parsing command:', input); 630 631 // Zuerst versuchen wir die definierten Patterns 632 for (const [commandType, config] of Object.entries(this.commands)) { 633 for (const pattern of config.patterns) { 634 const match = input.match(pattern); 635 if (match) { 636 const result = { 637 type: commandType, 638 action: config.action, 639 contactName: match[1]?.trim(), 640 context: match[2]?.trim() || '' 641 }; 642 console.log('Command parsed with pattern:', result); 643 return result; 644 } 645 } 646 } 647 648 // ULTRA-FLEXIBLE ERKENNUNG 649 const inputLower = input.toLowerCase().trim(); 650 const words = inputLower.split(/\s+/); 651 652 // 1. Fuzzy-Matching für Kontakte (Teilübereinstimmung, Tippfehler-tolerant) 653 let bestMatch = null; 654 let bestScore = 0; 655 656 for (const contact of this.contacts) { 657 if (!contact.name) continue; 658 const contactNameLower = contact.name.toLowerCase(); 659 const contactWords = contactNameLower.split(/\s+/); 660 661 // Score berechnen basierend auf verschiedenen Faktoren 662 let score = 0; 663 664 // Exakte Ãbereinstimmung 665 if (inputLower.includes(contactNameLower)) { 666 score = 100; 667 } 668 // Alle Wörter des Kontaktnamens im Input 669 else if (contactWords.every(word => inputLower.includes(word))) { 670 score = 90; 671 } 672 // Teilweise Ãbereinstimmung der Wörter 673 else { 674 for (const contactWord of contactWords) { 675 for (const inputWord of words) { 676 // Exakte Wortübereinstimmung 677 if (inputWord === contactWord) { 678 score += 30; 679 } 680 // Wort beginnt gleich (mindestens 3 Zeichen) 681 else if (contactWord.length >= 3 && inputWord.length >= 3) { 682 if (contactWord.startsWith(inputWord) || inputWord.startsWith(contactWord)) { 683 score += 20; 684 } 685 // Levenshtein-ähnliche Prüfung für ähnliche Wörter 686 else if (this.areSimilar(inputWord, contactWord)) { 687 score += 15; 688 } 689 } 690 } 691 } 692 } 693 694 // Bonus für Vornamen-Match am Anfang 695 const firstName = contactWords[0]; 696 if (firstName && words[0] === firstName) { 697 score += 10; 698 } 699 700 if (score > bestScore || (score === bestScore && score > 0 && contact.name.length > (bestMatch?.name?.length || 0))) { 701 bestScore = score; 702 bestMatch = contact; 703 } 704 } 705 706 // Wenn wir einen guten Match haben (Score > 30), verwenden wir ihn 707 if (bestMatch && bestScore > 30) { 708 console.log(`Found contact with fuzzy matching: ${bestMatch.name} (score: ${bestScore})`); 709 710 // Extrahiere Kontext intelligent 711 let context = input; 712 const contactNameRegex = new RegExp( 713 bestMatch.name.split(/\s+/).join('\\s*'), 714 'gi' 715 ); 716 context = context.replace(contactNameRegex, '').trim(); 717 718 // Entferne alle Anruf-bezogenen Wörter für saubereren Kontext 719 const callWords = [ 720 'rufe?', 'anrufen', 'kontaktiere', 'telefoniere', 'spreche?',
721 'call', 'bitte', 'mal', 'doch', 'ich möchte', 'ich will', 722 'kannst du', 'könntest du', 'würdest du', 'an', 'mit' 723 ]; 724 725 for (const word of callWords) { 726 context = context.replace(new RegExp(`\\b${word}\\b`, 'gi'), ''); 727 } 728 729 // Entferne Verbindungswörter am Anfang/Ende 730 context = context.replace(/^(und|um|wegen|für|bezüglich|über|dass|ob|zur|zum|mit)\s+/i, ''); 731 context = context.replace(/\s+(und|um|wegen|für|bezüglich|über)$/i, ''); 732 context = context.trim(); 733 734 const result = { 735 type: 'call', 736 action: 'initiateCall', 737 contactName: bestMatch.name, 738 context: context 739 }; 740 console.log('Command parsed with fuzzy detection:', result); 741 return result; 742 } 743 744 // 2. Extrem flexibel: Wenn nur 1-3 Wörter eingegeben wurden, 745 // suche nac
745h dem besten Kontakt-Match 746 if (words.length <= 3 && words.length >= 1) { 747 // Versuche jeden möglichen Kontakt zu finden 748 const searchTerm = inputLower; 749 const matches = this.contacts.filter(contact => { 750 if (!contact.name) return false; 751 const cName = contact.name.toLowerCase(); 752 753 // Prüfe ob alle eingegebenen Wörter im Namen vorkommen 754 return words.every(word => { 755 // Wort ist im Namen enthalten ODER 756 // Wort ist der Anfang eines Namensbestandteils 757 return cName.includes(word) || 758 cName.split(/\s+/).some(namePart => namePart.startsWith(word)); 759 }); 760 }); 761 762 if (matches.length === 1) { 763 // Eindeutiger Match 764 const result = { 765 type: 'call', 766 action: 'initiateCall', 767 contactName: matches[0].name, 768 context: '' 769 }; 770 console.log('Command parsed with single word match:', result); 771 return result; 772 } else if (matches.length > 1) { 773 // Mehrere Matches - nimm den mit der besten Ãbereinstimmung 774 const exactMatch = matches.find(m => 775 m.name.toLowerCase() === searchTerm 776 ); 777 if (exactMatch) { 778 const result = { 779 type: 'call', 780 action: 'initiateCall', 781 contactName: exactMatch.name, 782 context: '' 783 }; 784 console.log('Command parsed with exact match from multiple:', result); 785 return result; 786 } 787 } 788 } 789 790 // 3. Letzter Versuch: Interpretiere alles als möglichen Namen 791 // wenn es wie ein Name aussieht (beginnt mit GroÃbuchstabe) 792 if (input.match(/^[A-ZÃÃÃ]/)) { 793 // Versuche progressiv kürzere Teile des Inputs als Namen 794 for (let nameLength = Math.min(3, words.length); nameLength > 0; nameLength--) { 795 const possibleName = words.slice(0, nameLength).join(' '); 796 797 // Fuzzy-Suche nach diesem Namen 798 const nameMatches = this.contacts.filter(c => { 799 if (!c.name) return false; 800 const cName = c.name.toLowerCase(); 801 const searchName = possibleName.toLowerCase(); 802 803 // Flexibler Match 804 return cName.includes(searchName) || 805 searchName.split(' ').every(part => cName.includes(part)) || 806 this.areSimilar(cName, searchName); 807 }); 808 809 if (nameMatches.length > 0) { 810 const bestNameMatch = nameMatches[0]; 811 const context = words.slice(nameLength).join(' '); 812 813 const result = { 814 type: 'call', 815 action: 'initiateCall', 816 contactName: bestNameMatch.name, 817 context: context 818 }; 819 console.log('Command parsed with progressive name search:', result); 820 return result; 821 } 822 } 823 } 824 825 console.log('No command pattern matched - input too ambiguous'); 826 return null; 827 } 828 829 // Hilfsfunktion für Ãhnlichkeitsprüfung (vereinfachte Levenshtein-Distanz) 830 areSimilar(str1, str2) { 831 // Strings zu kurz für sinnvollen Vergleich 832 if (str1.length < 3 || str2.length < 3) return false; 833 834 // Mehr als 2 Zeichen Längenunterschied = unähnlich 835 if (Math.abs(str1.length - str2.length) > 2) return false; 836 837 // Zähle unterschiedliche Zeichen 838 let differences = 0; 839 const maxLen = Math.max(str1.length, str2.length); 840 841 for (let i = 0; i < maxLen; i++) { 842 if (str1[i] !== str2[i]) { 843 differences++; 844 } 845 } 846 847 // Erlaubt 1-2 Tippfehler je nach Wortlänge 848 return differences <= Math.ceil(maxLen * 0.3); 849 } 850 851 async executeCommand(command) { 852 switch (command.action) { 853 case 'initiateCall': 854 await this.initiateCall(command.contactName, command.context); 855 break; 856 case 'scheduleAppointment': 857 await this.scheduleAppointment(command.contactName, command.context); 858 break; 859 } 860 } 861 862 async initiateCall(contactName, context) { 863 console.log('Searching for contact:', contactName); 864 console.log('Available contacts:', this.contacts.length); 865 866 // Use previously selected contact if available 867 let contact = null; 868 if (this.selectedContact) { 869 contact = this.selectedContact; 870 console.log('Using previously selected contact:', contact.name, contact.phone_number); 871 this.selectedContact = null; 872 } else { 873 // Reload contacts if none are loaded 874 if (this.contacts.length === 0) { 875 console.log('No contacts loaded, reloading...'); 876 await this.loadContacts(); 877 } 878 879 // Normalize search term 880 const searchTerm = contactName.trim().toLowerCase(); 881 console.log('Normalized search term:', searchTerm); 882 883 // Find contact - try exact match first 884 contact = this.contacts.find(c => { 885 const name = c.name?.trim().toLowerCase() || ''; 886 return name === searchTerm; 887 }); 888 889 if (contact) { 890 console.log('Found exact match:', contact.name); 891 } 892 893 // If no exact match, try partial match 894 if (!contact) { 895 console.log('No exact match, trying partial match...'); 896 const searchWords = searchTerm.split(' ').filter(w => w.length > 0); 897 898 // Find contacts where all search words appear in the name 899 const matches = this.contacts.filter(c => { 900 const contactNameLower = c.name?.toLowerCase() || ''; 901 return searchWords.every(word => contactNameLower.includes(word)); 902 }); 903 904 console.log(`Found ${matches.length} partial matches`); 905 906 if (matches.length === 1) { 907 contact = matches[0]; 908 console.log('Using single partial match:', contact.name); 909 } else if (matches.length > 1) { 910 // Try to find best match 911 const exactWordMatch = matches.find(c => { 912 const contactWords = c.name.toLowerCase().split(' '); 913 return searchWords.every(sw => contactWords.some(cw => cw === sw)); 914 }); 915 916 if (exactWordMatch) { 917 contact = exactWordMatch; 918 console.log('Found exact word match:', contact.name); 919 } else { 920 console.log('Multiple matches found:', matches.slice(0, 5).map(c => c.name)); 921 UI.showError(t(`Mehrere Kontakte gefunden für "${contactName}". Bitte spezifizieren Sie genauer.`)); 922 return; 923 } 924 } 925 } 926 927 if (!contact) { 928 UI.showError(t(`Kontakt "${contactName}" nicht gefunden.`)); 929 console.log('Contact not found. Search term:', contactName); 930 console.log('First 20 contact names:', this.contacts.slice(0, 20).map(c => c.name)); 931 return; 932 } 933 } 934 935 if (!contact.phone_number) { 936 UI.showError(t(`Keine Telefonnummer für ${contactName} hinterlegt.`)); 937 return; 938 } 939 940 // Generate appropriate template based on context - use full transcript if context is incomplete 941 const fullContext = context || this.currentTranscript.replace(new RegExp(`rufe?\\s+${contactName}\\s+an\\s+(und\\s+)?`, 'i'), '').trim(); 942 const template = await this.selectTemplate(fullContext); 943 944 // Create task description from context or original command 945 const taskDescription = context ? 946 `${context.charAt(0).toUpperCase() + context.slice(1)}` : 947 t('Anruf durchführen'); 948 949 // Create MultiCall entry data 950 const multicallData = { 951 task: `${t('Sprachbefehl')}: ${taskDescription}`, 952 greeting: template.greeting.replace('{name}', contact.name), 953 message: template.message, 954 message2: template.message2 || '', 955 details: `${t('Kontakt')}: ${contact.name}\n${t('Befehl')}: "${this.currentTranscript}"`, 956 phone_number: contact.phone_number,
957 contact_id: contact.id, 958 created_via: 'voice_command' 959 }; 960 961 // Show confirmation 962 const confirmed = await this.showConfirmation(contact, multicallData); 963 964 if (confirmed) { 965 try { 966 // Create task for the contact with call data 967 await this.createContactTask(contact.id, taskDescription, multicallData); 968 969 // Create MultiCall entry and initiate call 970 await this.createAndStartCall(multicallData); 971 UI.showSuccess(t(`Anruf an ${contact.name} wird gestartet...`)); 972 } catch (error) { 973 console.error('Error in voice call initiation:', error); 974 UI.showError(t('Fehler beim Erstellen des Anrufs')); 975 } 976 } 977 } 978 979 async createContactTask(contactId, taskDescription, callData = {}) { 980 try { 981 const user = Auth.getCurrentUser(); 982 if (!user) { 983 throw new Error('User not authenticated'); 984 } 985 986 // phase8-voicecontrol-migrated: tenant + outbound-agent via API 987 let tenantId = null; 988 try { tenantId = await Auth.getCurrentTenantId(); } catch (e) {} 989 const _me = await _vc_getUserMe(); 990 if (!tenantId && _me) tenantId = _me.tenantID || _me.tenant_id || _me.tenantId; 991 let agentId = null; 992 const fromNumber = _me && _me.from_number; 993 if (fromNumber) agentId = await _vc_findOutboundAgentId(fromNumber); 994 if (!agentId) { 995 const a = await _vc_findAgentForUser(user.uid); 996 if (a) agentId = a.id; 997 } 998 999 // phase8-voicecontrol-migrated: API 1000 const created = await _vc_createContactTask(contactId, { 1001 task: taskDescription, 1002 status: 'offen', 1003 attempt: 0, 1004 greeting: callData.greeting || '', 1005 message: callData.message || '', 1006 message2: callData.message2 || '', 1007 agent_id: agentId || undefined, 1008 source: 'voice_command', 1009 original_command: this.currentTranscript, 1010 }); 1011 console.log('Task created for contact:', created.id); 1012 return created.id; 1013 } catch (error) { 1014 console.error('Error creating contact task:', error); 1015 throw error; 1016 } 1017 } 1018 1019 async selectTemplate(context) { 1020 const contextLower = context?.toLowerCase() || ''; 1021 1022 // Get agent data for name 1023 let agentName = 'Ihr VoiceOne Assistent'; 1024 let companyName = 'VoiceOne'; 1025 1026 try { 1027 const user = Auth.getCurrentUser(); 1028 if (user) { 1029 let foundAgent = false; 1030 1031 // phase8-voicecontrol-migrated: selectTemplate 1032 const _me = await _vc_getUserMe(); 1033 if (_me) { 1034 companyName = _me.company || companyName; 1035 const fromNumber = _me.from_number; 1036 if (fromNumber) { 1037 const aid = await _vc_findOutboundAgentId(fromNumber); 1038 if (aid) { 1039 const a = await agentsApi.get(aid).catch(() => null); 1040 if (a) { 1041 agentName = a.name || a.agentName || agentName; 1042 companyName = a.company || companyName; 1043 foundAgent = true; 1044 } 1045 } 1046 } 1047 } 1048 if (!foundAgent) { 1049 const a = await _vc_findAgentForUser(user.uid); 1050 if (a) agentName = a.name || a.agentName || agentName; 1051 } 1052 } 1053 } catch (error) { 1054 console.log('Could not get agent data for template:', error); 1055 } 1056 1057 // ALWAYS try to generate intelligent message using GPT first 1058 // Only skip for very simple cases 1059 const isSimpleCommand = (contextLower === 'termin' || contextLower === 'angebot' || contextLower === 'frage'); 1060 1061 if (!isSimpleCommand) { 1062 try { 1063 console.log('Attempting GPT generation for context:', context);
1064 const gptTemplate = await this.generateSmartMessage(context, agentName, companyName); 1065 if (gptTemplate) { 1066 console.log('Using GPT-generated template:', gptTemplate); 1067 return gptTemplate; 1068 } 1069 } catch (error) { 1070 console.error('Error generating smart message, falling back to templates:', error); 1071 } 1072 } 1073 1074 // Fallback to context-specific templates only if context is simple 1075 if (contextLower.includes('termin') && !contextLower.includes('preis') && !contextLower.includes('â¬')) { 1076 return { 1077 greeting: t(`Guten Tag {name}, hier ist der ${agentName} von ${companyName}.`), 1078 message: t(`Ich rufe Sie an, um ${context}. Hätten Sie in den nächsten Tagen Zeit für ein Gespräch?`), 1079 message2: t('Ich kann Ihnen gerne einige Terminvorschläge machen.') 1080 }; 1081 } else if (contextLower.includes('angebot')) { 1082 return { 1083 greeting: t(`Guten Tag {name}, hier ist der ${agentName} von ${companyName}.`), 1084 message: t(`Der Grund meines Anrufs: Ich möchte mit Ihnen ${context}. Haben Sie bereits Gelegenheit gehabt, darüber nachzudenken?`), 1085 message2: '' 1086 }; 1087 } else if (contextLower.includes('frage')) { 1088 return { 1089 greeting: t(`Guten Tag {name}, hier ist der ${agentName} von ${companyName}.`), 1090 message: t(`Ich möchte ${context}. Passt es Ihnen gerade?`), 1091 message2: '' 1092 }; 1093 } else if (context) { 1094 // Generic template with context 1095 return { 1096 greeting: t(`Guten Tag {name}, hier ist der ${agentName} von ${companyName}.`), 1097 message: t(`Der Grund meines Anrufs: Ich möchte mit Ihnen ${context}.`), 1098 message2: '' 1099 }; 1100 } 1101 1102 // Default template without specific context 1103 return this.getDefaultTemplate(agentName, companyName); 1104 } 1105 1106 getDefaultTemplate(agentName = 'Ihr VoiceOne Assistent', companyName = 'VoiceOne') { 1107 return { 1108 greeting: t(`Guten Tag {name}, mein Name ist der ${agentName} von ${companyName}. Ich hoffe, ich erwische Sie gerade nicht ungünstig?`), 1109 message: t('Der Grund meines Anrufs: Ich möchte mit Ihnen über ein wichtiges Thema sprechen.'), 1110 message2: '' 1111 }; 1112 } 1113 1114 async generateSmartMessage(voiceCommand, agentName, companyName) { 1115 try { 1116 const user = Auth.getCurrentUser(); 1117 if (!user) { 1118 throw new Error('User not authenticated'); 1119 } 1120 1121 const response = await fetch('/api/voice/generate-message', { 1122 method: 'POST', 1123 headers: { 1124 'Content-Type': 'application/json', 1125 'Authorization': 'Bearer ' + await user.getIdToken() 1126 }, 1127 body: JSON.stringify({ 1128 voiceCommand: voiceCommand, 1129 agentName: agentName, 1130 companyName: companyName, 1131 originalTranscript: this.currentTranscript 1132 }) 1133 }); 1134 1135 if (!response.ok) { 1136 throw new Error('Failed to generate message'); 1137 } 1138 1139 const result = await response.json(); 1140 return { 1141 greeting: result.greeting || t(`Guten Tag {name}, hier ist der ${agentName} von ${companyName}.`), 1142 message: result.message || voiceCommand, 1143 message2: result.message2 || '' 1144 }; 1145 } catch (error) { 1146 console.error('Error calling GPT API:', error); 1147 return null; 1148 } 1149 } 1150 1151 async loadAgentsForDropdown(contactName) { 1152 try { 1153 const user = Auth.getCurrentUser(); 1154 if (!user) return; 1155 1156 const agentSelect = document.getElementById('voiceCallAgent'); 1157 if (!agentSelect) return; 1158 1159 // phase8-voicecontrol-migrated: agents via API 1160 const _me = await _vc_getUserMe(); 1161 const tenantId = _me ? (_me.tenantID || _me.tenant_id) : null; 1162 const fromNumber = _me && _me.from_number; 1163 if (!tenantId) return; 1164 const _all = await agentsApi.list(); 1165 const agents = _all 1166 .filter(a => a && a.name) 1167 .map(a => ({ id: a.id, name: a.name, company: a.company, selfHosted: !!a.selfHosted })); 1168 // Sort: selfHosted agents first, then alphabetically 1169 agents.sort((a, b) => { 1170 if (a.selfHosted && !b.selfHosted) return -1; 1171 if (!a.selfHosted && b.selfHosted) return 1; 1172 return (a.name || '').localeCompare(b.name || ''); 1173 }); 1174 1175 if (agents.length === 0) { 1176 agentSelect.innerHTML = `<option value="">${t('Kein Agent verfügbar')}</option>`; 1177 return; 1178 } 1179 1180 // Determine which agent should be pre-selected (phone number assignment) 1181 let preselectedAgentId = ''; 1182 if (fromNumber) { 1183 preselectedAgentId = (await _vc_findOutboundAgentId(fromNumber)) || ''; 1184 } 1185 1186 // Populate dropdown 1187 agentSelect.innerHTML = agents.map(agent => { 1188 const selected = agent.id === preselectedAgentId ? 'selected' : ''; 1189 const label = agent.company ? `${agent.name} (${agent.company})` : agent.name;
1190 return `<option value="${agent.id}" ${selected}>${label}</option>`; 1191 }).join(''); 1192 1193 // Update greeting when agent changes 1194 agentSelect.addEventListener('change', () => { 1195 const selectedAgent = agents.find(a => a.id === agentSelect.value); 1196 if (selectedAgent) { 1197 const greetingField = document.getElementById('voiceCallGreeting'); 1198 if (greetingField) { 1199 const name = contactName || '{name}'; 1200 greetingField.value = `Guten Tag ${name}, hier ist der ${selectedAgent.name} von ${selectedAgent.company || ''}.`; 1201 } 1202 } 1203 }); 1204 1205 } catch (error) { 1206 console.error('Error loading agents for dropdown:', error); 1207 const agentSelect = document.getElementById('voiceCallAgent'); 1208 if (agentSelect) { 1209 agentSelect.innerHTML = `<option value="">${t('Fehler beim Laden')}</option>`; 1210 } 1211 } 1212 } 1213 1214 async showConfirmation(contact, callData) { 1215 return new Promise((resolve) => { 1216 const modal = document.createElement('div'); 1217 modal.className = 'modal fade'; 1218 modal.style.display = 'block'; 1219 modal.style.backgroundColor = 'rgba(0,0,0,0.5)'; 1220 1221 // Create unique ID for modal 1222 const modalId = 'voiceConfirmModal' + Date.now(); 1223 modal.id = modalId; 1224 1225 modal.innerHTML = ` 1226 <div class="modal-dialog modal-lg"> 1227 <div class="modal-content"> 1228 <div class="modal-header"> 1229 <h5 class="modal-title">${t('Anruf bearbeiten und bestätigen')}</h5> 1230 <button type="button" class="btn-close" data-action="cancel"></button> 1231 </div> 1232 <div class="modal-body"> 1233 <form id="voiceCallForm"> 1234 <div class="row mb-3"> 1235 <div class="col-md-6"> 1236 <label class="form-label"><strong>${t('Kontakt')}:</strong></label> 1237 <input type="text" class="form-control-plaintext" value="${contact.name}" readonly> 1238 </div> 1239 <div class="col-md-6"> 1240 <label class="form-label"><strong>${t('Telefon')}:</strong></label> 1241 <input type="text" class="form-control-plaintext" value="${contact.phone_number}" readonly> 1242 </div> 1243 </div> 1244 1245 <div class="mb-3"> 1246 <label for="voiceCallAgent" class="form-label"><strong>${t('Agent')}:</strong></label> 1247 <select id="voiceCallAgent" class="form-select"> 1248 <option value="">${t('Wird geladen...')}</option> 1249 </select> 1250 </div> 1251 1252 <div class="mb-3"> 1253 <label for="voiceCallTask" class="form-label"><strong>${t('Aufgabe')}:</strong></label> 1254 <textarea id="voiceCallTask" class="form-control" rows="2">${callData.task}</textarea> 1255 </div> 1256 1257 <div class="mb-3"> 1258 <label for="voiceCallGreeting" class="form-label"><strong>${t('BegrüÃung')}:</strong></label> 1259 <textarea id="voiceCallGreeting" class="form-control" rows="3">${callData.greeting}</textarea> 1260 <small class="form-text text-muted">${t('Verwenden Sie {name} als Platzhalter für den Kontaktnamen')}</small> 1261 </div> 1262 1263 <div class="mb-3"> 1264 <label for="voiceCallMessage" class="form-label"><strong>${t('Nachricht')}:</strong></label> 1265 <textarea id="voiceCallMessage" class="form-control" rows="3">${callData.message}</textarea> 1266 </div> 1267 1268 <div class="mb-3"> 1269 <label for="voiceCallMessage2" class="form-label"><strong>${t('Weitere Details')}:</strong></label> 1270 <textarea id="voiceCallMessage2" class="form-control" rows="3">${callData.message2 || ''}</textarea> 1271 </div> 1272 1273 <div class="mb-3"> 1274 <label for="voiceCallDetails" class="form-label"><strong>${t('Interne Notizen')}:</strong></label> 1275 <textarea id="voiceCallDetails" class="form-control" rows="2">${callData.details || ''}</textarea> 1276 </div> 1277 </form> 1278 </div> 1279 <div class="modal-footer"> 1280 <button class="btn btn-secondary" data-action="cancel"> 1281 ${t('Abbrechen')} 1282 </button> 1283 <button class="btn btn-primary" data-action="confirm"> 1284 <i class="fas fa-phone me-1"></i> 1285 ${t('Anruf starten')} 1286 </button> 1287 </div> 1288 </div> 1289 </div> 1290 `; 1291 1292 document.body.appendChild(modal); 1293 1294 // Load agents into dropdown 1295 this.loadAgentsForDropdown(contact.name).catch(err => { 1296 console.error('Error loading agents:', err); 1297 }); 1298 1299 // Add Bootstrap modal classes after adding to DOM 1300 setTimeout(() => { 1301 modal.classList.add('show'); 1302 }, 10); 1303 1304 // Handle button clicks 1305 const handleClick = (e) => { 1306 const action = e.target.dataset.action; 1307 if (action === 'confirm') { 1308 // Update callData with edited values 1309 callData.task = document.getElementById('voiceCallTask').value; 1310 callData.greeting = document.getElementById('voiceCallGreeting').value; 1311 callData.message = document.getElementById('voiceCallMessage').value; 1312 callData.message2 = document.getElementById('voiceCallMessage2').value; 1313 callData.details = document.getElementById('voiceCallDetails').value; 1314 // Store selected agent ID 1315 const agentSelect = document.getElementById('voiceCallAgent'); 1316 if (agentSelect && agentSelect.value) { 1317 callData.agent_id = agentSelect.value; 1318 } 1319 1320 modal.classList.remove('show'); 1321 setTimeout(() => { 1322 document.body.removeChild(modal); 1323 }, 300); 1324 resolve(true); 1325 } else if (action === 'cancel') { 1326 modal.classList.remove('show'); 1327 setTimeout(() => { 1328 document.body.removeChild(modal); 1329 }, 300); 1330 resolve(false); 1331 } 1332 }; 1333 1334 modal.addEventListener('click', handleClick); 1335 }); 1336 } 1337 1338 async createAndStartCall(callData) { 1339 try { 1340 const user = Auth.getCurrentUser(); 1341 if (!user) { 1342 throw new Error('User not authenticated'); 1343 } 1344 1345 // phase8-voicecontrol-migrated: tenant + multicall create via API 1346 let tenantId = null; 1347 try { tenantId = await Auth.getCurrentTenantId(); } catch (e) {} 1348 const _me2 = await _vc_getUserMe(); 1349 if (!tenantId && _me2) tenantId = _me2.tenantID || _me2.tenant_id; 1350 if (!tenantId) throw new Error('No tenant ID found'); 1351 const newEntry = { 1352 ...callData, 1353 tenantID: String(tenantId), 1354 uid: user.uid, 1355 status: 'pending', 1356 voiceInitiated: true, 1357 }; 1358 const created = await _vc_createMulticallEntry(newEntry); 1359 const docRef = { id: created.id || created._id || (created.data && created.data.id) }; 1360 1361 console.log('Voice-initiated call created:', docRef.id); 1362 1363 // Use agent selected in dropdown, or find by phone number 1364 let agentId = callData.agent_id || null; 1365 if (agentId) { 1366 console.log('Using agent selected in dropdown:', agentId); 1367 } else {
1368 console.log('Looking for agent based on outbound phone number for user:', user.uid); 1369 } 1370 1371 // phase8-voicecontrol-migrated: agent lookup via API 1372 if (!agentId) { 1373 try { 1374 const fromNumber2 = _me2 && _me2.from_number; 1375 if (fromNumber2) agentId = await _vc_findOutboundAgentId(fromNumber2); 1376 if (!agentId) { 1377 const _all = await agentsApi.list(); 1378 const def = _all.find(a => a && (a.isDefault === true) && (String(a.tenantID || a.tenantId || a.tenant_id) === String(tenantId))); 1379 if (def) agentId = def.id; 1380 else if (_all.length) agentId = _all[0].id; 1381 } 1382 } catch (error) { console.error('Error finding agent:', error); } 1383 } 1384 1385 if (!agentId) { 1386 UI.showError(t('Kein Agent für Anrufe konfiguriert')); 1387 throw new Error('No agent configured'); 1388 } 1389 1390 console.log('Found agent for call:', agentId); 1391 1392 // Trigger the call through the backend 1393 try { 1394 const requestData = { 1395 multicallId: docRef.id, 1396 phoneNumber: callData.phone_number, 1397 workflowId: 'default_outbound', // You can make this configurable 1398 agentPrompt: this.generateAgentPrompt(callData), 1399 metadata: { 1400 initiated_via: 'voice_command', 1401 contact_id: callData.contact_id, 1402 original_command: this.currentTranscript, 1403 agent_id: agentId, 1404 tenant_id: tenantId 1405 } 1406 }; 1407 1408 console.log('Sending request to /api/multicall/start:', requestData); 1409 1410 const response = await fetch('/api/multicall/start', { 1411 method: 'POST', 1412 headers: { 1413 'Content-Type': 'application/json', 1414 'Authorization': 'Bearer ' + await user.getIdToken() 1415 }, 1416 body: JSON.stringify(requestData) 1417 }); 1418 1419 if (!response.ok) { 1420 const errorText = await response.text(); 1421 console.error('Server error response:', errorText); 1422 throw new Error('Failed to start call: ' + errorText); 1423 } 1424 1425 const result = await response.json(); 1426 console.log('Call started:', result); 1427 1428 } catch (apiError) { 1429 console.error('Error triggering call workflow:', apiError); 1430 // Don't throw - call entry is created, just couldn't start it immediately 1431 UI.showWarning(t('Anruf wurde erstellt, konnte aber nicht sofort gestartet werden.')); 1432 } 1433 1434 return docRef.id; 1435 } catch (error) { 1436 console.error('Error creating voice-initiated call:', error); 1437 throw error; 1438 } 1439 } 1440 1441 generateAgentPrompt(callData) { 1442 return `${callData.greeting}\n\n${callData.message}${callData.message2 ? '\n\n' + callData.message2 : ''}`; 1443 } 1444 1445 async scheduleAppointment(contactName, context) { 1446 // This would integrate with your calendar system 1447 UI.showInfo(t(`Terminvereinbarung mit ${contactName} - Diese Funktion wird bald verfügbar sein.`)); 1448 } 1449 1450 close() { 1451 console.log('Closing voice control'); 1452 this.manualStop = true; 1453 this.stopListening(); 1454 1455 const panel = document.getElementById('voiceInputPanel'); 1456 if (panel) { 1457 panel.classList.remove('active'); 1458 } 1459 1460 const input = document.getElementById('voiceCommandInput'); 1461 if (input) { 1462 input.value = ''; 1463 } 1464 1465 this.currentTranscript = ''; 1466 1467 // Clear suggestions 1468 const suggestionsContainer = document.getElementById('voiceSuggestions'); 1469 if (suggestionsContainer) { 1470 suggestionsContainer.innerHTML = ''; 1471 } 1472 } 1473 1474 cancel() { 1475 this.close(); 1476 } 1477} 1478 1479// Create singleton instance 1480let voiceControlInstance = null; 1481 1482// Initialize on page load 1483document.addEventListener('DOMContentLoaded', () => { 1484 voiceControlInstance = new VoiceControl(); 1485}); 1486 1487// Export static methods for HTML onclick handlers 1488window.VoiceControl = { 1489 close: () => { 1490 console.log('VoiceControl.close called'); 1491 if (voiceControlInstance) { 1492 voiceControlInstance.close(); 1493 } else { 1494 console.error('VoiceControl instance not initialized'); 1495 } 1496 }, 1497 cancel: () => { 1498 console.log('VoiceControl.cancel called'); 1499 if (voiceControlInstance) { 1500 voiceControlInstance.cancel(); 1501 } else { 1502 console.error('VoiceControl instance not initialized'); 1503 } 1504 }, 1505 execute: () => { 1506 console.log('VoiceControl.execute called'); 1507 if (voiceControlInstance) { 1508 voiceControlInstance.execute(); 1509 } else { 1510 console.error('VoiceControl instance not initialized'); 1511 } 1512 } 1513};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.