|
|
|
|
|
|
|
|
|
Prism.plugins.autoloader.languages_path = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/'; |
|
|
|
|
|
const uiElements = { |
|
chatArea: document.getElementById('chatArea'), |
|
chatBox: document.getElementById('chatBox'), |
|
initialContent: document.getElementById('initialContent'), |
|
form: document.getElementById('footerForm'), |
|
input: document.getElementById('userInput'), |
|
sendBtn: document.getElementById('sendBtn'), |
|
stopBtn: document.getElementById('stopBtn'), |
|
fileBtn: document.getElementById('fileBtn'), |
|
audioBtn: document.getElementById('audioBtn'), |
|
fileInput: document.getElementById('fileInput'), |
|
audioInput: document.getElementById('audioInput'), |
|
filePreview: document.getElementById('filePreview'), |
|
audioPreview: document.getElementById('audioPreview'), |
|
promptItems: document.querySelectorAll('.prompt-item'), |
|
chatHeader: document.getElementById('chatHeader'), |
|
clearBtn: document.getElementById('clearBtn'), |
|
messageLimitWarning: document.getElementById('messageLimitWarning'), |
|
conversationTitle: document.getElementById('conversationTitle'), |
|
sidebar: document.getElementById('sidebar'), |
|
sidebarToggle: document.getElementById('sidebarToggle'), |
|
conversationList: document.getElementById('conversationList'), |
|
newConversationBtn: document.getElementById('newConversationBtn'), |
|
swipeHint: document.getElementById('swipeHint'), |
|
settingsBtn: document.getElementById('settingsBtn'), |
|
settingsModal: document.getElementById('settingsModal'), |
|
closeSettingsBtn: document.getElementById('closeSettingsBtn'), |
|
settingsForm: document.getElementById('settingsForm'), |
|
historyToggle: document.getElementById('historyToggle') |
|
}; |
|
|
|
|
|
let streamMsg = null; |
|
let conversationHistory = JSON.parse(sessionStorage.getItem('conversationHistory') || '[]'); |
|
let currentAssistantText = ''; |
|
let isRequestActive = false; |
|
let abortController = null; |
|
let mediaRecorder = null; |
|
let audioChunks = []; |
|
let isRecording = false; |
|
let currentConversationId = window.conversationId || null; |
|
let currentConversationTitle = window.conversationTitle || null; |
|
let isSidebarOpen = false; |
|
|
|
|
|
function autoResizeTextarea() { |
|
if (uiElements.input) { |
|
uiElements.input.style.height = 'auto'; |
|
uiElements.input.style.height = `${Math.min(uiElements.input.scrollHeight, 200)}px`; |
|
} |
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => { |
|
AOS.init({ |
|
duration: 800, |
|
easing: 'ease-out-cubic', |
|
once: true, |
|
offset: 50, |
|
}); |
|
if (currentConversationId && checkAuth()) { |
|
await loadConversation(currentConversationId); |
|
} else if (conversationHistory.length > 0) { |
|
enterChatView(); |
|
conversationHistory.forEach(msg => addMsg(msg.role, msg.content)); |
|
} |
|
if (checkAuth()) { |
|
await loadConversations(); |
|
} |
|
autoResizeTextarea(); |
|
updateSendButtonState(); |
|
if (uiElements.swipeHint) { |
|
setTimeout(() => { |
|
uiElements.swipeHint.style.display = 'none'; |
|
}, 3000); |
|
} |
|
setupTouchGestures(); |
|
}); |
|
|
|
|
|
function checkAuth() { |
|
return localStorage.getItem('token'); |
|
} |
|
|
|
|
|
function updateSendButtonState() { |
|
if (uiElements.sendBtn && uiElements.input && uiElements.fileInput && uiElements.audioInput) { |
|
uiElements.sendBtn.disabled = uiElements.input.value.trim() === '' && |
|
uiElements.fileInput.files.length === 0 && |
|
uiElements.audioInput.files.length === 0; |
|
} |
|
} |
|
|
|
|
|
function renderMarkdown(el) { |
|
const raw = el.dataset.text || ''; |
|
const html = marked.parse(raw, { |
|
gfm: true, |
|
breaks: true, |
|
smartLists: true, |
|
smartypants: false, |
|
headerIds: false, |
|
}); |
|
el.innerHTML = `<div class="md-content">${html}</div>`; |
|
const wrapper = el.querySelector('.md-content'); |
|
wrapper.querySelectorAll('table').forEach(t => { |
|
if (!t.parentNode.classList?.contains('table-wrapper')) { |
|
const div = document.createElement('div'); |
|
div.className = 'table-wrapper'; |
|
t.parentNode.insertBefore(div, t); |
|
div.appendChild(t); |
|
} |
|
}); |
|
wrapper.querySelectorAll('hr').forEach(h => h.classList.add('styled-hr')); |
|
Prism.highlightAllUnder(wrapper); |
|
} |
|
|
|
|
|
function enterChatView() { |
|
if (uiElements.chatHeader) { |
|
uiElements.chatHeader.classList.remove('hidden'); |
|
uiElements.chatHeader.setAttribute('aria-hidden', 'false'); |
|
if (currentConversationTitle && uiElements.conversationTitle) { |
|
uiElements.conversationTitle.textContent = currentConversationTitle; |
|
} |
|
} |
|
if (uiElements.chatBox) uiElements.chatBox.classList.remove('hidden'); |
|
if (uiElements.initialContent) uiElements.initialContent.classList.add('hidden'); |
|
} |
|
|
|
|
|
function leaveChatView() { |
|
if (uiElements.chatHeader) { |
|
uiElements.chatHeader.classList.add('hidden'); |
|
uiElements.chatHeader.setAttribute('aria-hidden', 'true'); |
|
} |
|
if (uiElements.chatBox) uiElements.chatBox.classList.add('hidden'); |
|
if (uiElements.initialContent) uiElements.initialContent.classList.remove('hidden'); |
|
} |
|
|
|
|
|
function addMsg(who, text) { |
|
const div = document.createElement('div'); |
|
div.className = `bubble ${who === 'user' ? 'bubble-user' : 'bubble-assist'}`; |
|
div.dataset.text = text; |
|
renderMarkdown(div); |
|
if (uiElements.chatBox) { |
|
uiElements.chatBox.appendChild(div); |
|
uiElements.chatBox.classList.remove('hidden'); |
|
uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight; |
|
} |
|
return div; |
|
} |
|
|
|
|
|
function clearAllMessages() { |
|
stopStream(true); |
|
conversationHistory = []; |
|
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
|
currentAssistantText = ''; |
|
if (streamMsg) { |
|
streamMsg.querySelector('.loading')?.remove(); |
|
streamMsg = null; |
|
} |
|
if (uiElements.chatBox) uiElements.chatBox.innerHTML = ''; |
|
if (uiElements.input) uiElements.input.value = ''; |
|
if (uiElements.sendBtn) uiElements.sendBtn.disabled = true; |
|
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none'; |
|
if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex'; |
|
if (uiElements.filePreview) uiElements.filePreview.style.display = 'none'; |
|
if (uiElements.audioPreview) uiElements.audioPreview.style.display = 'none'; |
|
if (uiElements.messageLimitWarning) uiElements.messageLimitWarning.classList.add('hidden'); |
|
currentConversationId = null; |
|
currentConversationTitle = null; |
|
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = 'MGZon AI Assistant'; |
|
enterChatView(); |
|
autoResizeTextarea(); |
|
} |
|
|
|
|
|
function previewFile() { |
|
if (uiElements.fileInput?.files.length > 0) { |
|
const file = uiElements.fileInput.files[0]; |
|
if (file.type.startsWith('image/')) { |
|
const reader = new FileReader(); |
|
reader.onload = e => { |
|
if (uiElements.filePreview) { |
|
uiElements.filePreview.innerHTML = `<img src="${e.target.result}" class="upload-preview">`; |
|
uiElements.filePreview.style.display = 'block'; |
|
} |
|
if (uiElements.audioPreview) uiElements.audioPreview.style.display = 'none'; |
|
updateSendButtonState(); |
|
}; |
|
reader.readAsDataURL(file); |
|
} |
|
} |
|
if (uiElements.audioInput?.files.length > 0) { |
|
const file = uiElements.audioInput.files[0]; |
|
if (file.type.startsWith('audio/')) { |
|
const reader = new FileReader(); |
|
reader.onload = e => { |
|
if (uiElements.audioPreview) { |
|
uiElements.audioPreview.innerHTML = `<audio controls src="${e.target.result}"></audio>`; |
|
uiElements.audioPreview.style.display = 'block'; |
|
} |
|
if (uiElements.filePreview) uiElements.filePreview.style.display = 'none'; |
|
updateSendButtonState(); |
|
}; |
|
reader.readAsDataURL(file); |
|
} |
|
} |
|
} |
|
|
|
|
|
function startVoiceRecording() { |
|
if (isRequestActive || isRecording) return; |
|
isRecording = true; |
|
if (uiElements.sendBtn) uiElements.sendBtn.classList.add('recording'); |
|
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { |
|
mediaRecorder = new MediaRecorder(stream); |
|
audioChunks = []; |
|
mediaRecorder.start(); |
|
mediaRecorder.addEventListener('dataavailable', event => audioChunks.push(event.data)); |
|
}).catch(err => { |
|
console.error('Error accessing microphone:', err); |
|
alert('Failed to access microphone. Please check permissions.'); |
|
isRecording = false; |
|
if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording'); |
|
}); |
|
} |
|
|
|
function stopVoiceRecording() { |
|
if (mediaRecorder?.state === 'recording') { |
|
mediaRecorder.stop(); |
|
if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording'); |
|
isRecording = false; |
|
mediaRecorder.addEventListener('stop', async () => { |
|
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); |
|
const formData = new FormData(); |
|
formData.append('file', audioBlob, 'voice-message.webm'); |
|
await submitAudioMessage(formData); |
|
}); |
|
} |
|
} |
|
|
|
|
|
async function submitAudioMessage(formData) { |
|
enterChatView(); |
|
addMsg('user', 'Voice message'); |
|
conversationHistory.push({ role: 'user', content: 'Voice message' }); |
|
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
|
streamMsg = addMsg('assistant', ''); |
|
const loadingEl = document.createElement('span'); |
|
loadingEl.className = 'loading'; |
|
streamMsg.appendChild(loadingEl); |
|
updateUIForRequest(); |
|
|
|
isRequestActive = true; |
|
abortController = new AbortController(); |
|
|
|
try { |
|
const response = await sendRequest('/api/audio-transcription', formData); |
|
const data = await response.json(); |
|
if (!data.transcription) throw new Error('No transcription received from server'); |
|
const transcription = data.transcription || 'Error: No transcription generated.'; |
|
if (streamMsg) { |
|
streamMsg.dataset.text = transcription; |
|
renderMarkdown(streamMsg); |
|
streamMsg.dataset.done = '1'; |
|
} |
|
conversationHistory.push({ role: 'assistant', content: transcription }); |
|
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
|
if (checkAuth() && currentConversationId) { |
|
await saveMessageToConversation(currentConversationId, 'assistant', transcription); |
|
} |
|
if (checkAuth() && data.conversation_id) { |
|
currentConversationId = data.conversation_id; |
|
currentConversationTitle = data.conversation_title || 'Untitled Conversation'; |
|
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; |
|
history.pushState(null, '', `/chat/${currentConversationId}`); |
|
await loadConversations(); |
|
} |
|
finalizeRequest(); |
|
} catch (error) { |
|
handleRequestError(error); |
|
} |
|
} |
|
|
|
|
|
async function sendRequest(endpoint, body, headers = {}) { |
|
const token = checkAuth(); |
|
if (token) headers['Authorization'] = `Bearer ${token}`; |
|
return await fetch(endpoint, { |
|
method: 'POST', |
|
body, |
|
headers, |
|
signal: abortController.signal, |
|
}); |
|
} |
|
|
|
|
|
function updateUIForRequest() { |
|
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'inline-flex'; |
|
if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'none'; |
|
if (uiElements.input) uiElements.input.value = ''; |
|
if (uiElements.sendBtn) uiElements.sendBtn.disabled = true; |
|
if (uiElements.filePreview) uiElements.filePreview.style.display = 'none'; |
|
if (uiElements.audioPreview) uiElements.audioPreview.style.display = 'none'; |
|
autoResizeTextarea(); |
|
} |
|
|
|
|
|
function finalizeRequest() { |
|
streamMsg = null; |
|
isRequestActive = false; |
|
abortController = null; |
|
if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex'; |
|
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none'; |
|
} |
|
|
|
|
|
function handleRequestError(error) { |
|
if (streamMsg) { |
|
streamMsg.querySelector('.loading')?.remove(); |
|
streamMsg.dataset.text = `Error: ${error.message || 'An error occurred during the request.'}`; |
|
renderMarkdown(streamMsg); |
|
streamMsg.dataset.done = '1'; |
|
streamMsg = null; |
|
} |
|
console.error('Request error:', error); |
|
isRequestActive = false; |
|
abortController = null; |
|
if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex'; |
|
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none'; |
|
} |
|
|
|
|
|
async function loadConversations() { |
|
if (!checkAuth()) return; |
|
try { |
|
const response = await fetch('/api/conversations', { |
|
headers: { 'Authorization': `Bearer ${checkAuth()}` } |
|
}); |
|
if (!response.ok) throw new Error('Failed to load conversations'); |
|
const conversations = await response.json(); |
|
if (uiElements.conversationList) { |
|
uiElements.conversationList.innerHTML = ''; |
|
conversations.forEach(conv => { |
|
const li = document.createElement('li'); |
|
li.className = `flex items-center justify-between text-white hover:bg-gray-700 p-2 rounded cursor-pointer transition-colors ${conv.conversation_id === currentConversationId ? 'bg-gray-700' : ''}`; |
|
li.dataset.conversationId = conv.conversation_id; |
|
li.innerHTML = ` |
|
<div class="flex items-center flex-1" data-conversation-id="${conv.conversation_id}"> |
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path> |
|
</svg> |
|
<span class="truncate flex-1">${conv.title || 'Untitled Conversation'}</span> |
|
</div> |
|
<button class="delete-conversation-btn text-red-400 hover:text-red-600 p-1" title="Delete Conversation" data-conversation-id="${conv.conversation_id}"> |
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5-4h4M3 7h18"></path> |
|
</svg> |
|
</button> |
|
`; |
|
li.querySelector('[data-conversation-id]').addEventListener('click', () => loadConversation(conv.conversation_id)); |
|
li.querySelector('.delete-conversation-btn').addEventListener('click', () => deleteConversation(conv.conversation_id)); |
|
uiElements.conversationList.appendChild(li); |
|
}); |
|
} |
|
} catch (error) { |
|
console.error('Error loading conversations:', error); |
|
} |
|
} |
|
|
|
|
|
async function loadConversation(conversationId) { |
|
try { |
|
const response = await fetch(`/api/conversations/${conversationId}`, { |
|
headers: { 'Authorization': `Bearer ${checkAuth()}` } |
|
}); |
|
if (!response.ok) { |
|
if (response.status === 401) window.location.href = '/login'; |
|
throw new Error('Failed to load conversation'); |
|
} |
|
const data = await response.json(); |
|
currentConversationId = data.conversation_id; |
|
currentConversationTitle = data.title || 'Untitled Conversation'; |
|
conversationHistory = data.messages.map(msg => ({ role: msg.role, content: msg.content })); |
|
if (uiElements.chatBox) uiElements.chatBox.innerHTML = ''; |
|
conversationHistory.forEach(msg => addMsg(msg.role, msg.content)); |
|
enterChatView(); |
|
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; |
|
history.pushState(null, '', `/chat/${currentConversationId}`); |
|
toggleSidebar(false); |
|
} catch (error) { |
|
console.error('Error loading conversation:', error); |
|
alert('Failed to load conversation. Please try again or log in.'); |
|
} |
|
} |
|
|
|
|
|
async function deleteConversation(conversationId) { |
|
if (!confirm('Are you sure you want to delete this conversation?')) return; |
|
try { |
|
const response = await fetch(`/api/conversations/${conversationId}`, { |
|
method: 'DELETE', |
|
headers: { 'Authorization': `Bearer ${checkAuth()}` } |
|
}); |
|
if (!response.ok) { |
|
if (response.status === 401) window.location.href = '/login'; |
|
throw new Error('Failed to delete conversation'); |
|
} |
|
if (conversationId === currentConversationId) { |
|
clearAllMessages(); |
|
currentConversationId = null; |
|
currentConversationTitle = null; |
|
history.pushState(null, '', '/chat'); |
|
} |
|
await loadConversations(); |
|
} catch (error) { |
|
console.error('Error deleting conversation:', error); |
|
alert('Failed to delete conversation. Please try again.'); |
|
} |
|
} |
|
|
|
|
|
async function saveMessageToConversation(conversationId, role, content) { |
|
try { |
|
const response = await fetch(`/api/conversations/${conversationId}`, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'Authorization': `Bearer ${checkAuth()}` |
|
}, |
|
body: JSON.stringify({ role, content }) |
|
}); |
|
if (!response.ok) throw new Error('Failed to save message'); |
|
} catch (error) { |
|
console.error('Error saving message:', error); |
|
} |
|
} |
|
|
|
|
|
async function createNewConversation() { |
|
if (!checkAuth()) { |
|
alert('Please log in to create a new conversation.'); |
|
window.location.href = '/login'; |
|
return; |
|
} |
|
try { |
|
const response = await fetch('/api/conversations', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'Authorization': `Bearer ${checkAuth()}` |
|
}, |
|
body: JSON.stringify({ title: 'New Conversation' }) |
|
}); |
|
if (!response.ok) { |
|
if (response.status === 401) { |
|
localStorage.removeItem('token'); |
|
window.location.href = '/login'; |
|
} |
|
throw new Error('Failed to create conversation'); |
|
} |
|
const data = await response.json(); |
|
currentConversationId = data.conversation_id; |
|
currentConversationTitle = data.title; |
|
conversationHistory = []; |
|
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
|
if (uiElements.chatBox) uiElements.chatBox.innerHTML = ''; |
|
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; |
|
history.pushState(null, '', `/chat/${currentConversationId}`); |
|
enterChatView(); |
|
await loadConversations(); |
|
toggleSidebar(false); |
|
} catch (error) { |
|
console.error('Error creating conversation:', error); |
|
alert('Failed to create new conversation. Please try again.'); |
|
} |
|
} |
|
|
|
|
|
async function updateConversationTitle(conversationId, newTitle) { |
|
try { |
|
const response = await fetch(`/api/conversations/${conversationId}/title`, { |
|
method: 'PUT', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'Authorization': `Bearer ${checkAuth()}` |
|
}, |
|
body: JSON.stringify({ title: newTitle }) |
|
}); |
|
if (!response.ok) throw new Error('Failed to update title'); |
|
const data = await response.json(); |
|
currentConversationTitle = data.title; |
|
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; |
|
await loadConversations(); |
|
} catch (error) { |
|
console.error('Error updating title:', error); |
|
alert('Failed to update conversation title.'); |
|
} |
|
} |
|
|
|
|
|
function toggleSidebar(show) { |
|
if (uiElements.sidebar) { |
|
if (window.innerWidth >= 768) { |
|
isSidebarOpen = true; |
|
uiElements.sidebar.style.transform = 'translateX(0)'; |
|
if (uiElements.swipeHint) uiElements.swipeHint.style.display = 'none'; |
|
} else { |
|
isSidebarOpen = show !== undefined ? show : !isSidebarOpen; |
|
uiElements.sidebar.style.transform = isSidebarOpen ? 'translateX(0)' : 'translateX(-100%)'; |
|
if (uiElements.swipeHint && !isSidebarOpen) { |
|
uiElements.swipeHint.style.display = 'block'; |
|
setTimeout(() => { |
|
uiElements.swipeHint.style.display = 'none'; |
|
}, 3000); |
|
} else if (uiElements.swipeHint) { |
|
uiElements.swipeHint.style.display = 'none'; |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
function setupTouchGestures() { |
|
if (!uiElements.sidebar) return; |
|
const hammer = new Hammer(uiElements.sidebar); |
|
const mainContent = document.querySelector('.flex-1'); |
|
const hammerMain = new Hammer(mainContent); |
|
|
|
hammer.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL }); |
|
hammer.on('pan', e => { |
|
if (!isSidebarOpen) return; |
|
let translateX = Math.max(-uiElements.sidebar.offsetWidth, Math.min(0, e.deltaX)); |
|
uiElements.sidebar.style.transform = `translateX(${translateX}px)`; |
|
}); |
|
hammer.on('panend', e => { |
|
if (!isSidebarOpen) return; |
|
if (e.deltaX < -50) { |
|
toggleSidebar(false); |
|
} else { |
|
toggleSidebar(true); |
|
} |
|
}); |
|
|
|
hammerMain.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL }); |
|
hammerMain.on('panstart', e => { |
|
if (isSidebarOpen) return; |
|
if (e.center.x < 50 || e.center.x > window.innerWidth - 50) { |
|
uiElements.sidebar.style.transition = 'none'; |
|
} |
|
}); |
|
hammerMain.on('pan', e => { |
|
if (isSidebarOpen) return; |
|
if (e.center.x < 50 || e.center.x > window.innerWidth - 50) { |
|
let translateX = e.center.x < 50 |
|
? Math.min(uiElements.sidebar.offsetWidth, Math.max(0, e.deltaX)) |
|
: Math.max(-uiElements.sidebar.offsetWidth, Math.min(0, e.deltaX)); |
|
uiElements.sidebar.style.transform = `translateX(${translateX - uiElements.sidebar.offsetWidth}px)`; |
|
} |
|
}); |
|
hammerMain.on('panend', e => { |
|
uiElements.sidebar.style.transition = 'transform 0.3s ease-in-out'; |
|
if (e.center.x < 50 && e.deltaX > 50) { |
|
toggleSidebar(true); |
|
} else if (e.center.x > window.innerWidth - 50 && e.deltaX < -50) { |
|
toggleSidebar(true); |
|
} else { |
|
toggleSidebar(false); |
|
} |
|
}); |
|
} |
|
|
|
|
|
async function submitMessage() { |
|
if (isRequestActive || isRecording) return; |
|
let message = uiElements.input?.value.trim() || ''; |
|
let payload = null; |
|
let formData = null; |
|
let endpoint = '/api/chat'; |
|
let headers = {}; |
|
let inputType = 'text'; |
|
let outputFormat = 'text'; |
|
let title = null; |
|
|
|
if (uiElements.fileInput?.files.length > 0) { |
|
const file = uiElements.fileInput.files[0]; |
|
if (file.type.startsWith('image/')) { |
|
endpoint = '/api/image-analysis'; |
|
inputType = 'image'; |
|
message = 'Analyze this image'; |
|
formData = new FormData(); |
|
formData.append('file', file); |
|
formData.append('output_format', 'text'); |
|
} |
|
} else if (uiElements.audioInput?.files.length > 0) { |
|
const file = uiElements.audioInput.files[0]; |
|
if (file.type.startsWith('audio/')) { |
|
endpoint = '/api/audio-transcription'; |
|
inputType = 'audio'; |
|
message = 'Transcribe this audio'; |
|
formData = new FormData(); |
|
formData.append('file', file); |
|
} |
|
} else if (message) { |
|
payload = { |
|
message, |
|
system_prompt: 'You are an expert assistant providing detailed, comprehensive, and well-structured responses.', |
|
history: conversationHistory, |
|
temperature: 0.7, |
|
max_new_tokens: 128000, |
|
enable_browsing: true, |
|
output_format: 'text', |
|
title: title |
|
}; |
|
headers['Content-Type'] = 'application/json'; |
|
} else { |
|
return; |
|
} |
|
|
|
enterChatView(); |
|
addMsg('user', message); |
|
conversationHistory.push({ role: 'user', content: message }); |
|
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
|
streamMsg = addMsg('assistant', ''); |
|
const loadingEl = document.createElement('span'); |
|
loadingEl.className = 'loading'; |
|
streamMsg.appendChild(loadingEl); |
|
updateUIForRequest(); |
|
|
|
isRequestActive = true; |
|
abortController = new AbortController(); |
|
|
|
try { |
|
const response = await sendRequest(endpoint, payload ? JSON.stringify(payload) : formData, headers); |
|
if (!response.ok) { |
|
if (response.status === 403) { |
|
if (uiElements.messageLimitWarning) uiElements.messageLimitWarning.classList.remove('hidden'); |
|
if (uiElements.input) uiElements.input.disabled = true; |
|
if (streamMsg) streamMsg.querySelector('.loading')?.remove(); |
|
streamMsg = null; |
|
isRequestActive = false; |
|
abortController = null; |
|
if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex'; |
|
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none'; |
|
setTimeout(() => window.location.href = '/login', 3000); |
|
return; |
|
} |
|
if (response.status === 401) { |
|
localStorage.removeItem('token'); |
|
window.location.href = '/login'; |
|
return; |
|
} |
|
throw new Error(`Request failed with status ${response.status}`); |
|
} |
|
|
|
if (endpoint === '/api/audio-transcription') { |
|
const data = await response.json(); |
|
if (!data.transcription) throw new Error('No transcription received from server'); |
|
const transcription = data.transcription || 'Error: No transcription generated.'; |
|
if (streamMsg) { |
|
streamMsg.dataset.text = transcription; |
|
renderMarkdown(streamMsg); |
|
streamMsg.dataset.done = '1'; |
|
} |
|
conversationHistory.push({ role: 'assistant', content: transcription }); |
|
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
|
if (checkAuth() && currentConversationId) { |
|
await saveMessageToConversation(currentConversationId, 'assistant', transcription); |
|
} |
|
if (checkAuth() && data.conversation_id) { |
|
currentConversationId = data.conversation_id; |
|
currentConversationTitle = data.conversation_title || 'Untitled Conversation'; |
|
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; |
|
history.pushState(null, '', `/chat/${currentConversationId}`); |
|
await loadConversations(); |
|
} |
|
} else if (endpoint === '/api/image-analysis') { |
|
const data = await response.json(); |
|
const analysis = data.image_analysis || 'Error: No analysis generated.'; |
|
if (streamMsg) { |
|
streamMsg.dataset.text = analysis; |
|
renderMarkdown(streamMsg); |
|
streamMsg.dataset.done = '1'; |
|
} |
|
conversationHistory.push({ role: 'assistant', content: analysis }); |
|
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
|
if (checkAuth() && currentConversationId) { |
|
await saveMessageToConversation(currentConversationId, 'assistant', analysis); |
|
} |
|
if (checkAuth() && data.conversation_id) { |
|
currentConversationId = data.conversation_id; |
|
currentConversationTitle = data.conversation_title || 'Untitled Conversation'; |
|
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; |
|
history.pushState(null, '', `/chat/${currentConversationId}`); |
|
await loadConversations(); |
|
} |
|
} else { |
|
const contentType = response.headers.get('Content-Type'); |
|
if (contentType?.includes('application/json')) { |
|
const data = await response.json(); |
|
const responseText = data.response || 'Error: No response generated.'; |
|
if (streamMsg) { |
|
streamMsg.dataset.text = responseText; |
|
renderMarkdown(streamMsg); |
|
streamMsg.dataset.done = '1'; |
|
} |
|
conversationHistory.push({ role: 'assistant', content: responseText }); |
|
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
|
if (checkAuth() && currentConversationId) { |
|
await saveMessageToConversation(currentConversationId, 'assistant', responseText); |
|
} |
|
if (checkAuth() && data.conversation_id) { |
|
currentConversationId = data.conversation_id; |
|
currentConversationTitle = data.conversation_title || 'Untitled Conversation'; |
|
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle; |
|
history.pushState(null, '', `/chat/${currentConversationId}`); |
|
await loadConversations(); |
|
} |
|
} else { |
|
const reader = response.body.getReader(); |
|
const decoder = new TextDecoder(); |
|
let buffer = ''; |
|
while (true) { |
|
const { done, value } = await reader.read(); |
|
if (done) break; |
|
buffer += decoder.decode(value, { stream: true }); |
|
if (streamMsg) { |
|
streamMsg.dataset.text = buffer; |
|
currentAssistantText = buffer; |
|
streamMsg.querySelector('.loading')?.remove(); |
|
renderMarkdown(streamMsg); |
|
if (uiElements.chatBox) uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight; |
|
} |
|
} |
|
if (streamMsg) streamMsg.dataset.done = '1'; |
|
conversationHistory.push({ role: 'assistant', content: buffer }); |
|
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); |
|
if (checkAuth() && currentConversationId) { |
|
await saveMessageToConversation(currentConversationId, 'assistant', buffer); |
|
} |
|
} |
|
} |
|
finalizeRequest(); |
|
} catch (error) { |
|
handleRequestError(error); |
|
} |
|
} |
|
|
|
|
|
function stopStream(forceCancel = false) { |
|
if (!isRequestActive && !isRecording) return; |
|
if (isRecording) stopVoiceRecording(); |
|
isRequestActive = false; |
|
if (abortController) { |
|
abortController.abort(); |
|
abortController = null; |
|
} |
|
if (streamMsg && !forceCancel) { |
|
streamMsg.querySelector('.loading')?.remove(); |
|
streamMsg.dataset.text += ''; |
|
renderMarkdown(streamMsg); |
|
streamMsg.dataset.done = '1'; |
|
streamMsg = null; |
|
} |
|
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none'; |
|
if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex'; |
|
if (uiElements.stopBtn) uiElements.stopBtn.style.pointerEvents = 'auto'; |
|
} |
|
|
|
|
|
if (uiElements.settingsBtn) { |
|
uiElements.settingsBtn.addEventListener('click', () => { |
|
if (!checkAuth()) { |
|
alert('Please log in to access settings.'); |
|
window.location.href = '/login'; |
|
return; |
|
} |
|
uiElements.settingsModal.classList.remove('hidden'); |
|
fetch('/api/settings', { |
|
headers: { 'Authorization': `Bearer ${checkAuth()}` } |
|
}) |
|
.then(res => { |
|
if (!res.ok) { |
|
if (res.status === 401) { |
|
localStorage.removeItem('token'); |
|
window.location.href = '/login'; |
|
} |
|
throw new Error('Failed to fetch settings'); |
|
} |
|
return res.json(); |
|
}) |
|
.then(data => { |
|
|
|
document.getElementById('display_name').value = data.user_settings.display_name || ''; |
|
document.getElementById('preferred_model').value = data.user_settings.preferred_model || 'standard'; |
|
document.getElementById('job_title').value = data.user_settings.job_title || ''; |
|
document.getElementById('education').value = data.user_settings.education || ''; |
|
document.getElementById('interests').value = data.user_settings.interests || ''; |
|
document.getElementById('additional_info').value = data.user_settings.additional_info || ''; |
|
document.getElementById('conversation_style').value = data.user_settings.conversation_style || 'default'; |
|
|
|
|
|
const modelSelect = document.getElementById('preferred_model'); |
|
modelSelect.innerHTML = ''; |
|
data.available_models.forEach(model => { |
|
const option = document.createElement('option'); |
|
option.value = model.alias; |
|
option.textContent = `${model.alias} - ${model.description}`; |
|
modelSelect.appendChild(option); |
|
}); |
|
|
|
|
|
const styleSelect = document.getElementById('conversation_style'); |
|
styleSelect.innerHTML = ''; |
|
data.conversation_styles.forEach(style => { |
|
const option = document.createElement('option'); |
|
option.value = style; |
|
option.textContent = style.charAt(0).toUpperCase() + style.slice(1); |
|
styleSelect.appendChild(option); |
|
}); |
|
}) |
|
.catch(err => { |
|
console.error('Error fetching settings:', err); |
|
alert('Failed to load settings. Please try again.'); |
|
}); |
|
}); |
|
} |
|
|
|
if (uiElements.closeSettingsBtn) { |
|
uiElements.closeSettingsBtn.addEventListener('click', () => { |
|
uiElements.settingsModal.classList.add('hidden'); |
|
}); |
|
} |
|
|
|
if (uiElements.settingsForm) { |
|
uiElements.settingsForm.addEventListener('submit', (e) => { |
|
e.preventDefault(); |
|
if (!checkAuth()) { |
|
alert('Please log in to save settings.'); |
|
window.location.href = '/login'; |
|
return; |
|
} |
|
const formData = new FormData(uiElements.settingsForm); |
|
const data = Object.fromEntries(formData); |
|
fetch('/users/me', { |
|
method: 'PUT', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'Authorization': `Bearer ${checkAuth()}` |
|
}, |
|
body: JSON.stringify(data) |
|
}) |
|
.then(res => { |
|
if (!res.ok) { |
|
if (res.status === 401) { |
|
localStorage.removeItem('token'); |
|
window.location.href = '/login'; |
|
} |
|
throw new Error('Failed to update settings'); |
|
} |
|
return res.json(); |
|
}) |
|
.then(() => { |
|
alert('Settings updated successfully!'); |
|
uiElements.settingsModal.classList.add('hidden'); |
|
toggleSidebar(false); |
|
}) |
|
.catch(err => { |
|
console.error('Error updating settings:', err); |
|
alert('Error updating settings: ' + err.message); |
|
}); |
|
}); |
|
} |
|
|
|
|
|
if (uiElements.historyToggle) { |
|
uiElements.historyToggle.addEventListener('click', () => { |
|
if (uiElements.conversationList) { |
|
uiElements.conversationList.classList.toggle('hidden'); |
|
uiElements.historyToggle.innerHTML = uiElements.conversationList.classList.contains('hidden') |
|
? `<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path> |
|
</svg>Show History` |
|
: `<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> |
|
</svg>Hide History`; |
|
} |
|
}); |
|
} |
|
|
|
|
|
uiElements.promptItems.forEach(p => { |
|
p.addEventListener('click', e => { |
|
e.preventDefault(); |
|
if (uiElements.input) { |
|
uiElements.input.value = p.dataset.prompt; |
|
autoResizeTextarea(); |
|
} |
|
if (uiElements.sendBtn) uiElements.sendBtn.disabled = false; |
|
submitMessage(); |
|
}); |
|
}); |
|
|
|
if (uiElements.fileBtn) uiElements.fileBtn.addEventListener('click', () => uiElements.fileInput?.click()); |
|
if (uiElements.audioBtn) uiElements.audioBtn.addEventListener('click', () => uiElements.audioInput?.click()); |
|
if (uiElements.fileInput) uiElements.fileInput.addEventListener('change', previewFile); |
|
if (uiElements.audioInput) uiElements.audioInput.addEventListener('change', previewFile); |
|
|
|
if (uiElements.sendBtn) { |
|
uiElements.sendBtn.addEventListener('mousedown', e => { |
|
if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return; |
|
startVoiceRecording(); |
|
}); |
|
uiElements.sendBtn.addEventListener('mouseup', () => isRecording && stopVoiceRecording()); |
|
uiElements.sendBtn.addEventListener('mouseleave', () => isRecording && stopVoiceRecording()); |
|
uiElements.sendBtn.addEventListener('touchstart', e => { |
|
e.preventDefault(); |
|
if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return; |
|
startVoiceRecording(); |
|
}); |
|
uiElements.sendBtn.addEventListener('touchend', e => { |
|
e.preventDefault(); |
|
if (isRecording) stopVoiceRecording(); |
|
}); |
|
uiElements.sendBtn.addEventListener('touchcancel', e => { |
|
e.preventDefault(); |
|
if (isRecording) stopVoiceRecording(); |
|
}); |
|
} |
|
|
|
if (uiElements.form) { |
|
uiElements.form.addEventListener('submit', e => { |
|
e.preventDefault(); |
|
if (!isRecording) submitMessage(); |
|
}); |
|
} |
|
|
|
if (uiElements.input) { |
|
uiElements.input.addEventListener('keydown', e => { |
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
e.preventDefault(); |
|
if (!isRecording && !uiElements.sendBtn.disabled) submitMessage(); |
|
} |
|
}); |
|
uiElements.input.addEventListener('input', () => { |
|
autoResizeTextarea(); |
|
updateSendButtonState(); |
|
}); |
|
} |
|
|
|
if (uiElements.stopBtn) { |
|
uiElements.stopBtn.addEventListener('click', () => { |
|
uiElements.stopBtn.style.pointerEvents = 'none'; |
|
stopStream(); |
|
}); |
|
} |
|
|
|
if (uiElements.clearBtn) uiElements.clearBtn.addEventListener('click', clearAllMessages); |
|
|
|
if (uiElements.conversationTitle) { |
|
uiElements.conversationTitle.addEventListener('click', () => { |
|
if (!checkAuth()) return alert('Please log in to edit the conversation title.'); |
|
const newTitle = prompt('Enter new conversation title:', currentConversationTitle || ''); |
|
if (newTitle && currentConversationId) { |
|
updateConversationTitle(currentConversationId, newTitle); |
|
} |
|
}); |
|
} |
|
|
|
if (uiElements.sidebarToggle) { |
|
uiElements.sidebarToggle.addEventListener('click', () => toggleSidebar()); |
|
} |
|
|
|
if (uiElements.newConversationBtn) { |
|
uiElements.newConversationBtn.addEventListener('click', createNewConversation); |
|
} |
|
|