Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>TumorTalk: Virtual Patient Simulator</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <link rel="stylesheet" href="/static/style.css"> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div id="app" class="min-h-screen flex flex-col"> | |
| <!-- Navigation --> | |
| <nav class="bg-indigo-700 text-white shadow-lg"> | |
| <div class="container mx-auto px-4 py-3 flex justify-between items-center"> | |
| <div class="flex items-center space-x-2"> | |
| <i data-feather="activity" class="w-6 h-6"></i> | |
| <h1 class="text-xl font-bold">TumorTalk</h1> | |
| </div> | |
| <div class="flex items-center space-x-4" v-if="isAuthenticated"> | |
| <span class="text-sm">Welcome, {{ currentUser.username }}</span> | |
| <button @click="logout" class="flex items-center space-x-1 bg-indigo-600 hover:bg-indigo-800 px-3 py-1 rounded transition"> | |
| <i data-feather="log-out" class="w-4 h-4"></i> | |
| <span>Logout</span> | |
| </button> | |
| </div> | |
| </div> | |
| </nav> | |
| <!-- Main Content --> | |
| <main class="flex-grow container mx-auto px-4 py-6"> | |
| <!-- Login Form --> | |
| <div v-if="!isAuthenticated" class="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden p-6"> | |
| <div class="text-center mb-6"> | |
| <h2 class="text-2xl font-bold text-gray-800">Sign In</h2> | |
| <p class="text-gray-600">Access the virtual patient simulator</p> | |
| </div> | |
| <form @submit.prevent="login" class="space-y-4"> | |
| <div> | |
| <label for="username" class="block text-sm font-medium text-gray-700">Username</label> | |
| <input v-model="loginForm.username" type="text" id="username" required | |
| class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <div> | |
| <label for="password" class="block text-sm font-medium text-gray-700">Password</label> | |
| <input v-model="loginForm.password" type="password" id="password" required | |
| class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <div> | |
| <button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | |
| Sign In | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| <!-- Chat Interface --> | |
| <div v-if="isAuthenticated" class="flex flex-col h-full"> | |
| <!-- Session Management --> | |
| <div class="flex justify-between items-center mb-4"> | |
| <div class="flex space-x-2"> | |
| <button @click="createNewSession" class="flex items-center space-x-1 bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded transition"> | |
| <i data-feather="plus" class="w-4 h-4"></i> | |
| <span>New Session</span> | |
| </button> | |
| <select v-model="currentSessionId" @change="loadSession" class="border border-gray-300 rounded px-3 py-1 focus:outline-none focus:ring-1 focus:ring-indigo-500"> | |
| <option v-for="session in sessions" :value="session.id" :key="session.id"> | |
| {{ formatDate(session.created_at) }} - {{ session.patient_profile.name }} | |
| </option> | |
| </select> | |
| </div> | |
| <button v-if="currentUser.role === 'admin'" @click="goToAdmin" class="flex items-center space-x-1 bg-purple-600 hover:bg-purple-700 text-white px-3 py-1 rounded transition"> | |
| <i data-feather="settings" class="w-4 h-4"></i> | |
| <span>Admin Panel</span> | |
| </button> | |
| </div> | |
| <!-- Patient Profile --> | |
| <div v-if="currentSession" class="bg-white rounded-lg shadow-md p-4 mb-4"> | |
| <div class="flex items-start"> | |
| <div class="flex-shrink-0 bg-indigo-100 rounded-full p-3"> | |
| <i data-feather="user" class="w-6 h-6 text-indigo-600"></i> | |
| </div> | |
| <div class="ml-4"> | |
| <h3 class="text-lg font-semibold text-gray-800">{{ currentSession.patient_profile.name }}</h3> | |
| <p class="text-sm text-gray-600">{{ currentSession.patient_profile.age }} years, {{ currentSession.patient_profile.gender }}</p> | |
| <div class="mt-2"> | |
| <span class="inline-block bg-red-100 text-red-800 text-xs px-2 py-1 rounded-full">Diagnosis: {{ currentSession.patient_profile.diagnosis }}</span> | |
| <span v-for="(symptom, index) in currentSession.patient_profile.symptoms" :key="index" class="inline-block ml-2 bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">{{ symptom }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Chat Messages --> | |
| <div v-if="currentSession" class="flex-grow bg-white rounded-lg shadow-md overflow-hidden mb-4"> | |
| <div class="h-96 overflow-y-auto p-4 space-y-4" ref="chatContainer"> | |
| <div v-for="(message, index) in currentSession.messages" :key="index" class="flex" :class="{'justify-end': message.sender === 'user', 'justify-start': message.sender === 'patient'}"> | |
| <div :class="{'bg-indigo-100 text-indigo-800': message.sender === 'user', 'bg-gray-100 text-gray-800': message.sender === 'patient'}" class="max-w-xs lg:max-w-md px-4 py-2 rounded-lg shadow"> | |
| <div class="text-xs font-semibold mb-1">{{ message.sender === 'user' ? 'You' : currentSession.patient_profile.name }}</div> | |
| <div>{{ message.content }}</div> | |
| <div class="text-xs text-gray-500 mt-1 text-right">{{ formatTime(message.timestamp) }}</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="border-t border-gray-200 p-4 bg-gray-50"> | |
| <form @submit.prevent="sendMessage" class="flex space-x-2"> | |
| <input v-model="newMessage" type="text" placeholder="Type your question..." required | |
| class="flex-grow px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"> | |
| <button type="submit" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md shadow-sm"> | |
| <i data-feather="send" class="w-4 h-4"></i> | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="bg-gray-800 text-white py-4"> | |
| <div class="container mx-auto px-4 text-center text-sm"> | |
| <p>TumorTalk - Virtual Patient Simulator for Medical Education</p> | |
| </div> | |
| </footer> | |
| </div> | |
| <script> | |
| const { createApp, ref, onMounted, nextTick } = Vue; | |
| createApp({ | |
| setup() { | |
| const isAuthenticated = ref(false); | |
| const currentUser = ref({}); | |
| const loginForm = ref({ | |
| username: '', | |
| password: '' | |
| }); | |
| const sessions = ref([]); | |
| const currentSessionId = ref(null); | |
| const currentSession = ref(null); | |
| const newMessage = ref(''); | |
| const chatContainer = ref(null); | |
| // Format date for display | |
| const formatDate = (dateString) => { | |
| const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }; | |
| return new Date(dateString).toLocaleDateString(undefined, options); | |
| }; | |
| const formatTime = (dateString) => { | |
| const options = { hour: '2-digit', minute: '2-digit' }; | |
| return new Date(dateString).toLocaleTimeString(undefined, options); | |
| }; | |
| // Scroll chat to bottom | |
| const scrollToBottom = () => { | |
| nextTick(() => { | |
| if (chatContainer.value) { | |
| chatContainer.value.scrollTop = chatContainer.value.scrollHeight; | |
| } | |
| }); | |
| }; | |
| // Check authentication status | |
| const checkAuth = async () => { | |
| try { | |
| const response = await axios.get('/api/auth/status'); | |
| isAuthenticated.value = response.data.authenticated; | |
| currentUser.value = response.data.user || {}; | |
| if (isAuthenticated.value) { | |
| await loadSessions(); | |
| } | |
| } catch (error) { | |
| console.error('Authentication check failed:', error); | |
| } | |
| }; | |
| // Login | |
| const login = async () => { | |
| try { | |
| const response = await axios.post('/api/auth/login', loginForm.value); | |
| isAuthenticated.value = true; | |
| currentUser.value = response.data.user; | |
| await loadSessions(); | |
| } catch (error) { | |
| alert('Login failed. Please check your credentials.'); | |
| console.error('Login error:', error); | |
| } | |
| }; | |
| // Logout | |
| const logout = async () => { | |
| try { | |
| await axios.post('/api/auth/logout'); | |
| isAuthenticated.value = false; | |
| currentUser.value = {}; | |
| sessions.value = []; | |
| currentSessionId.value = null; | |
| currentSession.value = null; | |
| } catch (error) { | |
| console.error('Logout error:', error); | |
| } | |
| }; | |
| // Load user sessions | |
| const loadSessions = async () => { | |
| try { | |
| const response = await axios.get('/api/sessions'); | |
| sessions.value = response.data.sessions; | |
| if (sessions.value.length > 0 && !currentSessionId.value) { | |
| currentSessionId.value = sessions.value[0].id; | |
| await loadSession(); | |
| } | |
| } catch (error) { | |
| console.error('Error loading sessions:', error); | |
| } | |
| }; | |
| // Load a specific session | |
| const loadSession = async () => { | |
| if (!currentSessionId.value) return; | |
| try { | |
| const response = await axios.get(`/api/sessions/${currentSessionId.value}`); | |
| currentSession.value = response.data.session; | |
| scrollToBottom(); | |
| } catch (error) { | |
| console.error('Error loading session:', error); | |
| } | |
| }; | |
| // Create a new session | |
| const createNewSession = async () => { | |
| try { | |
| const response = await axios.post('/api/sessions'); | |
| sessions.value.unshift(response.data.session); | |
| currentSessionId.value = response.data.session.id; | |
| currentSession.value = response.data.session; | |
| } catch (error) { | |
| console.error('Error creating new session:', error); | |
| } | |
| }; | |
| // Send a message | |
| const sendMessage = async () => { | |
| if (!newMessage.value.trim() || !currentSession.value) return; | |
| const userMessage = { | |
| sender: 'user', | |
| content: newMessage.value, | |
| timestamp: new Date().toISOString() | |
| }; | |
| // Optimistically update UI | |
| currentSession.value.messages.push(userMessage); | |
| newMessage.value = ''; | |
| scrollToBottom(); | |
| try { | |
| const response = await axios.post(`/api/sessions/${currentSessionId.value}/messages`, { | |
| content: userMessage.content | |
| }); | |
| // Update with the full response (including AI response) | |
| currentSession.value = response.data.session; | |
| scrollToBottom(); | |
| } catch (error) { | |
| console.error('Error sending message:', error); | |
| // Revert optimistic update | |
| currentSession.value.messages.pop(); | |
| } | |
| }; | |
| // Navigate to admin panel | |
| const goToAdmin = () => { | |
| window.location.href = '/admin.html'; | |
| }; | |
| // Initialize | |
| onMounted(async () => { | |
| await checkAuth(); | |
| feather.replace(); | |
| }); | |
| return { | |
| isAuthenticated, | |
| currentUser, | |
| loginForm, | |
| sessions, | |
| currentSessionId, | |
| currentSession, | |
| newMessage, | |
| chatContainer, | |
| formatDate, | |
| formatTime, | |
| login, | |
| logout, | |
| loadSession, | |
| createNewSession, | |
| sendMessage, | |
| goToAdmin | |
| }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| <script>feather.replace();</script> | |
| </body> | |
| </html> | |