Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Virtual Physician Assistant | Canadian Cardiovascular Training Center</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://cdnjs.cloudflare.com/ajax/libs/docx/7.1.0/docx.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); | |
| body { | |
| font-family: 'Poppins', sans-serif; | |
| background-color: #f0f4f8; | |
| } | |
| .pulse { | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.7); | |
| } | |
| 70% { | |
| box-shadow: 0 0 0 10px rgba(220, 38, 38, 0); | |
| } | |
| 100% { | |
| box-shadow: 0 0 0 0 rgba(220, 38, 38, 0); | |
| } | |
| } | |
| .typing { | |
| position: relative; | |
| } | |
| .typing::after { | |
| content: "|"; | |
| position: absolute; | |
| right: -5px; | |
| animation: blink 1s infinite; | |
| } | |
| @keyframes blink { | |
| 0% { opacity: 0; } | |
| 50% { opacity: 1; } | |
| 100% { opacity: 0; } | |
| } | |
| .card-gradient { | |
| background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); | |
| } | |
| .voice-wave { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 4px; | |
| height: 40px; | |
| } | |
| .voice-wave span { | |
| display: block; | |
| width: 4px; | |
| height: 10px; | |
| background: #3b82f6; | |
| border-radius: 3px; | |
| animation: voice-wave 1.5s infinite ease-in-out; | |
| } | |
| .voice-wave span:nth-child(2) { | |
| animation-delay: 0.2s; | |
| } | |
| .voice-wave span:nth-child(3) { | |
| animation-delay: 0.4s; | |
| } | |
| .voice-wave span:nth-child(4) { | |
| animation-delay: 0.6s; | |
| } | |
| .voice-wave span:nth-child(5) { | |
| animation-delay: 0.8s; | |
| } | |
| @keyframes voice-wave { | |
| 0% { height: 10px; } | |
| 50% { height: 30px; } | |
| 100% { height: 10px; } | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="bg-red-700 text-white shadow-lg"> | |
| <div class="container mx-auto px-4 py-6 flex justify-between items-center"> | |
| <div class="flex items-center space-x-4"> | |
| <div class="bg-white rounded-full p-2"> | |
| <i class="fas fa-heartbeat text-red-700 text-2xl"></i> | |
| </div> | |
| <div> | |
| <h1 class="text-2xl font-bold">Canadian Cardiovascular Training Center</h1> | |
| <p class="text-sm opacity-80">Virtual Physician Assistant</p> | |
| </div> | |
| </div> | |
| <div class="hidden md:flex items-center space-x-2"> | |
| <i class="fas fa-user-md text-xl"></i> | |
| <span>Specialist Support System</span> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="flex-grow container mx-auto px-4 py-8"> | |
| <div class="max-w-4xl mx-auto"> | |
| <!-- Patient Info Card --> | |
| <div class="bg-white rounded-xl shadow-md overflow-hidden mb-8 card-gradient"> | |
| <div class="p-6"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Patient Information</h2> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700">Full Name</label> | |
| <p id="patient-name" class="mt-1 text-gray-900 font-medium">Not provided yet</p> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700">Date of Birth</label> | |
| <p id="patient-dob" class="mt-1 text-gray-900 font-medium">Not provided yet</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Virtual Assistant Interface --> | |
| <div class="bg-white rounded-xl shadow-md overflow-hidden mb-8"> | |
| <div class="p-6 border-b border-gray-200 bg-gray-50"> | |
| <div class="flex items-center justify-between"> | |
| <h2 class="text-xl font-semibold text-gray-800">Virtual Physician Assistant</h2> | |
| <div id="status-indicator" class="flex items-center space-x-2"> | |
| <span class="relative flex h-3 w-3"> | |
| <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span> | |
| <span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span> | |
| </span> | |
| <span class="text-sm text-gray-600">Listening</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Conversation Display --> | |
| <div class="p-6 h-96 overflow-y-auto" id="conversation-container"> | |
| <div class="flex mb-4"> | |
| <div class="flex-shrink-0 mr-3"> | |
| <div class="bg-blue-100 rounded-full p-2"> | |
| <i class="fas fa-heart text-blue-600"></i> | |
| </div> | |
| </div> | |
| <div class="bg-blue-50 rounded-lg p-4 max-w-3xl"> | |
| <p id="assistant-message" class="text-gray-800 typing">Hello, I'm your Virtual Physician Assistant. I'll be asking you some preliminary questions before your appointment. Let's start with your full name.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Controls --> | |
| <div class="p-6 border-t border-gray-200 bg-gray-50"> | |
| <div class="flex items-center justify-between"> | |
| <button id="voice-btn" class="bg-red-600 hover:bg-red-700 text-white rounded-full p-4 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 pulse"> | |
| <i class="fas fa-microphone text-xl"></i> | |
| </button> | |
| <div id="voice-wave" class="voice-wave hidden"> | |
| <span></span> | |
| <span></span> | |
| <span></span> | |
| <span></span> | |
| <span></span> | |
| </div> | |
| <button id="complete-btn" class="hidden bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"> | |
| Complete Screening | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Patient Responses Summary --> | |
| <div class="bg-white rounded-xl shadow-md overflow-hidden mb-8"> | |
| <div class="p-6 border-b border-gray-200 bg-gray-50"> | |
| <h2 class="text-xl font-semibold text-gray-800">Pre-Screening Summary</h2> | |
| </div> | |
| <div class="p-6"> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Question</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Response</th> | |
| </tr> | |
| </thead> | |
| <tbody id="responses-table" class="bg-white divide-y divide-gray-200"> | |
| <!-- Responses will be added here dynamically --> | |
| <tr> | |
| <td colspan="2" class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center">No responses recorded yet</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Download Button --> | |
| <div class="text-center mb-8"> | |
| <button id="download-btn" class="hidden bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> | |
| <i class="fas fa-download mr-2"></i> Download Summary | |
| </button> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="bg-gray-800 text-white py-6"> | |
| <div class="container mx-auto px-4"> | |
| <div class="flex flex-col md:flex-row justify-between items-center"> | |
| <div class="mb-4 md:mb-0"> | |
| <p class="text-sm">© 2023 Canadian Cardiovascular Training Center. All rights reserved.</p> | |
| </div> | |
| <div class="flex space-x-6"> | |
| <a href="#" class="text-gray-300 hover:text-white"> | |
| <i class="fas fa-shield-alt"></i> Privacy Policy | |
| </a> | |
| <a href="#" class="text-gray-300 hover:text-white"> | |
| <i class="fas fa-file-alt"></i> Terms of Service | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </footer> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const voiceBtn = document.getElementById('voice-btn'); | |
| const voiceWave = document.getElementById('voice-wave'); | |
| const completeBtn = document.getElementById('complete-btn'); | |
| const conversationContainer = document.getElementById('conversation-container'); | |
| const responsesTable = document.getElementById('responses-table'); | |
| const patientName = document.getElementById('patient-name'); | |
| const patientDob = document.getElementById('patient-dob'); | |
| const statusIndicator = document.getElementById('status-indicator'); | |
| const assistantMessage = document.getElementById('assistant-message'); | |
| const downloadBtn = document.getElementById('download-btn'); | |
| // Speech recognition setup | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| const recognition = new SpeechRecognition(); | |
| recognition.continuous = false; | |
| recognition.interimResults = false; | |
| recognition.lang = 'en-CA'; | |
| // Speech synthesis | |
| const synth = window.speechSynthesis; | |
| // Questions array (now split into separate name and DOB questions) | |
| const questions = [ | |
| "What's your first and last name?", | |
| "What's your date of birth?", | |
| "Have you experienced any chest pains within the last 2 months?", | |
| "Have you been experiencing any shortness of breath?", | |
| "Do you struggle walking up or down flights of stairs?", | |
| "Have you had any recent changes to your medication?", | |
| "Have you had any surgical procedures within the last 10 years?", | |
| "Are there any specific notes or details you'd like to mention to the doctor?" | |
| ]; | |
| // Responses object | |
| const responses = {}; | |
| let currentQuestionIndex = 0; | |
| let isListening = false; | |
| // Initialize responses object | |
| questions.forEach((question, index) => { | |
| responses[index] = { | |
| question: question, | |
| answer: "Not answered yet" | |
| }; | |
| }); | |
| // Speak function | |
| function speak(text) { | |
| if (synth.speaking) { | |
| synth.cancel(); | |
| } | |
| const utterance = new SpeechSynthesisUtterance(text); | |
| utterance.rate = 0.9; | |
| utterance.pitch = 1; | |
| utterance.onend = function() { | |
| // When the assistant finishes speaking, start listening | |
| if (currentQuestionIndex < questions.length) { | |
| startListening(); | |
| } | |
| }; | |
| // Update UI to show assistant is speaking | |
| assistantMessage.classList.add('typing'); | |
| assistantMessage.textContent = ""; | |
| // Simulate typing effect | |
| let i = 0; | |
| const typingInterval = setInterval(() => { | |
| if (i < text.length) { | |
| assistantMessage.textContent += text.charAt(i); | |
| i++; | |
| conversationContainer.scrollTop = conversationContainer.scrollHeight; | |
| } else { | |
| clearInterval(typingInterval); | |
| assistantMessage.classList.remove('typing'); | |
| synth.speak(utterance); | |
| } | |
| }, 30); | |
| } | |
| // Start listening function | |
| function startListening() { | |
| isListening = true; | |
| voiceWave.classList.remove('hidden'); | |
| voiceBtn.classList.add('bg-red-700', 'pulse'); | |
| voiceBtn.classList.remove('bg-red-600'); | |
| recognition.start(); | |
| // Update status indicator | |
| statusIndicator.innerHTML = ` | |
| <span class="relative flex h-3 w-3"> | |
| <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span> | |
| <span class="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span> | |
| </span> | |
| <span class="text-sm text-gray-600">Listening</span> | |
| `; | |
| } | |
| // Stop listening function | |
| function stopListening() { | |
| isListening = false; | |
| voiceWave.classList.add('hidden'); | |
| voiceBtn.classList.remove('bg-red-700', 'pulse'); | |
| voiceBtn.classList.add('bg-red-600'); | |
| // Update status indicator | |
| statusIndicator.innerHTML = ` | |
| <span class="relative flex h-3 w-3"> | |
| <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span> | |
| <span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span> | |
| </span> | |
| <span class="text-sm text-gray-600">Ready</span> | |
| `; | |
| } | |
| // Add message to conversation | |
| function addMessage(speaker, message) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'flex mb-4'; | |
| if (speaker === 'assistant') { | |
| messageDiv.innerHTML = ` | |
| <div class="flex-shrink-0 mr-3"> | |
| <div class="bg-blue-100 rounded-full p-2"> | |
| <i class="fas fa-heart text-blue-600"></i> | |
| </div> | |
| </div> | |
| <div class="bg-blue-50 rounded-lg p-4 max-w-3xl"> | |
| <p class="text-gray-800">${message}</p> | |
| </div> | |
| `; | |
| } else { | |
| messageDiv.innerHTML = ` | |
| <div class="flex-shrink-0 mr-3 ml-auto"> | |
| <div class="bg-green-100 rounded-full p-2"> | |
| <i class="fas fa-user text-green-600"></i> | |
| </div> | |
| </div> | |
| <div class="bg-green-50 rounded-lg p-4 max-w-3xl"> | |
| <p class="text-gray-800">${message}</p> | |
| </div> | |
| `; | |
| } | |
| conversationContainer.appendChild(messageDiv); | |
| conversationContainer.scrollTop = conversationContainer.scrollHeight; | |
| } | |
| // Update responses table | |
| function updateResponsesTable() { | |
| responsesTable.innerHTML = ''; | |
| for (const key in responses) { | |
| const response = responses[key]; | |
| const row = document.createElement('tr'); | |
| row.innerHTML = ` | |
| <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${response.question}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${response.answer}</td> | |
| `; | |
| responsesTable.appendChild(row); | |
| } | |
| } | |
| // Generate and download Word document | |
| function generateWordDocument() { | |
| const { Document, Paragraph, TextRun, Packer, HeadingLevel } = docx; | |
| // Format current date | |
| const today = new Date(); | |
| const dateString = today.toLocaleDateString('en-CA', { | |
| year: 'numeric', | |
| month: 'long', | |
| day: 'numeric' | |
| }); | |
| // Create document content | |
| const doc = new Document({ | |
| sections: [{ | |
| properties: {}, | |
| children: [ | |
| new Paragraph({ | |
| text: "Canadian Cardiovascular Training Center", | |
| heading: HeadingLevel.HEADING_1, | |
| spacing: { after: 200 } | |
| }), | |
| new Paragraph({ | |
| text: "Virtual Physician Assistant - Pre-Screening Report", | |
| heading: HeadingLevel.HEADING_2, | |
| spacing: { after: 200 } | |
| }), | |
| new Paragraph({ | |
| text: `Report Date: ${dateString}`, | |
| spacing: { after: 400 } | |
| }), | |
| new Paragraph({ | |
| text: `Patient Name: ${patientName.textContent}`, | |
| spacing: { after: 100 } | |
| }), | |
| new Paragraph({ | |
| text: `Date of Birth: ${patientDob.textContent}`, | |
| spacing: { after: 400 } | |
| }), | |
| new Paragraph({ | |
| text: "Pre-Screening Responses:", | |
| heading: HeadingLevel.HEADING_3, | |
| spacing: { after: 200 } | |
| }), | |
| // Add questions and answers | |
| ...Object.values(responses).map(response => { | |
| return new Paragraph({ | |
| children: [ | |
| new TextRun({ | |
| text: response.question + ": ", | |
| bold: true | |
| }), | |
| new TextRun({ | |
| text: response.answer | |
| }) | |
| ], | |
| spacing: { after: 150 } | |
| }); | |
| }) | |
| ] | |
| }] | |
| }); | |
| // Generate and download the document | |
| Packer.toBlob(doc).then(blob => { | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `PreScreening_${patientName.textContent.replace(/\s+/g, '_')}_${dateString.replace(/,/g, '')}.docx`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }); | |
| } | |
| // Handle voice button click | |
| voiceBtn.addEventListener('click', function() { | |
| if (isListening) { | |
| recognition.stop(); | |
| stopListening(); | |
| } else { | |
| if (currentQuestionIndex === 0) { | |
| speak(questions[currentQuestionIndex]); | |
| } else { | |
| startListening(); | |
| } | |
| } | |
| }); | |
| // Handle complete button click | |
| completeBtn.addEventListener('click', function() { | |
| speak("Thank you for completing the pre-screening questions. Please wait for the specialist to see you. Your responses have been recorded and will be reviewed by the doctor."); | |
| addMessage('assistant', "Pre-screening completed. Please wait for the specialist to see you."); | |
| completeBtn.classList.add('hidden'); | |
| downloadBtn.classList.remove('hidden'); | |
| // Disable voice button | |
| voiceBtn.disabled = true; | |
| voiceBtn.classList.remove('bg-red-600', 'bg-red-700', 'pulse'); | |
| voiceBtn.classList.add('bg-gray-400'); | |
| // Update status indicator | |
| statusIndicator.innerHTML = ` | |
| <span class="relative flex h-3 w-3"> | |
| <span class="relative inline-flex rounded-full h-3 w-3 bg-gray-500"></span> | |
| </span> | |
| <span class="text-sm text-gray-600">Completed</span> | |
| `; | |
| }); | |
| // Handle download button click | |
| downloadBtn.addEventListener('click', generateWordDocument); | |
| // Speech recognition events | |
| recognition.onresult = function(event) { | |
| const transcript = event.results[0][0].transcript; | |
| stopListening(); | |
| // Add user message to conversation | |
| addMessage('user', transcript); | |
| // Store response | |
| responses[currentQuestionIndex].answer = transcript; | |
| // Confirm the response back to the user | |
| let confirmationMessage = ""; | |
| if (currentQuestionIndex === 0) { | |
| // For name question | |
| patientName.textContent = transcript; | |
| confirmationMessage = `I have your name as ${transcript}.`; | |
| } else if (currentQuestionIndex === 1) { | |
| // For DOB question | |
| patientDob.textContent = transcript; | |
| confirmationMessage = `I have your date of birth as ${transcript}.`; | |
| } else { | |
| // For all other questions | |
| confirmationMessage = `You answered: ${transcript}.`; | |
| } | |
| // Add confirmation message | |
| setTimeout(() => { | |
| addMessage('assistant', confirmationMessage); | |
| // Update responses table | |
| updateResponsesTable(); | |
| // Move to next question or complete | |
| currentQuestionIndex++; | |
| if (currentQuestionIndex < questions.length) { | |
| setTimeout(() => { | |
| speak(questions[currentQuestionIndex]); | |
| }, 1000); | |
| } else { | |
| completeBtn.classList.remove('hidden'); | |
| } | |
| }, 1000); | |
| }; | |
| recognition.onerror = function(event) { | |
| console.error('Speech recognition error', event.error); | |
| stopListening(); | |
| if (event.error === 'no-speech') { | |
| addMessage('assistant', "I didn't hear anything. Please try again."); | |
| setTimeout(() => { | |
| speak(questions[currentQuestionIndex]); | |
| }, 1000); | |
| } else { | |
| addMessage('assistant', "There was an error with the speech recognition. Please try again."); | |
| voiceBtn.classList.remove('bg-red-700', 'pulse'); | |
| voiceBtn.classList.add('bg-red-600'); | |
| } | |
| }; | |
| // Start with the first question | |
| setTimeout(() => { | |
| speak(questions[0]); | |
| }, 1000); | |
| }); | |
| </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=CCTCASSISTANT/cctc-assistantai" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |