Mark-Lasfar
commited on
Commit
·
9aa52ab
1
Parent(s):
5e980fd
Update Model
Browse files- api/auth.py +51 -22
- static/js/chat.js +60 -60
api/auth.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
# api/auth.py
|
| 2 |
from fastapi_users import FastAPIUsers
|
| 3 |
from fastapi_users.authentication import CookieTransport, JWTStrategy, AuthenticationBackend
|
| 4 |
from fastapi_users.db import SQLAlchemyUserDatabase
|
|
@@ -74,6 +73,11 @@ class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
|
|
| 74 |
) -> UP:
|
| 75 |
logger.info(f"Processing OAuth callback for {oauth_name} with account_id: {account_id}, email: {account_email}")
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
oauth_account_dict = {
|
| 78 |
"oauth_name": oauth_name,
|
| 79 |
"access_token": access_token,
|
|
@@ -104,12 +108,17 @@ class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
|
|
| 104 |
"is_verified": is_verified_by_default,
|
| 105 |
}
|
| 106 |
user = User(**user_dict)
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
logger.info(f"Returning existing user: {user.email}")
|
| 114 |
return await self.on_after_login(user, request)
|
| 115 |
|
|
@@ -119,7 +128,7 @@ class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
|
|
| 119 |
result = self.user_db.session.execute(statement)
|
| 120 |
user = result.scalars().first()
|
| 121 |
if user is None:
|
| 122 |
-
logger.
|
| 123 |
# Create new user if not found
|
| 124 |
user_dict = {
|
| 125 |
"email": account_email,
|
|
@@ -128,14 +137,24 @@ class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
|
|
| 128 |
"is_verified": is_verified_by_default,
|
| 129 |
}
|
| 130 |
user = User(**user_dict)
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
oauth_account.user_id = user.id
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
return await self.on_after_login(user, request)
|
| 140 |
|
| 141 |
# Create new user
|
|
@@ -147,15 +166,25 @@ class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
|
|
| 147 |
"is_verified": is_verified_by_default,
|
| 148 |
}
|
| 149 |
user = User(**user_dict)
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
oauth_account.user_id = user.id
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
return await self.on_after_login(user, request)
|
| 160 |
|
| 161 |
async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
|
|
|
|
|
|
|
| 1 |
from fastapi_users import FastAPIUsers
|
| 2 |
from fastapi_users.authentication import CookieTransport, JWTStrategy, AuthenticationBackend
|
| 3 |
from fastapi_users.db import SQLAlchemyUserDatabase
|
|
|
|
| 73 |
) -> UP:
|
| 74 |
logger.info(f"Processing OAuth callback for {oauth_name} with account_id: {account_id}, email: {account_email}")
|
| 75 |
|
| 76 |
+
# Validate inputs
|
| 77 |
+
if not account_email or not account_id:
|
| 78 |
+
logger.error(f"Invalid OAuth callback data: email={account_email}, account_id={account_id}")
|
| 79 |
+
raise ValueError("Invalid OAuth callback data: email and account_id are required.")
|
| 80 |
+
|
| 81 |
oauth_account_dict = {
|
| 82 |
"oauth_name": oauth_name,
|
| 83 |
"access_token": access_token,
|
|
|
|
| 108 |
"is_verified": is_verified_by_default,
|
| 109 |
}
|
| 110 |
user = User(**user_dict)
|
| 111 |
+
try:
|
| 112 |
+
self.user_db.session.add(user)
|
| 113 |
+
self.user_db.session.commit()
|
| 114 |
+
self.user_db.session.refresh(user)
|
| 115 |
+
existing_oauth_account.user_id = user.id
|
| 116 |
+
self.user_db.session.commit()
|
| 117 |
+
logger.info(f"Created new user and linked to existing OAuth account: {user.email}")
|
| 118 |
+
except Exception as e:
|
| 119 |
+
self.user_db.session.rollback()
|
| 120 |
+
logger.error(f"Failed to create user for OAuth account {account_id}: {e}")
|
| 121 |
+
raise ValueError(f"Failed to create user: {e}")
|
| 122 |
logger.info(f"Returning existing user: {user.email}")
|
| 123 |
return await self.on_after_login(user, request)
|
| 124 |
|
|
|
|
| 128 |
result = self.user_db.session.execute(statement)
|
| 129 |
user = result.scalars().first()
|
| 130 |
if user is None:
|
| 131 |
+
logger.info(f"No user found for email {account_email}. Creating new user.")
|
| 132 |
# Create new user if not found
|
| 133 |
user_dict = {
|
| 134 |
"email": account_email,
|
|
|
|
| 137 |
"is_verified": is_verified_by_default,
|
| 138 |
}
|
| 139 |
user = User(**user_dict)
|
| 140 |
+
try:
|
| 141 |
+
self.user_db.session.add(user)
|
| 142 |
+
self.user_db.session.commit()
|
| 143 |
+
self.user_db.session.refresh(user)
|
| 144 |
+
logger.info(f"Created new user for email: {user.email}")
|
| 145 |
+
except Exception as e:
|
| 146 |
+
self.user_db.session.rollback()
|
| 147 |
+
logger.error(f"Failed to create user for email {account_email}: {e}")
|
| 148 |
+
raise ValueError(f"Failed to create user: {e}")
|
| 149 |
oauth_account.user_id = user.id
|
| 150 |
+
try:
|
| 151 |
+
self.user_db.session.add(oauth_account)
|
| 152 |
+
self.user_db.session.commit()
|
| 153 |
+
logger.info(f"Associated OAuth account with user: {user.email}")
|
| 154 |
+
except Exception as e:
|
| 155 |
+
self.user_db.session.rollback()
|
| 156 |
+
logger.error(f"Failed to associate OAuth account with user {user.email}: {e}")
|
| 157 |
+
raise ValueError(f"Failed to associate OAuth account: {e}")
|
| 158 |
return await self.on_after_login(user, request)
|
| 159 |
|
| 160 |
# Create new user
|
|
|
|
| 166 |
"is_verified": is_verified_by_default,
|
| 167 |
}
|
| 168 |
user = User(**user_dict)
|
| 169 |
+
try:
|
| 170 |
+
self.user_db.session.add(user)
|
| 171 |
+
self.user_db.session.commit()
|
| 172 |
+
self.user_db.session.refresh(user)
|
| 173 |
+
logger.info(f"Created new user: {user.email}")
|
| 174 |
+
except Exception as e:
|
| 175 |
+
self.user_db.session.rollback()
|
| 176 |
+
logger.error(f"Failed to create user for email {account_email}: {e}")
|
| 177 |
+
raise ValueError(f"Failed to create user: {e}")
|
| 178 |
|
| 179 |
oauth_account.user_id = user.id
|
| 180 |
+
try:
|
| 181 |
+
self.user_db.session.add(oauth_account)
|
| 182 |
+
self.user_db.session.commit()
|
| 183 |
+
logger.info(f"Linked OAuth account to new user: {user.email}")
|
| 184 |
+
except Exception as e:
|
| 185 |
+
self.user_db.session.rollback()
|
| 186 |
+
logger.error(f"Failed to link OAuth account to user {user.email}: {e}")
|
| 187 |
+
raise ValueError(f"Failed to link OAuth account: {e}")
|
| 188 |
return await self.on_after_login(user, request)
|
| 189 |
|
| 190 |
async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
|
static/js/chat.js
CHANGED
|
@@ -236,7 +236,7 @@ function startVoiceRecording() {
|
|
| 236 |
mediaRecorder.addEventListener('dataavailable', event => audioChunks.push(event.data));
|
| 237 |
}).catch(err => {
|
| 238 |
console.error('Error accessing microphone:', err);
|
| 239 |
-
alert('
|
| 240 |
isRecording = false;
|
| 241 |
if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording');
|
| 242 |
});
|
|
@@ -259,8 +259,8 @@ function stopVoiceRecording() {
|
|
| 259 |
// Send audio message
|
| 260 |
async function submitAudioMessage(formData) {
|
| 261 |
enterChatView();
|
| 262 |
-
addMsg('user', '
|
| 263 |
-
conversationHistory.push({ role: 'user', content: '
|
| 264 |
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
|
| 265 |
streamMsg = addMsg('assistant', '');
|
| 266 |
const loadingEl = document.createElement('span');
|
|
@@ -277,8 +277,8 @@ async function submitAudioMessage(formData) {
|
|
| 277 |
throw new Error(`Request failed with status ${response.status}`);
|
| 278 |
}
|
| 279 |
const data = await response.json();
|
| 280 |
-
if (!data.transcription) throw new Error('
|
| 281 |
-
const transcription = data.transcription || '
|
| 282 |
if (streamMsg) {
|
| 283 |
streamMsg.dataset.text = transcription;
|
| 284 |
renderMarkdown(streamMsg);
|
|
@@ -291,7 +291,7 @@ async function submitAudioMessage(formData) {
|
|
| 291 |
}
|
| 292 |
if (checkAuth() && data.conversation_id) {
|
| 293 |
currentConversationId = data.conversation_id;
|
| 294 |
-
currentConversationTitle = data.conversation_title || '
|
| 295 |
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
|
| 296 |
history.pushState(null, '', `/chat/${currentConversationId}`);
|
| 297 |
await loadConversations();
|
|
@@ -316,22 +316,22 @@ async function sendRequest(endpoint, body, headers = {}) {
|
|
| 316 |
if (!response.ok) {
|
| 317 |
if (response.status === 403) {
|
| 318 |
if (uiElements.messageLimitWarning) uiElements.messageLimitWarning.classList.remove('hidden');
|
| 319 |
-
throw new Error('
|
| 320 |
}
|
| 321 |
if (response.status === 401) {
|
| 322 |
localStorage.removeItem('token');
|
| 323 |
window.location.href = '/login';
|
| 324 |
-
throw new Error('
|
| 325 |
}
|
| 326 |
if (response.status === 503) {
|
| 327 |
-
throw new Error('
|
| 328 |
}
|
| 329 |
-
throw new Error(
|
| 330 |
}
|
| 331 |
return response;
|
| 332 |
} catch (error) {
|
| 333 |
if (error.name === 'AbortError') {
|
| 334 |
-
throw new Error('
|
| 335 |
}
|
| 336 |
throw error;
|
| 337 |
}
|
|
@@ -361,13 +361,13 @@ function finalizeRequest() {
|
|
| 361 |
function handleRequestError(error) {
|
| 362 |
if (streamMsg) {
|
| 363 |
streamMsg.querySelector('.loading')?.remove();
|
| 364 |
-
streamMsg.dataset.text =
|
| 365 |
renderMarkdown(streamMsg);
|
| 366 |
streamMsg.dataset.done = '1';
|
| 367 |
streamMsg = null;
|
| 368 |
}
|
| 369 |
-
console.error('
|
| 370 |
-
alert(
|
| 371 |
isRequestActive = false;
|
| 372 |
abortController = null;
|
| 373 |
if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex';
|
|
@@ -381,7 +381,7 @@ async function loadConversations() {
|
|
| 381 |
const response = await fetch('/api/conversations', {
|
| 382 |
headers: { 'Authorization': `Bearer ${checkAuth()}` }
|
| 383 |
});
|
| 384 |
-
if (!response.ok) throw new Error('
|
| 385 |
const conversations = await response.json();
|
| 386 |
if (uiElements.conversationList) {
|
| 387 |
uiElements.conversationList.innerHTML = '';
|
|
@@ -394,9 +394,9 @@ async function loadConversations() {
|
|
| 394 |
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
| 395 |
<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>
|
| 396 |
</svg>
|
| 397 |
-
<span class="truncate flex-1">${conv.title || '
|
| 398 |
</div>
|
| 399 |
-
<button class="delete-conversation-btn text-red-400 hover:text-red-600 p-1" title="
|
| 400 |
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
| 401 |
<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>
|
| 402 |
</svg>
|
|
@@ -408,8 +408,8 @@ async function loadConversations() {
|
|
| 408 |
});
|
| 409 |
}
|
| 410 |
} catch (error) {
|
| 411 |
-
console.error('
|
| 412 |
-
alert('
|
| 413 |
}
|
| 414 |
}
|
| 415 |
|
|
@@ -421,11 +421,11 @@ async function loadConversation(conversationId) {
|
|
| 421 |
});
|
| 422 |
if (!response.ok) {
|
| 423 |
if (response.status === 401) window.location.href = '/login';
|
| 424 |
-
throw new Error('
|
| 425 |
}
|
| 426 |
const data = await response.json();
|
| 427 |
currentConversationId = data.conversation_id;
|
| 428 |
-
currentConversationTitle = data.title || '
|
| 429 |
conversationHistory = data.messages.map(msg => ({ role: msg.role, content: msg.content }));
|
| 430 |
if (uiElements.chatBox) uiElements.chatBox.innerHTML = '';
|
| 431 |
conversationHistory.forEach(msg => addMsg(msg.role, msg.content));
|
|
@@ -434,14 +434,14 @@ async function loadConversation(conversationId) {
|
|
| 434 |
history.pushState(null, '', `/chat/${currentConversationId}`);
|
| 435 |
toggleSidebar(false);
|
| 436 |
} catch (error) {
|
| 437 |
-
console.error('
|
| 438 |
-
alert('
|
| 439 |
}
|
| 440 |
}
|
| 441 |
|
| 442 |
// Delete conversation
|
| 443 |
async function deleteConversation(conversationId) {
|
| 444 |
-
if (!confirm('
|
| 445 |
try {
|
| 446 |
const response = await fetch(`/api/conversations/${conversationId}`, {
|
| 447 |
method: 'DELETE',
|
|
@@ -449,7 +449,7 @@ async function deleteConversation(conversationId) {
|
|
| 449 |
});
|
| 450 |
if (!response.ok) {
|
| 451 |
if (response.status === 401) window.location.href = '/login';
|
| 452 |
-
throw new Error('
|
| 453 |
}
|
| 454 |
if (conversationId === currentConversationId) {
|
| 455 |
clearAllMessages();
|
|
@@ -459,8 +459,8 @@ async function deleteConversation(conversationId) {
|
|
| 459 |
}
|
| 460 |
await loadConversations();
|
| 461 |
} catch (error) {
|
| 462 |
-
console.error('
|
| 463 |
-
alert('
|
| 464 |
}
|
| 465 |
}
|
| 466 |
|
|
@@ -475,16 +475,16 @@ async function saveMessageToConversation(conversationId, role, content) {
|
|
| 475 |
},
|
| 476 |
body: JSON.stringify({ role, content })
|
| 477 |
});
|
| 478 |
-
if (!response.ok) throw new Error('
|
| 479 |
} catch (error) {
|
| 480 |
-
console.error('
|
| 481 |
}
|
| 482 |
}
|
| 483 |
|
| 484 |
// Create new conversation
|
| 485 |
async function createNewConversation() {
|
| 486 |
if (!checkAuth()) {
|
| 487 |
-
alert('
|
| 488 |
window.location.href = '/login';
|
| 489 |
return;
|
| 490 |
}
|
|
@@ -495,14 +495,14 @@ async function createNewConversation() {
|
|
| 495 |
'Content-Type': 'application/json',
|
| 496 |
'Authorization': `Bearer ${checkAuth()}`
|
| 497 |
},
|
| 498 |
-
body: JSON.stringify({ title: '
|
| 499 |
});
|
| 500 |
if (!response.ok) {
|
| 501 |
if (response.status === 401) {
|
| 502 |
localStorage.removeItem('token');
|
| 503 |
window.location.href = '/login';
|
| 504 |
}
|
| 505 |
-
throw new Error('
|
| 506 |
}
|
| 507 |
const data = await response.json();
|
| 508 |
currentConversationId = data.conversation_id;
|
|
@@ -516,8 +516,8 @@ async function createNewConversation() {
|
|
| 516 |
await loadConversations();
|
| 517 |
toggleSidebar(false);
|
| 518 |
} catch (error) {
|
| 519 |
-
console.error('
|
| 520 |
-
alert('
|
| 521 |
}
|
| 522 |
}
|
| 523 |
|
|
@@ -532,14 +532,14 @@ async function updateConversationTitle(conversationId, newTitle) {
|
|
| 532 |
},
|
| 533 |
body: JSON.stringify({ title: newTitle })
|
| 534 |
});
|
| 535 |
-
if (!response.ok) throw new Error('
|
| 536 |
const data = await response.json();
|
| 537 |
currentConversationTitle = data.title;
|
| 538 |
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
|
| 539 |
await loadConversations();
|
| 540 |
} catch (error) {
|
| 541 |
-
console.error('
|
| 542 |
-
alert('
|
| 543 |
}
|
| 544 |
}
|
| 545 |
|
|
@@ -632,7 +632,7 @@ async function submitMessage() {
|
|
| 632 |
if (file.type.startsWith('image/')) {
|
| 633 |
endpoint = '/api/image-analysis';
|
| 634 |
inputType = 'image';
|
| 635 |
-
message = '
|
| 636 |
formData = new FormData();
|
| 637 |
formData.append('file', file);
|
| 638 |
formData.append('output_format', 'text');
|
|
@@ -642,7 +642,7 @@ async function submitMessage() {
|
|
| 642 |
if (file.type.startsWith('audio/')) {
|
| 643 |
endpoint = '/api/audio-transcription';
|
| 644 |
inputType = 'audio';
|
| 645 |
-
message = '
|
| 646 |
formData = new FormData();
|
| 647 |
formData.append('file', file);
|
| 648 |
}
|
|
@@ -681,8 +681,8 @@ async function submitMessage() {
|
|
| 681 |
const response = await sendRequest(endpoint, payload ? JSON.stringify(payload) : formData, headers);
|
| 682 |
if (endpoint === '/api/audio-transcription') {
|
| 683 |
const data = await response.json();
|
| 684 |
-
if (!data.transcription) throw new Error('
|
| 685 |
-
const transcription = data.transcription || '
|
| 686 |
if (streamMsg) {
|
| 687 |
streamMsg.dataset.text = transcription;
|
| 688 |
renderMarkdown(streamMsg);
|
|
@@ -695,14 +695,14 @@ async function submitMessage() {
|
|
| 695 |
}
|
| 696 |
if (checkAuth() && data.conversation_id) {
|
| 697 |
currentConversationId = data.conversation_id;
|
| 698 |
-
currentConversationTitle = data.conversation_title || '
|
| 699 |
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
|
| 700 |
history.pushState(null, '', `/chat/${currentConversationId}`);
|
| 701 |
await loadConversations();
|
| 702 |
}
|
| 703 |
} else if (endpoint === '/api/image-analysis') {
|
| 704 |
const data = await response.json();
|
| 705 |
-
const analysis = data.image_analysis || '
|
| 706 |
if (streamMsg) {
|
| 707 |
streamMsg.dataset.text = analysis;
|
| 708 |
renderMarkdown(streamMsg);
|
|
@@ -715,7 +715,7 @@ async function submitMessage() {
|
|
| 715 |
}
|
| 716 |
if (checkAuth() && data.conversation_id) {
|
| 717 |
currentConversationId = data.conversation_id;
|
| 718 |
-
currentConversationTitle = data.conversation_title || '
|
| 719 |
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
|
| 720 |
history.pushState(null, '', `/chat/${currentConversationId}`);
|
| 721 |
await loadConversations();
|
|
@@ -724,7 +724,7 @@ async function submitMessage() {
|
|
| 724 |
const contentType = response.headers.get('Content-Type');
|
| 725 |
if (contentType?.includes('application/json')) {
|
| 726 |
const data = await response.json();
|
| 727 |
-
const responseText = data.response || '
|
| 728 |
if (streamMsg) {
|
| 729 |
streamMsg.dataset.text = responseText;
|
| 730 |
renderMarkdown(streamMsg);
|
|
@@ -737,7 +737,7 @@ async function submitMessage() {
|
|
| 737 |
}
|
| 738 |
if (checkAuth() && data.conversation_id) {
|
| 739 |
currentConversationId = data.conversation_id;
|
| 740 |
-
currentConversationTitle = data.conversation_title || '
|
| 741 |
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
|
| 742 |
history.pushState(null, '', `/chat/${currentConversationId}`);
|
| 743 |
await loadConversations();
|
|
@@ -749,7 +749,7 @@ async function submitMessage() {
|
|
| 749 |
while (true) {
|
| 750 |
const { done, value } = await reader.read();
|
| 751 |
if (done) {
|
| 752 |
-
if (!buffer.trim()) throw new Error('
|
| 753 |
break;
|
| 754 |
}
|
| 755 |
buffer += decoder.decode(value, { stream: true });
|
|
@@ -800,7 +800,7 @@ function stopStream(forceCancel = false) {
|
|
| 800 |
if (uiElements.settingsBtn) {
|
| 801 |
uiElements.settingsBtn.addEventListener('click', () => {
|
| 802 |
if (!checkAuth()) {
|
| 803 |
-
alert('
|
| 804 |
window.location.href = '/login';
|
| 805 |
return;
|
| 806 |
}
|
|
@@ -814,7 +814,7 @@ if (uiElements.settingsBtn) {
|
|
| 814 |
localStorage.removeItem('token');
|
| 815 |
window.location.href = '/login';
|
| 816 |
}
|
| 817 |
-
throw new Error('
|
| 818 |
}
|
| 819 |
return res.json();
|
| 820 |
})
|
|
@@ -846,8 +846,8 @@ if (uiElements.settingsBtn) {
|
|
| 846 |
});
|
| 847 |
})
|
| 848 |
.catch(err => {
|
| 849 |
-
console.error('
|
| 850 |
-
alert('
|
| 851 |
});
|
| 852 |
});
|
| 853 |
}
|
|
@@ -862,7 +862,7 @@ if (uiElements.settingsForm) {
|
|
| 862 |
uiElements.settingsForm.addEventListener('submit', (e) => {
|
| 863 |
e.preventDefault();
|
| 864 |
if (!checkAuth()) {
|
| 865 |
-
alert('
|
| 866 |
window.location.href = '/login';
|
| 867 |
return;
|
| 868 |
}
|
|
@@ -882,18 +882,18 @@ if (uiElements.settingsForm) {
|
|
| 882 |
localStorage.removeItem('token');
|
| 883 |
window.location.href = '/login';
|
| 884 |
}
|
| 885 |
-
throw new Error('
|
| 886 |
}
|
| 887 |
return res.json();
|
| 888 |
})
|
| 889 |
.then(() => {
|
| 890 |
-
alert('
|
| 891 |
uiElements.settingsModal.classList.add('hidden');
|
| 892 |
toggleSidebar(false);
|
| 893 |
})
|
| 894 |
.catch(err => {
|
| 895 |
-
console.error('
|
| 896 |
-
alert('
|
| 897 |
});
|
| 898 |
});
|
| 899 |
}
|
|
@@ -906,10 +906,10 @@ if (uiElements.historyToggle) {
|
|
| 906 |
uiElements.historyToggle.innerHTML = uiElements.conversationList.classList.contains('hidden')
|
| 907 |
? `<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
| 908 |
<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>
|
| 909 |
-
</svg
|
| 910 |
: `<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
| 911 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
| 912 |
-
</svg
|
| 913 |
}
|
| 914 |
});
|
| 915 |
}
|
|
@@ -985,8 +985,8 @@ if (uiElements.clearBtn) uiElements.clearBtn.addEventListener('click', clearAllM
|
|
| 985 |
|
| 986 |
if (uiElements.conversationTitle) {
|
| 987 |
uiElements.conversationTitle.addEventListener('click', () => {
|
| 988 |
-
if (!checkAuth()) return alert('
|
| 989 |
-
const newTitle = prompt('
|
| 990 |
if (newTitle && currentConversationId) {
|
| 991 |
updateConversationTitle(currentConversationId, newTitle);
|
| 992 |
}
|
|
|
|
| 236 |
mediaRecorder.addEventListener('dataavailable', event => audioChunks.push(event.data));
|
| 237 |
}).catch(err => {
|
| 238 |
console.error('Error accessing microphone:', err);
|
| 239 |
+
alert('Failed to access microphone. Please check permissions.');
|
| 240 |
isRecording = false;
|
| 241 |
if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording');
|
| 242 |
});
|
|
|
|
| 259 |
// Send audio message
|
| 260 |
async function submitAudioMessage(formData) {
|
| 261 |
enterChatView();
|
| 262 |
+
addMsg('user', 'Voice message');
|
| 263 |
+
conversationHistory.push({ role: 'user', content: 'Voice message' });
|
| 264 |
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
|
| 265 |
streamMsg = addMsg('assistant', '');
|
| 266 |
const loadingEl = document.createElement('span');
|
|
|
|
| 277 |
throw new Error(`Request failed with status ${response.status}`);
|
| 278 |
}
|
| 279 |
const data = await response.json();
|
| 280 |
+
if (!data.transcription) throw new Error('No transcription received from server');
|
| 281 |
+
const transcription = data.transcription || 'Error: No transcription generated.';
|
| 282 |
if (streamMsg) {
|
| 283 |
streamMsg.dataset.text = transcription;
|
| 284 |
renderMarkdown(streamMsg);
|
|
|
|
| 291 |
}
|
| 292 |
if (checkAuth() && data.conversation_id) {
|
| 293 |
currentConversationId = data.conversation_id;
|
| 294 |
+
currentConversationTitle = data.conversation_title || 'Untitled Conversation';
|
| 295 |
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
|
| 296 |
history.pushState(null, '', `/chat/${currentConversationId}`);
|
| 297 |
await loadConversations();
|
|
|
|
| 316 |
if (!response.ok) {
|
| 317 |
if (response.status === 403) {
|
| 318 |
if (uiElements.messageLimitWarning) uiElements.messageLimitWarning.classList.remove('hidden');
|
| 319 |
+
throw new Error('Message limit reached. Please log in to continue.');
|
| 320 |
}
|
| 321 |
if (response.status === 401) {
|
| 322 |
localStorage.removeItem('token');
|
| 323 |
window.location.href = '/login';
|
| 324 |
+
throw new Error('Unauthorized. Please log in again.');
|
| 325 |
}
|
| 326 |
if (response.status === 503) {
|
| 327 |
+
throw new Error('Model not available. Please try another model.');
|
| 328 |
}
|
| 329 |
+
throw new Error(`Request failed with status ${response.status}`);
|
| 330 |
}
|
| 331 |
return response;
|
| 332 |
} catch (error) {
|
| 333 |
if (error.name === 'AbortError') {
|
| 334 |
+
throw new Error('Request was aborted');
|
| 335 |
}
|
| 336 |
throw error;
|
| 337 |
}
|
|
|
|
| 361 |
function handleRequestError(error) {
|
| 362 |
if (streamMsg) {
|
| 363 |
streamMsg.querySelector('.loading')?.remove();
|
| 364 |
+
streamMsg.dataset.text = `Error: ${error.message || 'An error occurred during the request.'}`;
|
| 365 |
renderMarkdown(streamMsg);
|
| 366 |
streamMsg.dataset.done = '1';
|
| 367 |
streamMsg = null;
|
| 368 |
}
|
| 369 |
+
console.error('Request error:', error);
|
| 370 |
+
alert(`Error: ${error.message || 'An error occurred during the request.'}`);
|
| 371 |
isRequestActive = false;
|
| 372 |
abortController = null;
|
| 373 |
if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex';
|
|
|
|
| 381 |
const response = await fetch('/api/conversations', {
|
| 382 |
headers: { 'Authorization': `Bearer ${checkAuth()}` }
|
| 383 |
});
|
| 384 |
+
if (!response.ok) throw new Error('Failed to load conversations');
|
| 385 |
const conversations = await response.json();
|
| 386 |
if (uiElements.conversationList) {
|
| 387 |
uiElements.conversationList.innerHTML = '';
|
|
|
|
| 394 |
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
| 395 |
<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>
|
| 396 |
</svg>
|
| 397 |
+
<span class="truncate flex-1">${conv.title || 'Untitled Conversation'}</span>
|
| 398 |
</div>
|
| 399 |
+
<button class="delete-conversation-btn text-red-400 hover:text-red-600 p-1" title="Delete Conversation" data-conversation-id="${conv.conversation_id}">
|
| 400 |
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
| 401 |
<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>
|
| 402 |
</svg>
|
|
|
|
| 408 |
});
|
| 409 |
}
|
| 410 |
} catch (error) {
|
| 411 |
+
console.error('Error loading conversations:', error);
|
| 412 |
+
alert('Failed to load conversations. Please try again.');
|
| 413 |
}
|
| 414 |
}
|
| 415 |
|
|
|
|
| 421 |
});
|
| 422 |
if (!response.ok) {
|
| 423 |
if (response.status === 401) window.location.href = '/login';
|
| 424 |
+
throw new Error('Failed to load conversation');
|
| 425 |
}
|
| 426 |
const data = await response.json();
|
| 427 |
currentConversationId = data.conversation_id;
|
| 428 |
+
currentConversationTitle = data.title || 'Untitled Conversation';
|
| 429 |
conversationHistory = data.messages.map(msg => ({ role: msg.role, content: msg.content }));
|
| 430 |
if (uiElements.chatBox) uiElements.chatBox.innerHTML = '';
|
| 431 |
conversationHistory.forEach(msg => addMsg(msg.role, msg.content));
|
|
|
|
| 434 |
history.pushState(null, '', `/chat/${currentConversationId}`);
|
| 435 |
toggleSidebar(false);
|
| 436 |
} catch (error) {
|
| 437 |
+
console.error('Error loading conversation:', error);
|
| 438 |
+
alert('Failed to load conversation. Please try again or log in.');
|
| 439 |
}
|
| 440 |
}
|
| 441 |
|
| 442 |
// Delete conversation
|
| 443 |
async function deleteConversation(conversationId) {
|
| 444 |
+
if (!confirm('Are you sure you want to delete this conversation?')) return;
|
| 445 |
try {
|
| 446 |
const response = await fetch(`/api/conversations/${conversationId}`, {
|
| 447 |
method: 'DELETE',
|
|
|
|
| 449 |
});
|
| 450 |
if (!response.ok) {
|
| 451 |
if (response.status === 401) window.location.href = '/login';
|
| 452 |
+
throw new Error('Failed to delete conversation');
|
| 453 |
}
|
| 454 |
if (conversationId === currentConversationId) {
|
| 455 |
clearAllMessages();
|
|
|
|
| 459 |
}
|
| 460 |
await loadConversations();
|
| 461 |
} catch (error) {
|
| 462 |
+
console.error('Error deleting conversation:', error);
|
| 463 |
+
alert('Failed to delete conversation. Please try again.');
|
| 464 |
}
|
| 465 |
}
|
| 466 |
|
|
|
|
| 475 |
},
|
| 476 |
body: JSON.stringify({ role, content })
|
| 477 |
});
|
| 478 |
+
if (!response.ok) throw new Error('Failed to save message');
|
| 479 |
} catch (error) {
|
| 480 |
+
console.error('Error saving message:', error);
|
| 481 |
}
|
| 482 |
}
|
| 483 |
|
| 484 |
// Create new conversation
|
| 485 |
async function createNewConversation() {
|
| 486 |
if (!checkAuth()) {
|
| 487 |
+
alert('Please log in to create a new conversation.');
|
| 488 |
window.location.href = '/login';
|
| 489 |
return;
|
| 490 |
}
|
|
|
|
| 495 |
'Content-Type': 'application/json',
|
| 496 |
'Authorization': `Bearer ${checkAuth()}`
|
| 497 |
},
|
| 498 |
+
body: JSON.stringify({ title: 'New Conversation' })
|
| 499 |
});
|
| 500 |
if (!response.ok) {
|
| 501 |
if (response.status === 401) {
|
| 502 |
localStorage.removeItem('token');
|
| 503 |
window.location.href = '/login';
|
| 504 |
}
|
| 505 |
+
throw new Error('Failed to create conversation');
|
| 506 |
}
|
| 507 |
const data = await response.json();
|
| 508 |
currentConversationId = data.conversation_id;
|
|
|
|
| 516 |
await loadConversations();
|
| 517 |
toggleSidebar(false);
|
| 518 |
} catch (error) {
|
| 519 |
+
console.error('Error creating conversation:', error);
|
| 520 |
+
alert('Failed to create new conversation. Please try again.');
|
| 521 |
}
|
| 522 |
}
|
| 523 |
|
|
|
|
| 532 |
},
|
| 533 |
body: JSON.stringify({ title: newTitle })
|
| 534 |
});
|
| 535 |
+
if (!response.ok) throw new Error('Failed to update title');
|
| 536 |
const data = await response.json();
|
| 537 |
currentConversationTitle = data.title;
|
| 538 |
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
|
| 539 |
await loadConversations();
|
| 540 |
} catch (error) {
|
| 541 |
+
console.error('Error updating title:', error);
|
| 542 |
+
alert('Failed to update conversation title.');
|
| 543 |
}
|
| 544 |
}
|
| 545 |
|
|
|
|
| 632 |
if (file.type.startsWith('image/')) {
|
| 633 |
endpoint = '/api/image-analysis';
|
| 634 |
inputType = 'image';
|
| 635 |
+
message = 'Analyze this image';
|
| 636 |
formData = new FormData();
|
| 637 |
formData.append('file', file);
|
| 638 |
formData.append('output_format', 'text');
|
|
|
|
| 642 |
if (file.type.startsWith('audio/')) {
|
| 643 |
endpoint = '/api/audio-transcription';
|
| 644 |
inputType = 'audio';
|
| 645 |
+
message = 'Transcribe this audio';
|
| 646 |
formData = new FormData();
|
| 647 |
formData.append('file', file);
|
| 648 |
}
|
|
|
|
| 681 |
const response = await sendRequest(endpoint, payload ? JSON.stringify(payload) : formData, headers);
|
| 682 |
if (endpoint === '/api/audio-transcription') {
|
| 683 |
const data = await response.json();
|
| 684 |
+
if (!data.transcription) throw new Error('No transcription received from server');
|
| 685 |
+
const transcription = data.transcription || 'Error: No transcription generated.';
|
| 686 |
if (streamMsg) {
|
| 687 |
streamMsg.dataset.text = transcription;
|
| 688 |
renderMarkdown(streamMsg);
|
|
|
|
| 695 |
}
|
| 696 |
if (checkAuth() && data.conversation_id) {
|
| 697 |
currentConversationId = data.conversation_id;
|
| 698 |
+
currentConversationTitle = data.conversation_title || 'Untitled Conversation';
|
| 699 |
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
|
| 700 |
history.pushState(null, '', `/chat/${currentConversationId}`);
|
| 701 |
await loadConversations();
|
| 702 |
}
|
| 703 |
} else if (endpoint === '/api/image-analysis') {
|
| 704 |
const data = await response.json();
|
| 705 |
+
const analysis = data.image_analysis || 'Error: No analysis generated.';
|
| 706 |
if (streamMsg) {
|
| 707 |
streamMsg.dataset.text = analysis;
|
| 708 |
renderMarkdown(streamMsg);
|
|
|
|
| 715 |
}
|
| 716 |
if (checkAuth() && data.conversation_id) {
|
| 717 |
currentConversationId = data.conversation_id;
|
| 718 |
+
currentConversationTitle = data.conversation_title || 'Untitled Conversation';
|
| 719 |
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
|
| 720 |
history.pushState(null, '', `/chat/${currentConversationId}`);
|
| 721 |
await loadConversations();
|
|
|
|
| 724 |
const contentType = response.headers.get('Content-Type');
|
| 725 |
if (contentType?.includes('application/json')) {
|
| 726 |
const data = await response.json();
|
| 727 |
+
const responseText = data.response || 'Error: No response generated.';
|
| 728 |
if (streamMsg) {
|
| 729 |
streamMsg.dataset.text = responseText;
|
| 730 |
renderMarkdown(streamMsg);
|
|
|
|
| 737 |
}
|
| 738 |
if (checkAuth() && data.conversation_id) {
|
| 739 |
currentConversationId = data.conversation_id;
|
| 740 |
+
currentConversationTitle = data.conversation_title || 'Untitled Conversation';
|
| 741 |
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
|
| 742 |
history.pushState(null, '', `/chat/${currentConversationId}`);
|
| 743 |
await loadConversations();
|
|
|
|
| 749 |
while (true) {
|
| 750 |
const { done, value } = await reader.read();
|
| 751 |
if (done) {
|
| 752 |
+
if (!buffer.trim()) throw new Error('Empty response from server');
|
| 753 |
break;
|
| 754 |
}
|
| 755 |
buffer += decoder.decode(value, { stream: true });
|
|
|
|
| 800 |
if (uiElements.settingsBtn) {
|
| 801 |
uiElements.settingsBtn.addEventListener('click', () => {
|
| 802 |
if (!checkAuth()) {
|
| 803 |
+
alert('Please log in to access settings.');
|
| 804 |
window.location.href = '/login';
|
| 805 |
return;
|
| 806 |
}
|
|
|
|
| 814 |
localStorage.removeItem('token');
|
| 815 |
window.location.href = '/login';
|
| 816 |
}
|
| 817 |
+
throw new Error('Failed to fetch settings');
|
| 818 |
}
|
| 819 |
return res.json();
|
| 820 |
})
|
|
|
|
| 846 |
});
|
| 847 |
})
|
| 848 |
.catch(err => {
|
| 849 |
+
console.error('Error fetching settings:', err);
|
| 850 |
+
alert('Failed to load settings. Please try again.');
|
| 851 |
});
|
| 852 |
});
|
| 853 |
}
|
|
|
|
| 862 |
uiElements.settingsForm.addEventListener('submit', (e) => {
|
| 863 |
e.preventDefault();
|
| 864 |
if (!checkAuth()) {
|
| 865 |
+
alert('Please log in to save settings.');
|
| 866 |
window.location.href = '/login';
|
| 867 |
return;
|
| 868 |
}
|
|
|
|
| 882 |
localStorage.removeItem('token');
|
| 883 |
window.location.href = '/login';
|
| 884 |
}
|
| 885 |
+
throw new Error('Failed to update settings');
|
| 886 |
}
|
| 887 |
return res.json();
|
| 888 |
})
|
| 889 |
.then(() => {
|
| 890 |
+
alert('Settings updated successfully!');
|
| 891 |
uiElements.settingsModal.classList.add('hidden');
|
| 892 |
toggleSidebar(false);
|
| 893 |
})
|
| 894 |
.catch(err => {
|
| 895 |
+
console.error('Error updating settings:', err);
|
| 896 |
+
alert('Error updating settings: ' + err.message);
|
| 897 |
});
|
| 898 |
});
|
| 899 |
}
|
|
|
|
| 906 |
uiElements.historyToggle.innerHTML = uiElements.conversationList.classList.contains('hidden')
|
| 907 |
? `<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
| 908 |
<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>
|
| 909 |
+
</svg>Show History`
|
| 910 |
: `<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
| 911 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
| 912 |
+
</svg>Hide History`;
|
| 913 |
}
|
| 914 |
});
|
| 915 |
}
|
|
|
|
| 985 |
|
| 986 |
if (uiElements.conversationTitle) {
|
| 987 |
uiElements.conversationTitle.addEventListener('click', () => {
|
| 988 |
+
if (!checkAuth()) return alert('Please log in to edit the conversation title.');
|
| 989 |
+
const newTitle = prompt('Enter new conversation title:', currentConversationTitle || '');
|
| 990 |
if (newTitle && currentConversationId) {
|
| 991 |
updateConversationTitle(currentConversationId, newTitle);
|
| 992 |
}
|