Help_Me_3 / static /bemor /index_not.html
giyos1212's picture
Upload 72 files
98b6d67 verified
<!DOCTYPE html>
<html lang="uz">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Help.me - Tez Tibbiy Yordam</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
/* ==================== ROOT VARIABLES ==================== */
:root {
--primary-blue: #4A90E2;
--primary-green: #50C878;
--accent-red: #EF4444;
--bg-white: #FFFFFF;
--bg-light: #F8FAFC;
--bg-gray: #F1F5F9;
--text-dark: #1E293B;
--text-medium: #475569;
--text-light: #94A3B8;
--border-color: #E2E8F0;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
}
/* ==================== GLOBAL RESET ==================== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, var(--primary-blue) 0%, var(--primary-green) 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
}
/* ==================== CONTAINER ==================== */
.app-container {
width: 100%;
max-width: 480px;
height: 95vh;
max-height: 900px;
background: var(--bg-white);
border-radius: 24px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
overflow: hidden;
animation: slideUp 0.5s ease-out;
}
@keyframes slideUp {
from {
transform: translateY(50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* ==================== HEADER ==================== */
.app-header {
background: linear-gradient(135deg, var(--primary-blue), var(--primary-green));
padding: 1.25rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow-md);
position: relative;
z-index: 10;
}
.header-left {
flex: 1;
}
.header-title {
display: flex;
align-items: center;
gap: 0.5rem;
color: white;
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.header-subtitle {
color: rgba(255, 255, 255, 0.9);
font-size: 0.875rem;
font-weight: 500;
}
.dispatcher-link {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.25rem;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
}
.dispatcher-link:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
/* ==================== LANGUAGE SELECTOR ==================== */
.language-selector {
background: var(--bg-light);
padding: 0.875rem 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
border-bottom: 1px solid var(--border-color);
}
.language-label {
color: var(--text-medium);
font-size: 0.875rem;
font-weight: 600;
}
.language-buttons {
display: flex;
gap: 0.5rem;
flex: 1;
}
.lang-btn {
flex: 1;
padding: 0.5rem;
border: 2px solid var(--border-color);
background: white;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
color: var(--text-medium);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
}
.lang-btn:hover {
border-color: var(--primary-blue);
background: var(--bg-light);
}
.lang-btn.active {
border-color: var(--primary-blue);
background: var(--primary-blue);
color: white;
}
/* ==================== CONNECTION STATUS ==================== */
.connection-status {
padding: 0.625rem 1.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 0.8rem;
font-weight: 500;
transition: all 0.3s ease;
}
.connection-status.connected {
background: #ECFDF5;
color: #059669;
}
.connection-status.disconnected {
background: #FEF2F2;
color: #DC2626;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-dot.connected {
background: #10B981;
}
.status-dot.disconnected {
background: #EF4444;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.95);
}
}
/* ==================== CHAT CONTAINER ==================== */
.chat-container {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
background: var(--bg-gray);
}
.chat-container::-webkit-scrollbar {
width: 6px;
}
.chat-container::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
/* ==================== MESSAGES ==================== */
.message {
display: flex;
gap: 0.75rem;
margin-bottom: 1.25rem;
animation: messageSlide 0.3s ease-out;
}
@keyframes messageSlide {
from {
opacity: 0;
transform: translateY(15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.user {
flex-direction: row-reverse;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
flex-shrink: 0;
}
.message.ai .message-avatar {
background: linear-gradient(135deg, var(--primary-blue), var(--primary-green));
color: white;
}
.message.user .message-avatar {
background: var(--bg-light);
color: var(--text-medium);
}
.message-content {
max-width: 75%;
padding: 0.875rem 1.125rem;
border-radius: 16px;
font-size: 0.9375rem;
line-height: 1.5;
box-shadow: var(--shadow-sm);
}
.message.ai .message-content {
background: white;
color: var(--text-dark);
border-bottom-left-radius: 4px;
}
.message.user .message-content {
background: var(--primary-blue);
color: white;
border-bottom-right-radius: 4px;
}
/* ==================== TYPING INDICATOR ==================== */
.typing-indicator {
display: none;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.25rem;
}
.typing-indicator.show {
display: flex;
}
.typing-dots {
display: flex;
gap: 4px;
padding: 0.875rem 1.125rem;
background: white;
border-radius: 16px;
border-bottom-left-radius: 4px;
}
.typing-dots span {
width: 8px;
height: 8px;
background: var(--text-light);
border-radius: 50%;
animation: typingBounce 1.4s infinite;
}
.typing-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dots span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typingBounce {
0%,
60%,
100% {
transform: translateY(0);
}
30% {
transform: translateY(-8px);
}
}
/* ==================== HINT BOX ==================== */
.hint-box {
background: #EEF2FF;
border-left: 4px solid var(--primary-blue);
padding: 0.875rem 1rem;
margin: 1rem 1.5rem;
border-radius: 8px;
display: flex;
align-items: center;
gap: 0.75rem;
}
.hint-icon {
font-size: 1.25rem;
color: var(--primary-blue);
}
.hint-text {
color: var(--text-medium);
font-size: 0.875rem;
line-height: 1.4;
}
.hint-text strong {
color: var(--text-dark);
font-weight: 600;
}
/* ==================== VOICE CONTROL ==================== */
.voice-control {
padding: 1.5rem;
background: white;
border-top: 1px solid var(--border-color);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.voice-button {
width: 80px;
height: 80px;
border-radius: 50%;
border: none;
background: linear-gradient(135deg, var(--accent-red), #DC2626);
color: white;
font-size: 2rem;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 8px 20px rgba(239, 68, 68, 0.3);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.voice-button:hover {
transform: scale(1.05);
box-shadow: 0 10px 25px rgba(239, 68, 68, 0.4);
}
.voice-button:active {
transform: scale(0.95);
}
.voice-button.recording {
background: linear-gradient(135deg, #DC2626, #B91C1C);
animation: recordingPulse 1.5s infinite;
}
@keyframes recordingPulse {
0%,
100% {
box-shadow: 0 8px 20px rgba(239, 68, 68, 0.3),
0 0 0 0 rgba(239, 68, 68, 0.7);
}
50% {
box-shadow: 0 10px 25px rgba(239, 68, 68, 0.4),
0 0 0 20px rgba(239, 68, 68, 0);
}
}
.voice-status {
text-align: center;
}
.voice-status-text {
color: var(--text-dark);
font-size: 0.9375rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.voice-status-hint {
color: var(--text-light);
font-size: 0.8125rem;
}
/* ==================== RESPONSIVE ==================== */
@media (max-width: 480px) {
.app-container {
height: 100vh;
max-height: 100vh;
border-radius: 0;
}
.app-header {
border-radius: 0;
}
.voice-button {
width: 70px;
height: 70px;
font-size: 1.75rem;
}
}
@media (max-height: 700px) {
.hint-box {
display: none;
}
}
</style>
</head>
<body>
<div class="app-container">
<!-- Header -->
<div class="app-header">
<div class="header-left">
<div class="header-title">
<i class="bi bi-heart-pulse-fill"></i>
<span id="header-main-title">HELP.ME</span>
</div>
<div class="header-subtitle" id="header-subtitle">Tez Tibbiy Yordam</div>
</div>
<a href="/dispatcher" class="dispatcher-link" title="Admin Panel">
<i class="bi bi-gear-fill"></i>
</a>
</div>
<!-- Language Selector -->
<div class="language-selector">
<span class="language-label" id="lang-label">Interface:</span>
<div class="language-buttons">
<button class="lang-btn active" data-lang="uz" onclick="changeLanguage('uz')">
πŸ‡ΊπŸ‡Ώ O'zbek
</button>
<button class="lang-btn" data-lang="en" onclick="changeLanguage('en')">
πŸ‡¬πŸ‡§ English
</button>
<button class="lang-btn" data-lang="ru" onclick="changeLanguage('ru')">
πŸ‡·πŸ‡Ί Русский
</button>
</div>
</div>
<!-- Connection Status -->
<div id="connection-status" class="connection-status disconnected">
<span class="status-dot disconnected"></span>
<span id="status-text">Serverga ulanmoqda...</span>
</div>
<!-- Chat Container -->
<div id="chat-container" class="chat-container">
<!-- Typing Indicator -->
<div id="typing-indicator" class="typing-indicator">
<div class="message-avatar"
style="background: linear-gradient(135deg, var(--primary-blue), var(--primary-green)); color: white;">
<i class="bi bi-stethoscope"></i>
</div>
<div class="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
<!-- Hint Box -->
<div class="hint-box">
<i class="bi bi-info-circle-fill hint-icon"></i>
<div class="hint-text" id="hint-text">
<strong>3 tilda</strong> gaplashishingiz mumkin: O'zbekcha, Inglizcha yoki Ruscha
</div>
</div>
<!-- Voice Control -->
<div class="voice-control">
<button id="record-button" class="voice-button" onclick="toggleRecording()">
<i class="bi bi-mic-fill"></i>
</button>
<div class="voice-status">
<div class="voice-status-text" id="voice-status-text">Gapirish uchun bosing</div>
<div class="voice-status-hint" id="voice-status-hint">Tugmani bosib, muammoingizni ayting</div>
</div>
</div>
</div>
<script>
// ==================== GLOBAL STATE ====================
let ws = null;
let caseId = null;
let mediaRecorder = null;
let audioChunks = [];
let isRecording = false;
let currentLanguage = 'uz';
const translations = {
uz: {
headerTitle: 'HELP.ME',
headerSubtitle: 'Tez Tibbiy Yordam',
langLabel: 'Interface:',
statusConnected: 'Ulangan',
statusDisconnected: 'Serverga ulanmoqda...',
hintText: '<strong>3 tilda</strong> gaplashishingiz mumkin: O\'zbekcha, Inglizcha yoki Ruscha',
voiceStart: 'Gapirish uchun bosing',
voiceRecording: 'Yozilmoqda... To\'xtatish uchun bosing',
voiceHint: 'Tugmani bosib, muammoingizni ayting'
},
en: {
headerTitle: 'HELP.ME',
headerSubtitle: 'Emergency Medical Service',
langLabel: 'Interface:',
statusConnected: 'Connected',
statusDisconnected: 'Connecting to server...',
hintText: '<strong>3 languages</strong> supported: Uzbek, English or Russian',
voiceStart: 'Press to speak',
voiceRecording: 'Recording... Press to stop',
voiceHint: 'Press button and describe your problem'
},
ru: {
headerTitle: 'HELP.ME',
headerSubtitle: 'Бкорая ΠœΠ΅Π΄ΠΈΡ†ΠΈΠ½ΡΠΊΠ°Ρ ΠŸΠΎΠΌΠΎΡ‰ΡŒ',
langLabel: 'Π˜Π½Ρ‚Π΅Ρ€Ρ„Π΅ΠΉΡ:',
statusConnected: 'ΠŸΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΎ',
statusDisconnected: 'ΠŸΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΊ сСрвСру...',
hintText: '<strong>3 языка</strong> ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°ΡŽΡ‚ΡΡ: УзбСкский, Английский ΠΈΠ»ΠΈ Русский',
voiceStart: 'НаТмитС Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π³ΠΎΠ²ΠΎΡ€ΠΈΡ‚ΡŒ',
voiceRecording: 'Π—Π°ΠΏΠΈΡΡŒ... НаТмитС для остановки',
voiceHint: 'НаТмитС ΠΊΠ½ΠΎΠΏΠΊΡƒ ΠΈ ΠΎΠΏΠΈΡˆΠΈΡ‚Π΅ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡƒ'
}
};
// ==================== DOM ELEMENTS ====================
const chatContainer = document.getElementById('chat-container');
const typingIndicator = document.getElementById('typing-indicator');
const recordButton = document.getElementById('record-button');
const connectionStatus = document.getElementById('connection-status');
// ==================== INITIALIZATION ====================
window.addEventListener('DOMContentLoaded', function () {
console.log('πŸš€ Help.me interface loaded');
connectWebSocket();
updateClock();
setInterval(updateClock, 60000);
});
// ==================== LANGUAGE SWITCHING ====================
function changeLanguage(lang) {
currentLanguage = lang;
// Update buttons
document.querySelectorAll('.lang-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-lang="${lang}"]`).classList.add('active');
// Update UI text
const t = translations[lang];
document.getElementById('header-main-title').textContent = t.headerTitle;
document.getElementById('header-subtitle').textContent = t.headerSubtitle;
document.getElementById('lang-label').textContent = t.langLabel;
document.getElementById('hint-text').innerHTML = t.hintText;
document.getElementById('voice-status-text').textContent = t.voiceStart;
document.getElementById('voice-status-hint').textContent = t.voiceHint;
// Update status if needed
if (ws && ws.readyState === WebSocket.OPEN) {
document.getElementById('status-text').textContent = t.statusConnected;
} else {
document.getElementById('status-text').textContent = t.statusDisconnected;
}
console.log('🌐 Language changed to:', lang);
}
// ==================== WEBSOCKET ====================
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/chat`;
console.log('πŸ”Œ Connecting to WebSocket:', wsUrl);
ws = new WebSocket(wsUrl);
ws.onopen = function () {
console.log('βœ… WebSocket connected');
updateConnectionStatus(true);
};
ws.onmessage = function (event) {
if (typeof event.data === 'string') {
try {
const message = JSON.parse(event.data);
handleWebSocketMessage(message);
} catch (e) {
console.error('❌ JSON parse error:', e);
}
}
};
ws.onerror = function (error) {
console.error('❌ WebSocket error:', error);
};
ws.onclose = function () {
console.log('πŸ”Œ WebSocket disconnected');
updateConnectionStatus(false);
setTimeout(connectWebSocket, 3000);
};
}
function updateConnectionStatus(connected) {
const t = translations[currentLanguage];
const statusEl = document.getElementById('connection-status');
const statusText = document.getElementById('status-text');
const statusDot = statusEl.querySelector('.status-dot');
if (connected) {
statusEl.classList.remove('disconnected');
statusEl.classList.add('connected');
statusDot.classList.remove('disconnected');
statusDot.classList.add('connected');
statusText.textContent = t.statusConnected;
} else {
statusEl.classList.remove('connected');
statusEl.classList.add('disconnected');
statusDot.classList.remove('connected');
statusDot.classList.add('disconnected');
statusText.textContent = t.statusDisconnected;
}
}
function handleWebSocketMessage(message) {
console.log('πŸ“¨ Message received:', message.type);
switch (message.type) {
case 'welcome':
caseId = message.case_id;
console.log('βœ… Case ID:', caseId);
break;
case 'transcription':
hideTypingIndicator();
addMessage('user', message.text);
showTypingIndicator();
break;
case 'ai_response':
hideTypingIndicator();
addMessage('ai', message.text);
if (message.audio_url) {
playAudioResponse(message.audio_url);
}
break;
case 'error':
hideTypingIndicator();
console.error('❌ Server error:', message.message);
break;
default:
console.log('πŸ“¦ Unknown message type:', message.type);
}
}
// ==================== VOICE RECORDING ====================
async function toggleRecording() {
if (isRecording) {
stopRecording();
} else {
await startRecording();
}
}
async function startRecording() {
try {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('WebSocket ulanmagan. Iltimos, kuting...');
return;
}
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = function (event) {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
mediaRecorder.onstop = function () {
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
sendAudioToServer(audioBlob);
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
isRecording = true;
recordButton.classList.add('recording');
recordButton.innerHTML = '<i class="bi bi-stop-fill"></i>';
const t = translations[currentLanguage];
document.getElementById('voice-status-text').textContent = t.voiceRecording;
console.log('🎀 Recording started');
} catch (error) {
console.error('❌ Microphone error:', error);
alert('Mikrofonni ishlatib bo\'lmadi. Ruxsat bering.');
}
}
function stopRecording() {
if (mediaRecorder && isRecording) {
mediaRecorder.stop();
isRecording = false;
recordButton.classList.remove('recording');
recordButton.innerHTML = '<i class="bi bi-mic-fill"></i>';
const t = translations[currentLanguage];
document.getElementById('voice-status-text').textContent = t.voiceStart;
console.log('🎀 Recording stopped');
showTypingIndicator();
}
}
async function sendAudioToServer(audioBlob) {
try {
const arrayBuffer = await audioBlob.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
ws.send(uint8Array);
ws.send('__END__');
console.log('πŸ“€ Audio sent:', uint8Array.length, 'bytes');
} catch (error) {
console.error('❌ Error sending audio:', error);
hideTypingIndicator();
}
}
// ==================== MESSAGES ====================
function addMessage(sender, text) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.innerHTML = sender === 'ai'
? '<i class="bi bi-stethoscope"></i>'
: '<i class="bi bi-person-fill"></i>';
const content = document.createElement('div');
content.className = 'message-content';
content.textContent = text;
messageDiv.appendChild(avatar);
messageDiv.appendChild(content);
chatContainer.insertBefore(messageDiv, typingIndicator);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function showTypingIndicator() {
typingIndicator.classList.add('show');
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function hideTypingIndicator() {
typingIndicator.classList.remove('show');
}
function playAudioResponse(audioUrl) {
try {
console.log('πŸ”Š Playing audio:', audioUrl);
const audio = new Audio(audioUrl);
audio.play().catch(error => {
console.error('❌ Audio play error:', error);
});
} catch (error) {
console.error('❌ playAudioResponse error:', error);
}
}
// ==================== UTILITIES ====================
function updateClock() {
// Optional: Add clock if needed
}
</script>
</body>
</html>