| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const Chat = { |
| |
| isLoading: false, |
| messages: [], |
| |
| |
| recordingTimer: null, |
| recordingSeconds: 0, |
| |
| |
| messageIdCounter: 0, |
| |
| |
| elements: {}, |
| |
| |
| |
| |
| init() { |
| this.cacheElements(); |
| this.bindEvents(); |
| this.initVoiceInput(); |
| this.initTTS(); |
| this.createTTSPlayer(); |
| console.log('[Chat] Initialized'); |
| }, |
| |
| |
| |
| |
| cacheElements() { |
| this.elements = { |
| |
| btnBack: document.getElementById('btn-back-diagnosis'), |
| btnLanguage: document.getElementById('btn-chat-language'), |
| chatLangDisplay: document.getElementById('chat-lang-display'), |
| |
| |
| contextBanner: document.getElementById('chat-context-banner'), |
| contextDiseaseName: document.getElementById('context-disease-name'), |
| contextConfidence: document.getElementById('context-confidence'), |
| contextSeverity: document.getElementById('context-severity'), |
| |
| |
| messagesContainer: document.getElementById('chat-messages'), |
| chatWelcome: document.getElementById('chat-welcome'), |
| |
| |
| chatInput: document.getElementById('chat-input'), |
| btnVoice: document.getElementById('btn-voice-input'), |
| btnSend: document.getElementById('btn-send-message'), |
| chatInputBox: document.querySelector('.chat-input-box'), |
| |
| |
| voiceOverlay: document.getElementById('voice-overlay'), |
| btnStopVoice: document.getElementById('btn-stop-voice') |
| }; |
| }, |
| |
| |
| |
| |
| bindEvents() { |
| const { btnBack, chatInput, btnVoice, btnSend, btnStopVoice } = this.elements; |
| |
| |
| btnBack?.addEventListener('click', () => App.navigateToDiagnosis()); |
| |
| |
| chatInput?.addEventListener('input', () => this.handleInputChange()); |
| chatInput?.addEventListener('keydown', (e) => this.handleKeyDown(e)); |
| |
| |
| btnSend?.addEventListener('click', () => this.sendMessage()); |
| |
| |
| btnVoice?.addEventListener('click', () => this.toggleVoiceRecording()); |
| btnStopVoice?.addEventListener('click', () => this.stopVoiceRecording()); |
| |
| |
| chatInput?.addEventListener('input', () => this.autoResizeInput()); |
| }, |
| |
| |
| |
| |
| initVoiceInput() { |
| VoiceInput.init({ |
| onTranscription: (text, result) => { |
| this.handleVoiceTranscription(text, result); |
| }, |
| onError: (error) => { |
| App.showToast(error, 'error'); |
| this.hideListeningIndicator(); |
| }, |
| onRecordingStart: () => { |
| this.showListeningIndicator(); |
| }, |
| onRecordingStop: () => { |
| this.hideListeningIndicator(); |
| } |
| }); |
| }, |
| |
| |
| |
| |
| initTTS() { |
| TTS.init({ |
| onPlayStart: () => { |
| console.log('[Chat] TTS playback started'); |
| }, |
| onPlayEnd: () => { |
| console.log('[Chat] TTS playback ended'); |
| this.updateListenButtons(); |
| }, |
| onError: (error) => { |
| App.showToast(error, 'error'); |
| this.updateListenButtons(); |
| } |
| }); |
| }, |
| |
| |
| |
| |
| createTTSPlayer() { |
| |
| if (document.getElementById('tts-player')) return; |
| |
| const player = document.createElement('div'); |
| player.id = 'tts-player'; |
| player.className = 'tts-player'; |
| player.innerHTML = ` |
| <div class="tts-player-header"> |
| <div class="tts-player-title"> |
| <span class="tts-player-title-icon">🔊</span> |
| <span>Now Playing</span> |
| </div> |
| <button class="btn-tts-close" id="tts-close" title="Close">×</button> |
| </div> |
| <div class="tts-progress-container" id="tts-progress-container"> |
| <div class="tts-progress-bar" id="tts-progress"></div> |
| </div> |
| <div class="tts-controls"> |
| <div class="tts-playback-controls"> |
| <button class="btn-tts-control" id="tts-play-pause" title="Play/Pause"> |
| <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20"> |
| <polygon points="5,3 19,12 5,21"/> |
| </svg> |
| </button> |
| <button class="btn-tts-control stop" id="tts-stop" title="Stop"> |
| <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"> |
| <rect x="6" y="6" width="12" height="12" rx="2"/> |
| </svg> |
| </button> |
| <span class="tts-time" id="tts-time">0:00</span> |
| </div> |
| <div class="tts-speed-controls"> |
| <span class="tts-speed-label">Speed:</span> |
| <button class="tts-speed-btn" data-rate="0.75">0.75x</button> |
| <button class="tts-speed-btn active" data-rate="1">1x</button> |
| <button class="tts-speed-btn" data-rate="1.25">1.25x</button> |
| <button class="tts-speed-btn" data-rate="1.5">1.5x</button> |
| </div> |
| </div> |
| `; |
| |
| document.body.appendChild(player); |
| |
| |
| document.getElementById('tts-close')?.addEventListener('click', () => { |
| TTS.stop(); |
| }); |
| |
| document.getElementById('tts-play-pause')?.addEventListener('click', () => { |
| TTS.togglePlayPause(); |
| }); |
| |
| document.getElementById('tts-stop')?.addEventListener('click', () => { |
| TTS.stop(); |
| }); |
| |
| |
| document.querySelectorAll('.tts-speed-btn').forEach(btn => { |
| btn.addEventListener('click', () => { |
| const rate = parseFloat(btn.dataset.rate); |
| TTS.setPlaybackRate(rate); |
| }); |
| }); |
| |
| console.log('[Chat] TTS player created'); |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| async onPageEnter() { |
| console.log('[Chat] Page entered'); |
| |
| |
| this.updateContextBanner(); |
| |
| |
| this.updateLanguageDisplay(); |
| |
| |
| await this.loadChat(); |
| |
| |
| this.elements.chatInput?.focus(); |
| }, |
| |
| |
| |
| |
| onPageLeave() { |
| console.log('[Chat] Page left'); |
| |
| if (VoiceInput.getIsRecording()) { |
| VoiceInput.cancelRecording(); |
| this.hideListeningIndicator(); |
| } |
| }, |
| |
| |
| |
| |
| updateContextBanner() { |
| const diagnosis = Diagnosis.getDiagnosis(); |
| |
| if (!diagnosis) { |
| this.elements.contextBanner?.classList.add('hidden'); |
| return; |
| } |
| |
| this.elements.contextBanner?.classList.remove('hidden'); |
| |
| const { detection } = diagnosis; |
| this.elements.contextDiseaseName.textContent = detection.disease_name || 'Unknown'; |
| this.elements.contextConfidence.textContent = `${Math.round(detection.confidence_percent || 0)}%`; |
| this.elements.contextSeverity.textContent = I18n.getSeverity(detection.severity_level || 'unknown'); |
| }, |
| |
| |
| |
| |
| updateLanguageDisplay() { |
| const lang = I18n.getLanguage(); |
| if (this.elements.chatLangDisplay) { |
| this.elements.chatLangDisplay.textContent = lang.toUpperCase(); |
| } |
| }, |
| |
| |
| |
| |
| async loadChat() { |
| |
| this.clearMessages(); |
| |
| try { |
| |
| const history = await FarmEyesAPI.getChatHistory(); |
| |
| if (history.success && history.messages && history.messages.length > 0) { |
| |
| this.messages = history.messages; |
| this.displayMessages(history.messages); |
| } else { |
| |
| const welcome = await FarmEyesAPI.getChatWelcome(I18n.getLanguage()); |
| |
| if (welcome.success && welcome.response) { |
| this.addMessage('assistant', welcome.response); |
| } else { |
| |
| this.showWelcome(); |
| } |
| } |
| } catch (error) { |
| console.error('[Chat] Load failed:', error); |
| this.showWelcome(); |
| } |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| handleInputChange() { |
| const text = this.elements.chatInput?.value?.trim(); |
| this.elements.btnSend.disabled = !text || this.isLoading; |
| }, |
| |
| |
| |
| |
| |
| handleKeyDown(event) { |
| |
| if (event.key === 'Enter' && !event.shiftKey) { |
| event.preventDefault(); |
| this.sendMessage(); |
| } |
| }, |
| |
| |
| |
| |
| autoResizeInput() { |
| const input = this.elements.chatInput; |
| if (!input) return; |
| |
| input.style.height = 'auto'; |
| const newHeight = Math.min(input.scrollHeight, 150); |
| input.style.height = `${newHeight}px`; |
| }, |
| |
| |
| |
| |
| async sendMessage() { |
| const input = this.elements.chatInput; |
| const message = input?.value?.trim(); |
| |
| if (!message || this.isLoading) return; |
| |
| |
| input.value = ''; |
| this.autoResizeInput(); |
| this.handleInputChange(); |
| |
| |
| this.addMessage('user', message); |
| |
| |
| this.isLoading = true; |
| this.showTypingIndicator(); |
| |
| try { |
| const response = await FarmEyesAPI.sendChatMessage(message, I18n.getLanguage()); |
| |
| if (response.success) { |
| this.addMessage('assistant', response.response); |
| } else { |
| throw new Error(response.error || 'Failed to get response'); |
| } |
| } catch (error) { |
| console.error('[Chat] Send failed:', error); |
| this.addMessage('assistant', 'Sorry, I encountered an error. Please try again.'); |
| App.showToast(error.message, 'error'); |
| } finally { |
| this.isLoading = false; |
| this.hideTypingIndicator(); |
| this.handleInputChange(); |
| } |
| }, |
| |
| |
| |
| |
| |
| |
| addMessage(role, content) { |
| |
| this.elements.chatWelcome?.classList.add('hidden'); |
| |
| |
| const messageEl = this.createMessageElement(role, content); |
| |
| |
| this.elements.messagesContainer?.appendChild(messageEl); |
| |
| |
| this.messages.push({ role, content, timestamp: new Date().toISOString() }); |
| |
| |
| this.scrollToBottom(); |
| }, |
| |
| |
| |
| |
| |
| |
| |
| createMessageElement(role, content) { |
| const div = document.createElement('div'); |
| div.className = `message ${role}`; |
| |
| |
| const messageId = `msg_${++this.messageIdCounter}_${Date.now()}`; |
| div.dataset.messageId = messageId; |
| |
| const avatar = document.createElement('div'); |
| avatar.className = 'message-avatar'; |
| avatar.textContent = role === 'user' ? '👤' : '🌱'; |
| |
| const contentWrapper = document.createElement('div'); |
| contentWrapper.className = 'message-content-wrapper'; |
| |
| const contentDiv = document.createElement('div'); |
| contentDiv.className = 'message-content'; |
| contentDiv.textContent = content; |
| |
| contentWrapper.appendChild(contentDiv); |
| |
| |
| if (role === 'assistant') { |
| const listenBtn = document.createElement('button'); |
| listenBtn.className = 'btn-listen'; |
| listenBtn.dataset.messageId = messageId; |
| listenBtn.dataset.text = content; |
| listenBtn.innerHTML = ` |
| <span class="btn-listen-icon">🔊</span> |
| <span class="btn-listen-text">Listen</span> |
| `; |
| listenBtn.title = 'Listen to this message'; |
| listenBtn.addEventListener('click', () => this.handleListenClick(listenBtn, content, messageId)); |
| |
| contentWrapper.appendChild(listenBtn); |
| } |
| |
| div.appendChild(avatar); |
| div.appendChild(contentWrapper); |
| |
| return div; |
| }, |
| |
| |
| |
| |
| |
| |
| |
| async handleListenClick(button, text, messageId) { |
| |
| if (TTS.currentMessageId === messageId) { |
| if (TTS.getIsPlaying()) { |
| TTS.pause(); |
| button.innerHTML = `<span class="btn-listen-icon">▶️</span><span class="btn-listen-text">Resume</span>`; |
| } else if (TTS.getIsPaused()) { |
| TTS.resume(); |
| button.innerHTML = `<span class="btn-listen-icon">⏸️</span><span class="btn-listen-text">Pause</span>`; |
| } |
| return; |
| } |
| |
| |
| this.updateListenButtons(); |
| |
| |
| button.classList.add('loading'); |
| button.innerHTML = `<span class="btn-listen-icon">🔊</span><span class="btn-listen-text">Loading...</span>`; |
| |
| |
| const language = I18n.getLanguage(); |
| |
| |
| const success = await TTS.speak(text, language, messageId); |
| |
| |
| button.classList.remove('loading'); |
| |
| if (success) { |
| button.classList.add('playing'); |
| button.innerHTML = `<span class="btn-listen-icon">⏸️</span><span class="btn-listen-text">Pause</span>`; |
| } else { |
| button.innerHTML = `<span class="btn-listen-icon">🔊</span><span class="btn-listen-text">Listen</span>`; |
| } |
| }, |
| |
| |
| |
| |
| updateListenButtons() { |
| document.querySelectorAll('.btn-listen').forEach(btn => { |
| btn.classList.remove('loading', 'playing'); |
| btn.innerHTML = `<span class="btn-listen-icon">🔊</span><span class="btn-listen-text">Listen</span>`; |
| }); |
| }, |
| |
| |
| |
| |
| |
| displayMessages(messages) { |
| this.elements.chatWelcome?.classList.add('hidden'); |
| |
| messages.forEach(msg => { |
| const messageEl = this.createMessageElement(msg.role, msg.content); |
| this.elements.messagesContainer?.appendChild(messageEl); |
| }); |
| |
| this.scrollToBottom(); |
| }, |
| |
| |
| |
| |
| clearMessages() { |
| if (this.elements.messagesContainer) { |
| this.elements.messagesContainer.innerHTML = ''; |
| |
| const welcome = document.createElement('div'); |
| welcome.id = 'chat-welcome'; |
| welcome.className = 'chat-welcome'; |
| welcome.innerHTML = ` |
| <div class="welcome-icon">🌱</div> |
| <p class="welcome-text">Start a conversation about your diagnosis</p> |
| `; |
| this.elements.messagesContainer.appendChild(welcome); |
| this.elements.chatWelcome = welcome; |
| } |
| this.messages = []; |
| }, |
| |
| |
| |
| |
| showWelcome() { |
| this.elements.chatWelcome?.classList.remove('hidden'); |
| }, |
| |
| |
| |
| |
| showTypingIndicator() { |
| |
| this.hideTypingIndicator(); |
| |
| const indicator = document.createElement('div'); |
| indicator.className = 'message assistant typing-message'; |
| indicator.innerHTML = ` |
| <div class="message-avatar">🌱</div> |
| <div class="message-content"> |
| <div class="typing-indicator"> |
| <div class="typing-dot"></div> |
| <div class="typing-dot"></div> |
| <div class="typing-dot"></div> |
| </div> |
| </div> |
| `; |
| |
| this.elements.messagesContainer?.appendChild(indicator); |
| this.scrollToBottom(); |
| }, |
| |
| |
| |
| |
| hideTypingIndicator() { |
| const indicator = this.elements.messagesContainer?.querySelector('.typing-message'); |
| indicator?.remove(); |
| }, |
| |
| |
| |
| |
| scrollToBottom() { |
| const container = this.elements.messagesContainer; |
| if (container) { |
| container.scrollTop = container.scrollHeight; |
| } |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| async toggleVoiceRecording() { |
| if (!VoiceInput.isSupported()) { |
| App.showToast('Voice input is not supported in this browser', 'error'); |
| return; |
| } |
| |
| if (VoiceInput.getIsRecording()) { |
| this.stopVoiceRecording(); |
| } else { |
| await this.startVoiceRecording(); |
| } |
| }, |
| |
| |
| |
| |
| async startVoiceRecording() { |
| const started = await VoiceInput.startRecording(); |
| |
| if (!started) { |
| |
| return; |
| } |
| }, |
| |
| |
| |
| |
| stopVoiceRecording() { |
| VoiceInput.stopRecording(); |
| }, |
| |
| |
| |
| |
| |
| |
| handleVoiceTranscription(text, result) { |
| if (!text) { |
| App.showToast('Could not understand audio. Please try again.', 'warning'); |
| return; |
| } |
| |
| |
| if (this.elements.chatInput) { |
| this.elements.chatInput.value = text; |
| this.autoResizeInput(); |
| this.handleInputChange(); |
| |
| |
| |
| } |
| |
| |
| if (result.language) { |
| console.log('[Chat] Detected language:', result.language); |
| } |
| }, |
| |
| |
| |
| |
| |
| showListeningIndicator() { |
| const inputBox = this.elements.chatInputBox; |
| const textarea = this.elements.chatInput; |
| const btnVoice = this.elements.btnVoice; |
| const btnSend = this.elements.btnSend; |
| |
| if (!inputBox) return; |
| |
| |
| textarea?.classList.add('hidden'); |
| btnSend?.classList.add('hidden'); |
| |
| |
| btnVoice?.classList.add('recording'); |
| |
| |
| const listeningIndicator = document.createElement('div'); |
| listeningIndicator.id = 'listening-indicator'; |
| listeningIndicator.className = 'listening-indicator'; |
| listeningIndicator.innerHTML = ` |
| <div class="listening-pulse"></div> |
| <span class="listening-text">Listening...</span> |
| <span class="listening-timer">0:00</span> |
| <button class="btn-stop-inline" title="Stop Recording"> |
| <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"> |
| <rect x="6" y="6" width="12" height="12" rx="2"/> |
| </svg> |
| </button> |
| `; |
| |
| |
| inputBox.insertBefore(listeningIndicator, btnVoice); |
| |
| |
| const btnStop = listeningIndicator.querySelector('.btn-stop-inline'); |
| btnStop?.addEventListener('click', () => this.stopVoiceRecording()); |
| |
| |
| this.recordingSeconds = 0; |
| this.updateRecordingTimer(); |
| this.recordingTimer = setInterval(() => { |
| this.recordingSeconds++; |
| this.updateRecordingTimer(); |
| }, 1000); |
| |
| console.log('[Chat] Listening indicator shown'); |
| }, |
| |
| |
| |
| |
| hideListeningIndicator() { |
| const inputBox = this.elements.chatInputBox; |
| const textarea = this.elements.chatInput; |
| const btnVoice = this.elements.btnVoice; |
| const btnSend = this.elements.btnSend; |
| |
| |
| const indicator = document.getElementById('listening-indicator'); |
| indicator?.remove(); |
| |
| |
| textarea?.classList.remove('hidden'); |
| btnSend?.classList.remove('hidden'); |
| |
| |
| btnVoice?.classList.remove('recording'); |
| |
| |
| if (this.recordingTimer) { |
| clearInterval(this.recordingTimer); |
| this.recordingTimer = null; |
| } |
| this.recordingSeconds = 0; |
| |
| console.log('[Chat] Listening indicator hidden'); |
| }, |
| |
| |
| |
| |
| updateRecordingTimer() { |
| const timerEl = document.querySelector('.listening-timer'); |
| if (timerEl) { |
| const minutes = Math.floor(this.recordingSeconds / 60); |
| const seconds = this.recordingSeconds % 60; |
| timerEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; |
| } |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| showVoiceOverlay() { |
| |
| this.showListeningIndicator(); |
| }, |
| |
| |
| |
| |
| hideVoiceOverlay() { |
| |
| this.hideListeningIndicator(); |
| } |
| }; |
|
|
| |
| window.Chat = Chat; |
|
|