Spaces:
Runtime error
Runtime error
Mark-Lasfar commited on
Commit ·
1b97733
1
Parent(s): d28afad
update main.py
Browse files- main.py +14 -6
- static/js/chat.js +121 -55
- templates/index.html +140 -84
main.py
CHANGED
|
@@ -31,7 +31,7 @@ import re
|
|
| 31 |
import anyio
|
| 32 |
|
| 33 |
# Setup logging
|
| 34 |
-
logging.basicConfig(level=logging.DEBUG)
|
| 35 |
logger = logging.getLogger(__name__)
|
| 36 |
logger.info("Starting application...")
|
| 37 |
logger.debug("Files in current directory: %s", os.listdir(os.getcwd()))
|
|
@@ -73,7 +73,7 @@ async def setup_mongo_index():
|
|
| 73 |
logger.error(f"Failed to create MongoDB index: {e}")
|
| 74 |
|
| 75 |
# Jinja2 setup
|
| 76 |
-
os.makedirs("templates", exist_ok=True)
|
| 77 |
templates = Jinja2Templates(directory="templates")
|
| 78 |
templates.env.filters['markdown'] = lambda text: markdown2.markdown(text)
|
| 79 |
|
|
@@ -117,28 +117,32 @@ app.add_middleware(
|
|
| 117 |
allow_origins=[
|
| 118 |
"https://mgzon-mgzon-app.hf.space",
|
| 119 |
"http://localhost:7860",
|
| 120 |
-
"http://localhost:8000",
|
| 121 |
"https://mgzon-mgzon-app.hf.space/auth/google/callback",
|
| 122 |
"https://mgzon-mgzon-app.hf.space/auth/github/callback",
|
| 123 |
],
|
| 124 |
allow_credentials=True,
|
| 125 |
allow_methods=["GET", "POST", "OPTIONS", "PUT", "DELETE"],
|
| 126 |
-
allow_headers=["Accept", "Content-Type", "Authorization", "X-Requested-With"],
|
| 127 |
)
|
| 128 |
logger.debug("CORS middleware configured with allowed origins")
|
| 129 |
|
| 130 |
# Include routers
|
| 131 |
app.include_router(api_router)
|
| 132 |
-
get_auth_router(app)
|
| 133 |
logger.debug("API and auth routers included")
|
| 134 |
|
| 135 |
# Add logout endpoint
|
| 136 |
@app.get("/logout")
|
| 137 |
async def logout(request: Request):
|
| 138 |
logger.info("User logout requested")
|
|
|
|
| 139 |
request.session.clear()
|
|
|
|
| 140 |
response = RedirectResponse("/login")
|
| 141 |
response.delete_cookie("access_token")
|
|
|
|
|
|
|
| 142 |
return response
|
| 143 |
|
| 144 |
# Debug routes endpoint
|
|
@@ -200,7 +204,11 @@ async def handle_oauth_error(request: Request, exc: GetIdEmailError):
|
|
| 200 |
@app.get("/", response_class=HTMLResponse)
|
| 201 |
async def root(request: Request, user: User = Depends(current_active_user)):
|
| 202 |
logger.debug(f"Root endpoint accessed by user: {user.email if user else 'Anonymous'}")
|
| 203 |
-
return templates.TemplateResponse("index.html", {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
# Google verification
|
| 206 |
@app.get("/google97468ef1f6b6e804.html", response_class=PlainTextResponse)
|
|
|
|
| 31 |
import anyio
|
| 32 |
|
| 33 |
# Setup logging
|
| 34 |
+
logging.basicConfig(level=logging.DEBUG)
|
| 35 |
logger = logging.getLogger(__name__)
|
| 36 |
logger.info("Starting application...")
|
| 37 |
logger.debug("Files in current directory: %s", os.listdir(os.getcwd()))
|
|
|
|
| 73 |
logger.error(f"Failed to create MongoDB index: {e}")
|
| 74 |
|
| 75 |
# Jinja2 setup
|
| 76 |
+
os.makedirs("templates", exist_ok=True)
|
| 77 |
templates = Jinja2Templates(directory="templates")
|
| 78 |
templates.env.filters['markdown'] = lambda text: markdown2.markdown(text)
|
| 79 |
|
|
|
|
| 117 |
allow_origins=[
|
| 118 |
"https://mgzon-mgzon-app.hf.space",
|
| 119 |
"http://localhost:7860",
|
| 120 |
+
"http://localhost:8000",
|
| 121 |
"https://mgzon-mgzon-app.hf.space/auth/google/callback",
|
| 122 |
"https://mgzon-mgzon-app.hf.space/auth/github/callback",
|
| 123 |
],
|
| 124 |
allow_credentials=True,
|
| 125 |
allow_methods=["GET", "POST", "OPTIONS", "PUT", "DELETE"],
|
| 126 |
+
allow_headers=["Accept", "Content-Type", "Authorization", "X-Requested-With", "X-Session-ID"],
|
| 127 |
)
|
| 128 |
logger.debug("CORS middleware configured with allowed origins")
|
| 129 |
|
| 130 |
# Include routers
|
| 131 |
app.include_router(api_router)
|
| 132 |
+
get_auth_router(app)
|
| 133 |
logger.debug("API and auth routers included")
|
| 134 |
|
| 135 |
# Add logout endpoint
|
| 136 |
@app.get("/logout")
|
| 137 |
async def logout(request: Request):
|
| 138 |
logger.info("User logout requested")
|
| 139 |
+
session_data = request.session.copy()
|
| 140 |
request.session.clear()
|
| 141 |
+
logger.debug(f"Cleared session data: {session_data}")
|
| 142 |
response = RedirectResponse("/login")
|
| 143 |
response.delete_cookie("access_token")
|
| 144 |
+
response.delete_cookie("session")
|
| 145 |
+
logger.debug("Session and access_token cookies deleted")
|
| 146 |
return response
|
| 147 |
|
| 148 |
# Debug routes endpoint
|
|
|
|
| 204 |
@app.get("/", response_class=HTMLResponse)
|
| 205 |
async def root(request: Request, user: User = Depends(current_active_user)):
|
| 206 |
logger.debug(f"Root endpoint accessed by user: {user.email if user else 'Anonymous'}")
|
| 207 |
+
return templates.TemplateResponse("index.html", {
|
| 208 |
+
"request": request,
|
| 209 |
+
"user": user,
|
| 210 |
+
"is_authenticated": user is not None
|
| 211 |
+
})
|
| 212 |
|
| 213 |
# Google verification
|
| 214 |
@app.get("/google97468ef1f6b6e804.html", response_class=PlainTextResponse)
|
static/js/chat.js
CHANGED
|
@@ -33,51 +33,24 @@ const uiElements = {
|
|
| 33 |
settingsModal: document.getElementById('settingsModal'),
|
| 34 |
closeSettingsBtn: document.getElementById('closeSettingsBtn'),
|
| 35 |
settingsForm: document.getElementById('settingsForm'),
|
| 36 |
-
historyToggle: document.
|
| 37 |
-
};
|
| 38 |
-
|
| 39 |
-
// Track state
|
| 40 |
-
let streamMsg = null;
|
| 41 |
-
let conversationHistory = JSON.parse(sessionStorage.getItem('conversationHistory') || '[]');
|
| 42 |
-
let currentAssistantText = '';
|
| 43 |
-
let isRequestActive = false;
|
| 44 |
-
let abortController = null;
|
| 45 |
-
let mediaRecorder = null;
|
| 46 |
-
let audioChunks = [];
|
| 47 |
-
let isRecording = false;
|
| 48 |
-
let currentConversationId = window.conversationId || null;
|
| 49 |
-
let currentConversationTitle = window.conversationTitle || null;
|
| 50 |
-
let isSidebarOpen = false;
|
| 51 |
-
|
| 52 |
-
// Auto-resize textarea
|
| 53 |
-
function autoResizeTextarea() {
|
| 54 |
-
if (uiElements.input) {
|
| 55 |
-
uiElements.input.style.height = 'auto';
|
| 56 |
-
uiElements.input.style.height = `${Math.min(uiElements.input.scrollHeight, 200)}px`;
|
| 57 |
-
}
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
// Detect Arabic text
|
| 61 |
-
function isArabicText(text) {
|
| 62 |
-
return /[\u0600-\u06FF]/.test(text);
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
// Initialize page
|
| 66 |
-
document.addEventListener('DOMContentLoaded', async () => {
|
| 67 |
-
AOS.init({
|
| 68 |
duration: 800,
|
| 69 |
easing: 'ease-out-cubic',
|
| 70 |
once: true,
|
| 71 |
offset: 50,
|
| 72 |
});
|
| 73 |
if (currentConversationId && checkAuth()) {
|
|
|
|
| 74 |
await loadConversation(currentConversationId);
|
| 75 |
} else if (conversationHistory.length > 0) {
|
|
|
|
| 76 |
enterChatView();
|
| 77 |
-
conversationHistory.forEach(msg =>
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
autoResizeTextarea();
|
| 83 |
updateSendButtonState();
|
|
@@ -91,7 +64,22 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
| 91 |
|
| 92 |
// Check authentication token
|
| 93 |
function checkAuth() {
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
}
|
| 96 |
|
| 97 |
// Update send button state
|
|
@@ -156,11 +144,15 @@ function addMsg(who, text) {
|
|
| 156 |
const div = document.createElement('div');
|
| 157 |
div.className = `bubble ${who === 'user' ? 'bubble-user' : 'bubble-assist'} ${isArabicText(text) ? 'rtl' : ''}`;
|
| 158 |
div.dataset.text = text;
|
|
|
|
| 159 |
renderMarkdown(div);
|
| 160 |
if (uiElements.chatBox) {
|
| 161 |
uiElements.chatBox.appendChild(div);
|
| 162 |
uiElements.chatBox.classList.remove('hidden');
|
| 163 |
uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight;
|
|
|
|
|
|
|
|
|
|
| 164 |
}
|
| 165 |
return div;
|
| 166 |
}
|
|
@@ -226,14 +218,22 @@ function previewFile() {
|
|
| 226 |
|
| 227 |
// Voice recording
|
| 228 |
function startVoiceRecording() {
|
| 229 |
-
if (isRequestActive || isRecording)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
isRecording = true;
|
| 231 |
if (uiElements.sendBtn) uiElements.sendBtn.classList.add('recording');
|
| 232 |
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
|
| 233 |
mediaRecorder = new MediaRecorder(stream);
|
| 234 |
audioChunks = [];
|
| 235 |
mediaRecorder.start();
|
| 236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
}).catch(err => {
|
| 238 |
console.error('Error accessing microphone:', err);
|
| 239 |
alert('Failed to access microphone. Please check permissions.');
|
|
@@ -248,6 +248,7 @@ function stopVoiceRecording() {
|
|
| 248 |
if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording');
|
| 249 |
isRecording = false;
|
| 250 |
mediaRecorder.addEventListener('stop', async () => {
|
|
|
|
| 251 |
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
| 252 |
const formData = new FormData();
|
| 253 |
formData.append('file', audioBlob, 'voice-message.webm');
|
|
@@ -306,6 +307,8 @@ async function submitAudioMessage(formData) {
|
|
| 306 |
async function sendRequest(endpoint, body, headers = {}) {
|
| 307 |
const token = checkAuth();
|
| 308 |
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
|
|
|
|
|
| 309 |
try {
|
| 310 |
const response = await fetch(endpoint, {
|
| 311 |
method: 'POST',
|
|
@@ -330,9 +333,7 @@ async function sendRequest(endpoint, body, headers = {}) {
|
|
| 330 |
}
|
| 331 |
return response;
|
| 332 |
} catch (error) {
|
| 333 |
-
|
| 334 |
-
throw new Error('Request was aborted');
|
| 335 |
-
}
|
| 336 |
throw error;
|
| 337 |
}
|
| 338 |
}
|
|
@@ -362,6 +363,11 @@ 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;
|
|
@@ -372,6 +378,7 @@ function handleRequestError(error) {
|
|
| 372 |
abortController = null;
|
| 373 |
if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex';
|
| 374 |
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none';
|
|
|
|
| 375 |
}
|
| 376 |
|
| 377 |
// Load conversations for sidebar
|
|
@@ -627,6 +634,11 @@ async function submitMessage() {
|
|
| 627 |
let outputFormat = 'text';
|
| 628 |
let title = null;
|
| 629 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
if (uiElements.fileInput?.files.length > 0) {
|
| 631 |
const file = uiElements.fileInput.files[0];
|
| 632 |
if (file.type.startsWith('image/')) {
|
|
@@ -660,8 +672,6 @@ async function submitMessage() {
|
|
| 660 |
title: title
|
| 661 |
};
|
| 662 |
headers['Content-Type'] = 'application/json';
|
| 663 |
-
} else {
|
| 664 |
-
return;
|
| 665 |
}
|
| 666 |
|
| 667 |
enterChatView();
|
|
@@ -796,6 +806,31 @@ function stopStream(forceCancel = false) {
|
|
| 796 |
if (uiElements.stopBtn) uiElements.stopBtn.style.pointerEvents = 'auto';
|
| 797 |
}
|
| 798 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 799 |
// Settings Modal
|
| 800 |
if (uiElements.settingsBtn) {
|
| 801 |
uiElements.settingsBtn.addEventListener('click', () => {
|
|
@@ -933,36 +968,60 @@ if (uiElements.fileInput) uiElements.fileInput.addEventListener('change', previe
|
|
| 933 |
if (uiElements.audioInput) uiElements.audioInput.addEventListener('change', previewFile);
|
| 934 |
|
| 935 |
if (uiElements.sendBtn) {
|
| 936 |
-
uiElements.sendBtn.addEventListener('
|
|
|
|
| 937 |
if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return;
|
| 938 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 939 |
});
|
| 940 |
-
uiElements.sendBtn.addEventListener('
|
| 941 |
-
|
| 942 |
-
|
|
|
|
|
|
|
|
|
|
| 943 |
e.preventDefault();
|
| 944 |
if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return;
|
| 945 |
-
|
|
|
|
|
|
|
| 946 |
});
|
| 947 |
-
uiElements.sendBtn.addEventListener('touchend', e => {
|
| 948 |
e.preventDefault();
|
|
|
|
| 949 |
if (isRecording) stopVoiceRecording();
|
| 950 |
});
|
| 951 |
-
uiElements.sendBtn.addEventListener('touchcancel', e => {
|
| 952 |
e.preventDefault();
|
|
|
|
| 953 |
if (isRecording) stopVoiceRecording();
|
| 954 |
});
|
| 955 |
}
|
| 956 |
|
| 957 |
if (uiElements.form) {
|
| 958 |
-
uiElements.form.addEventListener('submit', e => {
|
| 959 |
e.preventDefault();
|
| 960 |
-
if (!isRecording
|
|
|
|
|
|
|
| 961 |
});
|
| 962 |
}
|
| 963 |
|
| 964 |
if (uiElements.input) {
|
| 965 |
-
uiElements.input.addEventListener('keydown', e => {
|
| 966 |
if (e.key === 'Enter' && !e.shiftKey) {
|
| 967 |
e.preventDefault();
|
| 968 |
if (!isRecording && !uiElements.sendBtn.disabled) submitMessage();
|
|
@@ -1000,3 +1059,10 @@ if (uiElements.sidebarToggle) {
|
|
| 1000 |
if (uiElements.newConversationBtn) {
|
| 1001 |
uiElements.newConversationBtn.addEventListener('click', createNewConversation);
|
| 1002 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
settingsModal: document.getElementById('settingsModal'),
|
| 34 |
closeSettingsBtn: document.getElementById('closeSettingsBtn'),
|
| 35 |
settingsForm: document.getElementById('settingsForm'),
|
| 36 |
+
historyToggle: document.getElementBy AOS.init({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
duration: 800,
|
| 38 |
easing: 'ease-out-cubic',
|
| 39 |
once: true,
|
| 40 |
offset: 50,
|
| 41 |
});
|
| 42 |
if (currentConversationId && checkAuth()) {
|
| 43 |
+
console.log('Loading conversation with ID:', currentConversationId);
|
| 44 |
await loadConversation(currentConversationId);
|
| 45 |
} else if (conversationHistory.length > 0) {
|
| 46 |
+
console.log('Restoring conversation history from sessionStorage:', conversationHistory);
|
| 47 |
enterChatView();
|
| 48 |
+
conversationHistory.forEach(msg => {
|
| 49 |
+
console.log('Adding message from history:', msg);
|
| 50 |
+
addMsg(msg.role, msg.content);
|
| 51 |
+
});
|
| 52 |
+
} else {
|
| 53 |
+
console.log('No conversation history or ID, starting fresh');
|
| 54 |
}
|
| 55 |
autoResizeTextarea();
|
| 56 |
updateSendButtonState();
|
|
|
|
| 64 |
|
| 65 |
// Check authentication token
|
| 66 |
function checkAuth() {
|
| 67 |
+
const token = localStorage.getItem('token');
|
| 68 |
+
console.log('Auth token:', token ? 'Found' : 'Not found');
|
| 69 |
+
return token;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Handle session for non-logged-in users
|
| 73 |
+
async function handleSession() {
|
| 74 |
+
const sessionId = sessionStorage.getItem('session_id');
|
| 75 |
+
if (!sessionId) {
|
| 76 |
+
const newSessionId = crypto.randomUUID();
|
| 77 |
+
sessionStorage.setItem('session_id', newSessionId);
|
| 78 |
+
console.log('New session_id created:', newSessionId);
|
| 79 |
+
return newSessionId;
|
| 80 |
+
}
|
| 81 |
+
console.log('Existing session_id:', sessionId);
|
| 82 |
+
return sessionId;
|
| 83 |
}
|
| 84 |
|
| 85 |
// Update send button state
|
|
|
|
| 144 |
const div = document.createElement('div');
|
| 145 |
div.className = `bubble ${who === 'user' ? 'bubble-user' : 'bubble-assist'} ${isArabicText(text) ? 'rtl' : ''}`;
|
| 146 |
div.dataset.text = text;
|
| 147 |
+
console.log('Adding message:', { who, text });
|
| 148 |
renderMarkdown(div);
|
| 149 |
if (uiElements.chatBox) {
|
| 150 |
uiElements.chatBox.appendChild(div);
|
| 151 |
uiElements.chatBox.classList.remove('hidden');
|
| 152 |
uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight;
|
| 153 |
+
console.log('Message added to chatBox:', div);
|
| 154 |
+
} else {
|
| 155 |
+
console.error('chatBox is null');
|
| 156 |
}
|
| 157 |
return div;
|
| 158 |
}
|
|
|
|
| 218 |
|
| 219 |
// Voice recording
|
| 220 |
function startVoiceRecording() {
|
| 221 |
+
if (isRequestActive || isRecording) {
|
| 222 |
+
console.log('Voice recording blocked: Request active or already recording');
|
| 223 |
+
return;
|
| 224 |
+
}
|
| 225 |
+
console.log('Starting voice recording...');
|
| 226 |
isRecording = true;
|
| 227 |
if (uiElements.sendBtn) uiElements.sendBtn.classList.add('recording');
|
| 228 |
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
|
| 229 |
mediaRecorder = new MediaRecorder(stream);
|
| 230 |
audioChunks = [];
|
| 231 |
mediaRecorder.start();
|
| 232 |
+
console.log('MediaRecorder started');
|
| 233 |
+
mediaRecorder.addEventListener('dataavailable', event => {
|
| 234 |
+
audioChunks.push(event.data);
|
| 235 |
+
console.log('Audio chunk received:', event.data);
|
| 236 |
+
});
|
| 237 |
}).catch(err => {
|
| 238 |
console.error('Error accessing microphone:', err);
|
| 239 |
alert('Failed to access microphone. Please check permissions.');
|
|
|
|
| 248 |
if (uiElements.sendBtn) uiElements.sendBtn.classList.remove('recording');
|
| 249 |
isRecording = false;
|
| 250 |
mediaRecorder.addEventListener('stop', async () => {
|
| 251 |
+
console.log('Stopping voice recording, sending audio...');
|
| 252 |
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
| 253 |
const formData = new FormData();
|
| 254 |
formData.append('file', audioBlob, 'voice-message.webm');
|
|
|
|
| 307 |
async function sendRequest(endpoint, body, headers = {}) {
|
| 308 |
const token = checkAuth();
|
| 309 |
if (token) headers['Authorization'] = `Bearer ${token}`;
|
| 310 |
+
headers['X-Session-ID'] = await handleSession();
|
| 311 |
+
console.log('Sending request to:', endpoint, 'with headers:', headers);
|
| 312 |
try {
|
| 313 |
const response = await fetch(endpoint, {
|
| 314 |
method: 'POST',
|
|
|
|
| 333 |
}
|
| 334 |
return response;
|
| 335 |
} catch (error) {
|
| 336 |
+
console.error('Send request error:', error);
|
|
|
|
|
|
|
| 337 |
throw error;
|
| 338 |
}
|
| 339 |
}
|
|
|
|
| 363 |
if (streamMsg) {
|
| 364 |
streamMsg.querySelector('.loading')?.remove();
|
| 365 |
streamMsg.dataset.text = `Error: ${error.message || 'An error occurred during the request.'}`;
|
| 366 |
+
const retryBtn = document.createElement('button');
|
| 367 |
+
retryBtn.innerText = 'Retry';
|
| 368 |
+
retryBtn.className = 'retry-btn text-sm text-blue-400 hover:text-blue-600';
|
| 369 |
+
retryBtn.onclick = () => submitMessage();
|
| 370 |
+
streamMsg.appendChild(retryBtn);
|
| 371 |
renderMarkdown(streamMsg);
|
| 372 |
streamMsg.dataset.done = '1';
|
| 373 |
streamMsg = null;
|
|
|
|
| 378 |
abortController = null;
|
| 379 |
if (uiElements.sendBtn) uiElements.sendBtn.style.display = 'inline-flex';
|
| 380 |
if (uiElements.stopBtn) uiElements.stopBtn.style.display = 'none';
|
| 381 |
+
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
|
| 382 |
}
|
| 383 |
|
| 384 |
// Load conversations for sidebar
|
|
|
|
| 634 |
let outputFormat = 'text';
|
| 635 |
let title = null;
|
| 636 |
|
| 637 |
+
if (!message && !uiElements.fileInput?.files.length && !uiElements.audioInput?.files.length) {
|
| 638 |
+
console.log('No message, file, or audio to send');
|
| 639 |
+
return;
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
if (uiElements.fileInput?.files.length > 0) {
|
| 643 |
const file = uiElements.fileInput.files[0];
|
| 644 |
if (file.type.startsWith('image/')) {
|
|
|
|
| 672 |
title: title
|
| 673 |
};
|
| 674 |
headers['Content-Type'] = 'application/json';
|
|
|
|
|
|
|
| 675 |
}
|
| 676 |
|
| 677 |
enterChatView();
|
|
|
|
| 806 |
if (uiElements.stopBtn) uiElements.stopBtn.style.pointerEvents = 'auto';
|
| 807 |
}
|
| 808 |
|
| 809 |
+
// Logout handler
|
| 810 |
+
const logoutBtn = document.getElementById('logoutBtn');
|
| 811 |
+
if (logoutBtn) {
|
| 812 |
+
logoutBtn.addEventListener('click', async () => {
|
| 813 |
+
console.log('Logout button clicked');
|
| 814 |
+
try {
|
| 815 |
+
const response = await fetch('/logout', {
|
| 816 |
+
method: 'GET',
|
| 817 |
+
credentials: 'include'
|
| 818 |
+
});
|
| 819 |
+
if (response.ok) {
|
| 820 |
+
localStorage.removeItem('token');
|
| 821 |
+
console.log('Token removed from localStorage');
|
| 822 |
+
window.location.href = '/login';
|
| 823 |
+
} else {
|
| 824 |
+
console.error('Logout failed:', response.status);
|
| 825 |
+
alert('Failed to log out. Please try again.');
|
| 826 |
+
}
|
| 827 |
+
} catch (error) {
|
| 828 |
+
console.error('Logout error:', error);
|
| 829 |
+
alert('Error during logout: ' + error.message);
|
| 830 |
+
}
|
| 831 |
+
});
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
// Settings Modal
|
| 835 |
if (uiElements.settingsBtn) {
|
| 836 |
uiElements.settingsBtn.addEventListener('click', () => {
|
|
|
|
| 968 |
if (uiElements.audioInput) uiElements.audioInput.addEventListener('change', previewFile);
|
| 969 |
|
| 970 |
if (uiElements.sendBtn) {
|
| 971 |
+
uiElements.sendBtn.addEventListener('click', (e) => {
|
| 972 |
+
e.preventDefault();
|
| 973 |
if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return;
|
| 974 |
+
if (uiElements.input.value.trim()) {
|
| 975 |
+
submitMessage();
|
| 976 |
+
}
|
| 977 |
+
});
|
| 978 |
+
|
| 979 |
+
let pressTimer;
|
| 980 |
+
uiElements.sendBtn.addEventListener('mousedown', (e) => {
|
| 981 |
+
if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return;
|
| 982 |
+
pressTimer = setTimeout(() => {
|
| 983 |
+
startVoiceRecording();
|
| 984 |
+
}, 500);
|
| 985 |
+
});
|
| 986 |
+
uiElements.sendBtn.addEventListener('mouseup', () => {
|
| 987 |
+
clearTimeout(pressTimer);
|
| 988 |
+
if (isRecording) stopVoiceRecording();
|
| 989 |
});
|
| 990 |
+
uiElements.sendBtn.addEventListener('mouseleave', () => {
|
| 991 |
+
clearTimeout(pressTimer);
|
| 992 |
+
if (isRecording) stopVoiceRecording();
|
| 993 |
+
});
|
| 994 |
+
|
| 995 |
+
uiElements.sendBtn.addEventListener('touchstart', (e) => {
|
| 996 |
e.preventDefault();
|
| 997 |
if (uiElements.sendBtn.disabled || isRequestActive || isRecording) return;
|
| 998 |
+
pressTimer = setTimeout(() => {
|
| 999 |
+
startVoiceRecording();
|
| 1000 |
+
}, 500);
|
| 1001 |
});
|
| 1002 |
+
uiElements.sendBtn.addEventListener('touchend', (e) => {
|
| 1003 |
e.preventDefault();
|
| 1004 |
+
clearTimeout(pressTimer);
|
| 1005 |
if (isRecording) stopVoiceRecording();
|
| 1006 |
});
|
| 1007 |
+
uiElements.sendBtn.addEventListener('touchcancel', (e) => {
|
| 1008 |
e.preventDefault();
|
| 1009 |
+
clearTimeout(pressTimer);
|
| 1010 |
if (isRecording) stopVoiceRecording();
|
| 1011 |
});
|
| 1012 |
}
|
| 1013 |
|
| 1014 |
if (uiElements.form) {
|
| 1015 |
+
uiElements.form.addEventListener('submit', (e) => {
|
| 1016 |
e.preventDefault();
|
| 1017 |
+
if (!isRecording && uiElements.input.value.trim()) {
|
| 1018 |
+
submitMessage();
|
| 1019 |
+
}
|
| 1020 |
});
|
| 1021 |
}
|
| 1022 |
|
| 1023 |
if (uiElements.input) {
|
| 1024 |
+
uiElements.input.addEventListener('keydown', (e) => {
|
| 1025 |
if (e.key === 'Enter' && !e.shiftKey) {
|
| 1026 |
e.preventDefault();
|
| 1027 |
if (!isRecording && !uiElements.sendBtn.disabled) submitMessage();
|
|
|
|
| 1059 |
if (uiElements.newConversationBtn) {
|
| 1060 |
uiElements.newConversationBtn.addEventListener('click', createNewConversation);
|
| 1061 |
}
|
| 1062 |
+
|
| 1063 |
+
// Debug localStorage
|
| 1064 |
+
const originalRemoveItem = localStorage.removeItem;
|
| 1065 |
+
localStorage.removeItem = function(key) {
|
| 1066 |
+
console.log('Removing from localStorage:', key);
|
| 1067 |
+
originalRemoveItem.apply(this, arguments);
|
| 1068 |
+
};
|
templates/index.html
CHANGED
|
@@ -4,24 +4,20 @@
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<meta name="description" content="MGZon Chatbot – AI-powered assistant for code generation, web search, and e-commerce solutions by Mark Al-Asfar from Alexandria, Egypt.">
|
| 7 |
-
<meta name="keywords" content="MGZon Chatbot, AI assistant, code generation, e-commerce, Mark Al-Asfar,
|
| 8 |
-
United States, FastAPI, Gradio">
|
| 9 |
<meta name="author" content="Mark Al-Asfar">
|
| 10 |
<meta name="robots" content="index, follow">
|
| 11 |
<title>MGZon Chatbot – Powered by AI</title>
|
| 12 |
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
| 13 |
-
<!-- manifest for Android/Chrome -->
|
| 14 |
-
<link rel="manifest" href="/static/manifest.json">
|
| 15 |
-
|
| 16 |
-
<
|
| 17 |
-
<
|
| 18 |
-
<meta name="apple-mobile-web-app-
|
| 19 |
-
<meta name="apple-mobile-web-app-
|
| 20 |
-
<
|
| 21 |
-
|
| 22 |
-
<!-- General Theme -->
|
| 23 |
-
<meta name="theme-color" content="#2d3748">
|
| 24 |
-
|
| 25 |
<!-- Open Graph -->
|
| 26 |
<meta property="og:title" content="MGZon Chatbot – AI-Powered Solutions">
|
| 27 |
<meta property="og:description" content="Explore MGZon Chatbot for code generation, web search, and e-commerce solutions by Mark Al-Asfar.">
|
|
@@ -34,29 +30,29 @@ United States, FastAPI, Gradio">
|
|
| 34 |
<meta name="twitter:description" content="Explore MGZon Chatbot for code generation, web search, and e-commerce solutions by Mark Al-Asfar.">
|
| 35 |
<meta name="twitter:image" content="/static/images/mg.svg">
|
| 36 |
<!-- JSON-LD -->
|
| 37 |
-
<script type="application/ld+json">
|
| 38 |
-
{
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
| 56 |
}
|
| 57 |
}
|
| 58 |
-
|
| 59 |
-
</script>
|
| 60 |
<!-- Tailwind (v3) -->
|
| 61 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 62 |
<!-- Boxicons -->
|
|
@@ -134,8 +130,13 @@ United States, FastAPI, Gradio">
|
|
| 134 |
<a href="/" class="px-4 py-2 rounded-lg bg-emerald-600">Home</a>
|
| 135 |
<a href="/docs" class="px-4 py-2 rounded-lg hover:bg-emerald-600 transition">API Documentation</a>
|
| 136 |
<a href="/about" class="px-4 py-2 rounded-lg hover:bg-emerald-600 transition">About MGZon</a>
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
<a href="https://hager-zon.vercel.app/community" class="px-4 py-2 rounded-lg hover:bg-emerald-600 transition">Community</a>
|
| 140 |
<a href="https://mark-elasfar.web.app/" target="_blank" class="px-4 py-2 rounded-lg hover:bg-emerald-600 transition">Mark Al-Asfar</a>
|
| 141 |
</nav>
|
|
@@ -150,17 +151,15 @@ United States, FastAPI, Gradio">
|
|
| 150 |
Welcome to MGZon Chatbot 🚀
|
| 151 |
</h1>
|
| 152 |
<p class="text-lg max-w-2xl mx-auto mb-8">
|
| 153 |
-
MGZon Chatbot is an AI-powered assistant for code generation, web search, and e-commerce solutions. Built with Gradio and FastAPI by Mark Al-Asfar from
|
| 154 |
-
United States. Ready to code smarter and shop better?
|
| 155 |
-
Discover MGZon Chatbot, an AI-powered assistant by Mark Al-Asfar for code generation, real-time web search, and e-commerce automation. Built with FastAPI and Hugging Face AI, MGZon rivals tools like ChatGPT, Grok, and DeepSeek.
|
| 156 |
</p>
|
| 157 |
-
{% if
|
| 158 |
<div class="flex justify-center gap-4">
|
| 159 |
<a href="/chat" id="launchBtn" class="inline-flex items-center bg-gradient-to-r from-emerald-500 to-teal-600 text-white px-8 py-4 rounded-full text-lg font-semibold hover:scale-105 transition-transform shadow-lg hover:shadow-xl">
|
| 160 |
Launch Chatbot <i class="bx bx-rocket ml-2"></i>
|
| 161 |
<span id="spinner" class="loading hidden"></span>
|
| 162 |
</a>
|
| 163 |
-
<a href="/
|
| 164 |
Logout <i class="bx bx-log-out ml-2"></i>
|
| 165 |
</a>
|
| 166 |
</div>
|
|
@@ -246,7 +245,7 @@ United States. Ready to code smarter and shop better?
|
|
| 246 |
<!-- Footer -->
|
| 247 |
<footer class="bg-gradient-to-r from-teal-900 to-emerald-900 py-12 mt-8">
|
| 248 |
<div class="container max-w-6xl mx-auto text-center">
|
| 249 |
-
|
| 250 |
<p class="mb-4">
|
| 251 |
Developed by <a href="https://mark-elasfar.web.app/" target="_blank" class="text-emerald-300 hover:underline">Mark Al-Asfar</a>
|
| 252 |
| Powered by <a href="https://hager-zon.vercel.app/" target="_blank" class="text-emerald-300 hover:underline">MGZon AI</a>
|
|
@@ -314,75 +313,132 @@ United States. Ready to code smarter and shop better?
|
|
| 314 |
</div>
|
| 315 |
<p class="mt-6">© 2025 Mark Al-Asfar & MGZon AI. All rights reserved.</p>
|
| 316 |
</div>
|
| 317 |
-
|
| 318 |
📲 Install MG Chat
|
| 319 |
</button>
|
| 320 |
</footer>
|
| 321 |
<script>
|
|
|
|
| 322 |
const sidebar = document.getElementById('sidebar');
|
| 323 |
const toggleBtn = document.getElementById('sidebarToggle');
|
| 324 |
const closeSidebarBtn = document.getElementById('closeSidebar');
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
document.addEventListener('click', (e) => {
|
| 336 |
if (!sidebar.contains(e.target) && !toggleBtn.contains(e.target) && window.innerWidth < 768) {
|
| 337 |
sidebar.classList.remove('active');
|
| 338 |
sidebar.classList.add('-translate-x-full');
|
| 339 |
sidebar.classList.remove('translate-x-0');
|
|
|
|
| 340 |
}
|
| 341 |
});
|
|
|
|
|
|
|
| 342 |
function showCardDetails(cardId) {
|
| 343 |
document.getElementById(`${cardId}-details`).classList.remove('hidden');
|
| 344 |
}
|
|
|
|
| 345 |
function closeCardDetails(cardId) {
|
| 346 |
document.getElementById(`${cardId}-details`).classList.add('hidden');
|
| 347 |
}
|
|
|
|
|
|
|
| 348 |
const launchBtn = document.getElementById('launchBtn');
|
| 349 |
const spinner = document.getElementById('spinner');
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
if ('serviceWorker' in navigator) {
|
| 356 |
-
navigator.serviceWorker.register('/static/js/sw.js')
|
| 357 |
-
.then(function(reg) {
|
| 358 |
-
console.log('✅ Service Worker Registered', reg);
|
| 359 |
-
}).catch(function(err) {
|
| 360 |
-
console.error('❌ Service Worker registration failed', err);
|
| 361 |
});
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
// التعامل مع حدث beforeinstallprompt لتثبيت التطبيق
|
| 365 |
-
let deferredPrompt;
|
| 366 |
-
window.addEventListener('beforeinstallprompt', (e) => {
|
| 367 |
-
e.preventDefault();
|
| 368 |
-
deferredPrompt = e;
|
| 369 |
-
const installBtn = document.getElementById('installAppBtn');
|
| 370 |
-
if (installBtn) {
|
| 371 |
-
installBtn.style.display = 'block';
|
| 372 |
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
} else {
|
| 379 |
-
console.
|
|
|
|
| 380 |
}
|
| 381 |
-
|
| 382 |
-
|
|
|
|
|
|
|
| 383 |
});
|
| 384 |
}
|
| 385 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
</script>
|
| 387 |
</body>
|
| 388 |
</html>
|
|
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<meta name="description" content="MGZon Chatbot – AI-powered assistant for code generation, web search, and e-commerce solutions by Mark Al-Asfar from Alexandria, Egypt.">
|
| 7 |
+
<meta name="keywords" content="MGZon Chatbot, AI assistant, code generation, e-commerce, Mark Al-Asfar, United States, FastAPI, Gradio">
|
|
|
|
| 8 |
<meta name="author" content="Mark Al-Asfar">
|
| 9 |
<meta name="robots" content="index, follow">
|
| 10 |
<title>MGZon Chatbot – Powered by AI</title>
|
| 11 |
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
| 12 |
+
<!-- manifest for Android/Chrome -->
|
| 13 |
+
<link rel="manifest" href="/static/manifest.json">
|
| 14 |
+
<!-- iOS Web App Support -->
|
| 15 |
+
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/mg-180.png">
|
| 16 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
| 17 |
+
<meta name="apple-mobile-web-app-title" content="MGZon">
|
| 18 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
| 19 |
+
<!-- General Theme -->
|
| 20 |
+
<meta name="theme-color" content="#2d3748">
|
|
|
|
|
|
|
|
|
|
| 21 |
<!-- Open Graph -->
|
| 22 |
<meta property="og:title" content="MGZon Chatbot – AI-Powered Solutions">
|
| 23 |
<meta property="og:description" content="Explore MGZon Chatbot for code generation, web search, and e-commerce solutions by Mark Al-Asfar.">
|
|
|
|
| 30 |
<meta name="twitter:description" content="Explore MGZon Chatbot for code generation, web search, and e-commerce solutions by Mark Al-Asfar.">
|
| 31 |
<meta name="twitter:image" content="/static/images/mg.svg">
|
| 32 |
<!-- JSON-LD -->
|
| 33 |
+
<script type="application/ld+json">
|
| 34 |
+
{
|
| 35 |
+
"@context": "https://schema.org",
|
| 36 |
+
"@type": "WebSite",
|
| 37 |
+
"name": "MGZon Chatbot",
|
| 38 |
+
"url": "https://mgzon-mgzon-app.hf.space/",
|
| 39 |
+
"description": "MGZon Chatbot by Mark Al-Asfar: Your AI assistant for code generation, real-time web search, and e-commerce solutions.",
|
| 40 |
+
"keywords": ["MGZon Chatbot", "AI chatbot", "Code generation AI", "E-commerce AI solutions", "Mark Al-Asfar", "AI assistant for developers", "FastAPI chatbot", "Real-time web search AI", "MGZon AI", "Python AI chatbot", "AI for coding", "E-commerce automation", "Hugging Face AI", "2025 AI trends", "chatgpt", "grok", "deepseek", "text generation"],
|
| 41 |
+
"potentialAction": {
|
| 42 |
+
"@type": "SearchAction",
|
| 43 |
+
"target": "https://mgzon-mgzon-app.hf.space/?q={search_term_string}",
|
| 44 |
+
"query-input": "required name=search_term_string"
|
| 45 |
+
},
|
| 46 |
+
"publisher": {
|
| 47 |
+
"@type": "Organization",
|
| 48 |
+
"name": "MGZon AI",
|
| 49 |
+
"logo": {
|
| 50 |
+
"@type": "ImageObject",
|
| 51 |
+
"url": "/static/images/mg.svg"
|
| 52 |
+
}
|
| 53 |
}
|
| 54 |
}
|
| 55 |
+
</script>
|
|
|
|
| 56 |
<!-- Tailwind (v3) -->
|
| 57 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 58 |
<!-- Boxicons -->
|
|
|
|
| 130 |
<a href="/" class="px-4 py-2 rounded-lg bg-emerald-600">Home</a>
|
| 131 |
<a href="/docs" class="px-4 py-2 rounded-lg hover:bg-emerald-600 transition">API Documentation</a>
|
| 132 |
<a href="/about" class="px-4 py-2 rounded-lg hover:bg-emerald-600 transition">About MGZon</a>
|
| 133 |
+
{% if is_authenticated %}
|
| 134 |
+
<a href="/chat" class="px-4 py-2 rounded-lg hover:bg-emerald-600 transition">Chat</a>
|
| 135 |
+
<a id="logoutBtn" href="/logout" class="px-4 py-2 rounded-lg hover:bg-red-600 transition">Logout</a>
|
| 136 |
+
{% else %}
|
| 137 |
+
<a href="/login" class="px-4 py-2 rounded-lg hover:bg-emerald-600 transition">Login</a>
|
| 138 |
+
<a href="/register" class="px-4 py-2 rounded-lg hover:bg-emerald-600 transition">Register</a>
|
| 139 |
+
{% endif %}
|
| 140 |
<a href="https://hager-zon.vercel.app/community" class="px-4 py-2 rounded-lg hover:bg-emerald-600 transition">Community</a>
|
| 141 |
<a href="https://mark-elasfar.web.app/" target="_blank" class="px-4 py-2 rounded-lg hover:bg-emerald-600 transition">Mark Al-Asfar</a>
|
| 142 |
</nav>
|
|
|
|
| 151 |
Welcome to MGZon Chatbot 🚀
|
| 152 |
</h1>
|
| 153 |
<p class="text-lg max-w-2xl mx-auto mb-8">
|
| 154 |
+
MGZon Chatbot is an AI-powered assistant for code generation, web search, and e-commerce solutions. Built with Gradio and FastAPI by Mark Al-Asfar from United States. Ready to code smarter and shop better? Discover MGZon Chatbot, an AI-powered assistant by Mark Al-Asfar for code generation, real-time web search, and e-commerce automation. Built with FastAPI and Hugging Face AI, MGZon rivals tools like ChatGPT, Grok, and DeepSeek.
|
|
|
|
|
|
|
| 155 |
</p>
|
| 156 |
+
{% if is_authenticated %}
|
| 157 |
<div class="flex justify-center gap-4">
|
| 158 |
<a href="/chat" id="launchBtn" class="inline-flex items-center bg-gradient-to-r from-emerald-500 to-teal-600 text-white px-8 py-4 rounded-full text-lg font-semibold hover:scale-105 transition-transform shadow-lg hover:shadow-xl">
|
| 159 |
Launch Chatbot <i class="bx bx-rocket ml-2"></i>
|
| 160 |
<span id="spinner" class="loading hidden"></span>
|
| 161 |
</a>
|
| 162 |
+
<a id="logoutBtn" href="/logout" class="inline-flex items-center bg-gradient-to-r from-red-500 to-orange-600 text-white px-8 py-4 rounded-full text-lg font-semibold hover:scale-105 transition-transform shadow-lg hover:shadow-xl">
|
| 163 |
Logout <i class="bx bx-log-out ml-2"></i>
|
| 164 |
</a>
|
| 165 |
</div>
|
|
|
|
| 245 |
<!-- Footer -->
|
| 246 |
<footer class="bg-gradient-to-r from-teal-900 to-emerald-900 py-12 mt-8">
|
| 247 |
<div class="container max-w-6xl mx-auto text-center">
|
| 248 |
+
<img src="/static/images/mg.svg" alt="MGZon Chatbot Logo by Mark Al-Asfar" class="w-32 h-32 mx-auto mb-6 animate-bounce">
|
| 249 |
<p class="mb-4">
|
| 250 |
Developed by <a href="https://mark-elasfar.web.app/" target="_blank" class="text-emerald-300 hover:underline">Mark Al-Asfar</a>
|
| 251 |
| Powered by <a href="https://hager-zon.vercel.app/" target="_blank" class="text-emerald-300 hover:underline">MGZon AI</a>
|
|
|
|
| 313 |
</div>
|
| 314 |
<p class="mt-6">© 2025 Mark Al-Asfar & MGZon AI. All rights reserved.</p>
|
| 315 |
</div>
|
| 316 |
+
<button id="installAppBtn" style="display: none;" class="fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg z-50">
|
| 317 |
📲 Install MG Chat
|
| 318 |
</button>
|
| 319 |
</footer>
|
| 320 |
<script>
|
| 321 |
+
// Sidebar toggle
|
| 322 |
const sidebar = document.getElementById('sidebar');
|
| 323 |
const toggleBtn = document.getElementById('sidebarToggle');
|
| 324 |
const closeSidebarBtn = document.getElementById('closeSidebar');
|
| 325 |
+
|
| 326 |
+
if (toggleBtn && sidebar) {
|
| 327 |
+
toggleBtn.addEventListener('click', () => {
|
| 328 |
+
sidebar.classList.toggle('active');
|
| 329 |
+
sidebar.classList.toggle('-translate-x-full');
|
| 330 |
+
sidebar.classList.toggle('translate-x-0');
|
| 331 |
+
console.log('Sidebar toggled');
|
| 332 |
+
});
|
| 333 |
+
} else {
|
| 334 |
+
console.warn('Sidebar or toggle button not found');
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
if (closeSidebarBtn && sidebar) {
|
| 338 |
+
closeSidebarBtn.addEventListener('click', () => {
|
| 339 |
+
sidebar.classList.remove('active');
|
| 340 |
+
sidebar.classList.add('-translate-x-full');
|
| 341 |
+
sidebar.classList.remove('translate-x-0');
|
| 342 |
+
console.log('Sidebar closed');
|
| 343 |
+
});
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
// Close sidebar when clicking outside on mobile
|
| 347 |
document.addEventListener('click', (e) => {
|
| 348 |
if (!sidebar.contains(e.target) && !toggleBtn.contains(e.target) && window.innerWidth < 768) {
|
| 349 |
sidebar.classList.remove('active');
|
| 350 |
sidebar.classList.add('-translate-x-full');
|
| 351 |
sidebar.classList.remove('translate-x-0');
|
| 352 |
+
console.log('Sidebar closed due to outside click');
|
| 353 |
}
|
| 354 |
});
|
| 355 |
+
|
| 356 |
+
// Show/hide card details
|
| 357 |
function showCardDetails(cardId) {
|
| 358 |
document.getElementById(`${cardId}-details`).classList.remove('hidden');
|
| 359 |
}
|
| 360 |
+
|
| 361 |
function closeCardDetails(cardId) {
|
| 362 |
document.getElementById(`${cardId}-details`).classList.add('hidden');
|
| 363 |
}
|
| 364 |
+
|
| 365 |
+
// Launch button spinner
|
| 366 |
const launchBtn = document.getElementById('launchBtn');
|
| 367 |
const spinner = document.getElementById('spinner');
|
| 368 |
+
if (launchBtn && spinner) {
|
| 369 |
+
launchBtn.addEventListener('click', (e) => {
|
| 370 |
+
spinner.classList.remove('hidden');
|
| 371 |
+
setTimeout(() => spinner.classList.add('hidden'), 2000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
});
|
| 373 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
|
| 375 |
+
// Logout handler
|
| 376 |
+
const logoutBtn = document.getElementById('logoutBtn');
|
| 377 |
+
if (logoutBtn) {
|
| 378 |
+
logoutBtn.addEventListener('click', async (e) => {
|
| 379 |
+
e.preventDefault();
|
| 380 |
+
console.log('Logout button clicked');
|
| 381 |
+
try {
|
| 382 |
+
const response = await fetch('/logout', {
|
| 383 |
+
method: 'GET',
|
| 384 |
+
credentials: 'include'
|
| 385 |
+
});
|
| 386 |
+
if (response.ok) {
|
| 387 |
+
localStorage.removeItem('token');
|
| 388 |
+
console.log('Token removed from localStorage');
|
| 389 |
+
window.location.href = '/login';
|
| 390 |
} else {
|
| 391 |
+
console.error('Logout failed:', response.status);
|
| 392 |
+
alert('Failed to log out. Please try again.');
|
| 393 |
}
|
| 394 |
+
} catch (error) {
|
| 395 |
+
console.error('Logout error:', error);
|
| 396 |
+
alert('Error during logout: ' + error.message);
|
| 397 |
+
}
|
| 398 |
});
|
| 399 |
}
|
| 400 |
+
|
| 401 |
+
// Service Worker for PWA
|
| 402 |
+
if ('serviceWorker' in navigator) {
|
| 403 |
+
navigator.serviceWorker.register('/static/js/sw.js')
|
| 404 |
+
.then(function(reg) {
|
| 405 |
+
console.log('✅ Service Worker Registered', reg);
|
| 406 |
+
}).catch(function(err) {
|
| 407 |
+
console.error('❌ Service Worker registration failed', err);
|
| 408 |
+
});
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
// Handle beforeinstallprompt for PWA
|
| 412 |
+
let deferredPrompt;
|
| 413 |
+
window.addEventListener('beforeinstallprompt', (e) => {
|
| 414 |
+
e.preventDefault();
|
| 415 |
+
deferredPrompt = e;
|
| 416 |
+
const installBtn = document.getElementById('installAppBtn');
|
| 417 |
+
if (installBtn) {
|
| 418 |
+
installBtn.style.display = 'block';
|
| 419 |
+
installBtn.addEventListener('click', () => {
|
| 420 |
+
deferredPrompt.prompt();
|
| 421 |
+
deferredPrompt.userChoice.then(choice => {
|
| 422 |
+
if (choice.outcome === 'accepted') {
|
| 423 |
+
console.log('✅ User accepted the install prompt');
|
| 424 |
+
} else {
|
| 425 |
+
console.log('❌ User dismissed the install prompt');
|
| 426 |
+
}
|
| 427 |
+
deferredPrompt = null;
|
| 428 |
+
});
|
| 429 |
+
});
|
| 430 |
+
}
|
| 431 |
+
});
|
| 432 |
+
|
| 433 |
+
// Card animations
|
| 434 |
+
document.querySelectorAll('.glass').forEach(card => {
|
| 435 |
+
card.addEventListener('mouseenter', () => {
|
| 436 |
+
card.style.transform = 'scale(1.05) rotate(1deg)';
|
| 437 |
+
});
|
| 438 |
+
card.addEventListener('mouseleave', () => {
|
| 439 |
+
card.style.transform = 'scale(1) rotate(0deg)';
|
| 440 |
+
});
|
| 441 |
+
});
|
| 442 |
</script>
|
| 443 |
</body>
|
| 444 |
</html>
|