Spaces:
Runtime error
Runtime error
| <html lang="en" dir="ltr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Chat - University AI</title> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --olive-light: #3A662A; | |
| --olive-dark: #5C6E4A; | |
| --bg-light: #F5F5F5; | |
| --bg-dark: #1A1A1A; | |
| --text-light: #2C2C2C; | |
| --text-dark: #F5F5F5; | |
| --card-light: #FFFFFF; | |
| --card-dark: #2D2D2D; | |
| --error-color: #c33; | |
| --error-hover: #a22; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| transition: all 0.3s ease; | |
| min-height: 100vh; | |
| } | |
| body.light-mode { | |
| background: var(--bg-light); | |
| color: var(--text-light); | |
| } | |
| body.dark-mode { | |
| background: var(--bg-dark); | |
| color: var(--text-dark); | |
| } | |
| /* Modal Styles */ | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| z-index: 1000; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.6); | |
| backdrop-filter: blur(5px); | |
| animation: fadeIn 0.3s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| .modal-content { | |
| background-color: white; | |
| margin: 5% auto; | |
| padding: 30px; | |
| border-radius: 16px; | |
| max-width: 500px; | |
| width: 90%; | |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); | |
| text-align: center; | |
| animation: slideDown 0.4s ease; | |
| position: relative; | |
| } | |
| @keyframes slideDown { | |
| from { | |
| transform: translateY(-50px); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateY(0); | |
| opacity: 1; | |
| } | |
| } | |
| .modal-content h3 { | |
| color: var(--olive-light); | |
| font-size: 24px; | |
| margin-bottom: 10px; | |
| } | |
| .modal-content ol { | |
| margin-left: 20px; | |
| margin-bottom: 8px; | |
| margin-top: 5px; | |
| } | |
| .modal-content li { | |
| margin-bottom: 6px; | |
| text-align: left; | |
| } | |
| .modal-btn { | |
| background: var(--olive-light); | |
| color: white; | |
| border: none; | |
| padding: 12px 40px; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| margin-top: 20px; | |
| transition: all 0.3s ease; | |
| font-weight: 600; | |
| } | |
| .modal-btn:hover { | |
| background: var(--olive-dark); | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4); | |
| } | |
| /* Sidebar */ | |
| .sidebar { | |
| position: fixed; | |
| left: 0; | |
| top: 0; | |
| width: 250px; | |
| height: 100vh; | |
| height: 100dvh; | |
| padding: 20px; | |
| transition: transform 0.3s ease; | |
| z-index: 1000; | |
| display: flex; | |
| flex-direction: column; | |
| box-sizing: border-box; | |
| } | |
| .light-mode .sidebar { | |
| background: var(--card-light); | |
| box-shadow: 2px 0 10px rgba(0,0,0,0.1); | |
| } | |
| .dark-mode .sidebar { | |
| background: var(--card-dark); | |
| box-shadow: 2px 0 10px rgba(0,0,0,0.3); | |
| } | |
| /* Mobile menu button */ | |
| .menu-toggle { | |
| display: none; | |
| position: fixed; | |
| top: 20px; | |
| left: 20px; | |
| z-index: 101; | |
| background: var(--olive-light); | |
| color: white; | |
| border: none; | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 20px; | |
| align-items: center; | |
| justify-content: center; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.2); | |
| transition: all 0.3s ease; | |
| } | |
| .menu-toggle.active { | |
| left: 260px; | |
| } | |
| .menu-toggle:hover { | |
| transform: scale(1.1); | |
| } | |
| /* Overlay for mobile */ | |
| .sidebar-overlay { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0,0,0,0.5); | |
| z-index: 99; | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| pointer-events: none; | |
| } | |
| .sidebar-overlay.active { | |
| opacity: 1; | |
| pointer-events: auto; | |
| } | |
| .sidebar-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 30px; | |
| width: 100%; | |
| position: relative; | |
| } | |
| .logo { | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: var(--olive-light); | |
| margin: 0; | |
| } | |
| .new-chat-btn { | |
| width: 100%; | |
| padding: 12px; | |
| background: var(--olive-light); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| margin-bottom: 20px; | |
| font-size: 16px; | |
| transition: all 0.3s ease; | |
| } | |
| .new-chat-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4); | |
| } | |
| .conversations-list { | |
| flex: 1; | |
| max-height: none; | |
| overflow-y: auto; | |
| margin-bottom: 20px; | |
| } | |
| .conversation-item { | |
| padding: 12px; | |
| margin-bottom: 8px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .light-mode .conversation-item { | |
| background: var(--bg-light); | |
| } | |
| .dark-mode .conversation-item { | |
| background: var(--bg-dark); | |
| } | |
| .conversation-item:hover { | |
| background: rgba(58, 102, 42, 0.1); | |
| color: var(--olive-light); | |
| } | |
| .conversation-item.active { | |
| background: var(--olive-light); | |
| color: white; | |
| box-shadow: 0 4px 12px rgba(58, 102, 42, 0.3); | |
| } | |
| .conversation-title { | |
| flex: 1; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| font-size: 14px; | |
| } | |
| .delete-conv-btn { | |
| background: rgba(255, 255, 255, 0.2); | |
| border: none; | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: all 0.2s ease; | |
| } | |
| .delete-conv-btn:hover { | |
| background: rgba(255, 255, 255, 0.3); | |
| transform: scale(1.1); | |
| } | |
| .logout-btn { | |
| position: static; | |
| margin-top: auto; | |
| padding: 12px; | |
| background: var(--error-color); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| width: 100%; | |
| } | |
| .logout-btn:hover { | |
| background: var(--error-hover); | |
| } | |
| /* Main Content */ | |
| .main-content { | |
| margin-left: 250px; | |
| padding: 0; | |
| height: 100vh; | |
| height: 100dvh; | |
| display: flex; | |
| flex-direction: column; | |
| transition: margin-left 0.3s ease; | |
| } | |
| .header-bar { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 15px 20px; | |
| flex-shrink: 0; | |
| } | |
| .header-bar h1 { | |
| font-size: 24px; | |
| color: var(--olive-light); | |
| } | |
| .theme-toggle { | |
| width: 50px; | |
| height: 26px; | |
| background: var(--olive-light); | |
| border-radius: 13px; | |
| position: relative; | |
| top: auto; | |
| right: auto; | |
| z-index: 1; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| flex-shrink: 0; | |
| } | |
| .theme-toggle::after { | |
| content: '☀️'; | |
| position: absolute; | |
| top: 3px; | |
| left: 3px; | |
| width: 20px; | |
| height: 20px; | |
| background: white; | |
| border-radius: 50%; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 12px; | |
| } | |
| .dark-mode .theme-toggle::after { | |
| content: '🌙'; | |
| left: 27px; | |
| } | |
| /* Chat Area */ | |
| .chat-area { | |
| display: flex; | |
| flex-direction: column; | |
| flex: 1; | |
| height: auto; | |
| border-radius: 0; | |
| overflow: hidden; | |
| background: transparent; | |
| } | |
| .messages-container { | |
| flex: 1; | |
| padding: 20px; | |
| overflow-y: auto; | |
| } | |
| .message { | |
| margin-bottom: 20px; | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .message.user { | |
| justify-content: flex-end; | |
| } | |
| .message.ai { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| .message-content { | |
| max-width: 75%; | |
| padding: 12px 18px; | |
| border-radius: 18px; | |
| line-height: 1.6; | |
| word-wrap: break-word; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.05); | |
| position: relative; | |
| font-size: 15px; | |
| } | |
| .message.user .message-content { | |
| background: var(--olive-light); | |
| color: white; | |
| border-bottom-right-radius: 4px; | |
| } | |
| .light-mode .message.ai .message-content { | |
| background: white; | |
| border: 1px solid rgba(0,0,0,0.05); | |
| border-bottom-left-radius: 4px; | |
| } | |
| .dark-mode .message.ai .message-content { | |
| background: var(--bg-dark); | |
| border: 1px solid rgba(255,255,255,0.05); | |
| border-bottom-left-radius: 4px; | |
| } | |
| /* Markdown & Code Styles */ | |
| .message-content pre { | |
| background: #2d2d2d; | |
| color: #f8f8f2; | |
| padding: 15px; | |
| border-radius: 8px; | |
| overflow-x: auto; | |
| margin: 10px 0; | |
| position: relative; | |
| } | |
| .message-content code { | |
| font-family: 'Consolas', 'Monaco', 'Courier New', monospace; | |
| font-size: 14px; | |
| } | |
| .message-content p { | |
| margin-bottom: 10px; | |
| } | |
| .message-content ul, .message-content ol { | |
| margin-left: 20px; | |
| margin-bottom: 10px; | |
| } | |
| .code-copy-btn { | |
| position: absolute; | |
| top: 5px; | |
| right: 5px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border: none; | |
| color: #fff; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 12px; | |
| } | |
| /* Feedback Buttons */ | |
| .feedback-buttons { | |
| display: flex; | |
| gap: 8px; | |
| margin-top: 8px; | |
| margin-left: 5px; | |
| } | |
| .feedback-btn { | |
| background: none; | |
| border: none; | |
| font-size: 18px; | |
| cursor: pointer; | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| transition: all 0.2s ease; | |
| opacity: 0.6; | |
| } | |
| .feedback-btn:hover { | |
| opacity: 1; | |
| transform: scale(1.2); | |
| } | |
| .feedback-btn.active { | |
| opacity: 1; | |
| } | |
| .feedback-btn.thumbs-up:hover, | |
| .feedback-btn.thumbs-up.active { | |
| background: rgba(76, 175, 80, 0.1); | |
| } | |
| .feedback-btn.thumbs-down:hover, | |
| .feedback-btn.thumbs-down.active { | |
| background: rgba(244, 67, 54, 0.1); | |
| } | |
| .chat-input-area { | |
| padding: 20px; | |
| background: transparent; | |
| } | |
| .input-wrapper { | |
| display: flex; | |
| gap: 10px; | |
| align-items: flex-end; | |
| background: var(--bg-light); | |
| padding: 10px; | |
| border-radius: 26px; | |
| box-shadow: 0 5px 20px rgba(0,0,0,0.08); | |
| border: 1px solid rgba(0,0,0,0.05); | |
| } | |
| .dark-mode .input-wrapper { | |
| background: var(--bg-dark); | |
| border-color: rgba(255,255,255,0.1); | |
| } | |
| #messageInput { | |
| flex: 1; | |
| padding: 12px 15px; | |
| border: none; | |
| background: transparent; | |
| font-size: 16px; | |
| resize: none; | |
| font-family: inherit; | |
| height: 54px; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| } | |
| .light-mode #messageInput { | |
| color: var(--text-light); | |
| } | |
| .dark-mode #messageInput { | |
| color: var(--text-dark); | |
| } | |
| #messageInput:focus { | |
| outline: none; | |
| } | |
| .send-btn { | |
| width: auto; | |
| height: auto; | |
| padding: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: transparent; | |
| color: var(--olive-light); | |
| border: none; | |
| cursor: pointer; | |
| font-size: 24px; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| } | |
| .send-btn:hover:not(:disabled) { | |
| transform: scale(1.1); | |
| box-shadow: none; | |
| background: transparent; | |
| } | |
| .send-btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .send-btn .spinner { | |
| display: inline-block; | |
| width: 16px; | |
| height: 16px; | |
| border: 2px solid rgba(58, 102, 42, 0.3); | |
| border-radius: 50%; | |
| border-top-color: var(--olive-light); | |
| animation: spin 0.8s linear infinite; | |
| } | |
| /* Tooltip styles */ | |
| .send-btn::after { | |
| content: attr(data-tooltip); | |
| position: absolute; | |
| bottom: 120%; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(10px); | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 6px 12px; | |
| border-radius: 6px; | |
| font-size: 12px; | |
| white-space: nowrap; | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: all 0.2s ease; | |
| pointer-events: none; | |
| } | |
| .send-btn:hover::after { | |
| opacity: 1; | |
| visibility: visible; | |
| transform: translateX(-50%) translateY(0); | |
| } | |
| .empty-state { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| opacity: 0.5; | |
| text-align: center; | |
| } | |
| .empty-state img { | |
| width: 150px; | |
| margin-bottom: 20px; | |
| } | |
| .empty-state h2 { | |
| margin-bottom: 10px; | |
| } | |
| /* Loading indicator */ | |
| .loading-indicator { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid rgba(255,255,255,.3); | |
| border-radius: 50%; | |
| border-top-color: white; | |
| animation: spin 1s ease-in-out infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* Typing indicator */ | |
| .typing-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 15px 20px; | |
| max-width: 70%; | |
| border-radius: 12px; | |
| } | |
| .light-mode .typing-indicator { | |
| background: var(--bg-light); | |
| } | |
| .dark-mode .typing-indicator { | |
| background: var(--bg-dark); | |
| } | |
| .typing-dots { | |
| display: flex; | |
| gap: 4px; | |
| } | |
| .typing-dot { | |
| width: 8px; | |
| height: 8px; | |
| background: var(--olive-light); | |
| border-radius: 50%; | |
| animation: typing 1.4s infinite; | |
| } | |
| .typing-dot:nth-child(2) { | |
| animation-delay: 0.2s; | |
| } | |
| .typing-dot:nth-child(3) { | |
| animation-delay: 0.4s; | |
| } | |
| @keyframes typing { | |
| 0%, 60%, 100% { | |
| transform: translateY(0); | |
| opacity: 0.7; | |
| } | |
| 30% { | |
| transform: translateY(-10px); | |
| opacity: 1; | |
| } | |
| } | |
| /* Mobile responsive */ | |
| @media (max-width: 768px) { | |
| .menu-toggle { | |
| display: flex; | |
| } | |
| .sidebar { | |
| transform: translateX(-100%); | |
| width: 250px; | |
| } | |
| .sidebar.active { | |
| transform: translateX(0); | |
| } | |
| .sidebar-overlay { | |
| display: block; | |
| } | |
| .main-content { | |
| margin-left: 0; | |
| padding: 0; | |
| } | |
| .header-bar { | |
| padding-left: 70px; /* Space for menu toggle */ | |
| } | |
| .message-content { | |
| max-width: 85%; | |
| } | |
| .header-bar { | |
| margin-top: 0; | |
| } | |
| .header-bar h1 { | |
| font-size: 24px; | |
| } | |
| .chat-input-area { | |
| padding: 10px 0; | |
| } | |
| .modal-content { | |
| margin: 10% auto; | |
| padding: 20px; | |
| } | |
| } | |
| /* Delete Modal Styles */ | |
| .modal-overlay { | |
| display: none; | |
| position: fixed; | |
| top: 0; left: 0; width: 100%; height: 100%; | |
| background: rgba(0, 0, 0, 0.5); | |
| z-index: 2000; | |
| align-items: center; | |
| justify-content: center; | |
| backdrop-filter: blur(3px); | |
| animation: fadeIn 0.2s ease; | |
| } | |
| .modal-content-delete { | |
| background: var(--bg-light); | |
| padding: 30px; | |
| border-radius: 16px; | |
| text-align: center; | |
| max-width: 350px; | |
| width: 90%; | |
| box-shadow: 0 15px 50px rgba(0,0,0,0.2); | |
| transform: scale(0.9); | |
| animation: popIn 0.2s ease forwards; | |
| } | |
| .dark-mode .modal-content-delete { | |
| background: var(--card-dark); | |
| color: var(--text-dark); | |
| } | |
| .modal-icon { | |
| font-size: 40px; | |
| margin-bottom: 15px; | |
| background: #fee; | |
| width: 80px; | |
| height: 80px; | |
| line-height: 80px; | |
| border-radius: 50%; | |
| margin: 0 auto 15px auto; | |
| } | |
| .modal-actions { | |
| display: flex; | |
| gap: 12px; | |
| justify-content: center; | |
| margin-top: 25px; | |
| } | |
| .btn-cancel, .btn-confirm { | |
| padding: 12px 24px; | |
| border-radius: 10px; | |
| border: none; | |
| cursor: pointer; | |
| font-weight: 600; | |
| font-size: 14px; | |
| transition: all 0.2s; | |
| } | |
| .btn-cancel { | |
| background: #e0e0e0; | |
| color: #333; | |
| } | |
| .btn-confirm { | |
| background: #ff4444; | |
| color: white; | |
| box-shadow: 0 4px 12px rgba(255, 68, 68, 0.3); | |
| } | |
| .btn-confirm:hover { background: #cc0000; transform: translateY(-2px); } | |
| .btn-cancel:hover { background: #d0d0d0; } | |
| @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } | |
| @keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } } | |
| /* Desktop Sidebar Toggle */ | |
| .icon-btn { | |
| background: transparent; | |
| border: none; | |
| color: var(--olive-light); | |
| font-size: 24px; | |
| cursor: pointer; | |
| padding: 5px; | |
| border-radius: 5px; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .icon-btn:hover { | |
| background: rgba(58, 102, 42, 0.1); | |
| } | |
| @media (min-width: 769px) { | |
| body.sidebar-collapsed .sidebar { | |
| transform: translateX(-100%); | |
| } | |
| body.sidebar-collapsed .main-content { | |
| margin-left: 0 ; | |
| } | |
| body.sidebar-collapsed #expandSidebarBtn { | |
| display: block ; | |
| } | |
| } | |
| /* Resizer Styles */ | |
| .resizer { | |
| width: 6px; | |
| height: 100%; | |
| background: transparent; | |
| position: absolute; | |
| right: 0; | |
| top: 0; | |
| cursor: col-resize; | |
| z-index: 102; | |
| transition: background 0.2s; | |
| } | |
| .resizer:hover, .sidebar:hover .resizer { | |
| background: rgba(58, 102, 42, 0.2); | |
| } | |
| body.resizing { | |
| cursor: col-resize; | |
| user-select: none; | |
| } | |
| body.resizing .sidebar, | |
| body.resizing .main-content { | |
| transition: none ; | |
| } | |
| @media (max-width: 768px) { | |
| #collapseSidebarBtn { | |
| display: none; | |
| } | |
| #expandSidebarBtn { | |
| display: none ; | |
| } | |
| .resizer { | |
| display: none; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="light-mode"> | |
| <!-- Delete Confirmation Modal --> | |
| <div id="deleteModal" class="modal-overlay"> | |
| <div class="modal-content-delete"> | |
| <div class="modal-icon">🗑️</div> | |
| <h3>Delete Conversation?</h3> | |
| <p>Are you sure you want to delete this chat? This action cannot be undone.</p> | |
| <div class="modal-actions"> | |
| <button class="btn-cancel" onclick="closeDeleteModal()">Cancel</button> | |
| <button class="btn-confirm" onclick="confirmDelete()">Delete</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Welcome Modal --> | |
| <div id="welcomeModal" class="modal"> | |
| <div class="modal-content"> | |
| <div style="font-size: 40px; margin-bottom: 15px;">👋</div> | |
| <h3>Welcome to University AI!</h3> | |
| <div style="text-align: left; line-height: 1.5; opacity: 0.9; max-height: 300px; overflow-y: auto; padding-right: 10px;"> | |
| <p style="margin-bottom: 8px;"> | |
| Please note the following: You are only allowed to ask about lectures covered in the following courses: | |
| </p> | |
| <p style="margin-bottom: 5px; margin-top: 8px; font-weight: bold;">Semester 7 Courses:</p> | |
| <ol> | |
| <li>Networks</li> | |
| <li>Information Security</li> | |
| <li>Mobile Applications</li> | |
| <li>Computation Theory</li> | |
| <li>Operating Systems</li> | |
| </ol> | |
| <p style="margin-bottom: 5px; margin-top: 8px; font-weight: bold;">Semester 8 Courses:</p> | |
| <ol> | |
| <li>Human-Computer Interaction</li> | |
| <li>Computer Graphics</li> | |
| <li>Algorithm Analysis and Design</li> | |
| <li>Compiler Design</li> | |
| <li>Computer Architecture</li> | |
| <li>Machine Learning</li> | |
| </ol> | |
| </div> | |
| <button class="modal-btn" onclick="closeWelcomeModal()">Got it!</button> | |
| </div> | |
| </div> | |
| <!-- Mobile Menu Toggle --> | |
| <button class="menu-toggle" id="menuToggle">☰</button> | |
| <!-- Sidebar Overlay for Mobile --> | |
| <div class="sidebar-overlay" id="sidebarOverlay"></div> | |
| <!-- Sidebar --> | |
| <div class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <div class="logo">🎓 University AI</div> | |
| <div style="display: flex; gap: 10px; align-items: center;"> | |
| <div class="theme-toggle" id="themeToggle"></div> | |
| <button id="collapseSidebarBtn" class="icon-btn" title="Collapse Sidebar">◀</button> | |
| </div> | |
| </div> | |
| <button class="new-chat-btn" id="newChatBtn"> | |
| ➕ New Conversation | |
| </button> | |
| <div class="conversations-list" id="conversationsList"> | |
| <!-- Conversations will be loaded here --> | |
| </div> | |
| <button class="logout-btn" id="logoutBtn"> | |
| 🚪 Logout | |
| </button> | |
| <div class="resizer" id="sidebarResizer"></div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="main-content"> | |
| <div class="header-bar"> | |
| <button id="expandSidebarBtn" class="icon-btn" style="display: none; margin-right: 15px;" title="Show Sidebar">☰</button> | |
| <h1>Welcome 👋</h1> | |
| </div> | |
| <!-- Chat Area --> | |
| <div class="chat-area"> | |
| <div class="messages-container" id="messagesContainer"> | |
| <div class="empty-state"> | |
| <img src="/static/ch.png" alt="Start conversation" onerror="this.style.display='none'"> | |
| <h2>Start a new conversation</h2> | |
| <p>Ask any question about your lectures or study materials</p> | |
| </div> | |
| </div> | |
| <div class="chat-input-area"> | |
| <div class="input-wrapper"> | |
| <textarea | |
| id="messageInput" | |
| placeholder="Type your message here..." | |
| autocomplete="off" | |
| ></textarea> | |
| <button class="send-btn" id="sendBtn" data-tooltip="Send Message"> | |
| <svg viewBox="0 0 24 24" width="28" height="28" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Configuration | |
| const CONFIG = { | |
| API_URL: '', // Empty string uses the current origin (relative path) | |
| // RAG_URL removed: Client talks to Main API, Main API talks to RAG | |
| STORAGE_KEYS: { | |
| TOKEN: 'token', | |
| ROLE: 'role', | |
| THEME: 'theme', | |
| WELCOME_SHOWN: 'welcome_shown' | |
| }, | |
| ROLES: { | |
| STUDENT: 'student' | |
| } | |
| }; | |
| // State management | |
| const state = { | |
| currentConversationId: null, | |
| messageIdCounter: 0, | |
| isTyping: false | |
| }; | |
| // Delete Modal Logic | |
| let chatToDeleteId = null; | |
| function promptDelete(id) { | |
| chatToDeleteId = id; | |
| document.getElementById('deleteModal').style.display = 'flex'; | |
| } | |
| function closeDeleteModal() { | |
| document.getElementById('deleteModal').style.display = 'none'; | |
| chatToDeleteId = null; | |
| } | |
| async function confirmDelete() { | |
| if (!chatToDeleteId) return; | |
| const id = chatToDeleteId; | |
| closeDeleteModal(); | |
| await chat.deleteConversation(id); | |
| } | |
| // Utility functions | |
| function closeWelcomeModal() { | |
| document.getElementById('welcomeModal').style.display = 'none'; | |
| utils.setStorageItem(CONFIG.STORAGE_KEYS.WELCOME_SHOWN, 'true'); | |
| } | |
| function showWelcomeModal() { | |
| const welcomeShown = utils.getStorageItem(CONFIG.STORAGE_KEYS.WELCOME_SHOWN); | |
| if (!welcomeShown) { | |
| document.getElementById('welcomeModal').style.display = 'block'; | |
| } | |
| } | |
| const utils = { | |
| escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| }, | |
| getStorageItem(key) { | |
| try { | |
| return localStorage.getItem(key); | |
| } catch (e) { | |
| console.error('Error reading from localStorage:', e); | |
| return null; | |
| } | |
| }, | |
| setStorageItem(key, value) { | |
| try { | |
| localStorage.setItem(key, value); | |
| return true; | |
| } catch (e) { | |
| console.error('Error writing to localStorage:', e); | |
| return false; | |
| } | |
| }, | |
| removeStorageItem(key) { | |
| try { | |
| localStorage.removeItem(key); | |
| } catch (e) { | |
| console.error('Error removing from localStorage:', e); | |
| } | |
| }, | |
| clearStorage() { | |
| try { | |
| localStorage.clear(); | |
| } catch (e) { | |
| console.error('Error clearing localStorage:', e); | |
| } | |
| } | |
| }; | |
| // API functions | |
| const api = { | |
| async makeRequest(url, options = {}) { | |
| const token = utils.getStorageItem(CONFIG.STORAGE_KEYS.TOKEN); | |
| const defaultOptions = { | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| ...(token && { 'Authorization': `Bearer ${token}` }) | |
| } | |
| }; | |
| try { | |
| const response = await fetch(url, { ...defaultOptions, ...options }); | |
| return response; | |
| } catch (error) { | |
| console.error('Network error:', error); | |
| throw error; | |
| } | |
| }, | |
| async sendMessage(message) { | |
| // Send to Main API instead of internal RAG service | |
| const payload = { | |
| message: message | |
| }; | |
| if (state.currentConversationId) { | |
| payload.conversation_id = state.currentConversationId; | |
| } | |
| return await this.makeRequest(`${CONFIG.API_URL}/student/chat`, { | |
| method: 'POST', | |
| body: JSON.stringify(payload) | |
| }); | |
| }, | |
| async sendFeedback(messageId, feedbackType) { | |
| return await this.makeRequest(`${CONFIG.API_URL}/feedback`, { | |
| method: 'POST', | |
| body: JSON.stringify({ | |
| message_id: messageId, | |
| feedback_type: feedbackType | |
| }) | |
| }); | |
| }, | |
| async checkAuth() { | |
| return await this.makeRequest(`${CONFIG.API_URL}/user/me`); | |
| }, | |
| async getConversations() { | |
| return await this.makeRequest(`${CONFIG.API_URL}/student/conversations`); | |
| }, | |
| async getConversation(id) { | |
| return await this.makeRequest(`${CONFIG.API_URL}/student/conversation/${id}`); | |
| }, | |
| async deleteConversation(id) { | |
| return await this.makeRequest(`${CONFIG.API_URL}/student/conversations/${id}`, { | |
| method: 'DELETE' | |
| }); | |
| } | |
| }; | |
| // UI functions | |
| const ui = { | |
| elements: { | |
| messagesContainer: document.getElementById('messagesContainer'), | |
| messageInput: document.getElementById('messageInput'), | |
| sendBtn: document.getElementById('sendBtn'), | |
| newChatBtn: document.getElementById('newChatBtn'), | |
| logoutBtn: document.getElementById('logoutBtn'), | |
| themeToggle: document.getElementById('themeToggle'), | |
| conversationsList: document.getElementById('conversationsList'), | |
| menuToggle: document.getElementById('menuToggle'), | |
| sidebar: document.getElementById('sidebar'), | |
| sidebarOverlay: document.getElementById('sidebarOverlay'), | |
| collapseSidebarBtn: document.getElementById('collapseSidebarBtn'), | |
| expandSidebarBtn: document.getElementById('expandSidebarBtn') | |
| }, | |
| toggleSidebar() { | |
| this.elements.sidebar.classList.toggle('active'); | |
| this.elements.sidebarOverlay.classList.toggle('active'); | |
| this.elements.menuToggle.classList.toggle('active'); | |
| if (this.elements.menuToggle.classList.contains('active')) { | |
| this.elements.menuToggle.innerHTML = '←'; | |
| } else { | |
| this.elements.menuToggle.innerHTML = '☰'; | |
| } | |
| }, | |
| closeSidebar() { | |
| this.elements.sidebar.classList.remove('active'); | |
| this.elements.sidebarOverlay.classList.remove('active'); | |
| this.elements.menuToggle.classList.remove('active'); | |
| this.elements.menuToggle.innerHTML = '☰'; | |
| }, | |
| toggleDesktopSidebar() { | |
| document.body.classList.toggle('sidebar-collapsed'); | |
| const isCollapsed = document.body.classList.contains('sidebar-collapsed'); | |
| utils.setStorageItem('sidebar_collapsed', isCollapsed); | |
| }, | |
| initSidebarState() { | |
| const isCollapsed = utils.getStorageItem('sidebar_collapsed') === 'true'; | |
| if (isCollapsed && window.innerWidth > 768) { | |
| document.body.classList.add('sidebar-collapsed'); | |
| } | |
| }, | |
| initResizer() { | |
| const sidebar = this.elements.sidebar; | |
| const resizer = document.getElementById('sidebarResizer'); | |
| const mainContent = document.querySelector('.main-content'); | |
| let isResizing = false; | |
| // Load saved width | |
| const savedWidth = utils.getStorageItem('sidebar_width'); | |
| if (savedWidth && window.innerWidth > 768) { | |
| sidebar.style.width = savedWidth; | |
| // Only apply margin if not collapsed | |
| if (!document.body.classList.contains('sidebar-collapsed')) { | |
| mainContent.style.marginLeft = savedWidth; | |
| } | |
| } | |
| resizer.addEventListener('mousedown', (e) => { | |
| isResizing = true; | |
| document.body.classList.add('resizing'); | |
| const handleMouseMove = (e) => { | |
| if (!isResizing) return; | |
| let newWidth = e.clientX; | |
| if (newWidth < 200) newWidth = 200; // Min width | |
| if (newWidth > 500) newWidth = 500; // Max width | |
| sidebar.style.width = `${newWidth}px`; | |
| mainContent.style.marginLeft = `${newWidth}px`; | |
| }; | |
| const handleMouseUp = () => { | |
| isResizing = false; | |
| document.body.classList.remove('resizing'); | |
| document.removeEventListener('mousemove', handleMouseMove); | |
| document.removeEventListener('mouseup', handleMouseUp); | |
| utils.setStorageItem('sidebar_width', sidebar.style.width); | |
| }; | |
| document.addEventListener('mousemove', handleMouseMove); | |
| document.addEventListener('mouseup', handleMouseUp); | |
| }); | |
| }, | |
| clearEmptyState() { | |
| const emptyState = this.elements.messagesContainer.querySelector('.empty-state'); | |
| if (emptyState) { | |
| this.elements.messagesContainer.innerHTML = ''; | |
| } | |
| }, | |
| scrollToBottom() { | |
| this.elements.messagesContainer.scrollTop = this.elements.messagesContainer.scrollHeight; | |
| }, | |
| renderMarkdown(text, container) { | |
| // Parse Markdown | |
| container.innerHTML = marked.parse(text); | |
| // Add Copy Buttons to Code Blocks | |
| container.querySelectorAll('pre').forEach(pre => { | |
| if (pre.querySelector('.code-copy-btn')) return; | |
| const btn = document.createElement('button'); | |
| btn.className = 'code-copy-btn'; | |
| btn.textContent = 'Copy'; | |
| btn.onclick = () => { | |
| const code = pre.querySelector('code').innerText; | |
| navigator.clipboard.writeText(code); | |
| btn.textContent = 'Copied!'; | |
| setTimeout(() => btn.textContent = 'Copy', 2000); | |
| }; | |
| pre.appendChild(btn); | |
| }); | |
| }, | |
| addUserMessage(text) { | |
| this.clearEmptyState(); | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'message user'; | |
| messageDiv.innerHTML = `<div class='message-content'>${utils.escapeHtml(text)}</div>`; | |
| this.elements.messagesContainer.appendChild(messageDiv); | |
| this.scrollToBottom(); | |
| }, | |
| addAIMessage(text, messageId) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'message ai'; | |
| messageDiv.innerHTML = ` | |
| <div class='message-content'></div> | |
| <div class='feedback-buttons'> | |
| <button type="button" class='feedback-btn copy-btn' data-action="copy" title='Copy to clipboard'> | |
| 📋 | |
| </button> | |
| </div> | |
| `; | |
| const contentDiv = messageDiv.querySelector('.message-content'); | |
| if (text) { | |
| this.renderMarkdown(text, contentDiv); | |
| } | |
| this.elements.messagesContainer.appendChild(messageDiv); | |
| this.scrollToBottom(); | |
| return contentDiv; | |
| }, | |
| showTypingIndicator() { | |
| if (state.isTyping) return; | |
| state.isTyping = true; | |
| const typingDiv = document.createElement('div'); | |
| typingDiv.className = 'message ai'; | |
| typingDiv.id = 'typing-indicator'; | |
| typingDiv.innerHTML = ` | |
| <div class='typing-indicator'> | |
| <div class='typing-dots'> | |
| <div class='typing-dot'></div> | |
| <div class='typing-dot'></div> | |
| <div class='typing-dot'></div> | |
| </div> | |
| </div> | |
| `; | |
| this.elements.messagesContainer.appendChild(typingDiv); | |
| this.scrollToBottom(); | |
| }, | |
| hideTypingIndicator() { | |
| state.isTyping = false; | |
| const typingIndicator = document.getElementById('typing-indicator'); | |
| if (typingIndicator) { | |
| typingIndicator.remove(); | |
| } | |
| }, | |
| setInputState(disabled) { | |
| this.elements.messageInput.disabled = disabled; | |
| this.elements.sendBtn.disabled = disabled; | |
| if (disabled) { | |
| this.elements.sendBtn.innerHTML = '<span class="spinner"></span>'; | |
| } else { | |
| this.elements.sendBtn.innerHTML = `<svg viewBox="0 0 24 24" width="28" height="28" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path> | |
| </svg>`; | |
| } | |
| }, | |
| resetChat() { | |
| this.elements.messagesContainer.innerHTML = ` | |
| <div class="empty-state"> | |
| <img src="/static/ch.png" alt="Start conversation" onerror="this.style.display='none'"> | |
| <h2>New Conversation</h2> | |
| <p>Ask any question to start</p> | |
| </div> | |
| `; | |
| this.elements.messageInput.value = ''; | |
| state.messageIdCounter = 0; | |
| }, | |
| toggleTheme() { | |
| const body = document.body; | |
| const isDark = body.classList.contains('dark-mode'); | |
| body.classList.remove('light-mode', 'dark-mode'); | |
| body.classList.add(isDark ? 'light-mode' : 'dark-mode'); | |
| utils.setStorageItem(CONFIG.STORAGE_KEYS.THEME, isDark ? 'light' : 'dark'); | |
| }, | |
| initTheme() { | |
| const savedTheme = utils.getStorageItem(CONFIG.STORAGE_KEYS.THEME) || 'light'; | |
| document.body.classList.remove('light-mode', 'dark-mode'); | |
| document.body.classList.add(`${savedTheme}-mode`); | |
| }, | |
| async loadConversations() { | |
| try { | |
| const response = await api.getConversations(); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| this.renderConversations(data.conversations || []); | |
| } else { | |
| console.error('Failed to load conversations'); | |
| } | |
| } catch (error) { | |
| console.error('Error loading conversations:', error); | |
| } | |
| }, | |
| renderConversations(conversations) { | |
| const list = this.elements.conversationsList; | |
| list.innerHTML = ''; | |
| if (!conversations || conversations.length === 0) { | |
| list.innerHTML = '<p style="text-align: center; opacity: 0.5; padding: 20px; font-size: 14px;"></p>'; | |
| return; | |
| } | |
| conversations.forEach(conv => { | |
| const convDiv = document.createElement('div'); | |
| convDiv.className = `conversation-item ${conv.id === state.currentConversationId ? 'active' : ''}`; | |
| convDiv.dataset.conversationId = conv.id; | |
| const titleSpan = document.createElement('span'); | |
| titleSpan.className = 'conversation-title'; | |
| titleSpan.textContent = conv.title || 'New Conversation'; | |
| const deleteBtn = document.createElement('button'); | |
| deleteBtn.className = 'delete-conv-btn'; | |
| deleteBtn.innerHTML = '🗑️'; | |
| deleteBtn.title = 'Delete'; | |
| deleteBtn.dataset.action = 'delete'; | |
| deleteBtn.dataset.conversationId = conv.id; | |
| convDiv.appendChild(titleSpan); | |
| convDiv.appendChild(deleteBtn); | |
| list.appendChild(convDiv); | |
| }); | |
| }, | |
| async loadConversation(id) { | |
| try { | |
| const response = await api.getConversation(id); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| state.currentConversationId = parseInt(id); | |
| this.elements.messagesContainer.innerHTML = ''; | |
| if (data.messages && data.messages.length > 0) { | |
| data.messages.forEach(msg => { | |
| const isUser = msg.role === 'user' || msg.sender === 'user'; | |
| if (isUser) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'message user'; | |
| messageDiv.innerHTML = `<div class='message-content'>${utils.escapeHtml(msg.content)}</div>`; | |
| this.elements.messagesContainer.appendChild(messageDiv); | |
| } else { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'message ai'; | |
| messageDiv.innerHTML = ` | |
| <div class='message-content'></div> | |
| <div class='feedback-buttons'> | |
| <button type="button" class='feedback-btn copy-btn' data-action="copy" title='Copy to clipboard'> | |
| 📋 | |
| </button> | |
| </div> | |
| `; | |
| ui.renderMarkdown(msg.content, messageDiv.querySelector('.message-content')); | |
| this.elements.messagesContainer.appendChild(messageDiv); | |
| } | |
| }); | |
| this.scrollToBottom(); | |
| } | |
| document.querySelectorAll('.conversation-item').forEach(item => { | |
| item.classList.remove('active'); | |
| if (parseInt(item.dataset.conversationId) === parseInt(id)) { | |
| item.classList.add('active'); | |
| } | |
| }); | |
| this.closeSidebar(); | |
| } else { | |
| console.error('Failed to load conversation, status:', response.status); | |
| } | |
| } catch (error) { | |
| console.error('Error loading conversation:', error); | |
| } | |
| } | |
| }; | |
| // Chat functions | |
| const chat = { | |
| async sendMessage() { | |
| const message = ui.elements.messageInput.value.trim(); | |
| if (!message) return; | |
| ui.setInputState(true); | |
| ui.addUserMessage(message); | |
| ui.elements.messageInput.value = ''; | |
| ui.elements.messageInput.style.height = '54px'; | |
| ui.showTypingIndicator(); | |
| try { | |
| const response = await api.sendMessage(message); | |
| ui.hideTypingIndicator(); | |
| if (response.ok) { | |
| // Get IDs from headers | |
| const conversationId = response.headers.get("X-Conversation-Id"); | |
| const messageId = response.headers.get("X-Message-Id"); | |
| if (conversationId) state.currentConversationId = parseInt(conversationId); | |
| // Add empty AI message bubble | |
| const contentDiv = ui.addAIMessage("", messageId); | |
| // Read the stream | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let fullText = ""; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| const chunk = decoder.decode(value, { stream: true }); | |
| fullText += chunk; | |
| ui.renderMarkdown(fullText, contentDiv); | |
| ui.scrollToBottom(); | |
| } | |
| await ui.loadConversations(); | |
| } else { | |
| state.messageIdCounter++; | |
| ui.addAIMessage("⚠️ Error getting response from server"); | |
| } | |
| } catch (error) { | |
| console.error('Error sending message:', error); | |
| ui.hideTypingIndicator(); | |
| state.messageIdCounter++; | |
| ui.addAIMessage("⚠️ Error connecting to server. Please try again."); | |
| } finally { | |
| ui.setInputState(false); | |
| } | |
| }, | |
| async copyMessage(button) { | |
| const messageDiv = button.closest('.message'); | |
| const content = messageDiv.querySelector('.message-content').textContent; | |
| try { | |
| await navigator.clipboard.writeText(content); | |
| const originalText = button.textContent; | |
| button.textContent = '✅'; | |
| setTimeout(() => { | |
| button.textContent = originalText; | |
| }, 2000); | |
| } catch (error) { | |
| console.error('Failed to copy message:', error); | |
| button.textContent = '❌'; | |
| setTimeout(() => { | |
| button.textContent = '📋'; | |
| }, 2000); | |
| } | |
| }, | |
| async sendFeedback(button, messageId, feedbackType) { | |
| try { | |
| const response = await api.sendFeedback(messageId, feedbackType); | |
| if (response.ok) { | |
| const feedbackButtons = button.closest('.feedback-buttons'); | |
| const thumbsUp = feedbackButtons.querySelector('.thumbs-up'); | |
| const thumbsDown = feedbackButtons.querySelector('.thumbs-down'); | |
| thumbsUp.classList.remove('active'); | |
| thumbsDown.classList.remove('active'); | |
| if (feedbackType === 'positive') { | |
| thumbsUp.classList.add('active'); | |
| } else { | |
| thumbsDown.classList.add('active'); | |
| } | |
| } else { | |
| console.error('Failed to send feedback'); | |
| } | |
| } catch (error) { | |
| console.error('Error sending feedback:', error); | |
| } | |
| }, | |
| async deleteConversation(id) { | |
| try { | |
| const response = await api.deleteConversation(id); | |
| if (response.ok) { | |
| if (state.currentConversationId === parseInt(id)) { | |
| ui.resetChat(); | |
| state.currentConversationId = null; | |
| } | |
| await ui.loadConversations(); | |
| } else { | |
| console.error('Failed to delete conversation'); | |
| } | |
| } catch (error) { | |
| console.error('Error deleting conversation:', error); | |
| } | |
| } | |
| }; | |
| // Auth functions | |
| const auth = { | |
| async checkAuth() { | |
| const token = utils.getStorageItem(CONFIG.STORAGE_KEYS.TOKEN); | |
| const role = utils.getStorageItem(CONFIG.STORAGE_KEYS.ROLE); | |
| if (!token) { | |
| this.redirectToLogin(); | |
| return false; | |
| } | |
| try { | |
| const response = await api.checkAuth(); | |
| if (!response.ok) { | |
| console.error('Auth check failed with status:', response.status); | |
| this.redirectToLogin(); | |
| return false; | |
| } | |
| const userData = await response.json(); | |
| if (userData.role !== CONFIG.ROLES.STUDENT) { | |
| console.error('User is not a student'); | |
| this.redirectToLogin(); | |
| return false; | |
| } | |
| // Update Welcome Message with Name | |
| if (userData.email) { | |
| const namePart = userData.email.split('@')[0]; | |
| // Replace dots/underscores with spaces and capitalize | |
| const displayName = namePart | |
| .replace(/[._]/g, ' ') | |
| .split(' ') | |
| .map(word => word.charAt(0).toUpperCase() + word.slice(1)) | |
| .join(' '); | |
| const header = document.querySelector('.header-bar h1'); | |
| if (header) header.textContent = `Welcome, ${displayName} 👋`; | |
| } | |
| return true; | |
| } catch (error) { | |
| console.error('Auth check failed:', error); | |
| return false; | |
| } | |
| }, | |
| logout() { | |
| utils.clearStorage(); | |
| window.location.href = 'login.html'; | |
| }, | |
| redirectToLogin() { | |
| utils.clearStorage(); | |
| window.location.href = 'login.html'; | |
| } | |
| }; | |
| // Event handlers | |
| function setupEventListeners() { | |
| ui.elements.sendBtn.addEventListener('click', () => chat.sendMessage()); | |
| ui.elements.messageInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') { | |
| if (e.ctrlKey) { | |
| e.preventDefault(); | |
| ui.elements.messageInput.setRangeText("\n", ui.elements.messageInput.selectionStart, ui.elements.messageInput.selectionEnd, "end"); | |
| ui.elements.messageInput.dispatchEvent(new Event('input')); | |
| return; | |
| } | |
| e.preventDefault(); | |
| chat.sendMessage(); | |
| } | |
| }); | |
| ui.elements.messageInput.addEventListener('input', function() { | |
| this.style.height = '54px'; | |
| this.style.height = (this.scrollHeight) + 'px'; | |
| }); | |
| ui.elements.newChatBtn.addEventListener('click', () => { | |
| ui.resetChat(); | |
| state.currentConversationId = null; | |
| }); | |
| ui.elements.logoutBtn.addEventListener('click', () => auth.logout()); | |
| ui.elements.themeToggle.addEventListener('click', () => ui.toggleTheme()); | |
| ui.elements.messagesContainer.addEventListener('click', (e) => { | |
| const button = e.target.closest('.feedback-btn'); | |
| if (!button) return; | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const action = button.dataset.action; | |
| if (action === 'copy') { | |
| chat.copyMessage(button); | |
| } else if (action === 'feedback') { | |
| const messageId = parseInt(button.dataset.messageId); | |
| const feedbackType = button.dataset.feedbackType; | |
| chat.sendFeedback(button, messageId, feedbackType); | |
| } | |
| }); | |
| ui.elements.conversationsList.addEventListener('click', async (e) => { | |
| const deleteBtn = e.target.closest('[data-action="delete"]'); | |
| if (deleteBtn) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const id = deleteBtn.dataset.conversationId; | |
| promptDelete(id); | |
| return; | |
| } | |
| const conversationItem = e.target.closest('.conversation-item'); | |
| if (conversationItem) { | |
| const id = conversationItem.dataset.conversationId; | |
| await ui.loadConversation(id); | |
| } | |
| }); | |
| ui.elements.menuToggle.addEventListener('click', () => ui.toggleSidebar()); | |
| ui.elements.sidebarOverlay.addEventListener('click', () => ui.toggleSidebar()); | |
| ui.elements.collapseSidebarBtn.addEventListener('click', () => ui.toggleDesktopSidebar()); | |
| ui.elements.expandSidebarBtn.addEventListener('click', () => ui.toggleDesktopSidebar()); | |
| } | |
| // Initialize app | |
| async function init() { | |
| // [Patch] Initialize Markdown Parser & Syntax Highlighter settings | |
| marked.setOptions({ | |
| highlight: function(code, lang) { | |
| const language = hljs.getLanguage(lang) ? lang : 'plaintext'; | |
| return hljs.highlight(code, { language: language }).value; | |
| }, | |
| langPrefix: 'hljs language-', // Class prefix required for the CSS theme to work | |
| breaks: true, // Convert '\n' to <br> | |
| gfm: true // Enable GitHub Flavored Markdown | |
| }); | |
| ui.initTheme(); | |
| ui.initSidebarState(); | |
| ui.initResizer(); | |
| setupEventListeners(); | |
| const isAuthenticated = await auth.checkAuth(); | |
| if (isAuthenticated !== false) { | |
| await ui.loadConversations(); | |
| showWelcomeModal(); | |
| } | |
| } | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| } else { | |
| init(); | |
| } | |
| </script> | |
| </body> | |
| </html> |