| d mm<!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>AI Phone Assistant</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <script src="https://cdn.jsdelivr.net/npm/@rvcjs/core@latest/dist/rvc.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/@okada-tts/web@latest/dist/okada.min.js"></script> |
| <script> |
| tailwind.config = { |
| theme: { |
| extend: { |
| colors: { |
| iosbg: '#f2f2f7', |
| iosdark: '#1c1c1e', |
| accent: '#0a84ff', |
| accent2: '#5e5ce6', |
| } |
| } |
| } |
| } |
| </script> |
| <style> |
| |
| body { |
| -webkit-tap-highlight-color: transparent; |
| -webkit-overflow-scrolling: touch; |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
| } |
| |
| .screen { |
| display: none; |
| opacity: 0; |
| transition: opacity 0.3s ease; |
| } |
| |
| .screen.active { |
| display: block; |
| opacity: 1; |
| } |
| |
| |
| ::-webkit-scrollbar { |
| width: 6px; |
| } |
| |
| ::-webkit-scrollbar-track { |
| background: transparent; |
| } |
| |
| ::-webkit-scrollbar-thumb { |
| background: rgba(0,0,0,0.15); |
| border-radius: 3px; |
| } |
| |
| |
| .ios-switch { |
| position: relative; |
| display: inline-block; |
| width: 52px; |
| height: 32px; |
| } |
| |
| .ios-switch input { |
| opacity: 0; |
| width: 0; |
| height: 0; |
| } |
| |
| .ios-slider { |
| position: absolute; |
| cursor: pointer; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background-color: #e9e9ea; |
| transition: .4s; |
| border-radius: 16px; |
| } |
| |
| .ios-slider:before { |
| position: absolute; |
| content: ""; |
| height: 28px; |
| width: 28px; |
| left: 2px; |
| bottom: 2px; |
| background-color: white; |
| transition: .4s; |
| border-radius: 50%; |
| box-shadow: 0 1px 3px rgba(0,0,0,0.3); |
| } |
| |
| input:checked + .ios-slider { |
| background-color: #32d74b; |
| } |
| |
| input:checked + .ios-slider:before { |
| transform: translateX(20px); |
| } |
| |
| |
| .fab { |
| position: fixed; |
| right: 20px; |
| bottom: 20px; |
| width: 60px; |
| height: 60px; |
| background: linear-gradient(135deg, #0a84ff, #5e5ce6); |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: white; |
| font-size: 22px; |
| box-shadow: 0 4px 10px rgba(0,0,0,0.25); |
| z-index: 50; |
| cursor: pointer; |
| } |
| |
| |
| .ios-input { |
| background: rgba(118,118,128,0.12); |
| border-radius: 10px; |
| padding: 8px 12px; |
| font-size: 16px; |
| width: 100%; |
| transition: background 0.2s; |
| } |
| |
| .ios-input:focus { |
| background: rgba(118,118,128,0.18); |
| outline: none; |
| } |
| |
| |
| .ai-bubble { |
| background: rgba(118,118,128,0.12); |
| border-top-left-radius: 18px; |
| border-top-right-radius: 18px; |
| border-bottom-right-radius: 18px; |
| padding: 12px 16px; |
| max-width: 85%; |
| align-self: flex-start; |
| } |
| |
| .user-bubble { |
| background: #0a84ff; |
| color: white; |
| border-top-left-radius: 18px; |
| border-top-right-radius: 18px; |
| border-bottom-left-radius: 18px; |
| padding: 12px 16px; |
| max-width: 85%; |
| align-self: flex-end; |
| } |
| |
| |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translateY(10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| .animate-fadeIn { |
| animation: fadeIn 0.3s ease-out forwards; |
| } |
| </style> |
| </head> |
| <body class="bg-iosbg dark:bg-iosdark text-gray-900 dark:text-gray-200 min-h-screen"> |
| |
| <div class="fixed top-0 left-0 right-0 h-12 flex items-center px-4 z-50 bg-iosbg dark:bg-iosdark"> |
| <div class="text-left text-sm w-20">9:41</div> |
| <div class="flex-1 flex justify-center"> |
| <i class="fas fa-signal mr-2"></i> |
| <i class="fas fa-wifi mr-2"></i> |
| <i class="fas fa-battery-three-quarters"></i> |
| </div> |
| <div class="w-20 text-right text-xs">100%</div> |
| </div> |
|
|
| |
| <div class="relative pt-12 max-w-md mx-auto h-screen overflow-hidden"> |
| |
| <div id="homeScreen" class="screen active px-4 pt-4 h-full flex flex-col"> |
| <div class="mt-2"> |
| <h1 class="text-3xl font-bold">Call Assistant</h1> |
| <p class="text-gray-500 dark:text-gray-400 mt-1">AI that answers your calls and learns over time</p> |
| </div> |
| |
| |
| <div class="mt-6 bg-white dark:bg-gray-800 rounded-2xl p-5 shadow-sm"> |
| <div class="flex items-center justify-between mb-4"> |
| <div> |
| <h2 class="font-medium">Current Status</h2> |
| <p class="text-gray-500 dark:text-gray-400 text-sm mt-1">Assistant is active</p> |
| </div> |
| <label class="ios-switch"> |
| <input type="checkbox" checked> |
| <span class="ios-slider"></span> |
| </label> |
| </div> |
| |
| <div class="border-t border-gray-200 dark:border-gray-700 pt-4"> |
| <h3 class="font-medium flex items-center"> |
| <i class="fas fa-phone mr-2"></i> Connected Number |
| </h3> |
| <div class="mt-2 flex items-center justify-between"> |
| <span class="text-gray-500 dark:text-gray-400">+1 (562) 228-9429</span> |
| <button class="text-accent text-sm">Change</button> |
| </div> |
| </div> |
| |
| <div class="mt-4 flex items-center"> |
| <div class="w-10 h-10 rounded-full bg-accent flex items-center justify-center"> |
| <i class="fas fa-robot text-white"></i> |
| </div> |
| <div class="ml-3"> |
| <h3 class="font-medium">Today's Stats</h3> |
| <p class="text-gray-500 dark:text-gray-400 text-sm">Answered 5 calls, 12 min talk time</p> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="mt-4 flex gap-3"> |
| <div class="flex-1 bg-gradient-to-br from-accent to-accent2 rounded-2xl p-5 text-white"> |
| <i class="fas fa-comment-alt text-2xl"></i> |
| <h3 class="font-medium mt-3">Smart Replies</h3> |
| <p class="text-white text-opacity-80 text-sm mt-1">Teach the AI how to respond</p> |
| </div> |
| <div class="flex-1 bg-gray-800 dark:bg-gray-700 rounded-2xl p-5 text-white"> |
| <i class="fas fa-history text-2xl"></i> |
| <h3 class="font-medium mt-3">Call History</h3> |
| <p class="text-gray-300 text-sm mt-1">Review previous conversations</p> |
| </div> |
| </div> |
| |
| |
| <div class="mt-4 bg-white dark:bg-gray-800 rounded-2xl p-5 flex-1 overflow-hidden flex flex-col"> |
| <div class="flex items-center justify-between"> |
| <h2 class="font-medium">Recent Activity</h2> |
| <button class="text-accent text-sm">See All</button> |
| </div> |
| |
| <div class="mt-3 flex-1 overflow-y-auto space-y-4"> |
| <div class="flex items-start animate-fadeIn"> |
| <div class="w-10 h-10 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center"> |
| <i class="fas fa-phone-alt text-green-600 dark:text-green-400"></i> |
| </div> |
| <div class="ml-3 flex-1"> |
| <div class="flex justify-between"> |
| <h3 class="font-medium">Michael (Work)</h3> |
| <span class="text-xs text-gray-500 dark:text-gray-400">12:45 PM</span> |
| </div> |
| <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">AI Assistant handled call: "Meeting confirmed for tomorrow"</p> |
| </div> |
| </div> |
| |
| <div class="flex items-start animate-fadeIn"> |
| <div class="w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center"> |
| <i class="fas fa-phone-alt text-purple-600 dark:text-purple-400"></i> |
| </div> |
| <div class="ml-3 flex-1"> |
| <div class="flex justify-between"> |
| <h3 class="font-medium">Sarah (Spam)</h3> |
| <span class="text-xs text-gray-500 dark:text-gray-400">11:30 AM</span> |
| </div> |
| <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">AI blocked suspected spam call</p> |
| </div> |
| </div> |
| |
| <div class="flex items-start animate-fadeIn"> |
| <div class="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center"> |
| <i class="fas fa-phone-alt text-blue-600 dark:text-blue-400"></i> |
| </div> |
| <div class="ml-3 flex-1"> |
| <div class="flex justify-between"> |
| <h3 class="font-medium">Mom</h3> |
| <span class="text-xs text-gray-500 dark:text-gray-400">10:15 AM</span> |
| </div> |
| <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">AI transferred call to you after screening</p> |
| </div> |
| </div> |
| |
| <div class="flex items-start animate-fadeIn"> |
| <div class="w-10 h-10 rounded-full bg-amber-100 dark:bg-amber-900 flex items-center justify-center"> |
| <i class="fas fa-phone-alt text-amber-600 dark:text-amber-400"></i> |
| </div> |
| <div class="ml-3 flex-1"> |
| <div class="flex justify-between"> |
| <h3 class="font-medium">Dr. Smith Clinic</h3> |
| <span class="text-xs text-gray-500 dark:text-gray-400">9:20 AM</span> |
| </div> |
| <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">AI scheduled your appointment for next Monday</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="repliesScreen" class="screen h-full flex flex-col"> |
| <div class="px-4 pt-4"> |
| <div class="flex items-center"> |
| <button class="p-2 rounded-full" onclick="showScreen('homeScreen')"> |
| <i class="fas fa-arrow-left"></i> |
| </button> |
| <h2 class="text-xl font-bold ml-2">Smart Replies</h2> |
| </div> |
| <p class="text-gray-500 dark:text-gray-400 mt-1 ml-12">Customize how AI answers calls</p> |
| </div> |
| |
| <div class="mt-4 px-4 flex-1 overflow-y-auto"> |
| <div class="bg-white dark:bg-gray-800 rounded-2xl p-5 mb-4"> |
| <div class="flex items-center"> |
| <div class="w-12 h-12 rounded-full bg-gradient-to-br from-accent to-accent2 flex items-center justify-center"> |
| <i class="fas fa-brain text-white text-xl"></i> |
| </div> |
| <div class="ml-3"> |
| <h3 class="font-medium">AI Learning Mode</h3> |
| <p class="text-gray-500 dark:text-gray-400 text-sm">Improves responses over time</p> |
| </div> |
| </div> |
| |
| <div class="mt-4 flex items-center justify-between"> |
| <span>Learning from interactions</span> |
| <label class="ios-switch"> |
| <input type="checkbox" checked> |
| <span class="ios-slider"></span> |
| </label> |
| </div> |
| </div> |
| |
| <div class="bg-white dark:bg-gray-800 rounded-2xl overflow-hidden"> |
| <div class="px-5 pt-4"> |
| <h3 class="font-medium">Custom Response Templates</h3> |
| <p class="text-gray-500 dark:text-gray-400 text-sm mt-1">Set predefined responses</p> |
| </div> |
| |
| <div class="mt-4 space-y-2"> |
| <div class="flex items-center justify-between p-4 hover:bg-gray-100 dark:hover:bg-gray-750 cursor-pointer"> |
| <div> |
| <h4 class="font-medium">Business Calls</h4> |
| <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">"Hello, this is [Your Name]'s assistant..."</p> |
| </div> |
| <i class="fas fa-chevron-right text-gray-400"></i> |
| </div> |
| |
| <div class="flex items-center justify-between p-4 hover:bg-gray-100 dark:hover:bg-gray-750 cursor-pointer"> |
| <div> |
| <h4 class="font-medium">Personal Calls</h4> |
| <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">"Hi, this is [Name]'s phone..."</p> |
| </div> |
| <i class="fas fa-chevron-right text-gray-400"></i> |
| </div> |
| |
| <div class="flex items-center justify-between p-4 hover:bg-gray-100 dark:hover:bg-gray-750 cursor-pointer"> |
| <div> |
| <h4 class="font-medium">Spam Protection</h4> |
| <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">"Sorry, this number is not accepting calls..."</p> |
| </div> |
| <i class="fas fa-chevron-right text-gray-400"></i> |
| </div> |
| </div> |
| </div> |
| |
| <div class="bg-white dark:bg-gray-800 rounded-2xl mt-4 p-5"> |
| <h3 class="font-medium">Response Style</h3> |
| <div class="mt-4 space-y-4"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <h4 class="font-medium">Formal Tone</h4> |
| <p class="text-gray-500 dark:text-gray-400 text-sm">Professional business language</p> |
| </div> |
| <label class="ios-switch"> |
| <input type="checkbox" checked> |
| <span class="ios-slider"></span> |
| </label> |
| </div> |
| |
| <div class="flex items-center justify-between"> |
| <div> |
| <h4 class="font-medium">Friendly Tone</h4> |
| <p class="text-gray-500 dark:text-gray-400 text-sm">Casual conversation style</p> |
| </div> |
| <label class="ios-switch"> |
| <input type="checkbox"> |
| <span class="ios-slider"></span> |
| </label> |
| </div> |
| |
| <div class="flex items-center justify-between"> |
| <div> |
| <h4 class="font-medium">Use My Name</h4> |
| <p class="text-gray-500 dark:text-gray-400 text-sm">"This is [Your Name]'s phone"</p> |
| </div> |
| <label class="ios-switch"> |
| <input type="checkbox" checked> |
| <span class="ios-slider"></span> |
| </label> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="trainingScreen" class="screen h-full flex flex-col"> |
| <div class="px-4 pt-4"> |
| <div class="flex items-center"> |
| <button class="p-2 rounded-full" onclick="showScreen('homeScreen')"> |
| <i class="fas fa-arrow-left"></i> |
| </button> |
| <h2 class="text-xl font-bold ml-2">Train AI</h2> |
| </div> |
| <p class="text-gray-500 dark:text-gray-400 mt-1 ml-12">Help your assistant learn</p> |
| </div> |
| |
| <div class="mt-4 px-4 flex-1 overflow-hidden flex flex-col"> |
| <div class="bg-white dark:bg-gray-800 rounded-2xl p-5 flex-1 overflow-hidden flex flex-col"> |
| <div class="flex-1 overflow-y-auto pb-4"> |
| <div class="ai-bubble"> |
| <p>How would you like me to respond to calls from your family?</p> |
| </div> |
| |
| <div class="user-bubble mt-4"> |
| <p>Always transfer calls from Mom and Dad to me</p> |
| </div> |
| |
| <div class="ai-bubble mt-4"> |
| <p>Noted! I'll transfer calls from Mom and Dad immediately.</p> |
| <p class="mt-2">For other family members, how should I respond?</p> |
| </div> |
| |
| <div class="user-bubble mt-4"> |
| <p>Ask them for the reason of calling and text me if it's important</p> |
| </div> |
| |
| <div class="ai-bubble mt-4"> |
| <p>Got it. Here's the response I created based on your feedback:</p> |
| <div class="mt-2 bg-blue-50 dark:bg-blue-900 rounded-lg p-3"> |
| <p>"Hello, this is Alex's assistant. Could you let me know what you're calling about? I'll make sure they get your message."</p> |
| </div> |
| <p class="mt-2">Does this work?</p> |
| </div> |
| </div> |
| |
| <div class="mt-auto pt-4 border-t border-gray-200 dark:border-gray-700"> |
| <div class="flex gap-2"> |
| <input id="trainingInput" type="text" class="ios-input flex-1" placeholder="Teach your assistant..." onkeypress="handleTrainingKeyPress(event)"> |
| <button class="w-12 h-12 rounded-xl bg-accent flex items-center justify-center text-white" onclick="submitTraining()"> |
| <i class="fas fa-paper-plane"></i> |
| </button> |
| </div> |
| <p class="text-xs text-gray-500 dark:text-gray-400 mt-2 text-center">The AI learns from every interaction</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="fixed bottom-0 left-0 right-0 bg-white dark:bg-iosdark border-t border-gray-200 dark:border-gray-800 max-w-md mx-auto"> |
| <div class="flex justify-around py-2"> |
| <button class="py-2 px-4 rounded-xl flex flex-col items-center text-accent" onclick="showScreen('homeScreen')"> |
| <i class="fas fa-home text-lg"></i> |
| <span class="text-xs mt-1">Home</span> |
| </button> |
| <button class="py-2 px-4 rounded-xl flex flex-col items-center text-gray-500" onclick="showScreen('repliesScreen')"> |
| <i class="fas fa-comment-alt text-lg"></i> |
| <span class="text-xs mt-1">Replies</span> |
| </button> |
| <button class="py-2 px-4 rounded-xl flex flex-col items-center text-gray-500" onclick="showScreen('trainingScreen')"> |
| <i class="fas fa-graduation-cap text-lg"></i> |
| <span class="text-xs mt-1">Train</span> |
| </button> |
| <button class="py-2 px-4 rounded-xl flex flex-col items-center text-gray-500" onclick="showScreen('settingsScreen')"> |
| <i class="fas fa-cog text-lg"></i> |
| <span class="text-xs mt-1">Settings</span> |
| </button> |
| </div> |
| </div> |
| |
| |
| <div id="settingsScreen" class="screen h-full flex flex-col"> |
| <div class="px-4 pt-4"> |
| <div class="flex items-center"> |
| <button class="p-2 rounded-full" onclick="showScreen('homeScreen')"> |
| <i class="fas fa-arrow-left"></i> |
| </button> |
| <h2 class="text-xl font-bold ml-2">Voice Settings</h2> |
| </div> |
| <p class="text-gray-500 dark:text-gray-400 mt-1 ml-12">Customize voice personality</p> |
| </div> |
| |
| <div class="mt-4 px-4 flex-1 overflow-y-auto"> |
| <div class="bg-white dark:bg-gray-800 rounded-2xl p-5"> |
| <h3 class="font-medium">Voice Personality</h3> |
| |
| <div class="mt-4 space-y-4"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <h4 class="font-medium">Voice Model</h4> |
| <p class="text-gray-500 dark:text-gray-400 text-sm">RVC-based human voice</p> |
| </div> |
| <select id="voiceModel" class="ios-input"> |
| <option value="okada-female">OKADA Female</option> |
| <option value="okada-male">OKADA Male</option> |
| <option value="custom-rvc">Custom RVC</option> |
| </select> |
| </div> |
| |
| <div class="flex items-center justify-between"> |
| <div> |
| <h4 class="font-medium">Emotional Tone</h4> |
| <p class="text-gray-500 dark:text-gray-400 text-sm">Adjust voice emotions</p> |
| </div> |
| <select id="voiceTone" class="ios-input"> |
| <option value="neutral">Neutral</option> |
| <option value="friendly">Friendly</option> |
| <option value="professional">Professional</option> |
| <option value="empathic">Empathic</option> |
| </select> |
| </div> |
| |
| <div class="flex items-center justify-between"> |
| <div> |
| <h4 class="font-medium">Speed</h4> |
| <p class="text-gray-500 dark:text-gray-400 text-sm">Speech rate</p> |
| </div> |
| <select id="voiceSpeed" class="ios-input"> |
| <option value="1.0">Normal</option> |
| <option value="0.8">Slow</option> |
| <option value="1.2">Fast</option> |
| </select> |
| </div> |
| |
| <div class="mt-6"> |
| <button class="w-full py-3 bg-accent text-white rounded-xl" onclick="testVoiceSettings()"> |
| Test Voice Settings |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| <div class="bg-white dark:bg-gray-800 rounded-2xl mt-4 p-5"> |
| <h3 class="font-medium">Advanced RVC Settings</h3> |
| <div class="mt-4 space-y-2"> |
| <div> |
| <label class="text-sm text-gray-500 dark:text-gray-400">Pitch Shift</label> |
| <input type="range" id="pitchShift" min="-12" max="12" value="0" step="1" class="w-full mt-1"> |
| <div class="flex justify-between text-xs text-gray-500 mt-1"> |
| <span>Lower</span> |
| <span>Normal</span> |
| <span>Higher</span> |
| </div> |
| </div> |
| |
| <div class="mt-4"> |
| <div class="flex justify-between"> |
| <label class="text-sm">RVC Similarity</label> |
| <span id="similarityValue" class="text-sm">85%</span> |
| </div> |
| <input type="range" id="similarity" min="50" max="100" value="85" step="1" class="w-full mt-1"> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="fab" onclick="toggleRecording()"> |
| <i class="fas fa-microphone"></i> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let trainingData = { |
| learnedResponses: { |
| "business": [ |
| { |
| "input": "car detailing", |
| "response": "Thank you for calling [Your Business Name], premium auto detailing specialists. How can we help you today?", |
| "tokens": ["car", "detailing"], |
| "lastUsed": "2023-11-15T00:00:00.000Z" |
| }, |
| { |
| "input": "appointment", |
| "response": "For appointments, we have availability this week. Would you prefer a wash & wax ($75), full detail ($150) or ceramic coating ($400)?", |
| "tokens": ["appointment"], |
| "lastUsed": "2023-11-15T00:00:00.000Z" |
| }, |
| { |
| "input": "hours", |
| "response": "Our detailing center is open Monday-Friday 8am-6pm, Saturday 9am-4pm. We're closed Sundays.", |
| "tokens": ["hours"], |
| "lastUsed": "2023-11-15T00:00:00.000Z" |
| } |
| ] |
| }, |
| customResponses: { |
| "business": "Hello, you've reached [Your Business Name] auto detailing. Ask me about our services: basic wash ($35), premium detailing ($150), or ceramic coatings ($400+).", |
| "spam": "We don't accept sales calls. Please email us at info@yourbusiness.com for business inquiries." |
| }, |
| phoneNumber: "+1 (562) 228-9429", |
| callHandling: { |
| transferContacts: ["Mom", "Dad"], |
| screenContacts: true, |
| spamDetection: true |
| }, |
| personality: { |
| tone: "professional", |
| useName: true, |
| responseSpeed: "normal", |
| businessName: "Shine On Auto Detailing", |
| services: [ |
| {name: "Basic Wash", price: "$35", duration: "30 mins"}, |
| {name: "Deluxe Detail", price: "$150", duration: "3 hours"}, |
| {name: "Ceramic Coating", price: "$400+", duration: "1-2 days"} |
| ] |
| } |
| }; |
| |
| |
| const nlp = { |
| processInput: function(text) { |
| |
| text = text.toLowerCase().trim(); |
| |
| |
| const tokens = text |
| .replace(/[^\w\s]/g, '') |
| .split(/\s+/) |
| .filter(token => token.length > 2); |
| |
| |
| const contextScores = { |
| business: ['work', 'job', 'meeting', 'service', 'price', 'appointment', 'business', 'company'] |
| .filter(word => text.includes(word)).length, |
| personal: ['family', 'friend', 'mom', 'dad', 'home', 'personal'] |
| .filter(word => text.includes(word)).length, |
| spam: ['spam', 'block', 'unwanted', 'telemarketer', 'sales', 'offer'] |
| .filter(word => text.includes(word)).length * 2, |
| question: ['what', 'when', 'where', 'how', 'why', 'can you', 'would you', '?'] |
| .filter(word => text.includes(word)).length |
| }; |
| |
| |
| let primaryContext = 'general'; |
| let maxScore = 0; |
| for (const [context, score] of Object.entries(contextScores)) { |
| if (score > maxScore) { |
| maxScore = score; |
| primaryContext = context; |
| } |
| } |
| |
| return { |
| text, |
| tokens, |
| context: primaryContext, |
| isQuestion: contextScores.question > 0 |
| }; |
| }, |
| learnResponse: function(inputText, preferredResponse) { |
| |
| const tokens = inputText.toLowerCase().split(/\s+/); |
| const context = this.determineContext(inputText); |
| |
| if (!trainingData.learnedResponses[context]) { |
| trainingData.learnedResponses[context] = []; |
| } |
| |
| trainingData.learnedResponses[context].push({ |
| input: inputText, |
| response: preferredResponse, |
| tokens: tokens, |
| lastUsed: new Date() |
| }); |
| |
| this.saveToLocalStorage(); |
| return true; |
| }, |
| determineContext: function(text) { |
| text = text.toLowerCase(); |
| if (text.includes('work') || text.includes('business') || text.includes('job')) return 'business'; |
| if (text.includes('family') || text.includes('mom') || text.includes('dad')) return 'personal'; |
| if (text.includes('spam') || text.includes('block')) return 'spam'; |
| return 'general'; |
| }, |
| getBestResponse: function(inputText) { |
| const context = this.determineContext(inputText); |
| const possibleResponses = trainingData.learnedResponses[context] || []; |
| |
| if (possibleResponses.length === 0) { |
| return this.generateDefaultResponse(context); |
| } |
| |
| |
| const inputTokens = inputText.toLowerCase().split(/\s+/); |
| let bestMatch = {score: 0, response: possibleResponses[0]}; |
| |
| possibleResponses.forEach(item => { |
| let score = 0; |
| inputTokens.forEach(token => { |
| if (item.tokens.includes(token)) score++; |
| }); |
| if (score > bestMatch.score) { |
| bestMatch = {score, response: item}; |
| } |
| }); |
| |
| return bestMatch.response.response || this.generateDefaultResponse(context); |
| }, |
| generateDefaultResponse: function(context) { |
| const defaults = { |
| business: "Hello! Thank you for calling our auto detailing service. We offer car washes starting at $35 and full detailing for $150. How can we assist you?", |
| personal: "Hi, this is [Name]'s phone. I'll get your message to them if it's important.", |
| spam: "We don't accept sales calls. Please email us at info@yourbusiness.com for business inquiries.", |
| general: "I'll connect you with our detailing team. Are you calling about an appointment or would you like service information?" |
| }; |
| return defaults[context] || defaults.general; |
| }, |
| saveToLocalStorage: function() { |
| localStorage.setItem('aiCallAssistantTraining', JSON.stringify(trainingData)); |
| }, |
| loadFromLocalStorage: function() { |
| const savedData = localStorage.getItem('aiCallAssistantTraining'); |
| if (savedData) { |
| trainingData = JSON.parse(savedData); |
| } |
| } |
| }; |
| |
| |
| function showScreen(screenId) { |
| document.querySelectorAll('.screen').forEach(screen => { |
| screen.classList.remove('active'); |
| }); |
| document.getElementById(screenId).classList.add('active'); |
| |
| |
| const tabs = document.querySelectorAll('.fab ~ .fixed button'); |
| tabs.forEach(tab => { |
| const icon = tab.querySelector('i'); |
| const span = tab.querySelector('span'); |
| if (screenId === 'homeScreen' && icon.classList.contains('fa-home')) { |
| tab.classList.add('text-accent'); |
| } else if (screenId === 'repliesScreen' && icon.classList.contains('fa-comment-alt')) { |
| tab.classList.add('text-accent'); |
| } else if (screenId === 'trainingScreen' && icon.classList.contains('fa-graduation-cap')) { |
| tab.classList.add('text-accent'); |
| } else { |
| tab.classList.remove('text-accent'); |
| tab.classList.add('text-gray-500'); |
| } |
| }); |
| } |
| |
| |
| function simulateAIProgress() { |
| const progressBars = document.querySelectorAll('.progress'); |
| progressBars.forEach(bar => { |
| const width = 70 + Math.floor(Math.random() * 30); |
| bar.style.width = `${width}%`; |
| }); |
| } |
| |
| |
| function toggleRecording() { |
| const fab = document.querySelector('.fab'); |
| const icon = fab.querySelector('i'); |
| |
| if (icon.classList.contains('fa-microphone')) { |
| icon.classList.remove('fa-microphone'); |
| icon.classList.add('fa-stop'); |
| fab.style.background = 'linear-gradient(135deg, #ff375f, #ff2d55)'; |
| |
| |
| const notification = document.createElement('div'); |
| notification.innerHTML = ` |
| <div class="fixed top-16 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-4 py-2 rounded-full animate-fadeIn"> |
| Recording started |
| </div> |
| `; |
| document.body.appendChild(notification); |
| setTimeout(() => { |
| notification.remove(); |
| }, 2000); |
| } else { |
| icon.classList.remove('fa-stop'); |
| icon.classList.add('fa-microphone'); |
| fab.style.background = 'linear-gradient(135deg, #0a84ff, #5e5ce6)'; |
| |
| |
| simulateAIProgress(); |
| } |
| } |
| |
| |
| function addAppearAnimations() { |
| document.querySelectorAll('.animate-fadeIn').forEach((el, index) => { |
| el.style.animationDelay = `${index * 0.1}s`; |
| }); |
| } |
| |
| |
| function submitTraining() { |
| const input = document.getElementById('trainingInput'); |
| if (input.value.trim() !== '') { |
| const userMessage = input.value; |
| |
| |
| const chatContainer = document.querySelector('#trainingScreen .flex-1.overflow-y-auto'); |
| const userBubble = document.createElement('div'); |
| userBubble.className = 'user-bubble mt-4 animate-fadeIn'; |
| userBubble.innerHTML = `<p>${userMessage}</p>`; |
| chatContainer.appendChild(userBubble); |
| |
| |
| setTimeout(() => { |
| let aiResponse; |
| const isTeachingExample = /how should i respond|please reply with|respond with/i.test(userMessage); |
| |
| if (isTeachingExample) { |
| const teachingMatch = userMessage.match(/(how should i respond to|please reply to|respond to) (.*?) (with|by saying) (.*)/i); |
| if (teachingMatch) { |
| const question = teachingMatch[2]; |
| const answer = teachingMatch[4]; |
| nlp.learnResponse(question, answer); |
| |
| aiResponse = ` |
| <p>Got it! I've learned this response:</p> |
| <div class="mt-2 bg-blue-50 dark:bg-blue-900 rounded-lg p-3"> |
| <p><strong>When asked:</strong> ${question}</p> |
| <p><strong>I'll respond:</strong> ${answer}</p> |
| </div> |
| <p class="mt-2">I'll use this response pattern for similar questions.</p> |
| `; |
| } else { |
| aiResponse = ` |
| <div class="ai-bubble mt-4 animate-fadeIn"> |
| <p>I can learn how to respond! Try phrasing like:</p> |
| <div class="mt-2 bg-blue-50 dark:bg-blue-900 rounded-lg p-3"> |
| <p>"How should I respond to questions about pricing?"</p> |
| <p>"Please reply to appointment requests with: 'Would you like morning or afternoon?'"</p> |
| </div> |
| </div> |
| `; |
| } |
| } else { |
| const response = nlp.getBestResponse(userMessage); |
| aiResponse = ` |
| <p>Based on my training, here's how I would respond:</p> |
| <div class="mt-2 bg-blue-50 dark:bg-blue-900 rounded-lg p-3"> |
| <p>${response}</p> |
| </div> |
| <p class="mt-2">Would you like to:</p> |
| `; |
| } |
| |
| const aiBubble = document.createElement('div'); |
| aiBubble.className = 'ai-bubble mt-4 animate-fadeIn'; |
| aiBubble.innerHTML = aiResponse; |
| |
| if (!aiResponse.includes('Try phrasing')) { |
| aiBubble.innerHTML += ` |
| <div class="mt-3 flex gap-2"> |
| <button class="flex-1 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition" |
| onclick="acceptResponse(this)"> |
| Accept <i class="fas fa-check ml-1"></i> |
| </button> |
| <button class="flex-1 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition" |
| onclick="editResponse(this)"> |
| Edit <i class="fas fa-edit ml-1"></i> |
| </button> |
| <button class="px-3 py-2 bg-accent text-white rounded-lg hover:bg-blue-600 transition" |
| onclick="playResponse(this)"> |
| <i class="fas fa-volume-up"></i> |
| </button> |
| </div> |
| `; |
| } |
| |
| chatContainer.appendChild(aiBubble); |
| chatContainer.scrollTop = chatContainer.scrollHeight; |
| }, 500); |
| |
| input.value = ''; |
| } |
| } |
| |
| function acceptResponse(button) { |
| try { |
| const responseDiv = button.closest('.ai-bubble').querySelector('div.bg-blue-50'); |
| if (!responseDiv) throw new Error('Response not found'); |
| |
| let responseText = ''; |
| |
| const pTags = responseDiv.querySelectorAll('p'); |
| if (pTags.length > 1) { |
| responseText = Array.from(pTags) |
| .filter(p => !p.innerHTML.includes('<strong>')) |
| .map(p => p.textContent) |
| .join('\n'); |
| } else { |
| responseText = pTags[0].textContent; |
| } |
| |
| const context = nlp.determineContext(responseText); |
| |
| |
| const strongTags = responseDiv.querySelectorAll('strong'); |
| if (strongTags.length >= 2) { |
| const question = strongTags[0].nextSibling.textContent.trim(); |
| const answer = strongTags[1].nextSibling.textContent.trim(); |
| nlp.learnResponse(question, answer); |
| } else { |
| trainingData.customResponses[context] = responseText; |
| } |
| |
| nlp.saveToLocalStorage(); |
| |
| button.innerHTML = '<i class="fas fa-check-circle"></i> Saved'; |
| button.classList.remove('bg-green-500'); |
| button.classList.add('bg-emerald-600'); |
| button.disabled = true; |
| |
| |
| const buttons = button.closest('.flex').querySelectorAll('button'); |
| buttons.forEach(btn => { |
| if (btn !== button) { |
| btn.classList.add('opacity-50', 'cursor-not-allowed'); |
| btn.disabled = true; |
| } |
| }); |
| } catch (error) { |
| console.error('Error accepting response:', error); |
| alert('There was an error saving this response. Please try again.'); |
| } |
| } |
| |
| function editResponse(button) { |
| const bubble = button.closest('.ai-bubble'); |
| const responseDiv = bubble.querySelector('div.bg-blue-50'); |
| const pTags = responseDiv.querySelectorAll('p'); |
| |
| let currentText = ''; |
| if (pTags.length > 1) { |
| |
| currentText = Array.from(pTags) |
| .filter(p => !p.innerHTML.includes('<strong>')) |
| .map(p => p.textContent) |
| .join('\n\n'); |
| } else { |
| currentText = pTags[0].textContent; |
| } |
| |
| responseDiv.innerHTML = ` |
| <textarea class="w-full p-2 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700" |
| rows="4">${currentText}</textarea> |
| <div class="mt-2 flex justify-end gap-2"> |
| <button onclick="cancelEdit(this)" |
| class="px-4 py-1 bg-gray-300 dark:bg-gray-600 text-gray-800 dark:text-gray-200 rounded"> |
| Cancel |
| </button> |
| <button onclick="saveEditedResponse(this)" |
| class="px-4 py-1 bg-accent text-white rounded hover:bg-blue-600 transition"> |
| Save Changes |
| </button> |
| </div> |
| `; |
| } |
| |
| function cancelEdit(button) { |
| const bubble = button.closest('.ai-bubble'); |
| const responseDiv = button.closest('.bg-blue-50'); |
| const originalText = responseDiv.querySelector('textarea').value; |
| |
| |
| if (originalText.includes('\n\n')) { |
| responseDiv.innerHTML = originalText.split('\n\n') |
| .map(text => `<p>${text}</p>`) |
| .join(''); |
| } else { |
| responseDiv.innerHTML = `<p>${originalText}</p>`; |
| } |
| } |
| |
| function saveEditedResponse(button) { |
| try { |
| const newText = button.closest('.bg-blue-50').querySelector('textarea').value; |
| const bubble = button.closest('.ai-bubble'); |
| const responseDiv = button.closest('.bg-blue-50'); |
| |
| |
| const wasTeachingExample = bubble.innerHTML.includes('<strong>When asked:</strong>'); |
| |
| if (wasTeachingExample) { |
| |
| const lines = newText.split('\n'); |
| if (lines.length >= 2) { |
| const question = lines[0].replace('When asked:', '').trim(); |
| const answer = lines[1].replace('I\'ll respond:', '').trim(); |
| nlp.learnResponse(question, answer); |
| } |
| } else { |
| |
| const context = nlp.determineContext(newText); |
| trainingData.customResponses[context] = newText; |
| } |
| |
| nlp.saveToLocalStorage(); |
| |
| |
| if (newText.includes('\n')) { |
| responseDiv.innerHTML = newText.split('\n') |
| .map(text => `<p>${text}</p>`) |
| .join(''); |
| } else { |
| responseDiv.innerHTML = `<p>${newText}</p>`; |
| } |
| |
| |
| const actionDiv = document.createElement('div'); |
| actionDiv.className = 'mt-3 flex gap-2'; |
| actionDiv.innerHTML = ` |
| <button class="flex-1 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition" |
| onclick="acceptResponse(this)"> |
| Accept <i class="fas fa-check ml-1"></i> |
| </button> |
| <button class="flex-1 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition" |
| onclick="editResponse(this)"> |
| Edit <i class="fas fa-edit ml-1"></i> |
| </button> |
| <button class="px-3 py-2 bg-accent text-white rounded-lg hover:bg-blue-600 transition" |
| onclick="playResponse(this)"> |
| <i class="fas fa-volume-up"></i> |
| </button> |
| `; |
| responseDiv.insertAdjacentElement('afterend', actionDiv); |
| } catch (error) { |
| console.error('Error saving edits:', error); |
| alert('There was an error saving your changes. Please try again.'); |
| } |
| } |
| |
| function playResponse(button) { |
| try { |
| const responseDiv = button.closest('.ai-bubble').querySelector('div.bg-blue-50'); |
| let responseText = ''; |
| |
| if (responseDiv.querySelector('strong')) { |
| |
| const pTags = responseDiv.querySelectorAll('p'); |
| responseText = Array.from(pTags) |
| .filter(p => !p.innerHTML.includes('<strong>')) |
| .map(p => p.textContent) |
| .join(' '); |
| } else { |
| |
| responseText = responseDiv.textContent; |
| } |
| |
| |
| button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'; |
| button.disabled = true; |
| |
| setTimeout(() => { |
| button.innerHTML = '<i class="fas fa-volume-up"></i>'; |
| button.disabled = false; |
| console.log('Would play:', responseText); |
| |
| |
| |
| const bubble = button.closest('.ai-bubble'); |
| bubble.classList.add('ring-2', 'ring-accent'); |
| setTimeout(() => { |
| bubble.classList.remove('ring-2', 'ring-accent'); |
| }, 1000); |
| }, 1500); |
| } catch (error) { |
| console.error('Error playing response:', error); |
| button.innerHTML = '<i class="fas fa-volume-up"></i>'; |
| button.disabled = false; |
| } |
| } |
| |
| function handleTrainingKeyPress(e) { |
| if (e.key === 'Enter') { |
| submitTraining(); |
| } |
| } |
| |
| |
| let voiceProcessor = { |
| model: 'okada-female', |
| tone: 'neutral', |
| speed: 1.0, |
| pitch: 0, |
| similarity: 0.85, |
| |
| init: function() { |
| |
| this.tts = new OkadaTTS({ |
| voice: this.model, |
| speed: this.speed, |
| pitch: this.pitch |
| }); |
| |
| |
| this.rvc = new RVC({ |
| similarity: this.similarity, |
| pitchShift: this.pitch |
| }); |
| }, |
| |
| speak: function(text) { |
| |
| const ttsResult = this.tts.synthesize(text, this.tone); |
| |
| |
| return this.rvc.convert(ttsResult); |
| }, |
| |
| updateSettings: function() { |
| this.model = document.getElementById('voiceModel').value; |
| this.tone = document.getElementById('voiceTone').value; |
| this.speed = parseFloat(document.getElementById('voiceSpeed').value); |
| this.pitch = parseInt(document.getElementById('pitchShift').value); |
| this.similarity = parseInt(document.getElementById('similarity').value) / 100; |
| |
| this.init(); |
| } |
| }; |
| |
| |
| function showCreateTemplateModal() { |
| document.getElementById('templateModal').classList.remove('hidden'); |
| document.getElementById('templateName').focus(); |
| } |
| |
| function hideCreateTemplateModal() { |
| document.getElementById('templateModal').classList.add('hidden'); |
| } |
| |
| function saveNewTemplate() { |
| const name = document.getElementById('templateName').value.trim(); |
| const triggers = document.getElementById('templateTriggers').value.trim(); |
| const response = document.getElementById('templateResponse').value.trim(); |
| |
| if (!name || !response) { |
| alert('Please provide both a template name and response text'); |
| return; |
| } |
| |
| |
| if (!trainingData.learnedResponses.business) { |
| trainingData.learnedResponses.business = []; |
| } |
| |
| trainingData.learnedResponses.business.push({ |
| input: name, |
| response: response, |
| tokens: triggers.split(',').map(t => t.trim()), |
| lastUsed: new Date() |
| }); |
| |
| nlp.saveToLocalStorage(); |
| hideCreateTemplateModal(); |
| showScreen('repliesScreen'); |
| |
| |
| const notification = document.createElement('div'); |
| notification.className = 'fixed top-16 left-1/2 transform -translate-x-1/2 bg-green-500 text-white px-4 py-2 rounded-full animate-fadeIn z-50'; |
| notification.textContent = 'Template created successfully!'; |
| document.body.appendChild(notification); |
| setTimeout(() => notification.remove(), 2000); |
| |
| |
| document.getElementById('templateName').value = ''; |
| document.getElementById('templateTriggers').value = ''; |
| document.getElementById('templateResponse').value = ''; |
| } |
| |
| function testVoiceSettings() { |
| voiceProcessor.updateSettings(); |
| const audio = voiceProcessor.speak("Hello, this is your AI call assistant with new voice settings."); |
| audio.play(); |
| } |
| |
| |
| document.getElementById('similarity').addEventListener('input', function() { |
| document.getElementById('similarityValue').textContent = this.value + '%'; |
| }); |
| |
| function playResponse(button) { |
| const responseText = button.parentElement.querySelector('div p').textContent; |
| const audio = voiceProcessor.speak(responseText); |
| audio.play(); |
| } |
| |
| |
| const phoneHandler = { |
| client: null, |
| isCallActive: false, |
| currentCall: null, |
| |
| init: function() { |
| |
| nlp.loadFromLocalStorage(); |
| |
| |
| this.client = { |
| onIncomingCall: (call) => this.handleIncomingCall(call), |
| answerCall: (call) => this.answerCall(call), |
| endCall: () => this.endCall() |
| }; |
| |
| |
| this.checkActiveCall(); |
| |
| |
| voiceProcessor.init(); |
| addAppearAnimations(); |
| }, |
| |
| handleIncomingCall: function(call) { |
| this.isCallActive = true; |
| this.currentCall = call; |
| |
| |
| showScreen('homeScreen'); |
| document.querySelector('.fab i').classList.remove('fa-microphone'); |
| document.querySelector('.fab i').classList.add('fa-phone-alt'); |
| document.querySelector('.fab').style.background = '#ff375f'; |
| |
| |
| const callerInfo = call.callerID || 'Unknown Caller'; |
| const notification = document.createElement('div'); |
| notification.className = 'fixed top-16 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-4 py-2 rounded-full animate-fadeIn z-50'; |
| notification.innerHTML = ` |
| Incoming call from: ${callerInfo} |
| <div class="mt-2 flex justify-center gap-4"> |
| <button onclick="phoneHandler.answerCall()" class="bg-green-500 w-12 h-12 rounded-full flex items-center justify-center"> |
| <i class="fas fa-phone"></i> |
| </button> |
| <button onclick="phoneHandler.endCall()" class="bg-red-500 w-12 h-12 rounded-full flex items-center justify-center"> |
| <i class="fas fa-phone-slash"></i> |
| </button> |
| </div> |
| `; |
| document.body.appendChild(notification); |
| |
| |
| if (trainingData.callHandling.screenContacts || |
| (!callerInfo && trainingData.callHandling.spamDetection)) { |
| setTimeout(() => this.screenCall(), 2000); |
| } |
| }, |
| |
| screenCall: function() { |
| const callerInfo = this.currentCall.callerID || 'Unknown Caller'; |
| const isSpam = callerInfo === 'Unknown Caller' && trainingData.callHandling.spamDetection; |
| const isTransfer = trainingData.callHandling.transferContacts.some(name => |
| callerInfo.includes(name)); |
| |
| let response; |
| if (isTransfer) { |
| response = `Transferring call from ${callerInfo} to you...`; |
| this.currentCall.transfer(); |
| } else if (isSpam) { |
| response = trainingData.customResponses.spam || nlp.generateDefaultResponse('spam'); |
| this.currentCall.reject(); |
| } else { |
| response = nlp.getBestResponse(callerInfo); |
| this.currentCall.speakResponse(response); |
| } |
| |
| |
| this.logInteraction(callerInfo, response, isSpam ? 'blocked' : 'screened'); |
| }, |
| |
| answerCall: function() { |
| if (!this.isCallActive) return; |
| |
| const callerInfo = this.currentCall.callerID || 'Unknown Caller'; |
| this.currentCall.answer(); |
| |
| |
| const greeting = nlp.getBestResponse(`call from ${callerInfo}`); |
| this.currentCall.speakResponse(greeting); |
| |
| |
| this.currentCall.startVoiceRecognition((text) => { |
| const response = nlp.getBestResponse(text); |
| this.currentCall.speakResponse(response); |
| |
| |
| if (response.includes('transferring') || response.includes('connecting you')) { |
| this.currentCall.transfer(); |
| } |
| }); |
| |
| |
| this.logInteraction(callerInfo, greeting, 'answered'); |
| }, |
| |
| endCall: function() { |
| if (!this.isCallActive) return; |
| this.currentCall.hangup(); |
| this.isCallActive = false; |
| this.currentCall = null; |
| |
| |
| document.querySelector('.fab i').classList.remove('fa-phone-alt'); |
| document.querySelector('.fab i').classList.add('fa-microphone'); |
| document.querySelector('.fab').style.background = 'linear-gradient(135deg, #0a84ff, #5e5ce6)'; |
| }, |
| |
| logInteraction: function(caller, response, type) { |
| const logEntry = { |
| date: new Date(), |
| caller, |
| response, |
| type, |
| duration: type === 'answered' ? Math.floor(Math.random() * 120) + 5 : 0 |
| }; |
| |
| |
| const activityElement = document.createElement('div'); |
| activityElement.className = 'flex items-start animate-fadeIn'; |
| activityElement.innerHTML = ` |
| <div class="w-10 h-10 rounded-full ${type === 'blocked' ? 'bg-purple-100 dark:bg-purple-900' : 'bg-green-100 dark:bg-green-900'} flex items-center justify-center"> |
| <i class="fas fa-phone-alt ${type === 'blocked' ? 'text-purple-600 dark:text-purple-400' : 'text-green-600 dark:text-green-400'}"></i> |
| </div> |
| <div class="ml-3 flex-1"> |
| <div class="flex justify-between"> |
| <h3 class="font-medium">${caller}</h3> |
| <span class="text-xs text-gray-500 dark:text-gray-400">${logEntry.date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span> |
| </div> |
| <p class="text-sm text-gray-500 dark:text-gray-400 mt-1"> |
| AI ${type} call: "${response.substring(0, 40)}${response.length > 40 ? '...' : ''}" |
| </p> |
| </div> |
| `; |
| |
| |
| const activityContainer = document.querySelector('#homeScreen .space-y-4'); |
| if (activityContainer.firstChild) { |
| activityContainer.insertBefore(activityElement, activityContainer.firstChild); |
| } else { |
| activityContainer.appendChild(activityElement); |
| } |
| }, |
| |
| checkActiveCall: function() { |
| |
| const activeCall = false; |
| if (activeCall) { |
| this.handleIncomingCall({ |
| callerID: 'Incoming call', |
| answer: () => console.log('Call answered'), |
| speakResponse: (text) => voiceProcessor.speak(text).play() |
| }); |
| } |
| } |
| }; |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| phoneHandler.init(); |
| |
| |
| document.querySelectorAll('[onclick^="showScreen"]').forEach(btn => { |
| btn.addEventListener('click', function() { |
| if (this.getAttribute('onclick').includes('trainingScreen')) { |
| setTimeout(() => { |
| document.getElementById('trainingInput').focus(); |
| }, 300); |
| } |
| }); |
| }); |
| |
| |
| window.simulateCall = function(callerID) { |
| phoneHandler.handleIncomingCall({ |
| callerID: callerID || 'Unknown Caller', |
| answer: function() { |
| console.log('Call answered'); |
| const audio = voiceProcessor.speak("Hello, this call has been answered by the AI assistant."); |
| audio.play(); |
| }, |
| speakResponse: function(text) { |
| const audio = voiceProcessor.speak(text); |
| audio.play(); |
| }, |
| reject: function() { console.log('Call rejected'); }, |
| hangup: function() { console.log('Call ended'); }, |
| transfer: function() { console.log('Call transferred'); } |
| }); |
| }; |
| }); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=jjmandog/ccx" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |