Ming5261's picture
现在开发会话界面:和大模型对话,包括常用功能:如新建、历史、删除、增加等
84e8e19 verified
<!DOCTYPE html>
<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>