Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Nexa AI - Your Intelligent Assistant</title> | |
| <meta name="description" content="Chat Smarter, Create Faster with Nexa AI. Your personal AI assistant for work and creativity, featuring advanced chat and image generation."> | |
| <!-- Open Graph / Facebook --> | |
| <meta property="og:type" content="website"> | |
| <meta property="og:url" content="https://nexa-ai.hf.space/"> | |
| <meta property="og:title" content="Nexa AI - Your Intelligent Assistant"> | |
| <meta property="og:description" content="Chat Smarter, Create Faster with Nexa AI. Your personal AI assistant for work and creativity."> | |
| <meta property="og:image" content="https://nexa-ai.hf.space/og-image.png"> | |
| <!-- Twitter --> | |
| <meta property="twitter:card" content="summary_large_image"> | |
| <meta property="twitter:title" content="Nexa AI - Your Intelligent Assistant"> | |
| <meta property="twitter:description" content="Chat Smarter, Create Faster with Nexa AI. Your personal AI assistant for work and creativity."> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --bg-primary: #050505; | |
| --bg-secondary: #0f0f0f; | |
| --bg-tertiary: #1a1a1a; | |
| --text-primary: #ffffff; | |
| --text-secondary: #a3a3a3; | |
| --border-color: rgba(255, 255, 255, 0.08); | |
| --accent-blue: #10a37f; | |
| --accent-hover: #14b88f; | |
| } | |
| body.light-mode { | |
| --bg-primary: #ffffff; | |
| --bg-secondary: #f7f7f8; | |
| --bg-tertiary: #ececec; | |
| --text-primary: #374151; | |
| --text-secondary: #6b7280; | |
| --border-color: #e5e5e5; | |
| --accent-blue: #10a37f; | |
| --accent-hover: #1a7f64; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| /* Modern Sleek Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 10px; | |
| border: 2px solid transparent; | |
| background-clip: content-box; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| border: 2px solid transparent; | |
| background-clip: content-box; | |
| } | |
| .chat-history::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .app-container { | |
| display: flex; | |
| height: 100vh; | |
| position: relative; | |
| } | |
| /* Lightning/Glow effect behind chat */ | |
| .main-background-lighting { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| pointer-events: none; | |
| z-index: 0; | |
| overflow: hidden; | |
| } | |
| .ambient-glow { | |
| position: absolute; | |
| border-radius: 50%; | |
| filter: blur(80px); | |
| opacity: 0; | |
| transition: opacity 1.2s ease, transform 1s ease; | |
| } | |
| .ambient-glow.glow-1 { | |
| width: 400px; | |
| height: 400px; | |
| background: rgba(16, 163, 127, 0.15); | |
| top: -100px; | |
| right: -100px; | |
| } | |
| .ambient-glow.glow-2 { | |
| width: 500px; | |
| height: 500px; | |
| background: rgba(16, 163, 127, 0.1); | |
| bottom: -200px; | |
| left: -100px; | |
| } | |
| /* Activate glow when in hero-mode or when explicitly active */ | |
| .main-area.hero-mode .ambient-glow, | |
| body.glowing .ambient-glow { | |
| opacity: 1; | |
| transform: scale(1.1); | |
| animation: floatGlow 10s infinite alternate ease-in-out; | |
| } | |
| @keyframes floatGlow { | |
| 0% { | |
| transform: scale(1) translate(0, 0); | |
| } | |
| 100% { | |
| transform: scale(1.2) translate(20px, -20px); | |
| } | |
| } | |
| /* Sidebar Styles */ | |
| /* --- Mobile Responsive Sidebar Overhaul --- */ | |
| .sidebar-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.5); | |
| backdrop-filter: blur(4px); | |
| z-index: 99; | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: all 0.3s ease; | |
| } | |
| .sidebar-overlay.active { | |
| opacity: 1; | |
| visibility: visible; | |
| } | |
| .sidebar { | |
| width: 280px; | |
| background-color: var(--bg-secondary); | |
| display: flex; | |
| flex-direction: column; | |
| padding: 16px; | |
| border-right: 1px solid var(--border-color); | |
| transition: transform 0.4s var(--spring-easing), margin-left 0.4s ease, width 0.3s ease; | |
| z-index: 100; | |
| height: 100vh; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .sidebar-header-mobile { | |
| display: none; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| padding-bottom: 12px; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .sidebar-close-btn { | |
| background: transparent; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| padding: 8px; | |
| border-radius: 50%; | |
| transition: all 0.2s; | |
| } | |
| .sidebar-close-btn:hover { | |
| background: rgba(255, 255, 255, 0.05); | |
| color: var(--text-primary); | |
| } | |
| .top-bar { | |
| position: sticky; | |
| top: 0; | |
| z-index: 90; | |
| background: rgba(13, 13, 13, 0.8); | |
| backdrop-filter: blur(12px); | |
| padding: 12px 20px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .hamburger-menu { | |
| display: none; | |
| background: transparent; | |
| border: none; | |
| color: var(--text-primary); | |
| cursor: pointer; | |
| padding: 8px; | |
| border-radius: 8px; | |
| margin-right: 12px; | |
| } | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| position: fixed; | |
| left: 0; | |
| top: 0; | |
| width: 80%; | |
| max-width: 300px; | |
| transform: translateX(-100%); | |
| box-shadow: 20px 0 50px rgba(0,0,0,0.5); | |
| } | |
| .sidebar.open { | |
| transform: translateX(0); | |
| } | |
| .sidebar-header-mobile { | |
| display: flex; | |
| } | |
| .hamburger-menu { | |
| display: flex; | |
| } | |
| .model-selector svg { | |
| display: none; | |
| } | |
| body.sidebar-open { | |
| overflow: hidden; | |
| } | |
| } | |
| /* --- Premium UI/UX Polish --- */ | |
| :root { | |
| --spring-easing: cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| --smooth-easing: cubic-bezier(0.4, 0, 0.2, 1); | |
| --premium-blur: blur(20px); | |
| --glass-bg: rgba(255, 255, 255, 0.03); | |
| --glass-border: rgba(255, 255, 255, 0.08); | |
| --accent-glow: 0 0 20px rgba(16, 163, 127, 0.2); | |
| } | |
| .image-card, .tool-output-card { | |
| min-height: 100px; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| contain: content; | |
| } | |
| .img-skeleton { | |
| background: linear-gradient(90deg, rgba(255,255,255,0.03) 25%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.03) 75%); | |
| background-size: 200% 100%; | |
| animation: skeleton-loading 1.5s infinite; | |
| } | |
| @keyframes skeleton-loading { | |
| 0% { background-position: 200% 0; } | |
| 100% { background-position: -200% 0; } | |
| } | |
| /* Fix for layout shift in chat history */ | |
| .message-content { | |
| min-width: 0; | |
| overflow-wrap: break-word; | |
| } | |
| /* Message Entrance Animation */ | |
| .message { | |
| will-change: transform, opacity; | |
| animation: premiumFadeUp 0.5s var(--spring-easing) forwards; | |
| opacity: 0; | |
| } | |
| @keyframes premiumFadeUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(24px) scale(0.96); | |
| filter: blur(8px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0) scale(1); | |
| filter: blur(0); | |
| } | |
| } | |
| /* Staggered Entrance for Suggestions */ | |
| .suggestion-card { | |
| animation: premiumFadeUp 0.5s var(--spring-easing) backwards; | |
| } | |
| .suggestion-card:nth-child(1) { animation-delay: 0.1s; } | |
| .suggestion-card:nth-child(2) { animation-delay: 0.15s; } | |
| .suggestion-card:nth-child(3) { animation-delay: 0.2s; } | |
| .suggestion-card:nth-child(4) { animation-delay: 0.25s; } | |
| /* Smooth Sidebar Transitions */ | |
| .sidebar { | |
| transition: width 0.4s var(--smooth-easing), transform 0.4s var(--smooth-easing), background 0.4s ease; | |
| } | |
| .conversation-item { | |
| transition: all 0.25s var(--smooth-easing); | |
| transform-origin: left; | |
| } | |
| .conversation-item:hover { | |
| transform: translateX(4px); | |
| background: rgba(255, 255, 255, 0.06); | |
| } | |
| .conversation-item.active { | |
| box-shadow: var(--accent-glow); | |
| } | |
| /* Interactive Buttons */ | |
| .btn-primary, .btn-secondary, .send-button, .tool-btn, .new-chat-btn { | |
| transition: all 0.3s var(--smooth-easing) ; | |
| } | |
| .btn-primary:active, .btn-secondary:active, .send-button:active { | |
| transform: scale(0.95); | |
| } | |
| .tool-btn:hover { | |
| transform: translateY(-3px) scale(1.05); | |
| background: rgba(16, 163, 127, 0.15); | |
| color: var(--accent-blue); | |
| box-shadow: 0 8px 20px rgba(0,0,0,0.2); | |
| } | |
| /* Smooth Input Area */ | |
| .input-wrapper { | |
| transition: all 0.4s var(--smooth-easing); | |
| } | |
| .input-wrapper:focus-within { | |
| transform: translateY(-2px); | |
| border-color: rgba(16, 163, 127, 0.4); | |
| box-shadow: 0 12px 40px rgba(0,0,0,0.4), var(--accent-glow); | |
| background: rgba(30, 30, 30, 0.85); | |
| } | |
| /* Floating Animation for AI Avatar */ | |
| .message.ai .message-avatar { | |
| animation: floatingAvatar 3s ease-in-out infinite; | |
| } | |
| @keyframes floatingAvatar { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-4px); } | |
| } | |
| /* Loading State Micro-interaction */ | |
| .gen-loader { | |
| animation: pulseGlow 2s ease-in-out infinite; | |
| } | |
| .mermaid-render-container { | |
| background: rgba(255, 255, 255, 0.03); | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin: 15px 0; | |
| display: flex; | |
| justify-content: center; | |
| border: 1px solid var(--border-color); | |
| } | |
| .chart-render-canvas { | |
| background: rgba(255, 255, 255, 0.03); | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin: 15px 0; | |
| border: 1px solid var(--border-color); | |
| width: 100% ; | |
| } | |
| .error-box { | |
| color: #f87171; | |
| background: rgba(239, 68, 68, 0.1); | |
| padding: 10px; | |
| border-radius: 8px; | |
| font-size: 12px; | |
| } | |
| @keyframes pulseGlow { | |
| 0%, 100% { opacity: 0.6; transform: scale(1); } | |
| 50% { opacity: 1; transform: scale(1.1); filter: drop-shadow(0 0 8px var(--accent-blue)); } | |
| } | |
| /* Improved Sidebar Collapse (Icon Only) */ | |
| .sidebar.collapsed { | |
| width: 72px; | |
| padding: 16px 12px; | |
| } | |
| .sidebar.collapsed .sidebar-section-title, | |
| .sidebar.collapsed .research-toggle-container span, | |
| .sidebar.collapsed .new-chat-btn span, | |
| .sidebar.collapsed .menu-item span, | |
| .sidebar.collapsed .conversation-item span, | |
| .sidebar.collapsed .sidebar-footer .menu-item span, | |
| .sidebar.collapsed .mode-dropdown-trigger span, | |
| .sidebar.collapsed .mode-dropdown-trigger i:last-child { | |
| display: none; | |
| } | |
| .sidebar.collapsed .new-chat-btn { | |
| padding: 12px; | |
| width: 48px; | |
| height: 48px; | |
| justify-content: center; | |
| } | |
| .sidebar.collapsed .conversation-item { | |
| width: 48px; | |
| height: 48px; | |
| padding: 0; | |
| justify-content: center; | |
| border-radius: 12px; | |
| } | |
| .sidebar.collapsed .conversation-item i { | |
| margin: 0; | |
| width: 20px; | |
| height: 20px; | |
| } | |
| /* Smooth Message Transitions */ | |
| .streaming-text { | |
| display: inline; | |
| animation: textFadeIn 0.3s ease-out forwards; | |
| } | |
| @keyframes textFadeIn { | |
| from { opacity: 0; filter: blur(2px); transform: translateY(2px); } | |
| to { opacity: 1; filter: blur(0); transform: translateY(0); } | |
| } | |
| @keyframes floatingLogo { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-10px); } | |
| } | |
| /* Glassmorphism AI Bubble */ | |
| .message.ai .message-content { | |
| background: var(--glass-bg); | |
| backdrop-filter: var(--premium-blur); | |
| border: 1px solid var(--glass-border); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); | |
| line-height: 1.7; /* Better readability */ | |
| letter-spacing: 0.01em; | |
| } | |
| /* Improved Markdown Spacing */ | |
| .message-content h3 { | |
| margin-top: 24px; | |
| margin-bottom: 12px; | |
| color: #10a37f; | |
| font-size: 1.1em; | |
| font-weight: 700; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .message-content p { | |
| margin-bottom: 16px; | |
| } | |
| .message-content ul, .message-content ol { | |
| margin-bottom: 16px; | |
| padding-left: 20px; | |
| } | |
| .message-content li { | |
| margin-bottom: 8px; | |
| } | |
| .message-content strong { | |
| color: #10a37f; | |
| } | |
| .important-highlight { | |
| background: rgba(16, 163, 127, 0.1); | |
| color: #10a37f; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-weight: 600; | |
| } | |
| /* Premium Input Glow */ | |
| .input-wrapper:focus-within { | |
| border-color: rgba(16, 163, 127, 0.4); | |
| box-shadow: 0 0 0 4px rgba(16, 163, 127, 0.1), 0 20px 50px rgba(0,0,0,0.4); | |
| } | |
| .settings-modal-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.8); | |
| backdrop-filter: blur(12px); | |
| z-index: 3000; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| opacity: 0; | |
| transition: all 0.3s ease; | |
| } | |
| .settings-modal-overlay.open, | |
| .settings-modal-overlay.show { | |
| display: flex; | |
| opacity: 1; | |
| } | |
| .settings-modal { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 24px; | |
| width: 90%; | |
| max-width: 500px; | |
| padding: 32px; | |
| position: relative; | |
| box-shadow: 0 25px 50px rgba(0,0,0,0.5); | |
| animation: slideUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| } | |
| .settings-header { | |
| margin-bottom: 24px; | |
| } | |
| .settings-title { | |
| font-size: 24px; | |
| font-weight: 700; | |
| margin-bottom: 8px; | |
| } | |
| .settings-subtitle { | |
| color: var(--text-secondary); | |
| font-size: 14px; | |
| } | |
| .settings-section { | |
| margin-bottom: 24px; | |
| padding-bottom: 20px; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .settings-section:last-child { | |
| border-bottom: none; | |
| } | |
| .settings-section-title { | |
| font-size: 12px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| color: var(--text-secondary); | |
| margin-bottom: 16px; | |
| letter-spacing: 0.05em; | |
| } | |
| .settings-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 16px; | |
| } | |
| .settings-item-info h4 { | |
| font-size: 15px; | |
| margin-bottom: 4px; | |
| } | |
| .settings-item-info p { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| } | |
| .settings-btn { | |
| padding: 8px 16px; | |
| border-radius: 10px; | |
| font-size: 13px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| border: 1px solid var(--border-color); | |
| background: rgba(255, 255, 255, 0.03); | |
| color: var(--text-primary); | |
| } | |
| .settings-btn:hover { | |
| background: rgba(255, 255, 255, 0.08); | |
| } | |
| .settings-btn.danger { | |
| color: #ef4444; | |
| border-color: rgba(239, 68, 68, 0.2); | |
| background: rgba(239, 68, 68, 0.05); | |
| } | |
| .settings-btn.danger:hover { | |
| background: rgba(239, 68, 68, 0.1); | |
| border-color: rgba(239, 68, 68, 0.3); | |
| } | |
| .settings-close { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| padding: 8px; | |
| border-radius: 50%; | |
| transition: all 0.2s; | |
| } | |
| .settings-close:hover { | |
| background: rgba(255, 255, 255, 0.05); | |
| color: var(--text-primary); | |
| } | |
| /* Confirm Dialog */ | |
| .confirm-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.6); | |
| backdrop-filter: blur(4px); | |
| z-index: 4000; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .confirm-overlay.open { | |
| display: flex; | |
| } | |
| .confirm-card { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 20px; | |
| padding: 24px; | |
| max-width: 400px; | |
| width: 90%; | |
| text-align: center; | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.4); | |
| } | |
| .confirm-title { | |
| font-size: 18px; | |
| font-weight: 700; | |
| margin-bottom: 12px; | |
| color: #ef4444; | |
| } | |
| .confirm-text { | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| margin-bottom: 24px; | |
| line-height: 1.5; | |
| } | |
| .confirm-actions { | |
| display: flex; | |
| gap: 12px; | |
| } | |
| .confirm-btn { | |
| flex: 1; | |
| padding: 10px; | |
| border-radius: 12px; | |
| font-size: 14px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| border: none; | |
| } | |
| .confirm-btn.cancel { | |
| background: rgba(255, 255, 255, 0.05); | |
| color: var(--text-primary); | |
| } | |
| .confirm-btn.delete { | |
| background: #ef4444; | |
| color: white; | |
| } | |
| /* Date Grouping Labels */ | |
| .history-date-group { | |
| font-size: 11px; | |
| font-weight: 700; | |
| color: var(--text-secondary); | |
| text-transform: uppercase; | |
| letter-spacing: 0.1em; | |
| margin: 20px 0 8px 12px; | |
| opacity: 0.6; | |
| } | |
| .new-chat-btn { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 10px 16px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border-color); | |
| border-radius: 12px; | |
| color: var(--text-primary); | |
| font-size: 14px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| width: 100%; | |
| margin-bottom: 20px; | |
| justify-content: flex-start; | |
| position: relative; | |
| } | |
| .new-chat-btn:hover { | |
| background: rgba(255, 255, 255, 0.08); | |
| border-color: rgba(255, 255, 255, 0.2); | |
| } | |
| .new-chat-btn .shortcut-hint { | |
| margin-left: auto; | |
| display: flex; | |
| gap: 4px; | |
| } | |
| .new-chat-btn .key { | |
| background: rgba(255, 255, 255, 0.1); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 4px; | |
| padding: 2px 6px; | |
| font-size: 10px; | |
| color: var(--text-secondary); | |
| font-family: inherit; | |
| } | |
| .conversation-list { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| overflow-y: auto; | |
| margin-bottom: 16px; | |
| padding-right: 4px; | |
| } | |
| /* Custom scrollbar for conversation list */ | |
| .conversation-list::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| .conversation-list::-webkit-scrollbar-thumb { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 4px; | |
| } | |
| .conversation-item { | |
| padding: 12px 16px; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| border: 1px solid transparent; | |
| margin: 2px 0; | |
| } | |
| .conversation-item:hover { | |
| background: rgba(255, 255, 255, 0.04); | |
| color: var(--text-primary); | |
| transform: translateX(4px); | |
| } | |
| .conversation-item.active { | |
| background: #2f2f2f; | |
| color: white; | |
| font-weight: 500; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); | |
| } | |
| .conversation-item .delete-btn { | |
| position: absolute; | |
| right: 12px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| background: #2f2f2f; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| padding: 6px; | |
| border-radius: 8px; | |
| opacity: 0; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .conversation-item.active .delete-btn { | |
| background: #3f3f3f; | |
| color: white; | |
| } | |
| .sidebar-footer { | |
| border-top: 1px solid var(--border-color); | |
| padding: 16px 8px; | |
| margin-top: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| flex-shrink: 0; | |
| background: var(--bg-secondary); | |
| } | |
| .menu-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 10px 12px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| transition: background 0.2s; | |
| margin-bottom: 4px; | |
| } | |
| .menu-item:hover { | |
| background: var(--bg-tertiary); | |
| } | |
| .menu-item.upgrade { | |
| background: linear-gradient(135deg, #10a37f 0%, #1a7f64 100%); | |
| color: white; | |
| font-weight: 600; | |
| } | |
| /* Main Area Styles */ | |
| .main-area { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| overflow: hidden; | |
| height: 100vh; | |
| } | |
| /* --- 3D World & Advanced Animations --- */ | |
| :root { | |
| --3d-perspective: 1200px; | |
| --3d-tilt-intensity: 15deg; | |
| --transition-smooth: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); | |
| } | |
| body { | |
| perspective: var(--3d-perspective); | |
| transform-style: preserve-3d; | |
| } | |
| .main-area { | |
| transform-style: preserve-3d; | |
| transition: var(--transition-smooth); | |
| } | |
| /* 3D Glassmorphism Cards */ | |
| .suggestion-card, .feature-item, .image-card, .tool-output-card { | |
| position: relative; | |
| transform-style: preserve-3d; | |
| transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1), box-shadow 0.3s ease; | |
| will-change: transform; | |
| } | |
| .suggestion-card:hover, .feature-item:hover { | |
| transform: translateZ(20px) rotateX(2deg) rotateY(-2deg); | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.4), 0 0 0 1px var(--glass-border); | |
| } | |
| /* 3D Floating Particles Background */ | |
| #canvas3d { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: -1; | |
| pointer-events: none; | |
| opacity: 0.6; | |
| } | |
| /* Page Transition Animations */ | |
| .page-fade-enter { | |
| opacity: 0; | |
| transform: scale(0.98) translateZ(-50px); | |
| } | |
| .page-fade-exit { | |
| opacity: 0; | |
| transform: scale(1.02) translateZ(50px); | |
| } | |
| .transitioning { | |
| pointer-events: none; | |
| filter: blur(4px); | |
| transition: var(--transition-smooth); | |
| } | |
| /* Smooth 3D Button Micro-interactions */ | |
| .send-button, .action-btn, .new-chat-btn { | |
| transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.2s ease; | |
| } | |
| .send-button:hover, .action-btn:hover { | |
| transform: translateY(-2px) scale(1.05); | |
| box-shadow: 0 5px 15px rgba(16, 163, 127, 0.3); | |
| } | |
| .send-button:active { | |
| transform: translateY(0) scale(0.95); | |
| } | |
| /* High-Performance 3D Grid Overlay */ | |
| .grid-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-image: linear-gradient(var(--glass-border) 1px, transparent 1px), | |
| linear-gradient(90deg, var(--glass-border) 1px, transparent 1px); | |
| background-size: 50px 50px; | |
| perspective: 500px; | |
| transform: rotateX(60deg) translateY(-50%) translateZ(-200px); | |
| opacity: 0.05; | |
| z-index: -1; | |
| mask-image: radial-gradient(circle at center, black, transparent 80%); | |
| pointer-events: none; | |
| } | |
| /* Smooth Chat Entrance */ | |
| .message { | |
| transform-origin: left center; | |
| animation: messageAppear3d 0.6s cubic-bezier(0.23, 1, 0.32, 1) forwards; | |
| opacity: 0; | |
| } | |
| @keyframes messageAppear3d { | |
| 0% { | |
| opacity: 0; | |
| transform: translateX(-20px) rotateY(-15deg) scale(0.95); | |
| } | |
| 100% { | |
| opacity: 1; | |
| transform: translateX(0) rotateY(0) scale(1); | |
| } | |
| } | |
| .chat-history { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| z-index: 2; | |
| transition: opacity 0.6s ease; | |
| } | |
| /* When hero-mode is active, we push content down to focus on the center */ | |
| .main-area.hero-mode .chat-history { | |
| flex: 1; | |
| opacity: 1; | |
| } | |
| .main-area.hero-mode .chat-history.empty-mode { | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .top-bar { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 12px 20px; | |
| border-bottom: 1px solid var(--border-color); | |
| background: var(--bg-primary); | |
| } | |
| .model-selector { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 8px 12px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| font-size: 14px; | |
| transition: background 0.2s; | |
| } | |
| .model-selector:hover { | |
| background: var(--bg-tertiary); | |
| } | |
| .account-btn { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 10px; | |
| border-radius: 8px; | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| color: var(--text-primary); | |
| font-size: 14px; | |
| transition: background 0.2s; | |
| } | |
| .account-btn:hover { | |
| background: var(--bg-tertiary); | |
| } | |
| .account-avatar { | |
| width: 28px; | |
| height: 28px; | |
| border-radius: 4px; | |
| background: linear-gradient(135deg, #10a37f 0%, #1a7f64 100%); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-size: 12px; | |
| font-weight: 600; | |
| } | |
| .account-dropdown { | |
| position: absolute; | |
| top: 100%; | |
| right: 0; | |
| margin-top: 4px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| padding: 8px; | |
| min-width: 200px; | |
| display: none; | |
| z-index: 1000; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); | |
| } | |
| .account-dropdown.show { | |
| display: block; | |
| } | |
| .dropdown-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 10px 12px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| color: var(--text-primary); | |
| transition: background 0.2s; | |
| } | |
| .dropdown-item:hover { | |
| background: var(--bg-tertiary); | |
| } | |
| /* Empty State */ | |
| .empty-state { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| text-align: center; | |
| color: var(--text-secondary); | |
| animation: fadeIn 0.8s ease-out; | |
| transition: opacity 0.4s ease, transform 0.4s ease; | |
| } | |
| .empty-state h2 { | |
| font-size: 28px; | |
| font-weight: 600; | |
| margin-bottom: 24px; | |
| color: var(--text-primary); | |
| } | |
| .suggestions-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 12px; | |
| margin-top: 30px; | |
| width: 100%; | |
| max-width: 600px; | |
| } | |
| .suggestion-card { | |
| background: rgba(40, 40, 40, 0.4); | |
| border: 1px solid var(--border-color); | |
| padding: 16px 20px; | |
| border-radius: 12px; | |
| text-align: left; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| backdrop-filter: blur(8px); | |
| } | |
| .suggestion-card:hover { | |
| background: rgba(60, 60, 60, 0.6); | |
| transform: translateY(-2px); | |
| border-color: rgba(255, 255, 255, 0.15); | |
| } | |
| .suggestion-card h4 { | |
| font-size: 14px; | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| margin-bottom: 6px; | |
| } | |
| .suggestion-card p { | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| } | |
| /* Unified Tool Card Style */ | |
| .tool-output-card { | |
| background: rgba(30, 30, 30, 0.4); | |
| border: 1px solid var(--border-color); | |
| border-radius: 16px; | |
| padding: 16px; | |
| margin-top: 12px; | |
| backdrop-filter: blur(8px); | |
| animation: slideUp 0.3s ease-out; | |
| width: 100%; | |
| max-width: 600px; | |
| } | |
| .tool-output-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 12px; | |
| padding-bottom: 8px; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .tool-output-header i { | |
| color: var(--accent-blue); | |
| width: 18px; | |
| height: 18px; | |
| } | |
| .tool-output-header span { | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| /* Enhanced Search Results Styles */ | |
| .search-results { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| max-height: 300px; | |
| overflow-y: auto; | |
| padding: 12px; | |
| background: rgba(0, 0, 0, 0.2); | |
| border-radius: 12px; | |
| margin-bottom: 16px; | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| .search-card { | |
| background: rgba(45, 45, 45, 0.4); | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| border-radius: 10px; | |
| padding: 10px 14px; | |
| text-decoration: none; | |
| color: inherit; | |
| transition: all 0.2s ease; | |
| display: block; | |
| } | |
| .search-card:hover { | |
| background: rgba(55, 55, 55, 0.6); | |
| border-color: #10a37f; | |
| transform: translateX(4px); | |
| } | |
| .search-card h5 { | |
| color: #10a37f; | |
| font-size: 13px; | |
| margin-bottom: 4px; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-weight: 600; | |
| } | |
| .search-card p { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| line-height: 1.4; | |
| display: -webkit-box; | |
| -webkit-line-clamp: 2; | |
| -webkit-box-orient: vertical; | |
| overflow: hidden; | |
| } | |
| /* Pricing Page Styles */ | |
| .pricing-container { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: var(--bg-primary); | |
| z-index: 5000; | |
| display: none; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: flex-start; | |
| padding: 80px 20px; | |
| overflow-y: auto; | |
| } | |
| .pricing-header { | |
| text-align: center; | |
| margin-bottom: 50px; | |
| } | |
| .pricing-header h1 { | |
| font-size: 36px; | |
| font-weight: 700; | |
| margin-bottom: 12px; | |
| } | |
| .pricing-header p { | |
| color: var(--text-secondary); | |
| font-size: 18px; | |
| } | |
| .pricing-grid { | |
| display: flex; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| gap: 30px; | |
| max-width: 1000px; | |
| width: 100%; | |
| } | |
| .pricing-card { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 24px; | |
| padding: 40px; | |
| display: flex; | |
| flex-direction: column; | |
| width: 350px; | |
| transition: transform 0.3s ease, border-color 0.3s ease; | |
| position: relative; | |
| } | |
| .pricing-card:hover { | |
| transform: translateY(-10px); | |
| border-color: rgba(16, 163, 127, 0.4); | |
| } | |
| .pricing-card.featured { | |
| border: 2px solid var(--accent-blue); | |
| background: rgba(16, 163, 127, 0.02); | |
| } | |
| .pricing-card .badge { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| background: var(--accent-blue); | |
| color: white; | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| font-size: 12px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| } | |
| .pricing-card h3 { | |
| font-size: 24px; | |
| margin-bottom: 12px; | |
| } | |
| .pricing-card .price { | |
| font-size: 48px; | |
| font-weight: 800; | |
| margin-bottom: 30px; | |
| } | |
| .pricing-card .price span { | |
| font-size: 16px; | |
| color: var(--text-secondary); | |
| font-weight: 400; | |
| } | |
| .pricing-card ul { | |
| list-style: none; | |
| margin-bottom: 40px; | |
| flex: 1; | |
| padding: 0; | |
| } | |
| .pricing-card ul li { | |
| margin-bottom: 16px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| color: var(--text-secondary); | |
| font-size: 15px; | |
| } | |
| .pricing-card ul li i { | |
| color: var(--accent-blue); | |
| width: 18px; | |
| height: 18px; | |
| } | |
| .pricing-card .btn-primary { | |
| text-align: center; | |
| padding: 14px; | |
| border-radius: 12px; | |
| text-decoration: none; | |
| font-weight: 700; | |
| transition: all 0.2s; | |
| cursor: pointer; | |
| } | |
| .pricing-card.featured .btn-primary { | |
| background: var(--accent-blue); | |
| color: white; | |
| border: none; | |
| } | |
| .pricing-card:not(.featured) .btn-primary { | |
| background: rgba(255, 255, 255, 0.05); | |
| color: var(--text-primary); | |
| border: 1px solid var(--border-color); | |
| } | |
| .pricing-card:not(.featured) .btn-primary:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| } | |
| .compiling-loader { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 12px; | |
| background: rgba(16, 163, 127, 0.05); | |
| border-radius: 10px; | |
| margin-bottom: 16px; | |
| border: 1px dashed rgba(16, 163, 127, 0.3); | |
| font-size: 13px; | |
| color: #10a37f; | |
| font-weight: 500; | |
| } | |
| .compiling-loader i { | |
| animation: spin 2s linear infinite; | |
| } | |
| @keyframes spin { | |
| from { transform: rotate(0deg); } | |
| to { transform: rotate(360deg); } | |
| } | |
| .compiled-search-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| background: linear-gradient(135deg, rgba(16, 163, 127, 0.2) 0%, rgba(26, 127, 100, 0.2) 100%); | |
| color: #10a37f; | |
| padding: 6px 12px; | |
| border-radius: 8px; | |
| font-size: 12px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| margin-bottom: 16px; | |
| border: 1px solid rgba(16, 163, 127, 0.3); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
| } | |
| /* --- Premium Message Layout Overhaul --- */ | |
| .message-actions { | |
| display: flex; | |
| gap: 10px; | |
| margin-top: 14px; | |
| opacity: 0.8; /* Increased default visibility */ | |
| transition: all 0.2s ease; | |
| } | |
| .message:hover .message-actions { | |
| opacity: 1; | |
| transform: translateY(-2px); | |
| } | |
| .action-icon-btn { | |
| background: transparent; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| color: var(--text-secondary); | |
| padding: 6px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| } | |
| .action-icon-btn:hover { | |
| background: rgba(255,255,255,0.05); | |
| color: var(--accent-blue); | |
| transform: scale(1.15) translateY(-2px); | |
| border-color: var(--accent-blue); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
| } | |
| .action-icon-btn:active { | |
| transform: scale(0.95); | |
| } | |
| .action-icon-btn i, .action-icon-btn svg, | |
| .action-btn i, .action-btn svg, | |
| .send-button i, .send-button svg, | |
| .stop-button i, .stop-button svg { | |
| display: block ; | |
| width: 18px ; | |
| height: 18px ; | |
| stroke-width: 2.5px ; | |
| } | |
| .send-button svg, .stop-button svg, .send-button i, .stop-button i { | |
| color: white ; | |
| stroke: white ; | |
| } | |
| .action-btn svg, .action-btn i { | |
| color: var(--text-secondary) ; | |
| stroke: var(--text-secondary) ; | |
| } | |
| .action-btn:hover svg, .action-btn:hover i { | |
| color: var(--text-primary) ; | |
| stroke: var(--text-primary) ; | |
| } | |
| /* Consistent Blinking Emoji Logo */ | |
| .blinking-emoji { | |
| display: flex; | |
| gap: 3px; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .blinking-emoji .eye { | |
| width: 8px; | |
| height: 8px; | |
| background-color: white; | |
| border-radius: 50%; | |
| animation: eyeMove 4s infinite ease-in-out; | |
| } | |
| @keyframes eyeMove { | |
| 0%, 100% { transform: translate(0, 0); } | |
| 20% { transform: translate(2px, -1px); } | |
| 40% { transform: translate(-2px, 1px); } | |
| 60% { transform: translate(1px, 2px); } | |
| 80% { transform: translate(-1px, -2px); } | |
| /* Blinking */ | |
| 90% { transform: scaleY(1); } | |
| 95% { transform: scaleY(0.1); } | |
| } | |
| .message.ai .message-avatar { | |
| background: #10a37f; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .chat-history { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 40px 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 32px; | |
| scroll-behavior: smooth; | |
| max-width: 900px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| .message { | |
| display: flex; | |
| gap: 16px; | |
| width: 100%; | |
| animation: fadeUp 0.4s cubic-bezier(0.16, 1, 0.3, 1); | |
| position: relative; | |
| } | |
| @keyframes fadeUp { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .message.user { | |
| flex-direction: row-reverse; | |
| } | |
| .message-avatar { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| font-size: 12px; | |
| font-weight: 700; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .message.ai .message-avatar { | |
| background: linear-gradient(135deg, #10a37f 0%, #0d8a6b 100%); | |
| color: white; | |
| } | |
| .message.user .message-avatar { | |
| background: linear-gradient(135deg, #343541 0%, #202123 100%); | |
| color: var(--text-secondary); | |
| } | |
| .message-content { | |
| max-width: 80%; | |
| line-height: 1.6; | |
| padding: 14px 18px; | |
| word-wrap: break-word; | |
| border-radius: 20px; | |
| font-size: var(--chat-font-size, 15px); | |
| position: relative; | |
| transition: all 0.3s ease; | |
| } | |
| .message.user .message-content { | |
| background: #2f2f2f; | |
| color: var(--text-primary); | |
| border-bottom-right-radius: 4px; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.1), 0 0 1px rgba(255,255,255,0.1); | |
| border: 1px solid rgba(255,255,255,0.05); | |
| } | |
| .message.ai .message-content { | |
| background: rgba(255, 255, 255, 0.03); | |
| backdrop-filter: blur(10px); | |
| color: var(--text-primary); | |
| border-bottom-left-radius: 4px; | |
| border: 1px solid rgba(255,255,255,0.05); | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.15); | |
| } | |
| /* Markdown Readability Enhancements */ | |
| .message-content h1, .message-content h2, .message-content h3 { | |
| margin-top: 16px; | |
| margin-bottom: 8px; | |
| color: var(--accent-blue); | |
| font-weight: 700; | |
| } | |
| .message-content h3 { font-size: 1.1rem; } | |
| .message-content p { | |
| margin-bottom: 12px; | |
| line-height: 1.6; | |
| } | |
| .message-content ul, .message-content ol { | |
| margin-bottom: 12px; | |
| padding-left: 24px; | |
| } | |
| .message-content li { | |
| margin-bottom: 6px; | |
| } | |
| .message-content strong { | |
| color: var(--accent-blue); | |
| } | |
| .message-content code { | |
| background: rgba(255, 255, 255, 0.1); | |
| padding: 2px 4px; | |
| border-radius: 4px; | |
| font-family: 'Fira Code', monospace; | |
| font-size: 0.9em; | |
| } | |
| /* Message Actions */ | |
| .message-actions { | |
| display: flex; | |
| gap: 12px; | |
| margin-top: 12px; | |
| padding-top: 8px; | |
| border-top: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| .action-icon-btn { | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| padding: 6px; | |
| border-radius: 6px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s; | |
| gap: 4px; | |
| font-size: 12px; | |
| font-family: inherit; | |
| } | |
| .action-icon-btn:hover { | |
| background: rgba(255, 255, 255, 0.05); | |
| color: var(--text-primary); | |
| } | |
| .action-icon-btn i { | |
| width: 14px; | |
| height: 14px; | |
| } | |
| .action-icon-btn.active { | |
| color: #10a37f; | |
| } | |
| .mic-btn.recording { | |
| color: #ef4444; | |
| animation: pulse-red 1.5s infinite; | |
| } | |
| @keyframes pulse-red { | |
| 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } | |
| 70% { transform: scale(1.1); box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } | |
| 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } | |
| } | |
| /* Important Highlight */ | |
| .important-highlight { | |
| background: rgba(16, 163, 127, 0.15); | |
| border-bottom: 2px solid #10a37f; | |
| padding: 0 2px; | |
| border-radius: 2px; | |
| font-weight: 600; | |
| } | |
| /* --- Premium Image Card --- */ | |
| .image-card { | |
| background: rgba(30, 30, 30, 0.6); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| border-radius: 24px; | |
| padding: 12px; | |
| margin-top: 12px; | |
| overflow: hidden; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3); | |
| transition: transform 0.3s ease; | |
| max-width: 500px; | |
| } | |
| .image-card:hover { | |
| transform: translateY(-2px); | |
| } | |
| .image-card img { | |
| width: 100%; | |
| height: auto; | |
| border-radius: 16px; | |
| cursor: zoom-in; | |
| transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); | |
| } | |
| .image-card img:hover { | |
| transform: scale(1.02); | |
| } | |
| .image-card-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 12px; | |
| padding: 4px 8px; | |
| } | |
| .image-card-header span { | |
| font-size: 12px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: #10a37f; | |
| } | |
| .image-card-actions { | |
| display: flex; | |
| gap: 8px; | |
| margin-top: 12px; | |
| padding: 0 4px; | |
| } | |
| .img-btn { | |
| flex: 1; | |
| padding: 8px; | |
| border-radius: 10px; | |
| background: rgba(255,255,255,0.05); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| color: var(--text-secondary); | |
| font-size: 12px; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 6px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| text-decoration: none; | |
| } | |
| .img-btn:hover { | |
| background: rgba(255,255,255,0.1); | |
| color: var(--text-primary); | |
| } | |
| /* --- Floating Centered Input --- */ | |
| .input-container { | |
| max-width: 840px; | |
| width: 95%; | |
| margin: 0 auto 24px; | |
| z-index: 100; | |
| background: rgba(20, 20, 20, 0.7); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| border-radius: 24px; | |
| padding: 10px; | |
| box-shadow: 0 20px 50px rgba(0,0,0,0.5); | |
| transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); | |
| flex-shrink: 0; | |
| position: relative; | |
| } | |
| /* --- Empty State Experience --- */ | |
| .hero-section { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| text-align: center; | |
| animation: fadeIn 1s ease; | |
| padding: 40px 20px; | |
| } | |
| .hero-logo { | |
| font-size: 42px; | |
| font-weight: 800; | |
| background: linear-gradient(135deg, #10a37f 0%, #00d2ff 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 8px; | |
| } | |
| .hero-subtitle { | |
| font-size: 18px; | |
| color: var(--text-secondary); | |
| margin-bottom: 40px; | |
| } | |
| .suggestion-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 12px; | |
| max-width: 600px; | |
| width: 100%; | |
| } | |
| .suggestion-card { | |
| background: rgba(255, 255, 255, 0.03); | |
| border: 1px solid rgba(255,255,255,0.08); | |
| border-radius: 16px; | |
| padding: 16px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| text-align: left; | |
| } | |
| .suggestion-card:hover { | |
| background: rgba(255, 255, 255, 0.06); | |
| border-color: rgba(255,255,255,0.15); | |
| } | |
| .suggestion-card h4 { | |
| font-size: 14px; | |
| margin-bottom: 4px; | |
| color: var(--text-primary); | |
| } | |
| .suggestion-card p { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| } | |
| @media (max-width: 768px) { | |
| .chat-history { | |
| padding: 20px 10px; | |
| } | |
| .message-content { | |
| max-width: 90%; | |
| } | |
| .suggestion-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .input-container { | |
| bottom: 10px; | |
| margin-bottom: 10px; | |
| border-radius: 16px; | |
| } | |
| } | |
| /* Markdown Styles in messages */ | |
| .message-content pre { | |
| background-color: #000; | |
| padding: 12px; | |
| border-radius: 8px; | |
| overflow-x: auto; | |
| margin: 10px 0; | |
| border: 1px solid var(--border-color); | |
| } | |
| .message-content code { | |
| font-family: 'Fira Code', 'Courier New', monospace; | |
| font-size: 0.9em; | |
| background-color: rgba(255, 255, 255, 0.05); | |
| padding: 2px 4px; | |
| border-radius: 4px; | |
| } | |
| .message-content pre code { | |
| background-color: transparent; | |
| padding: 0; | |
| } | |
| .message-content p { | |
| margin-bottom: 8px; | |
| } | |
| .message-content ul, | |
| .message-content ol { | |
| margin-left: 20px; | |
| margin-bottom: 8px; | |
| } | |
| @keyframes messageSlide { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| } | |
| to { | |
| opacity: 1; | |
| } | |
| } | |
| /* Input Area Styles */ | |
| .input-container { | |
| padding: 24px 20px; | |
| background: linear-gradient(to top, var(--bg-primary) 80%, transparent); | |
| position: relative; | |
| z-index: 10; | |
| transition: all 0.8s cubic-bezier(0.2, 0.8, 0.2, 1); | |
| } | |
| /* Center input when in hero-mode */ | |
| .main-area.hero-mode .input-container { | |
| transform: translateY(-35vh) scale(1.05); | |
| background: transparent; | |
| } | |
| .input-wrapper { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| position: relative; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: stretch; | |
| background: rgba(30, 30, 30, 0.6); | |
| backdrop-filter: blur(16px); | |
| border-radius: 24px; | |
| padding: 12px 18px; | |
| border: 1px solid var(--border-color); | |
| box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); | |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| gap: 8px; | |
| } | |
| .input-wrapper.glow-active { | |
| box-shadow: 0 0 40px rgba(16, 163, 127, 0.2), 0 0 80px rgba(16, 163, 127, 0.1), 0 8px 32px rgba(0, 0, 0, 0.4); | |
| border-color: rgba(16, 163, 127, 0.4); | |
| background: rgba(30, 30, 30, 0.8); | |
| } | |
| .message-input { | |
| width: 100%; | |
| background: transparent; | |
| border: none; | |
| outline: none; | |
| color: var(--text-primary); | |
| font-size: 15px; | |
| line-height: 1.5; | |
| resize: none; | |
| max-height: 200px; | |
| min-height: 24px; | |
| padding: 4px 0; | |
| margin: 0; | |
| font-family: inherit; | |
| } | |
| .message-input::placeholder { | |
| color: var(--text-secondary); | |
| } | |
| .input-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .action-btn { | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| padding: 6px; | |
| border-radius: 8px; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .action-btn:hover { | |
| color: var(--text-primary); | |
| background: var(--bg-tertiary); | |
| } | |
| .image-mode-btn { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 6px 12px; | |
| border-radius: 12px; | |
| font-size: 13px; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| background: transparent; | |
| border: 1px solid transparent; | |
| } | |
| .image-mode-btn:hover { | |
| background: var(--bg-tertiary); | |
| color: var(--text-primary); | |
| } | |
| .image-mode-btn.active { | |
| background: rgba(16, 163, 127, 0.1); | |
| color: var(--accent-blue); | |
| border-color: rgba(16, 163, 127, 0.2); | |
| } | |
| .input-wrapper.image-mode { | |
| border-color: var(--accent-blue); | |
| box-shadow: 0 0 15px rgba(16, 163, 127, 0.15); | |
| } | |
| /* Feature Active (Extended) Chat Box */ | |
| .input-wrapper.feature-active { | |
| flex-direction: column; | |
| align-items: stretch; | |
| padding: 15px 18px 10px 18px; | |
| border-radius: 20px; | |
| gap: 12px; | |
| min-height: 110px; /* Force extension */ | |
| } | |
| .input-wrapper.feature-active .message-input { | |
| min-height: 40px; | |
| padding: 5px 0; | |
| font-size: 16px; | |
| display: block; /* Allow multi-line alignment */ | |
| } | |
| .input-wrapper.feature-active .input-container-bottom { | |
| margin-top: 5px; | |
| border-top: 1px solid rgba(255, 255, 255, 0.05); | |
| padding-top: 8px; | |
| } | |
| .input-container-bottom { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| width: 100%; | |
| } | |
| .input-actions-left, .input-actions-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| /* Feature Badge Styles */ | |
| .feature-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 4px 10px; | |
| background: rgba(16, 163, 127, 0.1); | |
| border: 1px solid rgba(16, 163, 127, 0.2); | |
| border-radius: 12px; | |
| color: var(--accent-blue); | |
| font-size: 13px; | |
| font-weight: 500; | |
| margin-right: 8px; | |
| animation: slideIn 0.2s ease; | |
| } | |
| .feature-badge i { | |
| width: 14px; | |
| height: 14px; | |
| } | |
| .remove-feature-btn { | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| padding: 2px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 4px; | |
| transition: all 0.2s; | |
| } | |
| .remove-feature-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: #ef4444; | |
| } | |
| .remove-feature-btn i { | |
| width: 12px; | |
| height: 12px; | |
| } | |
| .send-button { | |
| background-color: var(--accent-blue); | |
| color: white; | |
| border: none; | |
| border-radius: 12px; | |
| width: 34px; | |
| height: 34px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| opacity: 0.6; /* More visible when empty */ | |
| } | |
| .send-button.active { | |
| opacity: 1; | |
| box-shadow: 0 0 15px rgba(16, 163, 127, 0.5); | |
| } | |
| .send-button:hover.active { | |
| background-color: var(--accent-hover); | |
| transform: scale(1.05); | |
| } | |
| .stop-button { | |
| background-color: #ef4444; | |
| color: white; | |
| border: none; | |
| border-radius: 12px; | |
| width: 34px; | |
| height: 34px; | |
| display: none; /* Hidden by default */ | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| box-shadow: 0 0 15px rgba(239, 68, 68, 0.3); | |
| } | |
| .stop-button:hover { | |
| background-color: #dc2626; | |
| transform: scale(1.05); | |
| } | |
| .stop-button.active { | |
| display: flex; | |
| } | |
| .disclaimer { | |
| text-align: center; | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| margin-top: 12px; | |
| opacity: 0.7; | |
| } | |
| /* Thinking animation */ | |
| .thinking { | |
| display: flex; | |
| gap: 4px; | |
| padding: 8px 0; | |
| } | |
| .thinking span { | |
| width: 8px; | |
| height: 8px; | |
| background-color: var(--accent-blue); | |
| border-radius: 50%; | |
| animation: bounce 1.4s infinite ease-in-out both; | |
| box-shadow: 0 0 10px var(--accent-blue); | |
| } | |
| .thinking-status-text { | |
| font-size: 14px; | |
| color: var(--text-primary); | |
| font-weight: 600; | |
| letter-spacing: 0.02em; | |
| animation: statusPulse 2s infinite ease-in-out; | |
| } | |
| @keyframes statusPulse { | |
| 0%, 100% { opacity: 0.7; transform: translateX(0); } | |
| 50% { opacity: 1; transform: translateX(4px); } | |
| } | |
| .ai-processing-ring { | |
| position: relative; | |
| width: 40px; | |
| height: 40px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-bottom: 10px; | |
| } | |
| .ai-processing-ring::before { | |
| content: ''; | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| border: 2px solid var(--accent-blue); | |
| border-top-color: transparent; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| /* Drag and Drop Overlay */ | |
| .drag-drop-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(16, 163, 127, 0.15); | |
| backdrop-filter: blur(8px); | |
| border: 2px dashed var(--accent-blue); | |
| z-index: 500; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| flex-direction: column; | |
| gap: 20px; | |
| pointer-events: none; | |
| transition: all 0.3s ease; | |
| } | |
| .drag-drop-overlay.active { | |
| display: flex; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| .drag-drop-icon { | |
| width: 80px; | |
| height: 80px; | |
| background: rgba(16, 163, 127, 0.1); | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: var(--accent-blue); | |
| animation: pulse-glow 2s infinite; | |
| } | |
| @keyframes pulse-glow { | |
| 0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(16, 163, 127, 0.4); } | |
| 50% { transform: scale(1.1); box-shadow: 0 0 20px 10px rgba(16, 163, 127, 0); } | |
| } | |
| .drag-drop-text { | |
| font-size: 24px; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| } | |
| .llm-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border-color); | |
| padding: 4px 10px; | |
| border-radius: 8px; | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| } | |
| @keyframes bounce { | |
| 0%, | |
| 80%, | |
| 100% { | |
| transform: scale(0); | |
| } | |
| 40% { | |
| transform: scale(1.0); | |
| } | |
| } | |
| /* Auth Overlay */ | |
| .auth-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.7); | |
| backdrop-filter: blur(8px); | |
| z-index: 2000; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .auth-overlay.show { | |
| display: flex; | |
| } | |
| .auth-card { | |
| background-color: var(--bg-secondary); | |
| width: 400px; | |
| padding: 32px; | |
| border-radius: 20px; | |
| border: 1px solid var(--border-color); | |
| box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); | |
| animation: fadeIn 0.4s ease; | |
| } | |
| .auth-title { | |
| font-size: 24px; | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| text-align: center; | |
| } | |
| .auth-subtitle { | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| margin-bottom: 24px; | |
| text-align: center; | |
| } | |
| .auth-form { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| } | |
| .auth-input { | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-color); | |
| color: var(--text-primary); | |
| padding: 12px 16px; | |
| border-radius: 10px; | |
| outline: none; | |
| font-size: 14px; | |
| transition: border-color 0.2s; | |
| } | |
| .auth-input:focus { | |
| border-color: var(--accent-blue); | |
| } | |
| .password-container { | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .password-toggle { | |
| position: absolute; | |
| right: 12px; | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| padding: 4px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: color 0.2s; | |
| } | |
| .password-toggle:hover { | |
| color: var(--text-primary); | |
| } | |
| .password-toggle.active { | |
| color: var(--accent-blue); | |
| } | |
| .auth-submit { | |
| background-color: var(--accent-blue); | |
| color: white; | |
| border: none; | |
| padding: 12px; | |
| border-radius: 10px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| } | |
| .auth-submit:hover { | |
| background-color: var(--accent-hover); | |
| } | |
| .auth-error { | |
| color: #ef4444; | |
| font-size: 13px; | |
| text-align: center; | |
| margin-top: 12px; | |
| display: none; | |
| } | |
| .auth-footer { | |
| margin-top: 24px; | |
| text-align: center; | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| } | |
| .auth-toggle { | |
| color: var(--accent-blue); | |
| cursor: pointer; | |
| font-weight: 500; | |
| } | |
| /* Mobile Adjustments */ | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| position: absolute; | |
| left: 0; | |
| height: 100%; | |
| z-index: 1000; | |
| transform: translateX(-100%); | |
| } | |
| .sidebar.open { | |
| transform: translateX(0); | |
| box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); | |
| } | |
| .main-area.hero-mode .input-container { | |
| transform: translateY(-20vh); | |
| } | |
| .suggestions-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* Blinking Emoji */ | |
| .blinking-emoji { | |
| width: 32px; | |
| height: 32px; | |
| background: linear-gradient(135deg, #10a37f 0%, #1a7f64 100%); | |
| border-radius: 50%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 4px; | |
| box-shadow: 0 0 15px rgba(16, 163, 127, 0.4); | |
| position: relative; | |
| flex-shrink: 0; | |
| transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| } | |
| .blinking-emoji:hover { | |
| transform: scale(1.1) rotate(5deg); | |
| } | |
| .blinking-emoji .eye { | |
| width: 4px; | |
| height: 6px; | |
| background: white; | |
| border-radius: 2px; | |
| animation: eye-blink 4s infinite; | |
| } | |
| @keyframes eye-blink { | |
| 0%, | |
| 90%, | |
| 100% { | |
| transform: scaleY(1); | |
| } | |
| 92% { | |
| transform: scaleY(0.1); | |
| } | |
| } | |
| /* Attachment UI */ | |
| .attachments-container { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-bottom: 12px; | |
| max-width: 800px; | |
| margin: 0 auto 12px; | |
| } | |
| .file-attachment-card { | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 12px; | |
| padding: 8px 12px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 13px; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| .file-attachment-icon { | |
| width: 28px; | |
| height: 28px; | |
| border-radius: 6px; | |
| background: #e53e3e; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| } | |
| .file-attachment-info { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .file-attachment-name { | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| max-width: 150px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .file-attachment-type { | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| } | |
| .file-attachment-remove { | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| padding: 4px; | |
| border-radius: 4px; | |
| transition: background 0.2s; | |
| } | |
| .file-attachment-remove:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: #ef4444; | |
| } | |
| .upload-menu { | |
| position: absolute; | |
| bottom: calc(100% + 15px); | |
| left: 0; | |
| background: #1e1e1e; | |
| backdrop-filter: blur(16px); | |
| border: 1px solid var(--border-color); | |
| border-radius: 24px; | |
| padding: 12px; | |
| display: none; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 8px; | |
| z-index: 1000; | |
| box-shadow: 0 15px 50px rgba(0, 0, 0, 0.6); | |
| animation: slideUp 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.2); | |
| width: max-content; | |
| } | |
| .upload-menu.show { | |
| display: grid; | |
| } | |
| .upload-menu .tool-btn { | |
| position: relative; | |
| width: 80px; | |
| height: 80px; | |
| background: rgba(255, 255, 255, 0.03); | |
| border-radius: 18px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| color: var(--text-secondary); | |
| } | |
| .coming-soon-badge { | |
| position: absolute; | |
| bottom: 6px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| font-size: 7px; | |
| background: linear-gradient(135deg, #10a37f 0%, #0d8a6a 100%); | |
| color: white; | |
| padding: 1px 5px; | |
| border-radius: 4px; | |
| white-space: nowrap; | |
| font-weight: 800; | |
| text-transform: uppercase; | |
| letter-spacing: 0.02em; | |
| pointer-events: none; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.3); | |
| display: none; /* Hidden by default */ | |
| } | |
| .tool-btn.restricted .coming-soon-badge { | |
| display: block; | |
| } | |
| .tool-btn.restricted { | |
| opacity: 0.8; | |
| } | |
| .upload-menu .tool-btn:hover { | |
| background: rgba(255, 255, 255, 0.08); | |
| color: var(--text-primary); | |
| transform: translateY(-3px); | |
| border-color: var(--accent-blue); | |
| box-shadow: 0 5px 15px rgba(16, 163, 127, 0.1); | |
| } | |
| .upload-menu .tool-btn i { | |
| width: 24px; | |
| height: 24px; | |
| color: var(--accent-blue); | |
| } | |
| .upload-menu .tool-btn span { | |
| font-size: 11px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.02em; | |
| } | |
| /* Landing Page Styles */ | |
| .landing-page { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: var(--bg-primary); | |
| z-index: 2000; | |
| display: flex; | |
| flex-direction: column; | |
| overflow-y: auto; | |
| transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1), transform 0.8s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .landing-page.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| transform: scale(1.05); | |
| } | |
| .landing-header { | |
| padding: 24px 40px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| backdrop-filter: blur(10px); | |
| position: sticky; | |
| top: 0; | |
| z-index: 10; | |
| } | |
| .landing-logo { | |
| font-size: 24px; | |
| font-weight: 800; | |
| color: #fff; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| letter-spacing: -0.02em; | |
| } | |
| .landing-nav { | |
| display: flex; | |
| gap: 32px; | |
| align-items: center; | |
| } | |
| .nav-link { | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| font-size: 14px; | |
| font-weight: 600; | |
| transition: color 0.2s; | |
| } | |
| .nav-link:hover { | |
| color: var(--text-primary); | |
| } | |
| .hero-section { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 120px 20px; | |
| text-align: center; | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| } | |
| /* Lightning Effect */ | |
| .lightning-overlay { | |
| position: fixed; | |
| inset: 0; | |
| pointer-events: none; | |
| z-index: 1; | |
| opacity: 0; | |
| background: radial-gradient(circle at 50% 50%, rgba(0, 242, 255, 0.15) 0%, transparent 70%); | |
| transition: opacity 0.1s ease-out; | |
| } | |
| .lightning-flash { | |
| animation: flash 0.6s ease-out forwards; | |
| } | |
| @keyframes flash { | |
| 0% { opacity: 0; background: radial-gradient(circle at 50% 50%, rgba(0, 242, 255, 0.4) 0%, transparent 80%); } | |
| 10% { opacity: 1; } | |
| 20% { opacity: 0.3; } | |
| 30% { opacity: 0.8; } | |
| 100% { opacity: 0; } | |
| } | |
| .hero-badge { | |
| background: rgba(16, 163, 127, 0.1); | |
| color: #10a37f; | |
| padding: 8px 16px; | |
| border-radius: 100px; | |
| font-size: 13px; | |
| font-weight: 600; | |
| margin-bottom: 24px; | |
| border: 1px solid rgba(16, 163, 127, 0.2); | |
| animation: fadeInDown 0.8s ease-out; | |
| } | |
| /* Trust Badges in Hero */ | |
| .trust-badges { | |
| display: flex; | |
| gap: 24px; | |
| margin-top: 40px; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| animation: fadeInUp 0.8s ease-out 0.8s backwards; | |
| } | |
| .trust-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| } | |
| .trust-badge svg, .trust-badge i { width: 16px; height: 16px; color: #10a37f; } | |
| /* General Section Layout */ | |
| .section-wrapper { | |
| padding: 100px 40px; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| .section-header { | |
| text-align: center; | |
| margin-bottom: 60px; | |
| } | |
| .section-header h2 { | |
| font-size: 36px; | |
| font-weight: 700; | |
| margin-bottom: 16px; | |
| } | |
| .section-header p { | |
| font-size: 18px; | |
| color: var(--text-secondary); | |
| max-width: 600px; | |
| margin: 0 auto; | |
| } | |
| /* How It Works */ | |
| .how-it-works-steps { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 40px; | |
| } | |
| .step-item { | |
| text-align: center; | |
| position: relative; | |
| } | |
| .step-number { | |
| width: 48px; | |
| height: 48px; | |
| background: rgba(16, 163, 127, 0.1); | |
| color: #10a37f; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 700; | |
| margin: 0 auto 24px; | |
| border: 1px solid rgba(16, 163, 127, 0.2); | |
| font-size: 20px; | |
| } | |
| .step-item h3 { margin-bottom: 12px; font-size: 20px; font-weight: 600; } | |
| .step-item p { color: var(--text-secondary); line-height: 1.6; } | |
| /* Testimonials */ | |
| .testimonials-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 24px; | |
| } | |
| .testimonial-card { | |
| background: rgba(255, 255, 255, 0.02); | |
| border: 1px solid var(--border-color); | |
| padding: 32px; | |
| border-radius: 24px; | |
| transition: all 0.3s ease; | |
| } | |
| .testimonial-card:hover { | |
| background: rgba(255, 255, 255, 0.04); | |
| transform: translateY(-5px); | |
| } | |
| .testimonial-card p { | |
| font-style: italic; | |
| color: var(--text-secondary); | |
| line-height: 1.6; | |
| margin-bottom: 20px; | |
| } | |
| .testimonial-author { | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| font-size: 14px; | |
| } | |
| /* Pricing */ | |
| .pricing-grid { | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| max-width: 450px; | |
| margin: 0 auto; | |
| } | |
| .pricing-card { | |
| background: linear-gradient(135deg, rgba(16, 163, 127, 0.1) 0%, rgba(16, 163, 127, 0.02) 100%); | |
| border: 1px solid rgba(16, 163, 127, 0.3); | |
| padding: 48px; | |
| border-radius: 32px; | |
| text-align: center; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .pricing-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 4px; | |
| background: linear-gradient(90deg, #10a37f, #1a7f64); | |
| } | |
| /* FAQ */ | |
| .faq-container { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| } | |
| .faq-item { | |
| border-bottom: 1px solid var(--border-color); | |
| padding: 24px 0; | |
| } | |
| .faq-question { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| cursor: pointer; | |
| font-weight: 600; | |
| font-size: 18px; | |
| transition: color 0.2s; | |
| } | |
| .faq-question:hover { color: #10a37f; } | |
| .faq-answer { | |
| margin-top: 16px; | |
| color: var(--text-secondary); | |
| line-height: 1.6; | |
| display: none; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| .faq-item.active .faq-answer { display: block; } | |
| .faq-item.active .faq-question svg { transform: rotate(180deg); } | |
| .faq-question svg { transition: transform 0.3s ease; width: 20px; height: 20px; } | |
| /* Footer */ | |
| .landing-footer { | |
| padding: 100px 40px 40px; | |
| background: var(--bg-secondary); | |
| border-top: 1px solid var(--border-color); | |
| margin-top: 100px; | |
| } | |
| .footer-content { | |
| display: grid; | |
| grid-template-columns: 2fr 1fr 1fr 1fr; | |
| gap: 60px; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| .footer-info p { margin: 20px 0; line-height: 1.6; color: var(--text-secondary); } | |
| .footer-bottom { | |
| margin-top: 80px; | |
| padding-top: 32px; | |
| border-top: 1px solid var(--border-color); | |
| text-align: center; | |
| color: var(--text-secondary); | |
| font-size: 14px; | |
| } | |
| .hero-title { | |
| font-size: clamp(48px, 8vw, 84px); | |
| font-weight: 800; | |
| line-height: 1.1; | |
| margin-bottom: 24px; | |
| background: linear-gradient(to bottom, #fff 0%, #a3a3a3 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| letter-spacing: -0.04em; | |
| animation: fadeInUp 0.8s ease-out 0.2s backwards; | |
| } | |
| .typing-cursor { | |
| border-right: 4px solid var(--accent-blue); | |
| margin-left: 4px; | |
| animation: blink-cursor 0.75s step-end infinite; | |
| display: inline-block; | |
| vertical-align: middle; | |
| height: 1em; | |
| width: 0; | |
| } | |
| @keyframes blink-cursor { | |
| from, to { border-color: transparent } | |
| 50% { border-color: var(--accent-blue); } | |
| } | |
| .hero-subtitle { | |
| font-size: clamp(18px, 2vw, 22px); | |
| color: var(--text-secondary); | |
| max-width: 600px; | |
| line-height: 1.6; | |
| margin-bottom: 48px; | |
| animation: fadeInUp 0.8s ease-out 0.4s backwards; | |
| } | |
| .cta-container { | |
| display: flex; | |
| gap: 16px; | |
| animation: fadeInUp 0.8s ease-out 0.6s backwards; | |
| } | |
| .btn-primary { | |
| padding: 16px 32px; | |
| background: #10a37f; | |
| color: #fff; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| text-decoration: none; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| box-shadow: 0 4px 15px rgba(16, 163, 127, 0.3); | |
| cursor: pointer; | |
| border: none; | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 30px rgba(16, 163, 127, 0.4); | |
| background: #14b88f; | |
| } | |
| .btn-secondary { | |
| padding: 16px 32px; | |
| background: rgba(255, 255, 255, 0.05); | |
| color: #fff; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| text-decoration: none; | |
| transition: all 0.3s ease; | |
| border: 1px solid var(--border-color); | |
| cursor: pointer; | |
| } | |
| .btn-secondary:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-color: rgba(255, 255, 255, 0.2); | |
| transform: translateY(-2px); | |
| } | |
| .features-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 24px; | |
| padding: 100px 40px; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| width: 100%; | |
| animation: fadeInUp 1s ease-out 0.8s backwards; | |
| } | |
| .feature-card { | |
| background: rgba(255, 255, 255, 0.02); | |
| border: 1px solid var(--border-color); | |
| padding: 32px; | |
| border-radius: 24px; | |
| text-align: left; | |
| transition: all 0.3s ease; | |
| } | |
| .feature-card:hover { | |
| background: rgba(255, 255, 255, 0.04); | |
| transform: translateY(-5px); | |
| border-color: rgba(255, 255, 255, 0.15); | |
| } | |
| .feature-icon { | |
| width: 48px; | |
| height: 48px; | |
| background: rgba(16, 163, 127, 0.1); | |
| color: #10a37f; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-bottom: 20px; | |
| } | |
| .feature-card h3 { | |
| font-size: 20px; | |
| font-weight: 600; | |
| margin-bottom: 12px; | |
| } | |
| .feature-card p { | |
| font-size: 15px; | |
| color: var(--text-secondary); | |
| line-height: 1.6; | |
| } | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes fadeInDown { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @media (max-width: 992px) { | |
| .footer-content { grid-template-columns: 1fr 1fr; } | |
| .hero-title { font-size: 56px; } | |
| } | |
| @media (max-width: 768px) { | |
| .features-grid, .how-it-works-steps, .testimonials-grid, .footer-content { grid-template-columns: 1fr; } | |
| .landing-header { padding: 16px 20px; } | |
| .landing-nav .nav-link { display: none; } | |
| .landing-nav { gap: 12px; } | |
| .hero-title { font-size: 42px; } | |
| .hero-section { padding: 60px 20px; } | |
| .section-wrapper { padding: 60px 20px; } | |
| .cta-container { flex-direction: column; width: 100%; } | |
| .btn-primary, .btn-secondary { width: 100%; text-align: center; } | |
| .trust-badges { gap: 16px; } | |
| .landing-logo { font-size: 20px; } | |
| .blinking-emoji { width: 28px; height: 28px; } | |
| } | |
| /* Image Generation Modal */ | |
| .img-modal-overlay { | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0, 0, 0, 0.85); | |
| backdrop-filter: blur(12px); | |
| z-index: 3000; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| } | |
| .img-modal-overlay.open { | |
| display: flex; | |
| opacity: 1; | |
| } | |
| .img-modal-card { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 24px; | |
| width: min(800px, 94vw); | |
| max-height: 90vh; | |
| overflow-y: auto; | |
| padding: 40px; | |
| position: relative; | |
| box-shadow: 0 30px 60px rgba(0, 0, 0, 0.6); | |
| transform: translateY(20px); | |
| transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| } | |
| .img-modal-overlay.open .img-modal-card { | |
| transform: translateY(0); | |
| } | |
| .img-modal-close { | |
| position: absolute; | |
| top: 24px; | |
| right: 24px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: none; | |
| color: var(--text-secondary); | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .img-modal-close:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--text-primary); | |
| transform: rotate(90deg); | |
| } | |
| .img-gen-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 40px; | |
| align-items: start; | |
| } | |
| @media (max-width: 768px) { | |
| .img-gen-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .img-prompt-area h2 { | |
| font-size: 28px; | |
| margin-bottom: 12px; | |
| } | |
| .img-prompt-area p { | |
| color: var(--text-secondary); | |
| margin-bottom: 24px; | |
| font-size: 15px; | |
| } | |
| .img-textarea { | |
| width: 100%; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 16px; | |
| padding: 16px; | |
| color: var(--text-primary); | |
| font-family: inherit; | |
| font-size: 15px; | |
| resize: none; | |
| min-height: 120px; | |
| outline: none; | |
| transition: border-color 0.2s; | |
| margin-bottom: 20px; | |
| } | |
| .img-textarea:focus { | |
| border-color: var(--accent-blue); | |
| } | |
| .img-preview-container { | |
| aspect-ratio: 1; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 20px; | |
| overflow: hidden; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| } | |
| .img-preview-container img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| display: none; | |
| } | |
| .img-preview-container.visible img { | |
| display: block; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| .img-skeleton { | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(110deg, var(--bg-primary) 8%, var(--bg-tertiary) 18%, var(--bg-primary) 33%); | |
| background-size: 200% 100%; | |
| animation: skeleton-shimmer 1.5s infinite linear; | |
| display: none; | |
| } | |
| .img-skeleton.visible { | |
| display: block; | |
| } | |
| @keyframes skeleton-shimmer { | |
| to { | |
| background-position: -200% 0; | |
| } | |
| } | |
| .img-placeholder-text { | |
| color: var(--text-secondary); | |
| text-align: center; | |
| padding: 20px; | |
| } | |
| .img-preview-container.visible .img-placeholder-text { | |
| display: none; | |
| } | |
| .img-download-btn { | |
| position: absolute; | |
| bottom: 16px; | |
| right: 16px; | |
| background: rgba(0, 0, 0, 0.6); | |
| backdrop-filter: blur(8px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| color: white; | |
| padding: 8px 16px; | |
| border-radius: 10px; | |
| font-size: 13px; | |
| text-decoration: none; | |
| display: none; | |
| align-items: center; | |
| gap: 8px; | |
| transition: all 0.2s; | |
| } | |
| .img-preview-container.visible .img-download-btn { | |
| display: flex; | |
| } | |
| .img-download-btn:hover { | |
| background: rgba(0, 0, 0, 0.8); | |
| transform: translateY(-2px); | |
| } | |
| /* Image Response Styles */ | |
| .image-card { | |
| background: var(--bg-tertiary); | |
| border-radius: 16px; | |
| overflow: hidden; | |
| margin-top: 10px; | |
| border: 1px solid var(--border-color); | |
| box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3); | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| max-width: 512px; | |
| width: 100%; | |
| position: relative; | |
| } | |
| .image-card:hover { | |
| transform: translateY(-4px) scale(1.01); | |
| border-color: var(--accent-blue); | |
| } | |
| .image-card img { | |
| width: 100%; | |
| height: auto; | |
| display: block; | |
| opacity: 0; | |
| transition: opacity 0.8s ease; | |
| } | |
| .image-card img.loaded { | |
| opacity: 1; | |
| } | |
| /* Motion Image Animation */ | |
| .motion-image { | |
| animation: kenBurns 10s ease-in-out infinite alternate; | |
| transform-origin: center; | |
| } | |
| @keyframes kenBurns { | |
| 0% { transform: scale(1); } | |
| 100% { transform: scale(1.1); } | |
| } | |
| .image-card-actions { | |
| padding: 12px; | |
| display: flex; | |
| gap: 8px; | |
| background: rgba(0, 0, 0, 0.4); | |
| backdrop-filter: blur(8px); | |
| } | |
| .img-btn { | |
| flex: 1; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| padding: 10px; | |
| border-radius: 10px; | |
| border: 1px solid var(--border-color); | |
| background: rgba(255, 255, 255, 0.05); | |
| color: var(--text-primary); | |
| font-size: 13px; | |
| text-decoration: none; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .img-btn:hover { | |
| background: var(--accent-blue); | |
| color: white; | |
| border-color: var(--accent-blue); | |
| } | |
| .gen-status { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 16px; | |
| color: var(--accent-blue); | |
| font-size: 14px; | |
| font-weight: 500; | |
| } | |
| .gen-loader { | |
| width: 18px; | |
| height: 18px; | |
| border: 2px solid var(--accent-blue); | |
| border-bottom-color: transparent; | |
| border-radius: 50%; | |
| display: inline-block; | |
| animation: rotation 1s linear infinite; | |
| } | |
| @keyframes rotation { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| /* Command Help Popup */ | |
| .command-help { | |
| position: absolute; | |
| bottom: calc(100% + 15px); | |
| left: 20px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 14px; | |
| padding: 14px 18px; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); | |
| display: none; | |
| z-index: 1000; | |
| animation: slideUp 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| max-width: 320px; | |
| } | |
| @keyframes slideUp { | |
| from { transform: translateY(15px); opacity: 0; } | |
| to { transform: translateY(0); opacity: 1; } | |
| } | |
| .command-help.show { | |
| display: block; | |
| } | |
| .command-help h5 { | |
| font-size: 13px; | |
| color: var(--accent-blue); | |
| margin-bottom: 6px; | |
| font-weight: 600; | |
| } | |
| .command-help p { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| line-height: 1.4; | |
| } | |
| .command-help code { | |
| background: var(--bg-tertiary); | |
| padding: 4px 8px; | |
| border-radius: 6px; | |
| color: var(--text-primary); | |
| font-family: monospace; | |
| display: block; | |
| margin-top: 10px; | |
| font-size: 11px; | |
| border: 1px solid var(--border-color); | |
| white-space: pre-wrap; | |
| } | |
| /* Custom Video Player Styles */ | |
| .video-container { | |
| position: relative; | |
| width: 100%; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| background: #000; | |
| border: 1px solid var(--border-color); | |
| } | |
| .video-container video { | |
| width: 100%; | |
| display: block; | |
| } | |
| .custom-video-controls { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| background: linear-gradient(transparent, rgba(0, 0, 0, 0.8)); | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| padding: 10px 15px; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| z-index: 10; | |
| } | |
| .video-container:hover .custom-video-controls { | |
| opacity: 1; | |
| } | |
| .vid-ctrl-btn { | |
| background: none; | |
| border: none; | |
| color: white; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 5px; | |
| transition: transform 0.2s; | |
| } | |
| .vid-ctrl-btn:hover { | |
| transform: scale(1.1); | |
| color: var(--accent-blue); | |
| } | |
| .volume-container { | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .volume-slider-vertical { | |
| position: absolute; | |
| bottom: 40px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 30px; | |
| height: 100px; | |
| background: rgba(0, 0, 0, 0.8); | |
| border-radius: 15px; | |
| padding: 10px 5px; | |
| display: none; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 100; | |
| } | |
| .volume-container:hover .volume-slider-vertical { | |
| display: flex; | |
| } | |
| .volume-slider-vertical input { | |
| appearance: none; | |
| -webkit-appearance: none; | |
| width: 80px; | |
| height: 4px; | |
| background: rgba(255, 255, 255, 0.2); | |
| border-radius: 2px; | |
| outline: none; | |
| transform: rotate(-90deg); | |
| cursor: pointer; | |
| } | |
| .volume-slider-vertical input::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 12px; | |
| height: 12px; | |
| background: var(--accent-blue); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| box-shadow: 0 0 10px rgba(0, 163, 127, 0.5); | |
| } | |
| .video-progress { | |
| flex: 1; | |
| height: 4px; | |
| background: rgba(255, 255, 255, 0.2); | |
| border-radius: 2px; | |
| cursor: pointer; | |
| position: relative; | |
| } | |
| .video-progress-filled { | |
| height: 100%; | |
| background: var(--accent-blue); | |
| border-radius: 2px; | |
| width: 0%; | |
| } | |
| /* Auth Overlay Styles */ | |
| .auth-overlay { | |
| position: fixed; | |
| inset: 0; | |
| background: var(--bg-primary); | |
| z-index: 6000; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 20px; | |
| } | |
| .auth-card { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 24px; | |
| width: 100%; | |
| max-width: 420px; | |
| padding: 40px; | |
| box-shadow: 0 25px 50px rgba(0,0,0,0.4); | |
| animation: slideUp 0.4s ease; | |
| } | |
| .auth-logo { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| font-size: 24px; | |
| font-weight: 800; | |
| color: var(--accent-blue); | |
| } | |
| .auth-title { | |
| font-size: 24px; | |
| font-weight: 700; | |
| margin-bottom: 8px; | |
| text-align: center; | |
| } | |
| .auth-subtitle { | |
| color: var(--text-secondary); | |
| font-size: 14px; | |
| text-align: center; | |
| margin-bottom: 30px; | |
| } | |
| .auth-form { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| } | |
| .auth-input-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .auth-input-group label { | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| } | |
| .auth-input { | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 12px; | |
| padding: 12px 16px; | |
| color: var(--text-primary); | |
| font-size: 15px; | |
| outline: none; | |
| transition: border-color 0.2s; | |
| } | |
| .auth-input:focus { | |
| border-color: var(--accent-blue); | |
| } | |
| .auth-submit-btn { | |
| background: var(--accent-blue); | |
| color: white; | |
| border: none; | |
| border-radius: 12px; | |
| padding: 14px; | |
| font-size: 15px; | |
| font-weight: 700; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| margin-top: 10px; | |
| } | |
| .auth-submit-btn:hover { | |
| background: var(--accent-hover); | |
| transform: translateY(-2px); | |
| } | |
| .auth-divider { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin: 24px 0; | |
| color: var(--text-secondary); | |
| font-size: 13px; | |
| } | |
| .auth-divider::before, .auth-divider::after { | |
| content: ""; | |
| flex: 1; | |
| height: 1px; | |
| background: var(--border-color); | |
| } | |
| .oauth-btn { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 12px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 12px; | |
| padding: 12px; | |
| color: var(--text-primary); | |
| font-size: 14px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| } | |
| .oauth-btn:hover { | |
| background: var(--bg-tertiary); | |
| } | |
| .auth-switch { | |
| text-align: center; | |
| margin-top: 24px; | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| } | |
| /* Search Results Styles */ | |
| .search-results { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| margin-top: 10px; | |
| width: 100%; | |
| } | |
| .search-card { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 12px; | |
| padding: 14px; | |
| transition: all 0.2s ease; | |
| text-decoration: none; | |
| color: inherit; | |
| display: block; | |
| } | |
| .search-card:hover { | |
| border-color: var(--accent-blue); | |
| transform: translateY(-2px); | |
| background: var(--bg-tertiary); | |
| } | |
| .search-card h5 { | |
| color: var(--accent-blue); | |
| font-size: 15px; | |
| margin-bottom: 6px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .search-card p { | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| line-height: 1.5; | |
| } | |
| .search-card .link { | |
| font-size: 11px; | |
| color: #10a37f; | |
| opacity: 0.7; | |
| margin-top: 8px; | |
| word-break: break-all; | |
| } | |
| /* File Upload UI */ | |
| .file-status-card { | |
| background: rgba(16, 163, 127, 0.1); | |
| border: 1px solid rgba(16, 163, 127, 0.2); | |
| border-radius: 12px; | |
| padding: 12px 16px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 12px; | |
| animation: slideIn 0.3s ease; | |
| } | |
| .file-status-icon { | |
| color: var(--accent-blue); | |
| } | |
| .file-status-info { | |
| flex: 1; | |
| } | |
| .file-status-name { | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| .file-status-msg { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| } | |
| @keyframes slideIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* File Attachment Card (Inside Input) */ | |
| .attachments-container { | |
| display: none; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| padding: 12px 16px; | |
| border-bottom: 1px solid var(--border-color); | |
| background: rgba(255, 255, 255, 0.02); | |
| border-top-left-radius: 20px; | |
| border-top-right-radius: 20px; | |
| } | |
| .file-attachment-card { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 14px; | |
| padding: 10px 14px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| min-width: 180px; | |
| max-width: 240px; | |
| position: relative; | |
| animation: slideIn 0.3s ease; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
| } | |
| .file-attachment-icon { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| flex-shrink: 0; | |
| } | |
| .file-attachment-info { | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| flex: 1; | |
| } | |
| .file-attachment-name { | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| max-width: 130px; | |
| } | |
| .file-attachment-type { | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| text-transform: uppercase; | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .upload-progress-container { | |
| width: 100%; | |
| height: 3px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 10px; | |
| margin-top: 4px; | |
| overflow: hidden; | |
| display: none; | |
| } | |
| .upload-progress-bar { | |
| height: 100%; | |
| background: var(--accent-blue); | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| } | |
| .file-attachment-remove { | |
| position: absolute; | |
| top: -8px; | |
| right: -8px; | |
| width: 20px; | |
| height: 20px; | |
| background: #ef4444; | |
| color: white; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| border: 2px solid var(--bg-secondary); | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.2); | |
| z-index: 10; | |
| } | |
| .file-attachment-remove svg { | |
| width: 12px; | |
| height: 12px; | |
| } | |
| .file-attachment-remove:hover { | |
| background: #dc2626; | |
| transform: scale(1.1) rotate(90deg); | |
| } | |
| @keyframes slideIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* Gemini-style Typing Cursor */ | |
| .typing-cursor-ai { | |
| display: inline-block; | |
| width: 8px; | |
| height: 18px; | |
| background: var(--accent-blue); | |
| margin-left: 4px; | |
| vertical-align: middle; | |
| animation: blink 0.8s infinite; | |
| border-radius: 2px; | |
| box-shadow: 0 0 8px var(--accent-blue); | |
| } | |
| @keyframes blink { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0; } | |
| } | |
| /* Smooth Message Transitions */ | |
| .message { | |
| display: flex; | |
| gap: 16px; | |
| max-width: 850px; | |
| margin: 0 auto; | |
| width: 100%; | |
| padding: 10px 0; | |
| opacity: 0; | |
| transform: translateY(10px); | |
| animation: messageAppear 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) forwards; | |
| } | |
| @keyframes messageAppear { | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* AI Thinking Glow */ | |
| .message.ai.thinking-glow .message-content { | |
| filter: blur(0.5px); | |
| opacity: 0.8; | |
| transition: all 0.3s ease; | |
| } | |
| /* Feature List Styles */ | |
| .features-modal-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.8); | |
| backdrop-filter: blur(12px); | |
| z-index: 2000; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| opacity: 0; | |
| transition: all 0.3s ease; | |
| } | |
| .features-modal-overlay.open { | |
| display: flex; | |
| opacity: 1; | |
| } | |
| .features-modal { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| border-radius: 24px; | |
| width: 90%; | |
| max-width: 650px; | |
| max-height: 85vh; | |
| overflow-y: auto; | |
| padding: 32px; | |
| position: relative; | |
| box-shadow: 0 25px 50px rgba(0,0,0,0.5); | |
| } | |
| .features-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 20px; | |
| margin-top: 24px; | |
| } | |
| .feature-item { | |
| background: rgba(255, 255, 255, 0.03); | |
| border: 1px solid var(--border-color); | |
| border-radius: 16px; | |
| padding: 20px; | |
| transition: all 0.2s; | |
| } | |
| .feature-item:hover { | |
| background: rgba(255, 255, 255, 0.05); | |
| border-color: var(--accent-blue); | |
| } | |
| .feature-icon { | |
| width: 40px; | |
| height: 40px; | |
| background: rgba(16, 163, 127, 0.1); | |
| color: var(--accent-blue); | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-bottom: 16px; | |
| } | |
| .feature-item h3 { | |
| font-size: 16px; | |
| margin-bottom: 8px; | |
| color: var(--text-primary); | |
| } | |
| .feature-item p { | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| line-height: 1.5; | |
| } | |
| .features-close { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| padding: 8px; | |
| border-radius: 50%; | |
| transition: all 0.2s; | |
| } | |
| .features-close:hover { | |
| background: rgba(255, 255, 255, 0.05); | |
| color: var(--text-primary); | |
| } | |
| /* Memory Indicator */ | |
| .memory-indicator { | |
| position: absolute; | |
| top: 70px; | |
| right: 20px; | |
| z-index: 100; | |
| display: none; | |
| align-items: center; | |
| gap: 8px; | |
| background: rgba(16, 163, 127, 0.1); | |
| backdrop-filter: blur(12px); | |
| color: #10a37f; | |
| padding: 8px 16px; | |
| border-radius: 12px; | |
| font-size: 13px; | |
| font-weight: 600; | |
| border: 1px solid rgba(16, 163, 127, 0.2); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
| animation: slideInRight 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| } | |
| @keyframes slideInRight { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| .memory-indicator.visible { | |
| display: flex; | |
| } | |
| /* Mobile Adjustments */ | |
| @media (max-width: 768px) { | |
| .tools-panel { | |
| bottom: 100px; | |
| width: 90%; | |
| justify-content: space-around; | |
| padding: 6px; | |
| } | |
| .tool-btn { | |
| width: 50px; | |
| height: 50px; | |
| } | |
| } | |
| /* Status Badges */ | |
| .status-badge-container { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 12px; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| .status-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 4px 10px; | |
| border-radius: 20px; | |
| font-size: 11px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| border: 1px solid transparent; | |
| } | |
| .status-badge.auto-mode { | |
| background: rgba(16, 163, 127, 0.1); | |
| color: #10a37f; | |
| border-color: rgba(16, 163, 127, 0.2); | |
| box-shadow: 0 0 10px rgba(16, 163, 127, 0.1); | |
| } | |
| .status-badge.online { | |
| background: rgba(59, 130, 246, 0.1); | |
| color: #3b82f6; | |
| border-color: rgba(59, 130, 246, 0.2); | |
| } | |
| .status-badge.offline { | |
| background: rgba(239, 68, 68, 0.1); | |
| color: #ef4444; | |
| border-color: rgba(239, 68, 68, 0.2); | |
| animation: pulse-red 2s infinite; | |
| } | |
| @keyframes pulse-red { | |
| 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } | |
| 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } | |
| 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } | |
| } | |
| /* AI Modes & Deep Research Sidebar Styles */ | |
| .sidebar-section { | |
| margin-bottom: 20px; | |
| padding: 0 4px; | |
| } | |
| .sidebar-section-title { | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| margin-bottom: 12px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 0 4px; | |
| } | |
| .ai-modes-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 8px; | |
| margin-bottom: 16px; | |
| } | |
| .mode-btn { | |
| background: rgba(255, 255, 255, 0.03); | |
| border: 1px solid var(--border-color); | |
| border-radius: 10px; | |
| padding: 8px; | |
| color: var(--text-secondary); | |
| font-size: 12px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 4px; | |
| text-align: center; | |
| } | |
| .mode-btn i { | |
| width: 16px; | |
| height: 16px; | |
| } | |
| .mode-btn:hover { | |
| background: rgba(255, 255, 255, 0.08); | |
| color: var(--text-primary); | |
| border-color: rgba(255, 255, 255, 0.2); | |
| } | |
| .mode-btn.active { | |
| background: rgba(16, 163, 127, 0.1); | |
| color: var(--accent-blue); | |
| border-color: var(--accent-blue); | |
| box-shadow: 0 0 10px rgba(16, 163, 127, 0.1); | |
| } | |
| /* Toggle Switch Style */ | |
| .research-toggle-container { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 10px 12px; | |
| background: rgba(255, 255, 255, 0.03); | |
| border: 1px solid var(--border-color); | |
| border-radius: 12px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .research-toggle-container:hover { | |
| background: rgba(255, 255, 255, 0.06); | |
| } | |
| .research-label { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 13px; | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| } | |
| .research-label i { | |
| color: var(--accent-blue); | |
| width: 18px; | |
| height: 18px; | |
| } | |
| .switch { | |
| position: relative; | |
| display: inline-block; | |
| width: 36px; | |
| height: 20px; | |
| } | |
| .switch input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: #3f3f3f; | |
| transition: .3s; | |
| border-radius: 20px; | |
| } | |
| .slider:before { | |
| position: absolute; | |
| content: ""; | |
| height: 14px; | |
| width: 14px; | |
| left: 3px; | |
| bottom: 3px; | |
| background-color: white; | |
| transition: .3s; | |
| border-radius: 50%; | |
| } | |
| input:checked + .slider { | |
| background-color: var(--accent-blue); | |
| } | |
| input:checked + .slider:before { | |
| transform: translateX(16px); | |
| } | |
| /* Indicator in Chat */ | |
| .research-active-indicator { | |
| display: none; | |
| align-items: center; | |
| gap: 6px; | |
| background: rgba(16, 163, 127, 0.1); | |
| color: var(--accent-blue); | |
| padding: 6px 12px; | |
| border-radius: 12px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| margin-bottom: 10px; | |
| border: 1px solid rgba(16, 163, 127, 0.2); | |
| animation: fadeIn 0.3s ease; | |
| width: fit-content; | |
| } | |
| .research-active-indicator.visible { | |
| display: flex; | |
| } | |
| /* Premium Mode Dropdown Styles */ | |
| .mode-dropdown { | |
| position: relative; | |
| margin-right: 8px; | |
| padding-right: 8px; | |
| border-right: 1px solid var(--border-color); | |
| } | |
| .mode-dropdown-trigger { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid var(--border-color); | |
| border-radius: 12px; | |
| padding: 6px 12px; | |
| color: var(--text-primary); | |
| font-size: 13px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| min-width: 100px; | |
| } | |
| .mode-dropdown-trigger:hover { | |
| background: rgba(255, 255, 255, 0.08); | |
| border-color: rgba(255, 255, 255, 0.2); | |
| } | |
| .mode-dropdown-trigger i:first-child { | |
| color: var(--accent-blue); | |
| width: 14px; | |
| height: 14px; | |
| } | |
| .mode-dropdown-trigger i:last-child { | |
| width: 12px; | |
| height: 12px; | |
| color: var(--text-secondary); | |
| transition: transform 0.2s; | |
| } | |
| .mode-dropdown.open .mode-dropdown-trigger i:last-child { | |
| transform: rotate(180deg); | |
| } | |
| .mode-dropdown-content { | |
| position: absolute; | |
| bottom: calc(100% + 10px); | |
| left: 0; | |
| background: #1e1e1e; | |
| border: 1px solid var(--border-color); | |
| border-radius: 16px; | |
| padding: 8px; | |
| width: 180px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); | |
| display: none; | |
| flex-direction: column; | |
| gap: 4px; | |
| z-index: 1000; | |
| animation: slideUp 0.2s ease-out; | |
| } | |
| .mode-dropdown.open .mode-dropdown-content { | |
| display: flex; | |
| } | |
| .mode-option { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 12px; | |
| border-radius: 10px; | |
| color: var(--text-secondary); | |
| font-size: 13px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .mode-option:hover { | |
| background: rgba(255, 255, 255, 0.05); | |
| color: var(--text-primary); | |
| } | |
| .mode-option.active { | |
| background: rgba(16, 163, 127, 0.1); | |
| color: var(--accent-blue); | |
| } | |
| .mode-option i { | |
| width: 16px; | |
| height: 16px; | |
| } | |
| .dropdown-label { | |
| padding: 8px 12px 4px 12px; | |
| font-size: 10px; | |
| font-weight: 700; | |
| color: var(--text-secondary); | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .dropdown-divider { | |
| height: 1px; | |
| background: var(--border-color); | |
| margin: 4px 8px; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .mode-dropdown { | |
| display: none; | |
| } | |
| } | |
| /* FIXED: Added visual states for disabled buttons */ | |
| .send-button:disabled { | |
| opacity: 0.3 ; | |
| cursor: not-allowed ; | |
| transform: none ; | |
| } | |
| .btn-primary:disabled { | |
| opacity: 0.6 ; | |
| cursor: wait ; | |
| } | |
| /* FIXED: Ensure auth overlay is always on top */ | |
| #authOverlay { | |
| z-index: 9999 ; | |
| } | |
| /* FIXED: Send/Stop button positioning */ | |
| .input-actions { | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .stop-button { | |
| display: none; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-color); | |
| color: var(--text-primary); | |
| } | |
| .stop-button.active { | |
| display: flex; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Landing Page --> | |
| <div class="landing-page" id="landingPage"> | |
| <div class="lightning-overlay" id="lightningOverlay"></div> | |
| <header class="landing-header"> | |
| <div class="landing-logo"> | |
| <div class="blinking-emoji"> | |
| <div class="eye"></div> | |
| <div class="eye"></div> | |
| </div> | |
| Nexa AI | |
| </div> | |
| <nav class="landing-nav" id="landingNav"> | |
| <a href="javascript:void(0)" class="nav-link" data-section="featuresSection">Features</a> | |
| <a href="javascript:void(0)" class="nav-link" data-section="howItWorksSection">How It Works</a> | |
| <a href="javascript:void(0)" class="nav-link" data-section="safetySection">Safety</a> | |
| <a href="javascript:void(0)" class="nav-link" data-section="pricingSection">Pricing</a> | |
| <button onclick="enterApp()" class="btn-primary landing-cta" style="padding: 10px 20px; font-size: 14px;">Try Nexa AI</button> | |
| </nav> | |
| </header> | |
| <section class="hero-section"> | |
| <div class="hero-badge">Chat Smarter, Create Faster</div> | |
| <h1 class="hero-title">Your <span id="typing-text"></span><span class="typing-cursor"></span><br>AI Assistant.</h1> | |
| <p class="hero-subtitle">Chat with Nexa AI to get answers, generate stunning images, and boost your productivity. Your personal AI assistant for work and creativity.</p> | |
| <div class="cta-container"> | |
| <button onclick="enterApp()" class="btn-primary landing-cta">Start Chatting — Free Forever</button> | |
| <!-- FIXED: Replaced data-section with direct onclick for reliability --> | |
| <button class="btn-secondary" onclick="scrollToSection('featuresSection')">View Demo</button> | |
| </div> | |
| <div class="trust-badges"> | |
| <div class="trust-badge"><i data-lucide="shield-check"></i> No credit card required</div> | |
| <div class="trust-badge"><i data-lucide="lock"></i> Your data is encrypted</div> | |
| <div class="trust-badge"><i data-lucide="zap"></i> Free forever</div> | |
| </div> | |
| </section> | |
| <div class="features-grid" id="featuresSection"> | |
| <div class="feature-card"> | |
| <div class="feature-icon"><i data-lucide="message-square"></i></div> | |
| <h3>Fast Responses</h3> | |
| <p>Experience lightning-fast chat interactions. Nexa AI processes your queries in milliseconds for a seamless experience.</p> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon"><i data-lucide="image"></i></div> | |
| <h3>Image Generation</h3> | |
| <p>Create stunning, high-quality images directly from your chat. Powered by advanced AI models for creative freedom.</p> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon"><i data-lucide="shield"></i></div> | |
| <h3>Secure Chats</h3> | |
| <p>Your privacy is our priority. All conversations are encrypted and your personal data stays yours.</p> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon"><i data-lucide="infinity"></i></div> | |
| <h3>Unlimited Messages</h3> | |
| <p>Communicate without boundaries. Nexa AI is free forever with true unlimited power for everyone.</p> | |
| </div> | |
| </div> | |
| <!-- How It Works Section --> | |
| <section class="section-wrapper" id="howItWorksSection"> | |
| <div class="section-header"> | |
| <h2>How It Works</h2> | |
| <p>Getting started with Nexa AI is simple and takes less than a minute.</p> | |
| </div> | |
| <div class="how-it-works-steps"> | |
| <div class="step-item"> | |
| <div class="step-number">1</div> | |
| <h3>Sign Up</h3> | |
| <p>Create your free account in seconds. No credit card or complex setup required.</p> | |
| </div> | |
| <div class="step-item"> | |
| <div class="step-number">2</div> | |
| <h3>Start Chat</h3> | |
| <p>Type your questions, requests, or image prompts into the intuitive chat interface.</p> | |
| </div> | |
| <div class="step-item"> | |
| <div class="step-number">3</div> | |
| <h3>Get Answers</h3> | |
| <p>Receive intelligent responses, creative content, or generated images instantly.</p> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Safety Section --> | |
| <section class="section-wrapper" id="safetySection"> | |
| <div class="section-header"> | |
| <h2>Safety & Privacy</h2> | |
| <p>We build Nexa AI with safety and ethical considerations at the core.</p> | |
| </div> | |
| <div class="features-grid" style="padding: 0;"> | |
| <div class="feature-card"> | |
| <div class="feature-icon"><i data-lucide="user-check"></i></div> | |
| <h3>User First</h3> | |
| <p>We don't sell your data to third parties. Your interactions are private and secure.</p> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon"><i data-lucide="eye-off"></i></div> | |
| <h3>Anonymized Learning</h3> | |
| <p>Our models learn from patterns, not personal identities, ensuring your anonymity.</p> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon"><i data-lucide="alert-circle"></i></div> | |
| <h3>Content Filtering</h3> | |
| <p>Advanced filters ensure safe and helpful responses, preventing harmful or inappropriate content.</p> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Testimonials Section --> | |
| <section class="section-wrapper" id="testimonialsSection"> | |
| <div class="section-header"> | |
| <h2>What Our Users Say</h2> | |
| <p>Join thousands of users who rely on Nexa AI every day.</p> | |
| </div> | |
| <div class="testimonials-grid"> | |
| <div class="testimonial-card"> | |
| <p>"Nexa AI has completely transformed how I handle my daily tasks. The image generation is a game-changer!"</p> | |
| <div class="testimonial-author">Sarah J., Designer</div> | |
| </div> | |
| <div class="testimonial-card"> | |
| <p>"The fastest AI assistant I've used. It's like having a brilliant partner available 24/7."</p> | |
| <div class="testimonial-author">Michael R., Developer</div> | |
| </div> | |
| <div class="testimonial-card"> | |
| <p>"I love the focus on privacy. It's rare to find an AI tool that feels this secure and powerful at the same time."</p> | |
| <div class="testimonial-author">Elena T., Writer</div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Pricing Section --> | |
| <section class="section-wrapper" id="pricingSection"> | |
| <div class="section-header"> | |
| <h2>Free Forever for Everyone</h2> | |
| <p>Nexa AI is committed to open intelligence. All features are now completely free.</p> | |
| </div> | |
| <div class="pricing-grid"> | |
| <!-- Unified Free Plan --> | |
| <div class="pricing-card" style="border-color: var(--accent-blue); transform: scale(1.05); grid-column: 1 / -1; max-width: 600px; margin: 0 auto;"> | |
| <div style="position: absolute; top: -12px; left: 50%; transform: translateX(-50%); background: var(--accent-blue); color: white; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600;">UNLIMITED ACCESS</div> | |
| <div class="hero-badge" style="margin-bottom: 16px; background: rgba(16, 163, 127, 0.2);">Free Forever</div> | |
| <div style="font-size: 48px; font-weight: 700; margin-bottom: 8px;">$0<span style="font-size: 16px; color: var(--text-secondary);">/forever</span></div> | |
| <p style="color: var(--text-secondary); margin-bottom: 24px;">No credit card, no limits, just pure AI power.</p> | |
| <ul style="text-align: left; margin-bottom: 32px; list-style: none; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 12px;"> | |
| <li style="display: flex; align-items: center; gap: 8px;"><i data-lucide="check" style="color: #10a37f; width: 18px;"></i> Unlimited Messages</li> | |
| <li style="display: flex; align-items: center; gap: 8px;"><i data-lucide="check" style="color: #10a37f; width: 18px;"></i> All Premium Models (GPT-4o)</li> | |
| <li style="display: flex; align-items: center; gap: 8px;"><i data-lucide="check" style="color: #10a37f; width: 18px;"></i> Unlimited Image Generation</li> | |
| <li style="display: flex; align-items: center; gap: 8px;"><i data-lucide="check" style="color: #10a37f; width: 18px;"></i> AI Video Generation</li> | |
| <li style="display: flex; align-items: center; gap: 8px;"><i data-lucide="check" style="color: #10a37f; width: 18px;"></i> File Upload & Analysis</li> | |
| <li style="display: flex; align-items: center; gap: 8px;"><i data-lucide="check" style="color: #10a37f; width: 18px;"></i> Real-time Web Search</li> | |
| <li style="display: flex; align-items: center; gap: 8px;"><i data-lucide="check" style="color: #10a37f; width: 18px;"></i> Deep Research Mode</li> | |
| <li style="display: flex; align-items: center; gap: 8px;"><i data-lucide="check" style="color: #10a37f; width: 18px;"></i> Persistent Memory</li> | |
| </ul> | |
| <button onclick="enterApp()" class="btn-primary landing-cta" style="width: 100%; background: var(--accent-blue);">Get Started Now</button> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- FAQ Section --> | |
| <section class="section-wrapper" id="faqSection"> | |
| <div class="section-header"> | |
| <h2>Frequently Asked Questions</h2> | |
| <p>Everything you need to know about Nexa AI.</p> | |
| </div> | |
| <div class="faq-container"> | |
| <div class="faq-item"> | |
| <div class="faq-question">What is Nexa AI? <i data-lucide="chevron-down"></i></div> | |
| <div class="faq-answer">Nexa AI is an intelligent chat assistant designed to help you with writing, coding, image generation, and more.</div> | |
| </div> | |
| <div class="faq-item"> | |
| <div class="faq-question">Is it really free? <i data-lucide="chevron-down"></i></div> | |
| <div class="faq-answer">Yes! Nexa AI is now completely free with unlimited messages and access to all premium features for everyone.</div> | |
| </div> | |
| <div class="faq-item"> | |
| <div class="faq-question">How does image generation work? <i data-lucide="chevron-down"></i></div> | |
| <div class="faq-answer">Simply describe what you want to see in the chat, and Nexa AI will generate a high-quality image for you using integrated engines like Imagen 3.</div> | |
| </div> | |
| <div class="faq-item"> | |
| <div class="faq-question">Is my data secure? <i data-lucide="chevron-down"></i></div> | |
| <div class="faq-answer">Absolutely. We use industry-standard encryption and do not sell your personal data to anyone.</div> | |
| </div> | |
| <div class="faq-item"> | |
| <div class="faq-question">Can I use Nexa AI for coding? <i data-lucide="chevron-down"></i></div> | |
| <div class="faq-answer">Yes, Nexa AI is highly capable of writing, debugging, and explaining code across multiple programming languages.</div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Footer --> | |
| <footer class="landing-footer"> | |
| <div class="footer-content"> | |
| <div class="footer-info"> | |
| <div class="landing-logo" style="margin-bottom: 20px;">Nexa AI</div> | |
| <p style="color: var(--text-secondary); margin-bottom: 24px;">Your intelligent AI assistant for the modern age. Chat smarter, create faster, and explore the future of AI.</p> | |
| <div style="display: flex; gap: 16px;"> | |
| <a href="#" style="color: var(--text-secondary);"><i data-lucide="twitter"></i></a> | |
| <a href="#" style="color: var(--text-secondary);"><i data-lucide="github"></i></a> | |
| <a href="#" style="color: var(--text-secondary);"><i data-lucide="linkedin"></i></a> | |
| </div> | |
| </div> | |
| <div> | |
| <h4 style="margin-bottom: 20px;">Product</h4> | |
| <ul style="list-style: none; padding: 0; color: var(--text-secondary);"> | |
| <li style="margin-bottom: 12px;"><a href="javascript:void(0)" class="nav-link" data-section="featuresSection" style="color: inherit; text-decoration: none;">Features</a></li> | |
| <li style="margin-bottom: 12px;"><a href="javascript:void(0)" class="nav-link" data-section="pricingSection" style="color: inherit; text-decoration: none;">Pricing</a></li> | |
| <li style="margin-bottom: 12px;"><a href="javascript:void(0)" class="nav-link" data-section="howItWorksSection" style="color: inherit; text-decoration: none;">How It Works</a></li> | |
| </ul> | |
| </div> | |
| <div> | |
| <h4 style="margin-bottom: 20px;">Company</h4> | |
| <ul style="list-style: none; padding: 0; color: var(--text-secondary);"> | |
| <li style="margin-bottom: 12px;"><a href="#" style="color: inherit; text-decoration: none;">About Us</a></li> | |
| <li style="margin-bottom: 12px;"><a href="#" style="color: inherit; text-decoration: none;">Privacy Policy</a></li> | |
| <li style="margin-bottom: 12px;"><a href="#" style="color: inherit; text-decoration: none;">Terms of Service</a></li> | |
| </ul> | |
| </div> | |
| <div> | |
| <h4 style="margin-bottom: 20px;">Contact</h4> | |
| <p style="color: var(--text-secondary); margin-bottom: 12px;">support@nexa-ai.com</p> | |
| <p style="color: var(--text-secondary);">Available 24/7</p> | |
| </div> | |
| </div> | |
| <div class="footer-bottom"> | |
| © 2026 Nexa AI. All rights reserved. Made for Hugging Face. | |
| </div> | |
| </footer> | |
| </div> | |
| <!-- App Container --> | |
| <div class="sidebar-overlay" id="sidebarOverlay"></div> | |
| <div class="app-container"> | |
| <!-- Sidebar --> | |
| <aside class="sidebar" id="sidebar"> | |
| <div class="sidebar-header-mobile"> | |
| <div style="display: flex; align-items: center; gap: 8px;"> | |
| <div class="blinking-emoji"><div class="eye"></div><div class="eye"></div></div> | |
| <div class="landing-logo" style="font-size: 18px; margin: 0;">Nexa AI</div> | |
| </div> | |
| <button class="sidebar-close-btn"> | |
| <i data-lucide="x"></i> | |
| </button> | |
| </div> | |
| <button class="new-chat-btn"> | |
| <i data-lucide="plus-circle" style="width: 18px; height: 18px;"></i> | |
| <span>New Chat</span> | |
| <div class="shortcut-hint"> | |
| <span class="key">Ctrl</span> | |
| <span class="key">K</span> | |
| </div> | |
| </button> | |
| <div class="sidebar-section-title" style="margin-top: 10px; display: flex; align-items: center; gap: 8px;"> | |
| <i data-lucide="clock" style="width: 16px; height: 16px;"></i> Chat History | |
| </div> | |
| <div class="conversation-list" id="conversationList"> | |
| <!-- Conversations will be listed here --> | |
| </div> | |
| <div class="menu-item" id="allChatsBtn" style="margin-bottom: 20px;"> | |
| <span>All Chats</span> | |
| </div> | |
| <div class="sidebar-footer"> | |
| <div class="menu-item" id="sidebarProfileBtn"> | |
| <div class="account-avatar" id="sidebarAvatar">G</div> | |
| <span id="sidebarUserName">Guest User</span> | |
| </div> | |
| <div class="menu-item" id="sidebarFeaturesBtn" style="color: var(--accent-blue);"> | |
| <i data-lucide="sparkles"></i> | |
| <span>AI Features</span> | |
| </div> | |
| <div class="menu-item" id="sidebarExportBtn" style="color: #10a37f;"> | |
| <i data-lucide="file-down"></i> | |
| <span>Export Chat</span> | |
| </div> | |
| <div class="menu-item" id="sidebarLogoutBtn"> | |
| <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /> | |
| </svg> | |
| Log out | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Main Area --> | |
| <main class="main-area hero-mode" id="mainArea"> | |
| <div class="drag-drop-overlay" id="dragDropOverlay"> | |
| <div class="drag-drop-icon"> | |
| <i data-lucide="upload-cloud" style="width: 40px; height: 40px;"></i> | |
| </div> | |
| <div class="drag-drop-text">Drop files to upload to Nexa AI</div> | |
| </div> | |
| <div class="main-background-lighting"> | |
| <div class="ambient-glow glow-1"></div> | |
| <div class="ambient-glow glow-2"></div> | |
| <div class="grid-overlay"></div> | |
| <canvas id="canvas3d"></canvas> | |
| </div> | |
| <div id="memoryIndicator" class="memory-indicator"> | |
| <i data-lucide="brain"></i> | |
| <span>Nexa learned something new about you!</span> | |
| </div> | |
| <div class="top-bar"> | |
| <div class="model-selector"> | |
| <button class="hamburger-menu"> | |
| <i data-lucide="menu"></i> | |
| </button> | |
| <div class="blinking-emoji" id="headerEmoji"> | |
| <div class="eye"></div> | |
| <div class="eye"></div> | |
| </div> | |
| <span>Nexa AI</span> | |
| </div> | |
| <div class="top-right-menu"> | |
| <button class="account-btn"> | |
| <div class="account-avatar" id="accountAvatar">G</div> | |
| <span id="accountUserName">Guest User</span> | |
| <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> | |
| </svg> | |
| </button> | |
| <div class="account-dropdown" id="accountDropdown"> | |
| <div class="dropdown-item" id="dropdownProfileBtn"> | |
| <i data-lucide="user" style="width:16px;"></i> | |
| Profile | |
| </div> | |
| <div class="dropdown-item" id="dropdownSettingsBtn"> | |
| <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> | |
| </svg> | |
| Settings | |
| </div> | |
| <div class="dropdown-item" id="dropdownThemeBtn"> | |
| <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /> | |
| </svg> | |
| Toggle Dark/Light | |
| </div> | |
| <div class="dropdown-item" id="dropdownLogoutBtn"> | |
| <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /> | |
| </svg> | |
| Log out | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="chat-history empty-mode" id="chatHistory"> | |
| <div class="status-badge-container"> | |
| <div class="status-badge auto-mode"> | |
| <i data-lucide="zap" style="width:12px;"></i> Auto Mode | |
| </div> | |
| <div class="status-badge online" id="serverStatusBadge"> | |
| <i data-lucide="globe" style="width:12px;"></i> Online | |
| </div> | |
| </div> | |
| <div class="memory-indicator" id="memoryIndicator"> | |
| <i data-lucide="brain"></i> | |
| <span>Using memory...</span> | |
| </div> | |
| <div class="research-active-indicator" id="researchIndicator"> | |
| <i data-lucide="microscope"></i> | |
| <span>Deep Research Active</span> | |
| </div> | |
| <div class="empty-state" id="emptyState"> | |
| <div class="hero-logo-container"> | |
| <div class="blinking-emoji" style="width: 100px; height: 100px; gap: 12px; margin-bottom: 24px; animation: floatingLogo 3s ease-in-out infinite;"> | |
| <div class="eye" style="width: 12px; height: 20px; border-radius: 6px;"></div> | |
| <div class="eye" style="width: 12px; height: 20px; border-radius: 6px;"></div> | |
| </div> | |
| </div> | |
| <div class="blinking-emoji" style="margin-bottom: 20px; gap: 8px;"> | |
| <div class="eye" style="width: 12px; height: 12px;"></div> | |
| <div class="eye" style="width: 12px; height: 12px;"></div> | |
| </div> | |
| <h2 class="hero-logo" style="margin-bottom: 12px;">Nexa AI</h2> | |
| <p class="hero-subtitle">Your futuristic AI assistant.</p> | |
| <div class="suggestions-grid"> | |
| <div class="suggestion-card" onclick="document.getElementById('messageInput').value='Create a stunning digital art of a neon city'; sendMessage();"> | |
| <div style="display:flex; align-items:center; gap:10px; margin-bottom:8px;"> | |
| <i data-lucide="image" style="width:16px; color:#10a37f;"></i> | |
| <h4 style="margin:0;">Generate Images</h4> | |
| </div> | |
| <p>Like a neon futuristic city</p> | |
| </div> | |
| <div class="suggestion-card" onclick="document.getElementById('messageInput').value='Write a high-performance Python script for data analysis'; sendMessage();"> | |
| <div style="display:flex; align-items:center; gap:10px; margin-bottom:8px;"> | |
| <i data-lucide="code" style="width:16px; color:#10a37f;"></i> | |
| <h4 style="margin:0;">Write Code</h4> | |
| </div> | |
| <p>For data analysis or web apps</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="input-container"> | |
| <div class="attachments-container" id="attachmentsContainer" style="display: none;"> | |
| <!-- Uploaded file chips appear here --> | |
| </div> | |
| <div class="input-wrapper" id="inputWrapper"> | |
| <textarea class="message-input" id="messageInput" placeholder="Message Nexa AI..." rows="1"></textarea> | |
| <div class="input-container-bottom" id="inputBottomRow"> | |
| <div class="input-actions-left"> | |
| <button class="action-btn" id="uploadBtn" onclick="toggleUploadMenu()"> | |
| <i data-lucide="plus"></i> | |
| </button> | |
| <div class="upload-menu" id="uploadMenu"> | |
| <div class="tool-btn" data-action="search" title="Web Search"> | |
| <i data-lucide="search"></i> | |
| <span>Search</span> | |
| </div> | |
| <div class="tool-btn" data-action="image" title="AI Image"> | |
| <i data-lucide="image"></i> | |
| <span>Image</span> | |
| </div> | |
| <div class="tool-btn" data-action="upload" title="Upload File"> | |
| <i data-lucide="upload"></i> | |
| <span>Upload</span> | |
| </div> | |
| <div class="tool-btn" data-action="voice" id="toolVoiceBtn" title="Voice Mode"> | |
| <i data-lucide="mic"></i> | |
| <span>Voice</span> | |
| </div> | |
| <div class="tool-btn" data-action="web" title="Browse Web"> | |
| <i data-lucide="globe"></i> | |
| <span>Web</span> | |
| </div> | |
| <div class="tool-btn" id="videoToolBtn" data-action="video" title="AI Video"> | |
| <i data-lucide="video"></i> | |
| <span>Video</span> | |
| </div> | |
| </div> | |
| <div id="activeFeatureContainer" style="display: none;"></div> | |
| </div> | |
| <div class="input-actions-right"> | |
| <div class="mode-dropdown" id="modeDropdown"> | |
| <button class="mode-dropdown-trigger" onclick="toggleModeDropdown(event)"> | |
| <i data-lucide="zap" id="activeModeIcon"></i> | |
| <span id="activeModeLabel">Instant</span> | |
| <i data-lucide="chevron-down"></i> | |
| </button> | |
| <div class="mode-dropdown-content"> | |
| <div class="dropdown-label">Chat Modes</div> | |
| <div class="mode-option" onclick="setAiMode('instant')"> | |
| <i data-lucide="zap"></i> | |
| <span>Instant</span> | |
| </div> | |
| <div class="mode-option" onclick="setAiMode('study')"> | |
| <i data-lucide="book"></i> | |
| <span>Study</span> | |
| </div> | |
| <div class="mode-option" onclick="setAiMode('dev')"> | |
| <i data-lucide="code"></i> | |
| <span>Developer</span> | |
| </div> | |
| <div class="mode-option" onclick="setAiMode('research')"> | |
| <i data-lucide="search"></i> | |
| <span>Research</span> | |
| </div> | |
| <div class="mode-option" onclick="setAiMode('creative')"> | |
| <i data-lucide="palette"></i> | |
| <span>Creative</span> | |
| </div> | |
| <div class="dropdown-divider"></div> | |
| <div class="dropdown-label">Advanced Models</div> | |
| <div class="mode-option" onclick="setAiModel('gpt-4o')"> | |
| <i data-lucide="sparkles"></i> | |
| <span>GPT-4o</span> | |
| </div> | |
| <div class="mode-option" onclick="setAiModel('claude-3-5')"> | |
| <i data-lucide="brain"></i> | |
| <span>Claude 3.5</span> | |
| </div> | |
| <div class="mode-option" onclick="setAiModel('gemini-1-5')"> | |
| <i data-lucide="gem"></i> | |
| <span>Gemini 1.5 Pro</span> | |
| </div> | |
| </div> | |
| </div> | |
| <button class="action-btn mic-btn" id="micBtn" onclick="toggleSpeechRecognition()" title="Voice Input"> | |
| <i data-lucide="mic"></i> | |
| </button> | |
| <!-- FIXED: Removed inline onclick to prevent double-firing and used consistent SVG icons --> | |
| <button class="send-button" id="sendBtn" title="Send message (Enter)"> | |
| <i data-lucide="arrow-up"></i> | |
| </button> | |
| <button class="stop-button" id="stopBtn" title="Stop generating (Esc)"> | |
| <i data-lucide="square"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="disclaimer"> | |
| Nexa AI can make mistakes. Consider checking important information. | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Image Generation Modal Overlay --> | |
| <div class="features-modal-overlay" id="featuresModalOverlay" onclick="handleFeaturesOverlayClick(event)"> | |
| <div class="features-modal"> | |
| <button class="features-close" onclick="closeFeaturesModal()"> | |
| <i data-lucide="x"></i> | |
| </button> | |
| <h2 style="font-size: 24px; margin-bottom: 8px;">Nexa AI Features</h2> | |
| <p style="color: var(--text-secondary); font-size: 14px; margin-bottom: 24px;">Discover what your intelligent assistant can do</p> | |
| <div class="features-grid"> | |
| <div class="feature-item"> | |
| <div class="feature-icon"><i data-lucide="zap"></i></div> | |
| <h3>Instant Intelligence</h3> | |
| <p>Powered by Qwen 2.5, optimized for speed and complex reasoning.</p> | |
| </div> | |
| <div class="feature-item"> | |
| <div class="feature-icon"><i data-lucide="image"></i></div> | |
| <h3>AI Image Generation</h3> | |
| <p>Create stunning visuals directly in the chat using /image command.</p> | |
| </div> | |
| <div class="feature-item"> | |
| <div class="feature-icon"><i data-lucide="mic"></i></div> | |
| <h3>Voice Recognition</h3> | |
| <p>Hands-free interaction with high-accuracy Speech-to-Text.</p> | |
| </div> | |
| <div class="feature-item"> | |
| <div class="feature-icon"><i data-lucide="volume-2"></i></div> | |
| <h3>Text-to-Speech</h3> | |
| <p>AI responses can be read aloud with premium, natural voices.</p> | |
| </div> | |
| <div class="feature-item"> | |
| <div class="feature-icon"><i data-lucide="search"></i></div> | |
| <h3>Real-time Web Search</h3> | |
| <p>Access the latest news and facts with live internet browsing.</p> | |
| </div> | |
| <div class="feature-item"> | |
| <div class="feature-icon"><i data-lucide="file-text"></i></div> | |
| <h3>File Analysis</h3> | |
| <p>Upload PDFs, images, or docs for deep analysis and summaries.</p> | |
| </div> | |
| <div class="feature-item"> | |
| <div class="feature-icon"><i data-lucide="brain"></i></div> | |
| <h3>Persistent Memory</h3> | |
| <p>Learns your preferences and coding style over multiple sessions.</p> | |
| </div> | |
| <div class="feature-item"> | |
| <div class="feature-icon"><i data-lucide="download"></i></div> | |
| <h3>Export Chat</h3> | |
| <p>Save important responses as Markdown files for later use.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Settings Modal --> | |
| <div class="settings-modal-overlay" id="settingsModalOverlay" onclick="handleSettingsOverlayClick(event)"> | |
| <div class="settings-modal"> | |
| <button class="settings-close" onclick="closeSettings()"> | |
| <i data-lucide="x"></i> | |
| </button> | |
| <div class="settings-header"> | |
| <div class="settings-title">Settings</div> | |
| <div class="settings-subtitle">Manage your preferences and data</div> | |
| </div> | |
| <div class="settings-section"> | |
| <div class="settings-section-title">Personalization</div> | |
| <div class="settings-item"> | |
| <div class="settings-item-info"> | |
| <h4>Appearance</h4> | |
| <p>Switch between light and dark mode</p> | |
| </div> | |
| <button class="settings-btn" onclick="toggleTheme()">Toggle Theme</button> | |
| </div> | |
| <div class="settings-item"> | |
| <div class="settings-item-info"> | |
| <h4>Voice Output</h4> | |
| <p>Auto-speak AI responses</p> | |
| </div> | |
| <button class="settings-btn" id="voiceToggleBtn" onclick="toggleAutoVoice()">Enabled</button> | |
| </div> | |
| <div class="settings-item"> | |
| <div class="settings-item-info"> | |
| <h4>Font Size</h4> | |
| <p>Adjust chat text size</p> | |
| </div> | |
| <div style="display: flex; gap: 8px;"> | |
| <button class="settings-btn" onclick="setFontSize('small')">Small</button> | |
| <button class="settings-btn" onclick="setFontSize('medium')">Med</button> | |
| <button class="settings-btn" onclick="setFontSize('large')">Large</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="settings-section"> | |
| <div class="settings-section-title">Data Management</div> | |
| <div class="settings-item"> | |
| <div class="settings-item-info"> | |
| <h4>Clear Memory</h4> | |
| <p>Forget everything Nexa learned about you</p> | |
| </div> | |
| <button class="settings-btn danger" onclick="confirmClearMemory()">Clear Memory</button> | |
| </div> | |
| <div class="settings-item"> | |
| <div class="settings-item-info"> | |
| <h4>Delete All Chats</h4> | |
| <p>Permanently delete your entire conversation history</p> | |
| </div> | |
| <button class="settings-btn danger" onclick="confirmDeleteAllChats()">Delete All</button> | |
| </div> | |
| </div> | |
| <div class="settings-section"> | |
| <div class="settings-section-title">About</div> | |
| <div class="settings-item"> | |
| <div class="settings-item-info"> | |
| <h4>Version</h4> | |
| <p>Nexa AI v2.4.0 (Production)</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Confirmation Dialog --> | |
| <div class="confirm-overlay" id="confirmOverlay" onclick="handleConfirmOverlayClick(event)"> | |
| <div class="confirm-card"> | |
| <div class="confirm-title" id="confirmTitle">Delete All Chats?</div> | |
| <div class="confirm-text" id="confirmText">This action cannot be undone. All your conversations will be permanently deleted.</div> | |
| <div class="confirm-actions"> | |
| <button class="confirm-btn cancel" onclick="closeConfirm()">Cancel</button> | |
| <button class="confirm-btn delete" id="confirmDeleteBtn" onclick="">Delete Permanently</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="img-modal-overlay" id="imgModalOverlay" onclick="handleImgOverlayClick(event)"> | |
| <div class="img-modal-card"> | |
| <button class="img-modal-close" onclick="closeImageGenModal()"> | |
| <svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| <div class="img-gen-grid"> | |
| <div class="img-prompt-area"> | |
| <h2>Generate Image</h2> | |
| <p>Describe what you want to create with Nexa AI's advanced visual engine.</p> | |
| <textarea class="img-textarea" id="imgPromptInput" placeholder="A futuristic city with floating cars and neon lights, digital art style..."></textarea> | |
| <button class="btn-primary" style="width: 100%;" id="imgGenerateBtn" onclick="generateImage()"> | |
| <i data-lucide="sparkles"></i> Generate Masterpiece | |
| </button> | |
| <div id="imgModalStatus" style="margin-top: 16px; font-size: 14px; color: var(--accent-blue);"></div> | |
| </div> | |
| <div class="img-preview-area"> | |
| <div class="img-preview-container" id="imgResultArea"> | |
| <div class="img-skeleton" id="imgSkeleton"></div> | |
| <div class="img-placeholder-text">Your generated image will appear here</div> | |
| <img id="imgResultImg" src="" alt="Generated preview"> | |
| <a href="#" class="img-download-btn" id="imgDownloadBtn"> | |
| <i data-lucide="download"></i> Download | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Auth Overlay --> | |
| <div class="auth-overlay" id="authOverlay"> | |
| <div class="auth-card"> | |
| <button class="img-modal-close" onclick="loginAsGuest()" style="position: absolute; top: 15px; right: 15px;"> | |
| <i data-lucide="x"></i> | |
| </button> | |
| <div class="auth-title" id="authTitle">Sign In</div> | |
| <div class="auth-subtitle" id="authSubtitle">Use your credentials to access Nexa AI</div> | |
| <form class="auth-form" id="authForm" onsubmit="handleAuth(event)"> | |
| <input type="text" class="auth-input" id="authEmail" placeholder="Email or Username" required> | |
| <div class="password-container"> | |
| <input type="password" class="auth-input" id="authPassword" placeholder="Password" required style="width: 100%; padding-right: 45px;"> | |
| <button type="button" class="password-toggle" id="passwordToggle" onclick="togglePasswordVisibility()" title="Show Password"> | |
| <i data-lucide="check" id="toggleIcon"></i> | |
| </button> | |
| </div> | |
| <button type="submit" class="auth-submit" id="authSubmitBtn">Sign In</button> | |
| </form> | |
| <div class="auth-error" id="authError">Invalid credentials</div> | |
| <div class="auth-footer"> | |
| <span id="authToggleText">Don't have an account?</span> | |
| <span class="auth-toggle" id="authToggleBtn" onclick="toggleAuthMode()">Sign Up</span> | |
| </div> | |
| <div style="margin-top: 15px; text-align: center; font-size: 13px; opacity: 0.7;"> | |
| <span style="color: var(--text-secondary);">Or </span> | |
| <span class="auth-toggle" onclick="loginAsGuest()" style="color: #10a37f; cursor: pointer; font-weight: 600;">Continue as Guest</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Video Overlay Modal --> | |
| <div class="img-modal-overlay" id="videoOverlayModal" style="display:none; align-items: center; justify-content: center;" onclick="handleVideoOverlayClick(event)"> | |
| <div class="img-modal-card" style="width: min(500px, 96vw); max-height: 90vh; overflow-y: auto;"> | |
| <button class="img-modal-close" onclick="closeVideoOverlayModal()"> | |
| <svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| <div style="padding: 20px;"> | |
| <h3 style="margin-bottom: 20px; display: flex; align-items: center; gap: 10px;"> | |
| <i data-lucide="type" style="color: var(--accent-blue);"></i> Add Text Overlay | |
| </h3> | |
| <div style="margin-bottom: 15px;"> | |
| <label style="display: block; font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">Overlay Text</label> | |
| <textarea id="overlayTextInput" class="img-textarea" style="min-height: 80px;" placeholder="Enter text to overlay on video..."></textarea> | |
| </div> | |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;"> | |
| <div> | |
| <label style="display: block; font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">Position</label> | |
| <select id="overlayPositionSelect" class="btn-secondary" style="width: 100%; padding: 8px; background: rgba(255,255,255,0.05); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px;"> | |
| <option value="Top Left">Top Left</option> | |
| <option value="Top Center">Top Center</option> | |
| <option value="Top Right">Top Right</option> | |
| <option value="Center Left">Center Left</option> | |
| <option value="Center">Center</option> | |
| <option value="Center Right">Center Right</option> | |
| <option value="Bottom Left">Bottom Left</option> | |
| <option value="Bottom Center" selected>Bottom Center</option> | |
| <option value="Bottom Right">Bottom Right</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label style="display: block; font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">Font Size</label> | |
| <input type="number" id="overlayFontSizeInput" class="btn-secondary" style="width: 100%; padding: 8px; background: rgba(255,255,255,0.05); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px;" value="60"> | |
| </div> | |
| </div> | |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;"> | |
| <div> | |
| <label style="display: block; font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">Text Color</label> | |
| <input type="color" id="overlayTextColorInput" style="width: 100%; height: 35px; border: none; background: none; cursor: pointer;" value="#FFFFFF"> | |
| </div> | |
| <div> | |
| <label style="display: block; font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">BG Opacity</label> | |
| <input type="range" id="overlayBgOpacityInput" min="0" max="1" step="0.1" value="0.5" style="width: 100%;"> | |
| </div> | |
| </div> | |
| <button id="applyOverlayBtn" class="btn-primary" style="width: 100%; background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);" onclick="applyVideoOverlay()"> | |
| <i data-lucide="sparkles"></i> Process & Apply Overlay | |
| </button> | |
| <div id="overlayModalStatus" style="margin-top: 15px; font-size: 13px; text-align: center; color: var(--accent-blue);"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Global Selectors | |
| let sidebar, chatHistory, messageInput, sendBtn, stopBtn, accountDropdown, conversationList, authOverlay; | |
| // Global Error Handling | |
| window.onerror = function(message, source, lineno, colno, error) { | |
| console.error("Global Error:", message, "at", source, lineno, ":", colno); | |
| return false; | |
| }; | |
| // Critical Global Functions (Must be defined first) | |
| // FIXED: Robust enterApp to handle transitions and unauthenticated states | |
| function enterApp() { | |
| try { | |
| const landing = document.getElementById('landingPage'); | |
| const appContainer = document.querySelector('.app-container'); | |
| const authOvl = document.getElementById('authOverlay'); | |
| // Case 1: Unauthenticated - Immediate SPA routing to login | |
| if (!currentUser) { | |
| if (landing) landing.style.display = 'none'; | |
| window.history.pushState({}, '', '/login'); | |
| if (typeof handleRouting === 'function') { | |
| handleRouting(); | |
| } else { | |
| // Fallback if handleRouting is not available yet | |
| if (authOvl) { | |
| authOvl.style.display = 'flex'; | |
| authOvl.style.zIndex = '9999'; | |
| authOvl.classList.add('show'); | |
| } | |
| } | |
| if (window.lightningTimeout) { | |
| clearTimeout(window.lightningTimeout); | |
| window.lightningTimeout = null; | |
| } | |
| return; | |
| } | |
| // Case 2: Authenticated - Smooth transition to app | |
| if (landing) { | |
| landing.classList.add('hidden'); | |
| landing.style.pointerEvents = 'none'; | |
| } | |
| setTimeout(() => { | |
| if (landing) landing.style.display = 'none'; | |
| if (appContainer) { | |
| appContainer.style.display = 'flex'; | |
| appContainer.classList.add('visible'); | |
| } | |
| document.body.style.overflow = 'auto'; | |
| if (window.location.pathname !== '/chat') { | |
| window.history.pushState({}, '', '/chat'); | |
| } | |
| }, 800); | |
| if (window.lightningTimeout) { | |
| clearTimeout(window.lightningTimeout); | |
| window.lightningTimeout = null; | |
| } | |
| } catch (e) { | |
| console.error("enterApp error:", e); | |
| } | |
| } | |
| // FIXED: Smooth scroll with null-safety | |
| function scrollToSection(id) { | |
| try { | |
| const el = document.getElementById(id); | |
| if (el) { | |
| el.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| } catch (e) { | |
| console.error("scrollToSection error:", e); | |
| } | |
| } | |
| // State management | |
| let isLightMode = false; | |
| let isSignUp = false; | |
| let currentUser = JSON.parse(localStorage.getItem('chatgpt_current_user') || 'null'); | |
| let conversations = []; | |
| let currentConversationId = null; | |
| let messageCount = 0; | |
| let isGeneratingImage = false; | |
| let isSearching = false; | |
| let isAnalyzingFile = false; | |
| let activeUploadsCount = 0; | |
| let isTyping = false; | |
| let isVoiceActive = false; | |
| let searchCooldown = false; | |
| let abortController = null; | |
| const MAX_MESSAGES = Infinity; | |
| // Memory System | |
| let userMemory = JSON.parse(localStorage.getItem('nexa_ai_memory') || '{}'); | |
| function updateMemoryFromText(text) { | |
| const lowerText = text.toLowerCase(); | |
| let changed = false; | |
| // Simple preference extraction | |
| const preferences = [ | |
| { pattern: /i (?:prefer|like) (python|javascript|java|c\+\+|rust|go|php|ruby|html|css)/i, key: 'coding_preference' }, | |
| { pattern: /my name is ([\w\s]+)/i, key: 'user_name' }, | |
| { pattern: /i (?:am|work as) a ([\w\s]+)/i, key: 'occupation' }, | |
| { pattern: /i (?:live|reside) in ([\w\s]+)/i, key: 'location' }, | |
| { pattern: /i (?:love|enjoy) ([\w\s]+)/i, key: 'hobby' } | |
| ]; | |
| preferences.forEach(pref => { | |
| const match = text.match(pref.pattern); | |
| if (match && match[1]) { | |
| userMemory[pref.key] = match[1].trim(); | |
| changed = true; | |
| } | |
| }); | |
| if (changed) { | |
| localStorage.setItem('nexa_ai_memory', JSON.stringify(userMemory)); | |
| showMemoryIndicator(); | |
| } | |
| } | |
| function showMemoryIndicator() { | |
| const indicator = document.getElementById('memoryIndicator'); | |
| if (indicator) { | |
| indicator.classList.add('visible'); | |
| setTimeout(() => indicator.classList.remove('visible'), 3000); | |
| } | |
| } | |
| let currentAiMode = localStorage.getItem('nexa_ai_mode') || 'instant'; | |
| let currentAiModel = localStorage.getItem('nexa_ai_model') || 'llama-3'; | |
| let isDeepResearch = localStorage.getItem('nexa_ai_deep_research') === 'true'; | |
| function setAiMode(mode) { | |
| currentAiMode = mode; | |
| localStorage.setItem('nexa_ai_mode', mode); | |
| // Update Trigger UI | |
| const label = document.getElementById('activeModeLabel'); | |
| const icon = document.getElementById('activeModeIcon'); | |
| const modeMap = { | |
| 'instant': { label: 'Instant', icon: 'zap' }, | |
| 'study': { label: 'Study', icon: 'book' }, | |
| 'dev': { label: 'Developer', icon: 'code' }, | |
| 'research': { label: 'Research', icon: 'search' }, | |
| 'creative': { label: 'Creative', icon: 'palette' } | |
| }; | |
| if (label && icon && modeMap[mode]) { | |
| label.textContent = modeMap[mode].label; | |
| icon.setAttribute('data-lucide', modeMap[mode].icon); | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| // Update Options UI | |
| document.querySelectorAll('#modeDropdown .mode-option').forEach(opt => { | |
| const optLabel = opt.querySelector('span').textContent.toLowerCase(); | |
| if (optLabel.includes(mode) || (mode === 'dev' && optLabel === 'developer')) { | |
| opt.classList.add('active'); | |
| } else { | |
| opt.classList.remove('active'); | |
| } | |
| }); | |
| // Close dropdown | |
| const dropdown = document.getElementById('modeDropdown'); | |
| if (dropdown) dropdown.classList.remove('open'); | |
| } | |
| function setAiModel(model) { | |
| currentAiModel = model; | |
| localStorage.setItem('nexa_ai_model', model); | |
| const label = document.getElementById('activeModelLabel'); | |
| const icon = document.getElementById('activeModelIcon'); | |
| const modelMap = { | |
| 'gpt-4o': { label: 'GPT-4o', icon: 'sparkles' }, | |
| 'claude-3-5': { label: 'Claude 3.5', icon: 'brain' }, | |
| 'gemini-1-5': { label: 'Gemini 1.5 Pro', icon: 'gem' }, | |
| 'llama-3': { label: 'Llama 3', icon: 'cpu' }, | |
| 'mistral-large': { label: 'Mistral Large', icon: 'wind' } | |
| }; | |
| if (label && icon && modelMap[model]) { | |
| label.textContent = modelMap[model].label; | |
| icon.setAttribute('data-lucide', modelMap[model].icon); | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| document.querySelectorAll('#modelDropdown .mode-option').forEach(opt => { | |
| const optModel = opt.getAttribute('onclick').match(/'(.*)'/)[1]; | |
| if (optModel === model) opt.classList.add('active'); | |
| else opt.classList.remove('active'); | |
| }); | |
| const dropdown = document.getElementById('modelDropdown'); | |
| if (dropdown) dropdown.classList.remove('open'); | |
| } | |
| function toggleModeDropdown(e) { | |
| e.stopPropagation(); | |
| const dropdown = document.getElementById('modeDropdown'); | |
| if (dropdown) dropdown.classList.toggle('open'); | |
| const modelDropdown = document.getElementById('modelDropdown'); | |
| if (modelDropdown) modelDropdown.classList.remove('open'); | |
| } | |
| function toggleModelDropdown(e) { | |
| e.stopPropagation(); | |
| const dropdown = document.getElementById('modelDropdown'); | |
| if (dropdown) dropdown.classList.toggle('open'); | |
| const modeDropdown = document.getElementById('modeDropdown'); | |
| if (modeDropdown) modeDropdown.classList.remove('open'); | |
| } | |
| // Close dropdown when clicking outside | |
| window.addEventListener('click', () => { | |
| const modeDropdown = document.getElementById('modeDropdown'); | |
| const modelDropdown = document.getElementById('modelDropdown'); | |
| if (modeDropdown) modeDropdown.classList.remove('open'); | |
| if (modelDropdown) modelDropdown.classList.remove('open'); | |
| }); | |
| <!-- FIXED: Removed inversion logic and added explicit parameter --> | |
| function toggleDeepResearch(checked) { | |
| isDeepResearch = !!checked; | |
| localStorage.setItem('nexa_ai_deep_research', isDeepResearch); | |
| // Update UI indicator | |
| const indicator = document.getElementById('researchIndicator'); | |
| if (indicator) { | |
| if (isDeepResearch) indicator.classList.add('visible'); | |
| else indicator.classList.remove('visible'); | |
| } | |
| } | |
| function initAiModes() { | |
| setAiMode(currentAiMode); | |
| const toggle = document.getElementById('deepResearchToggle'); | |
| if (toggle) toggle.checked = isDeepResearch; | |
| const indicator = document.getElementById('researchIndicator'); | |
| if (indicator && isDeepResearch) indicator.classList.add('visible'); | |
| } | |
| function saveMemory(key, value) { | |
| userMemory[key] = value; | |
| localStorage.setItem('nexa_ai_memory', JSON.stringify(userMemory)); | |
| } | |
| function getMemoryString() { | |
| if (Object.keys(userMemory).length === 0) return ""; | |
| return "\n[AI Memory of User]: " + Object.entries(userMemory) | |
| .map(([k, v]) => `${k}: ${v}`) | |
| .join(", "); | |
| } | |
| function clearMemory() { | |
| userMemory = {}; | |
| localStorage.removeItem('nexa_ai_memory'); | |
| } | |
| // Tool Actions | |
| let activeFeature = null; | |
| function handleToolAction(action) { | |
| const input = document.getElementById('messageInput'); | |
| const uploadMenu = document.getElementById('uploadMenu'); | |
| if (uploadMenu) uploadMenu.classList.remove('show'); | |
| // Deactivate existing feature first (only one allowed) | |
| if (activeFeature) deactivateFeature(); | |
| switch(action) { | |
| case 'search': | |
| case 'web': | |
| activateFeature('search'); | |
| break; | |
| case 'image': | |
| activateFeature('image'); | |
| break; | |
| case 'video': | |
| activateFeature('video'); | |
| break; | |
| case 'upload': | |
| // Trigger hidden file input | |
| let fileInput = document.getElementById('hiddenFileInput'); | |
| if (!fileInput) { | |
| fileInput = document.createElement('input'); | |
| fileInput.type = 'file'; | |
| fileInput.id = 'hiddenFileInput'; | |
| fileInput.multiple = true; | |
| fileInput.style.display = 'none'; | |
| fileInput.onchange = (e) => handleFiles(e.target.files); | |
| document.body.appendChild(fileInput); | |
| } | |
| fileInput.click(); | |
| return; | |
| case 'voice': | |
| toggleVoiceMode(); | |
| return; | |
| } | |
| input.focus(); | |
| } | |
| function activateFeature(feature) { | |
| activeFeature = feature; | |
| const container = document.getElementById('activeFeatureContainer'); | |
| const input = document.getElementById('messageInput'); | |
| const wrapper = document.getElementById('inputWrapper'); | |
| if (!container || !input || !wrapper) return; | |
| let icon = 'sparkles'; | |
| let label = 'Feature'; | |
| let placeholder = 'Message Nexa AI...'; | |
| switch(feature) { | |
| case 'image': | |
| icon = 'image'; | |
| label = 'Image'; | |
| placeholder = 'Describe or edit an image'; | |
| wrapper.classList.add('image-mode'); | |
| break; | |
| case 'search': | |
| icon = 'search'; | |
| label = 'Search'; | |
| placeholder = 'Search for anything...'; | |
| break; | |
| case 'video': | |
| icon = 'video'; | |
| label = 'Video'; | |
| placeholder = 'Describe a video to generate...'; | |
| break; | |
| } | |
| container.innerHTML = ` | |
| <div class="feature-badge"> | |
| <i data-lucide="${icon}"></i> | |
| <span>${label}</span> | |
| <button class="remove-feature-btn" onclick="deactivateFeature()"> | |
| <i data-lucide="x"></i> | |
| </button> | |
| </div> | |
| `; | |
| container.style.display = 'flex'; | |
| input.placeholder = placeholder; | |
| wrapper.classList.add('feature-active'); | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| function deactivateFeature() { | |
| activeFeature = null; | |
| const container = document.getElementById('activeFeatureContainer'); | |
| const input = document.getElementById('messageInput'); | |
| const wrapper = document.getElementById('inputWrapper'); | |
| if (container) { | |
| container.innerHTML = ''; | |
| container.style.display = 'none'; | |
| } | |
| if (input) { | |
| input.placeholder = 'Message Nexa AI...'; | |
| } | |
| if (wrapper) { | |
| wrapper.classList.remove('image-mode'); | |
| wrapper.classList.remove('feature-active'); | |
| } | |
| } | |
| async function handleVideoCommand(fullText) { | |
| const prompt = fullText.replace('/video', '').trim(); | |
| if (!prompt) { | |
| addMessage('Please provide a prompt for the video. Example: `/video a cinematic sunset`', 'ai'); | |
| return; | |
| } | |
| // UI feedback removed (handled by sendMessage) | |
| messageInput.value = ''; | |
| messageInput.style.height = 'auto'; | |
| if (sendBtn) sendBtn.classList.remove('active'); | |
| if (!currentConversationId) { | |
| currentConversationId = await createConversation(fullText); | |
| } else { | |
| addMessageToConversation(currentConversationId, fullText, 'user'); | |
| } | |
| const thinkingDiv = showThinking(); | |
| const messageContent = thinkingDiv.querySelector('.message-content'); | |
| messageContent.innerHTML = ` | |
| <div class="tool-output-card"> | |
| <div class="tool-output-header"> | |
| <i data-lucide="video"></i> | |
| <span>AI Video Generation</span> | |
| </div> | |
| <div style="padding: 20px; text-align: center;"> | |
| <div class="gen-loader" style="margin: 0 auto 15px;"></div> | |
| <div style="color: var(--text-secondary); font-size: 14px; font-weight: 500;"> | |
| 🎬 Generating your video... (5-15 seconds) | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| if (window.lucide) lucide.createIcons(); | |
| smoothScrollToBottom(); | |
| try { | |
| const response = await fetch('/api/video/generate', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ prompt: prompt }) | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| hideThinking(thinkingDiv); | |
| addMessage(`AI Video ready: "${prompt}"`, 'ai'); | |
| const lastMsg = chatHistory.lastElementChild; | |
| const content = lastMsg.querySelector('.message-content'); | |
| const videoCard = document.createElement('div'); | |
| videoCard.className = 'image-card'; | |
| const videoId = 'vid-' + Date.now(); | |
| videoCard.innerHTML = ` | |
| <div class="video-container" id="${videoId}-container"> | |
| <video id="${videoId}" autoplay loop muted playsinline> | |
| <source src="${data.video_url}" type="video/mp4"> | |
| </video> | |
| <div class="custom-video-controls"> | |
| <button class="vid-ctrl-btn play-pause"> | |
| <i data-lucide="pause"></i> | |
| </button> | |
| <div class="video-progress"> | |
| <div class="video-progress-filled"></div> | |
| </div> | |
| <div class="volume-container"> | |
| <button class="vid-ctrl-btn volume-btn"> | |
| <i data-lucide="volume-2"></i> | |
| </button> | |
| <div class="volume-slider-vertical"> | |
| <input type="range" min="0" max="1" step="0.1" value="1"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="image-card-actions"> | |
| <a href="${data.video_url}" download class="img-btn"><i data-lucide="download"></i> Download</a> | |
| <button class="img-btn" onclick="openVideoOverlayModal('${data.video_url}')"><i data-lucide="type"></i> Add Text Overlay</button> | |
| </div> | |
| `; | |
| content.appendChild(videoCard); | |
| if (window.lucide) lucide.createIcons(); | |
| initCustomVideoPlayer(videoId); | |
| smoothScrollToBottom(); | |
| addMessageToConversation(currentConversationId, `Generated video: ${data.video_url}`, 'ai'); | |
| } else { | |
| throw new Error(data.error || "Video generation failed"); | |
| } | |
| } catch (err) { | |
| hideThinking(thinkingDiv); | |
| addMessage(err.message, 'ai'); | |
| } | |
| } | |
| async function applyVideoOverlay() { | |
| const videoUrl = document.getElementById('videoOverlayModal').getAttribute('data-video-url'); | |
| const text = document.getElementById('overlayTextInput').value; | |
| const pos = document.getElementById('overlayPositionSelect').value; | |
| const fontSize = document.getElementById('overlayFontSizeInput').value; | |
| const color = document.getElementById('overlayTextColorInput').value; | |
| const bgOpacity = document.getElementById('overlayBgOpacityInput').value; | |
| const btn = document.getElementById('applyOverlayBtn'); | |
| const status = document.getElementById('overlayModalStatus'); | |
| if (!text) { | |
| alert("Please enter some text to overlay."); | |
| return; | |
| } | |
| btn.disabled = true; | |
| btn.innerHTML = `<i data-lucide="loader-2" class="animate-spin"></i> Processing...`; | |
| if (window.lucide) lucide.createIcons(); | |
| status.textContent = "Applying text overlay to your video..."; | |
| try { | |
| // Since the video is now generated via URL, we might need a separate endpoint | |
| // that takes a URL instead of a file, or download it first. | |
| // For now, we'll keep the existing logic structure but notify the user. | |
| const response = await fetch('/api/video/overlay-url', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| video_url: videoUrl, | |
| text: text, | |
| pos: pos, | |
| font_size: fontSize, | |
| color: color, | |
| bg_opacity: bgOpacity | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| closeVideoOverlayModal(); | |
| addMessage(`Text overlay applied successfully!`, 'ai'); | |
| const lastMsg = chatHistory.lastElementChild; | |
| const content = lastMsg.querySelector('.message-content'); | |
| const videoCard = document.createElement('div'); | |
| videoCard.className = 'image-card'; | |
| const videoId = 'vid-' + Date.now(); | |
| videoCard.innerHTML = ` | |
| <div class="video-container" id="${videoId}-container"> | |
| <video id="${videoId}" autoplay loop muted playsinline> | |
| <source src="${data.video_url}" type="video/mp4"> | |
| </video> | |
| <div class="custom-video-controls"> | |
| <button class="vid-ctrl-btn play-pause"> | |
| <i data-lucide="pause"></i> | |
| </button> | |
| <div class="video-progress"> | |
| <div class="video-progress-filled"></div> | |
| </div> | |
| <div class="volume-container"> | |
| <button class="vid-ctrl-btn volume-btn"> | |
| <i data-lucide="volume-2"></i> | |
| </button> | |
| <div class="volume-slider-vertical"> | |
| <input type="range" min="0" max="1" step="0.1" value="1"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="image-card-actions"> | |
| <a href="${data.video_url}" download class="img-btn"><i data-lucide="download"></i> Download Video</a> | |
| </div> | |
| `; | |
| content.appendChild(videoCard); | |
| if (window.lucide) lucide.createIcons(); | |
| initCustomVideoPlayer(videoId); | |
| smoothScrollToBottom(); | |
| } else { | |
| status.textContent = "Error: " + data.error; | |
| status.style.color = "#ef4444"; | |
| } | |
| } catch (err) { | |
| console.error(err); | |
| status.textContent = "Processing failed. Try a shorter text."; | |
| status.style.color = "#ef4444"; | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = `<i data-lucide="sparkles"></i> Process & Apply Overlay`; | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| } | |
| function closeVideoOverlayModal() { | |
| document.getElementById('videoOverlayModal').style.display = 'none'; | |
| } | |
| function handleVideoOverlayClick(e) { | |
| if (e.target.id === 'videoOverlayModal') closeVideoOverlayModal(); | |
| } | |
| function initCustomVideoPlayer(videoId) { | |
| const video = document.getElementById(videoId); | |
| const container = document.getElementById(videoId + '-container'); | |
| if (!video || !container) return; | |
| // Task 21: Use event delegation to avoid per-element listeners and memory leaks | |
| container.addEventListener('click', (e) => { | |
| const playPauseBtn = e.target.closest('.play-pause'); | |
| const volumeBtn = e.target.closest('.volume-btn'); | |
| const progress = e.target.closest('.video-progress'); | |
| if (playPauseBtn) { | |
| if (video.paused) { | |
| video.play(); | |
| playPauseBtn.innerHTML = '<i data-lucide="pause"></i>'; | |
| } else { | |
| video.pause(); | |
| playPauseBtn.innerHTML = '<i data-lucide="play"></i>'; | |
| } | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| if (volumeBtn) { | |
| video.muted = !video.muted; | |
| volumeBtn.innerHTML = video.muted ? '<i data-lucide="volume-x"></i>' : '<i data-lucide="volume-2"></i>'; | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| if (progress) { | |
| const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration; | |
| video.currentTime = scrubTime; | |
| } | |
| }); | |
| video.addEventListener('timeupdate', () => { | |
| const progressFilled = container.querySelector('.video-progress-filled'); | |
| if (progressFilled) { | |
| const percent = (video.currentTime / video.duration) * 100; | |
| progressFilled.style.width = percent + '%'; | |
| } | |
| }); | |
| const volumeSlider = container.querySelector('input[type="range"]'); | |
| if (volumeSlider) { | |
| volumeSlider.addEventListener('input', (e) => { | |
| video.volume = e.target.value; | |
| video.muted = video.volume === 0; | |
| const volumeBtn = container.querySelector('.volume-btn'); | |
| if (volumeBtn) { | |
| volumeBtn.innerHTML = video.muted ? '<i data-lucide="volume-x"></i>' : '<i data-lucide="volume-2"></i>'; | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| }); | |
| } | |
| } | |
| function toggleVoiceMode() { | |
| if (!sendBtn) return; | |
| isVoiceActive = !isVoiceActive; | |
| const voiceBtn = document.getElementById('toolVoiceBtn'); | |
| if (isVoiceActive) { | |
| voiceBtn.classList.add('active'); | |
| addMessage("Voice mode activated. (Simulated)", "ai"); | |
| } else { | |
| voiceBtn.classList.remove('active'); | |
| addMessage("Voice mode deactivated.", "ai"); | |
| } | |
| } | |
| // Selectors (will be assigned in init) | |
| // Functions | |
| function toggleSidebar() { | |
| if (!sidebar) return; | |
| const isMobile = window.innerWidth <= 768; | |
| const overlay = document.getElementById('sidebarOverlay'); | |
| if (isMobile) { | |
| sidebar.classList.toggle('open'); | |
| if (overlay) overlay.classList.toggle('active'); | |
| document.body.classList.toggle('sidebar-open'); | |
| } else { | |
| sidebar.classList.toggle('collapsed'); | |
| // Persist desktop state | |
| localStorage.setItem('nexa_sidebar_collapsed', sidebar.classList.contains('collapsed')); | |
| } | |
| } | |
| function closeSidebarOnMobile() { | |
| if (window.innerWidth <= 768) { | |
| const overlay = document.getElementById('sidebarOverlay'); | |
| if (sidebar) sidebar.classList.remove('open'); | |
| if (overlay) overlay.classList.remove('active'); | |
| document.body.classList.remove('sidebar-open'); | |
| } | |
| } | |
| function toggleUploadMenu() { | |
| const uploadMenu = document.getElementById('uploadMenu'); | |
| if (uploadMenu) uploadMenu.classList.toggle('show'); | |
| } | |
| // FIXED: Comprehensive stopAI to reset all states and UI | |
| function stopAI() { | |
| try { | |
| if (abortController) { | |
| abortController.abort(); | |
| } | |
| } catch (e) { | |
| console.error("Stop AI error:", e); | |
| } finally { | |
| // Ensure all states are reset regardless of errors | |
| abortController = null; | |
| isTyping = false; | |
| isGeneratingImage = false; | |
| isSearching = false; | |
| isAnalyzingFile = false; | |
| // UI Cleanup | |
| const thinking = document.getElementById('thinkingMessage'); | |
| if (thinking) thinking.remove(); | |
| if (stopBtn) stopBtn.classList.remove('active'); | |
| if (sendBtn) { | |
| sendBtn.style.display = 'flex'; | |
| sendBtn.disabled = false; | |
| } | |
| updateSendButtonState(); | |
| } | |
| } | |
| function handleImageRemix(encodedQuery) { | |
| const query = atob(encodedQuery); | |
| const input = document.getElementById('messageInput'); | |
| if (input) { | |
| input.value = `Remix this image: ${query}`; | |
| input.focus(); | |
| autoResizeTextarea(); | |
| } | |
| } | |
| function getUserInitial() { | |
| return currentUser?.username?.charAt(0).toUpperCase() || 'G'; | |
| } | |
| function getConversationStorageKey() { | |
| return `chatgpt_conversations_${currentUser?.username || 'guest'}`; | |
| } | |
| function loadConversationsFromStorage() { | |
| const key = getConversationStorageKey(); | |
| conversations = JSON.parse(localStorage.getItem(key) || '[]'); | |
| } | |
| function updateMessageCounter() { | |
| // UI logic for message count if needed | |
| } | |
| function autoResizeTextarea() { | |
| if (!messageInput) return; | |
| let timeout; | |
| messageInput.addEventListener('input', function () { | |
| if (timeout) clearTimeout(timeout); | |
| timeout = setTimeout(() => { | |
| this.style.height = 'auto'; | |
| this.style.height = (this.scrollHeight) + 'px'; | |
| updateSendButtonState(); | |
| }, 10); | |
| }); | |
| } | |
| function setupDragAndDrop() { | |
| const mainArea = document.getElementById('mainArea'); | |
| const overlay = document.getElementById('dragDropOverlay'); | |
| if (!mainArea || !overlay) return; | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| mainArea.addEventListener(eventName, (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| }, false); | |
| }); | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| mainArea.addEventListener(eventName, () => { | |
| overlay.classList.add('active'); | |
| }, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| mainArea.addEventListener(eventName, () => { | |
| overlay.classList.remove('active'); | |
| }, false); | |
| }); | |
| mainArea.addEventListener('drop', (e) => { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| if (files && files.length > 0) { | |
| handleFiles(files); | |
| } | |
| }, false); | |
| } | |
| function handleFiles(files) { | |
| const container = document.getElementById('attachmentsContainer'); | |
| if (!container) return; | |
| container.style.display = 'flex'; | |
| Array.from(files).forEach(file => { | |
| const card = document.createElement('div'); | |
| card.className = 'file-attachment-card'; | |
| card.setAttribute('data-filename', file.name); | |
| const fileExt = file.name.split('.').pop().toUpperCase(); | |
| let iconColor = '#3182ce'; // Default blue | |
| let iconName = 'file-text'; | |
| if (['JPG', 'JPEG', 'PNG', 'GIF', 'WEBP'].includes(fileExt)) { | |
| iconColor = '#10a37f'; | |
| iconName = 'image'; | |
| } else if (fileExt === 'PDF') { | |
| iconColor = '#e53e3e'; | |
| iconName = 'file-text'; | |
| } | |
| card.innerHTML = ` | |
| <div class="file-attachment-icon" style="background: ${iconColor}"> | |
| <i data-lucide="${iconName}" style="width: 18px; height: 18px;"></i> | |
| </div> | |
| <div class="file-attachment-info"> | |
| <span class="file-attachment-name">${file.name}</span> | |
| <span class="file-attachment-type">${fileExt} File</span> | |
| </div> | |
| <div class="file-attachment-remove" onclick="this.parentElement.remove(); if(document.getElementById('attachmentsContainer').children.length === 0) document.getElementById('attachmentsContainer').style.display='none';"> | |
| <i data-lucide="x"></i> | |
| </div> | |
| `; | |
| container.appendChild(card); | |
| if (window.lucide) lucide.createIcons(); | |
| // Automatically start analysis if backend supports it | |
| handleFileAnalysis(file, card); | |
| }); | |
| } | |
| function setupEventListeners() { | |
| // FIXED: Restructured with early returns to prevent fall-through bugs | |
| document.addEventListener('click', (e) => { | |
| const target = e.target; | |
| // --- LANDING PAGE HANDLERS --- | |
| // 1. Landing Navigation Links (Features, Pricing, etc.) | |
| const navLink = target.closest('.nav-link[data-section]'); | |
| if (navLink) { | |
| e.preventDefault(); | |
| scrollToSection(navLink.getAttribute('data-section')); | |
| return; | |
| } | |
| // 3. FAQ Items | |
| const faqQuestion = target.closest('.faq-question'); | |
| if (faqQuestion) { | |
| const item = faqQuestion.parentElement; | |
| const wasActive = item.classList.contains('active'); | |
| document.querySelectorAll('.faq-item').forEach(i => i.classList.remove('active')); | |
| if (!wasActive) item.classList.add('active'); | |
| return; | |
| } | |
| // --- APP INTERFACE HANDLERS --- | |
| // 4. App Sidebar Toggle (Hamburger or Close) | |
| if (target.closest('.hamburger-menu') || target.closest('.sidebar-close-btn') || target.closest('.sidebar-overlay')) { | |
| toggleSidebar(); | |
| return; | |
| } | |
| // 5. New Chat Button | |
| if (target.closest('.new-chat-btn')) { | |
| startNewChat(); | |
| return; | |
| } | |
| // 6. Tool Actions (Using data-action instead of regex parsing) | |
| const toolBtn = target.closest('.tool-btn'); | |
| if (toolBtn) { | |
| e.preventDefault(); | |
| const action = toolBtn.dataset.action; | |
| if (action) handleToolAction(action); | |
| return; | |
| } | |
| // 7. Sidebar Conversation Items | |
| const convItem = target.closest('.conversation-item'); | |
| if (convItem && !target.closest('.delete-btn') && !target.closest('.action-btn')) { | |
| const convId = convItem.getAttribute('data-id'); | |
| if (convId) { | |
| closeSidebarOnMobile(); | |
| loadConversation(parseInt(convId)); | |
| } | |
| return; | |
| } | |
| // 8. Account Dropdown Toggle | |
| if (target.closest('.account-btn')) { | |
| toggleAccountDropdown(); | |
| return; | |
| } | |
| // 9. Close dropdowns when clicking outside | |
| if (!target.closest('.mode-dropdown') && !target.closest('.account-btn') && !target.closest('.top-right-menu')) { | |
| isDropdownOpen = false; | |
| document.querySelectorAll('.mode-dropdown, .account-dropdown').forEach(d => d.classList.remove('open', 'show')); | |
| } | |
| if (!target.closest('#uploadBtn') && !target.closest('#uploadMenu')) { | |
| const uploadMenu = document.getElementById('uploadMenu'); | |
| if (uploadMenu) uploadMenu.classList.remove('show'); | |
| } | |
| }); | |
| // --- FORM & INPUT LISTENERS --- | |
| messageInput = messageInput || document.getElementById('messageInput'); | |
| sendBtn = sendBtn || document.getElementById('sendBtn'); | |
| if (messageInput) { | |
| messageInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| } | |
| // The send button might still have onclick in HTML, but we add listener just in case | |
| if (sendBtn) { | |
| sendBtn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| sendMessage(); | |
| }); | |
| } | |
| } | |
| function showAuthOverlay() { | |
| if (authOverlay) { | |
| authOverlay.style.display = 'flex'; | |
| authOverlay.classList.add('show'); | |
| } | |
| } | |
| function hideAuthOverlay() { | |
| if (authOverlay) { | |
| authOverlay.style.display = 'none'; | |
| authOverlay.classList.remove('show'); | |
| } | |
| } | |
| // FIXED: Added loginAsGuest function for quick access via the close button | |
| function loginAsGuest() { | |
| currentUser = { | |
| id: 0, | |
| username: 'Guest', | |
| email: 'guest@nexa.ai', | |
| avatar: null | |
| }; | |
| localStorage.setItem('chatgpt_current_user', JSON.stringify(currentUser)); | |
| applyUserUI(); | |
| loadConversationsFromStorage(); | |
| hideAuthOverlay(); | |
| // Navigate to chat | |
| if (window.location.pathname !== '/chat') { | |
| window.history.pushState({}, '', '/chat'); | |
| } | |
| enterApp(); | |
| } | |
| function togglePasswordVisibility() { | |
| const passwordInput = document.getElementById('authPassword'); | |
| const toggleIcon = document.getElementById('toggleIcon'); | |
| const toggleBtn = document.getElementById('passwordToggle'); | |
| if (passwordInput.type === 'password') { | |
| passwordInput.type = 'text'; | |
| toggleBtn.classList.add('active'); | |
| toggleBtn.title = "Hide Password"; | |
| } else { | |
| passwordInput.type = 'password'; | |
| toggleBtn.classList.remove('active'); | |
| toggleBtn.title = "Show Password"; | |
| } | |
| } | |
| function toggleAuthMode() { | |
| isSignUp = !isSignUp; | |
| const title = document.getElementById('authTitle'); | |
| const submitBtn = document.getElementById('authSubmitBtn'); | |
| const toggleBtn = document.getElementById('authToggleBtn'); | |
| const toggleText = document.getElementById('authToggleText'); | |
| const emailInput = document.getElementById('authEmail'); | |
| if (isSignUp) { | |
| if (title) title.textContent = 'Sign Up'; | |
| if (submitBtn) submitBtn.textContent = 'Sign Up'; | |
| if (toggleText) toggleText.textContent = 'Already have an account?'; | |
| if (toggleBtn) toggleBtn.textContent = 'Sign In'; | |
| if (emailInput) { | |
| emailInput.placeholder = 'Email Address'; | |
| emailInput.type = 'email'; // Enable email validation only for signup | |
| } | |
| } else { | |
| if (title) title.textContent = 'Sign In'; | |
| if (submitBtn) submitBtn.textContent = 'Sign In'; | |
| if (toggleText) toggleText.textContent = "Don't have an account?"; | |
| if (toggleBtn) toggleBtn.textContent = 'Sign Up'; | |
| if (emailInput) { | |
| emailInput.placeholder = 'Email or Username'; | |
| emailInput.type = 'text'; // Disable email validation for login | |
| } | |
| } | |
| } | |
| async function handleAuth(event) { | |
| event.preventDefault(); | |
| const email = document.getElementById('authEmail').value; | |
| const password = document.getElementById('authPassword').value; | |
| const authError = document.getElementById('authError'); | |
| const endpoint = isSignUp ? '/api/auth/signup' : '/api/auth/login'; | |
| // For signup, backend will use email prefix as username | |
| const payload = { email, password }; | |
| try { | |
| const response = await fetch(endpoint, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| if (isSignUp) { | |
| isSignUp = false; | |
| toggleAuthMode(); | |
| if (authError) { | |
| authError.textContent = 'Account created! Please login.'; | |
| authError.style.color = '#10a37f'; | |
| authError.style.display = 'block'; | |
| } | |
| } else { | |
| currentUser = data.user; | |
| localStorage.setItem('chatgpt_current_user', JSON.stringify(currentUser)); | |
| applyUserUI(); | |
| loadConversationsFromStorage(); | |
| hideAuthOverlay(); | |
| // Navigate to chat if on landing | |
| if (document.getElementById('landingPage').style.display !== 'none') { | |
| enterApp(); | |
| } | |
| if (window.location.pathname !== '/chat') { | |
| window.history.pushState({}, '', '/chat'); | |
| } | |
| } | |
| } else { | |
| if (authError) { | |
| // FIXED: specific error message for failed login | |
| authError.textContent = isSignUp ? (data.error || 'Signup failed') : 'Incorrect candidate details'; | |
| authError.style.color = '#ef4444'; | |
| authError.style.display = 'block'; | |
| } | |
| } | |
| } catch (error) { | |
| if (authError) { | |
| authError.textContent = 'Server error. Please try again.'; | |
| authError.style.display = 'block'; | |
| } | |
| } | |
| } | |
| async function logoutUser() { | |
| try { | |
| await fetch('/api/auth/logout', { method: 'POST' }); | |
| } catch (e) {} | |
| currentUser = null; | |
| localStorage.removeItem('chatgpt_current_user'); | |
| location.href = '/'; | |
| } | |
| function applyUserUI() { | |
| if (currentUser) { | |
| const initial = getUserInitial(); | |
| const accAvatar = document.getElementById('accountAvatar'); | |
| const sideAvatar = document.getElementById('sidebarAvatar'); | |
| const accUser = document.getElementById('accountUserName'); | |
| const sideUser = document.getElementById('sidebarUserName'); | |
| if (accAvatar) accAvatar.textContent = initial; | |
| if (sideAvatar) sideAvatar.textContent = initial; | |
| if (accUser) accUser.textContent = currentUser.username; | |
| if (sideUser) sideUser.textContent = currentUser.username; | |
| } | |
| } | |
| function addMessage(content, type, isStreaming = false) { | |
| if (chatHistory.classList.contains('empty-mode')) { | |
| chatHistory.innerHTML = ''; | |
| chatHistory.classList.remove('empty-mode'); | |
| document.querySelector('.main-area').classList.remove('hero-mode'); | |
| } | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${type}`; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| if (type === 'ai') { | |
| avatar.innerHTML = '<div class="blinking-emoji"><div class="eye"></div><div class="eye"></div></div>'; | |
| } else { | |
| avatar.textContent = getUserInitial(); | |
| } | |
| const messageContent = document.createElement('div'); | |
| messageContent.className = 'message-content'; | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(messageContent); | |
| chatHistory.appendChild(messageDiv); | |
| refreshIcons(); | |
| if (type === 'ai' && isStreaming) { | |
| typeWordByWord(content, messageContent); | |
| } else { | |
| let processedContent = content; | |
| if (type === 'ai') { | |
| // Highlight important info wrapped in double asterisks or specific markers | |
| processedContent = content.replace(/\*\*(.*?)\*\*/g, '<span class="important-highlight">$1</span>'); | |
| } | |
| if (typeof marked !== 'undefined') { | |
| messageContent.innerHTML = marked.parse(processedContent); | |
| } else { | |
| messageContent.textContent = processedContent; | |
| } | |
| renderNexusComponents(messageContent); | |
| if (type === 'ai') { | |
| addMessageActions(messageContent, content); | |
| } | |
| smoothScrollToBottom(); | |
| } | |
| return messageDiv; | |
| } | |
| async function renderNexusComponents(container) { | |
| // Render Mermaid Diagrams | |
| const mermaidBlocks = container.querySelectorAll('pre code.language-mermaid'); | |
| mermaidBlocks.forEach(async (block, idx) => { | |
| const syntax = block.textContent; | |
| const id = `mermaid-${Date.now()}-${idx}`; | |
| const mermaidDiv = document.createElement('div'); | |
| mermaidDiv.className = 'mermaid-render-container'; | |
| mermaidDiv.id = id; | |
| block.parentElement.replaceWith(mermaidDiv); | |
| try { | |
| const { svg } = await mermaid.render(id + '-svg', syntax); | |
| mermaidDiv.innerHTML = svg; | |
| } catch (e) { | |
| console.error('Mermaid Render Error:', e); | |
| mermaidDiv.innerHTML = `<div class="error-box">Diagram render failed: ${e.message}</div>`; | |
| } | |
| }); | |
| // Render Chart.js Charts | |
| const chartBlocks = container.querySelectorAll('pre code.language-chart'); | |
| chartBlocks.forEach((block, idx) => { | |
| try { | |
| const config = JSON.parse(block.textContent); | |
| const canvas = document.createElement('canvas'); | |
| canvas.className = 'chart-render-canvas'; | |
| canvas.style.maxHeight = '300px'; | |
| block.parentElement.replaceWith(canvas); | |
| new Chart(canvas, config); | |
| } catch (e) { | |
| console.error('Chart Render Error:', e); | |
| } | |
| }); | |
| } | |
| async function typeWordByWord(content, container) { | |
| const words = content.split(' '); | |
| let currentText = ''; | |
| isTyping = true; | |
| if (stopBtn) stopBtn.classList.add('active'); | |
| if (sendBtn) sendBtn.style.display = 'none'; | |
| const textContainer = document.createElement('span'); | |
| const cursor = document.createElement('span'); | |
| cursor.className = 'typing-cursor-ai'; | |
| container.appendChild(textContainer); | |
| container.appendChild(cursor); | |
| // Optimized for maximum speed: Process in larger chunks with minimal delay | |
| for (let i = 0; i < words.length; i += 8) { | |
| if (!isTyping) break; | |
| const chunk = words.slice(i, i + 8).join(' '); | |
| const span = document.createElement('span'); | |
| span.className = 'streaming-text'; | |
| span.innerHTML = (i === 0 ? '' : ' ') + chunk; | |
| textContainer.appendChild(span); | |
| currentText += (i === 0 ? '' : ' ') + chunk; | |
| smoothScrollToBottom(); | |
| // Minimum possible delay for ultra-fast "writing" effect | |
| await new Promise(resolve => setTimeout(resolve, 5)); | |
| } | |
| cursor.remove(); | |
| // Final render with full markdown formatting | |
| const processedContent = content.replace(/\*\*(.*?)\*\*/g, '<span class="important-highlight">$1</span>'); | |
| container.innerHTML = marked.parse(processedContent); | |
| renderNexusComponents(container); | |
| addMessageActions(container, content); | |
| isTyping = false; | |
| if (stopBtn) stopBtn.classList.remove('active'); | |
| if (sendBtn) sendBtn.style.display = 'flex'; | |
| smoothScrollToBottom(); | |
| } | |
| function smoothScrollToBottom() { | |
| if (chatHistory) { | |
| chatHistory.scrollTo({ | |
| top: chatHistory.scrollHeight, | |
| behavior: 'smooth' | |
| }); | |
| } | |
| } | |
| function showThinking() { | |
| const thinkingDiv = document.createElement('div'); | |
| thinkingDiv.className = 'message ai thinking-glow'; | |
| thinkingDiv.id = 'thinkingMessage'; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| avatar.innerHTML = '<div class="blinking-emoji"><div class="eye"></div><div class="eye"></div></div>'; | |
| const messageContent = document.createElement('div'); | |
| messageContent.className = 'message-content'; | |
| const statusMessages = [ | |
| "Nexa is analyzing your request...", | |
| "Searching internal knowledge base...", | |
| "Synthesizing the perfect response...", | |
| "Optimizing for accuracy and clarity...", | |
| "Nexa is drafting your answer..." | |
| ]; | |
| let statusIdx = 0; | |
| const modelLabel = currentAiModel || 'Qwen 2.5'; | |
| messageContent.innerHTML = ` | |
| <div style="display:flex; flex-direction:column; align-items:flex-start; gap:12px; padding: 10px 0;"> | |
| <div class="llm-badge"> | |
| <i data-lucide="cpu" style="width:12px;"></i> | |
| <span>Using ${modelLabel}</span> | |
| </div> | |
| <div class="ai-processing-ring"> | |
| <div class="blinking-emoji" style="width:24px; height:24px;"> | |
| <div class="eye" style="width:3px; height:5px;"></div> | |
| <div class="eye" style="width:3px; height:5px;"></div> | |
| </div> | |
| </div> | |
| <div style="display:flex; align-items:center; gap:12px;"> | |
| <div class="thinking"> | |
| <span></span> | |
| <span></span> | |
| <span></span> | |
| </div> | |
| <span class="thinking-status-text" id="thinkingStatusText">Nexa is thinking...</span> | |
| </div> | |
| </div> | |
| `; | |
| thinkingDiv.appendChild(avatar); | |
| thinkingDiv.appendChild(messageContent); | |
| if (chatHistory) chatHistory.appendChild(thinkingDiv); | |
| if (window.lucide) lucide.createIcons(); | |
| smoothScrollToBottom(); | |
| // Dynamic status updates | |
| const statusInterval = setInterval(() => { | |
| const statusText = document.getElementById('thinkingStatusText'); | |
| if (statusText && isTyping) { | |
| statusIdx = (statusIdx + 1) % statusMessages.length; | |
| statusText.textContent = statusMessages[statusIdx]; | |
| } else { | |
| clearInterval(statusInterval); | |
| } | |
| }, 3000); | |
| return thinkingDiv; | |
| } | |
| function hideThinking(thinkingDiv) { | |
| if (thinkingDiv) thinkingDiv.remove(); | |
| } | |
| async function handleAgentMode(query) { | |
| const thinkingDiv = showThinking(); | |
| const messageContent = thinkingDiv.querySelector('.message-content'); | |
| // Task 15: Stream the AI response normally (backend handles tool detection) | |
| addMessage(query, 'user'); | |
| try { | |
| abortController = new AbortController(); | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| signal: abortController.signal, | |
| body: JSON.stringify({ | |
| message: query, | |
| username: currentUser ? currentUser.username : 'Guest', | |
| history: getConversationHistorySnapshot(currentConversationId), | |
| memory: userMemory, | |
| mode: currentAiMode, | |
| deep_research: isDeepResearch | |
| }) | |
| }); | |
| hideThinking(thinkingDiv); | |
| if (!response.ok) throw new Error('Chat API failed'); | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let fullResponse = ""; | |
| let aiMsgDiv = null; | |
| let aiContentEl = null; | |
| let cursorEl = null; | |
| let buffer = ''; | |
| let toolsUsed = []; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop(); | |
| for (const line of lines) { | |
| if (!line.trim()) continue; | |
| try { | |
| const data = JSON.parse(line); | |
| if (data.error) { | |
| hideThinking(thinkingDiv); | |
| addMessage(data.error, 'ai'); | |
| return; | |
| } | |
| // Task: Handle search results from backend synthesis | |
| if (data.search_results) { | |
| const searchCard = document.createElement('div'); | |
| searchCard.className = 'tool-output-card'; | |
| searchCard.innerHTML = ` | |
| <div class="tool-output-header"> | |
| <i data-lucide="search"></i> | |
| <span>Intelligence Sources Discovered</span> | |
| </div> | |
| <div class="search-results"> | |
| ${data.search_results.results.map(res => ` | |
| <a href="${res.href || res.link}" target="_blank" class="search-card"> | |
| <h5><i data-lucide="external-link" style="width:14px;"></i> ${res.title}</h5> | |
| <p>${res.body || res.snippet}</p> | |
| </a> | |
| `).join('')} | |
| </div> | |
| `; | |
| chatHistory.appendChild(searchCard); | |
| if (window.lucide) lucide.createIcons(); | |
| smoothScrollToBottom(); | |
| } | |
| if (data.token) { | |
| if (!aiMsgDiv) { | |
| aiMsgDiv = addMessage("", 'ai'); | |
| aiContentEl = aiMsgDiv.querySelector('.message-content'); | |
| // Add the research report badge if search was used | |
| if (toolsUsed.includes('search') || data.search_results) { | |
| aiContentEl.innerHTML = '<div class="compiled-search-badge"><i data-lucide="brain"></i> Synthesized Research Report</div>'; | |
| } else { | |
| aiContentEl.innerHTML = ''; | |
| } | |
| cursorEl = document.createElement('span'); | |
| cursorEl.className = 'typing-cursor-ai'; | |
| aiContentEl.appendChild(cursorEl); | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| fullResponse += data.token; | |
| const tempSpan = document.createElement('span'); | |
| tempSpan.className = 'streaming-text'; | |
| tempSpan.textContent = data.token; | |
| aiContentEl.insertBefore(tempSpan, cursorEl); | |
| if (data.tools_used) toolsUsed = data.tools_used; | |
| smoothScrollToBottom(); | |
| } | |
| } catch (e) { | |
| console.error("Stream parse error:", e); | |
| } | |
| } | |
| } | |
| if (aiMsgDiv) { | |
| if (cursorEl) cursorEl.remove(); | |
| const badge = (toolsUsed.includes('search')) ? '<div class="compiled-search-badge"><i data-lucide="brain"></i> Synthesized Research Report</div>' : ''; | |
| aiContentEl.innerHTML = badge + marked.parse(fullResponse); | |
| // Add actions immediately after content is parsed | |
| addMessageActions(aiContentEl, fullResponse); | |
| refreshIcons(); | |
| // If backend used video tool, call video API and append to message | |
| if (toolsUsed.includes('video')) { | |
| const videoContainer = document.createElement('div'); | |
| videoContainer.className = 'image-card'; | |
| videoContainer.innerHTML = `<div class="img-skeleton visible" style="height: 300px; background: rgba(255,255,255,0.05); border-radius: 12px; animation: pulse 1.5s infinite; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 10px;"> | |
| <i data-lucide="video" class="animate-pulse" style="width:40px; height:40px; color: #10a37f;"></i> | |
| <span style="font-size: 14px; color: var(--text-secondary);">Generating AI Video...</span> | |
| </div>`; | |
| aiContentEl.appendChild(videoContainer); | |
| smoothScrollToBottom(); | |
| if (window.lucide) lucide.createIcons(); | |
| try { | |
| const videoRes = await fetch('/api/video/generate', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ prompt: query }) | |
| }); | |
| const videoData = await videoRes.json(); | |
| if (videoData.status === 'success') { | |
| const videoId = 'vid-' + Date.now(); | |
| if (videoData.is_motion_image) { | |
| videoContainer.innerHTML = ` | |
| <div class="video-container" style="background:none; position: relative;"> | |
| <img src="${videoData.video_url}" alt="${query}" class="motion-image" style="width:100%; border-radius:12px; display:block;"> | |
| <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.5); border-radius: 50%; width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; pointer-events: none;"> | |
| <i data-lucide="play" style="width: 30px; height: 30px; color: white; fill: white;"></i> | |
| </div> | |
| </div> | |
| <div class="image-card-actions"> | |
| <a href="${videoData.video_url}" class="img-btn" onclick="downloadImage(event, '${videoData.video_url}')"><i data-lucide="download"></i> Download</a> | |
| <button class="img-btn" onclick="openVideoOverlayModal('${videoData.video_url}')"><i data-lucide="type"></i> Add Text</button> | |
| </div> | |
| `; | |
| } else { | |
| videoContainer.innerHTML = ` | |
| <div class="video-container" id="${videoId}-container"> | |
| <video id="${videoId}" autoplay loop muted playsinline> | |
| <source src="${videoData.video_url}" type="video/mp4"> | |
| </video> | |
| <div class="custom-video-controls"> | |
| <button class="vid-ctrl-btn play-pause"><i data-lucide="pause"></i></button> | |
| <div class="video-progress"><div class="video-progress-filled"></div></div> | |
| <div class="volume-container"> | |
| <button class="vid-ctrl-btn volume-btn"><i data-lucide="volume-x"></i></button> | |
| <div class="volume-slider-vertical"><input type="range" min="0" max="1" step="0.1" value="0"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="image-card-actions"> | |
| <a href="${videoData.video_url}" download class="img-btn"><i data-lucide="download"></i> Download Video</a> | |
| <button class="img-btn" onclick="openVideoOverlayModal('${videoData.video_url}')"><i data-lucide="type"></i> Add Text</button> | |
| </div> | |
| `; | |
| } | |
| if (window.lucide) lucide.createIcons(); | |
| if (!videoData.is_motion_image) initCustomVideoPlayer(videoId); | |
| } else { | |
| videoContainer.innerHTML = `<div style="padding: 20px; color: #f87171;"><i data-lucide="alert-circle"></i> Video failed: ${videoData.error}</div>`; | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| } catch (vErr) { | |
| videoContainer.innerHTML = `<div style="padding: 20px; color: #f87171;"><i data-lucide="alert-circle"></i> Video connection error.</div>`; | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| smoothScrollToBottom(); | |
| } | |
| // If backend used image tool, call image API and append to message | |
| if (toolsUsed.includes('image')) { | |
| const imageContainer = document.createElement('div'); | |
| imageContainer.className = 'image-card'; | |
| imageContainer.innerHTML = `<div class="img-skeleton visible" style="height: 300px; background: rgba(255,255,255,0.05); border-radius: 12px; animation: pulse 1.5s infinite;"></div>`; | |
| aiContentEl.appendChild(imageContainer); | |
| smoothScrollToBottom(); | |
| const imgRes = await fetch('/api/image', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ prompt: query }) | |
| }); | |
| const imgData = await imgRes.json(); | |
| imageContainer.innerHTML = ` | |
| <div class="image-card-header"><i data-lucide="image" style="width:14px;color:#10a37f;"></i><span>Generated Image</span></div> | |
| <img src="${imgData.image_url}" class="loaded" onload="this.parentElement.querySelector('.img-skeleton')?.remove();"> | |
| <div class="image-card-actions"> | |
| <a href="${imgData.image_url}" target="_blank" class="img-btn"><i data-lucide="external-link"></i> Open</a> | |
| <a href="${imgData.image_url}" download class="img-btn" onclick="downloadImage(event, '${imgData.image_url}')"><i data-lucide="download"></i> Download</a> | |
| </div> | |
| `; | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| addMessageToConversation(currentConversationId, fullResponse, 'ai'); | |
| if (window.lucide) lucide.createIcons({ attrs: { 'stroke-width': 2.5 } }); | |
| } | |
| } catch (e) { | |
| console.error("Agent mode failed:", e); | |
| hideThinking(thinkingDiv); | |
| addMessage("I encountered a problem while processing your request.", 'ai'); | |
| } finally { | |
| abortController = null; | |
| if (stopBtn) stopBtn.classList.remove('active'); | |
| if (sendBtn) sendBtn.style.display = 'flex'; | |
| } | |
| } | |
| async function handlePlannerMode(query) { | |
| const thinkingDiv = showThinking(); | |
| const messageContent = thinkingDiv.querySelector('.message-content'); | |
| messageContent.innerHTML = ` | |
| <div class="agent-thinking-text"><span class="gen-loader"></span> Planning your schedule...</div> | |
| <div class="planner-timeline" id="plannerTimeline"></div> | |
| `; | |
| smoothScrollToBottom(); | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| message: `Create a detailed hour-by-hour schedule for: "${query}". Format the response as a valid JSON array of objects with "time" and "task" keys only. Example: [{"time": "9:00 AM", "task": "Study Math"}]`, | |
| username: currentUser.username, | |
| history: [], | |
| conversation_id: currentConversationId | |
| }) | |
| }); | |
| const data = await response.json(); | |
| hideThinking(thinkingDiv); | |
| const aiMsgDiv = addMessage("Here is your structured plan:", 'ai'); | |
| const aiContent = aiMsgDiv.querySelector('.message-content'); | |
| const timeline = document.createElement('div'); | |
| timeline.className = 'planner-timeline'; | |
| try { | |
| // Try to parse the JSON from AI response | |
| const jsonStr = data.response.match(/\[.*\]/s)?.[0] || "[]"; | |
| const tasks = JSON.parse(jsonStr); | |
| tasks.forEach(item => { | |
| const taskEl = document.createElement('div'); | |
| taskEl.className = 'planner-item'; | |
| taskEl.innerHTML = ` | |
| <div class="planner-time">${item.time}</div> | |
| <div class="planner-task">${item.task}</div> | |
| `; | |
| timeline.appendChild(taskEl); | |
| }); | |
| aiContent.appendChild(timeline); | |
| } catch (e) { | |
| // Fallback to normal text if JSON fails | |
| aiContent.innerHTML += marked.parse(data.response); | |
| } | |
| addMessageToConversation(currentConversationId, query, 'user'); | |
| addMessageToConversation(currentConversationId, data.response, 'ai'); | |
| smoothScrollToBottom(); | |
| } catch (e) { | |
| hideThinking(thinkingDiv); | |
| addMessage("I couldn't generate the plan right now.", 'ai'); | |
| } | |
| } | |
| async function handleImageCommand(fullText) { | |
| if (isGeneratingImage) return; | |
| const promptPart = fullText.replace('/image', '').trim(); | |
| if (!promptPart) { | |
| addMessage('Please provide a prompt for the image. Example: `/image a cat in space`', 'ai'); | |
| return; | |
| } | |
| isGeneratingImage = true; | |
| // Extract parameters | |
| let prompt = promptPart; | |
| let width = 1024; | |
| let height = 1024; | |
| let seed = Math.floor(Math.random() * 1000000); | |
| const widthMatch = prompt.match(/--width\s+(\d+)/); | |
| if (widthMatch) { | |
| width = parseInt(widthMatch[1]); | |
| prompt = prompt.replace(widthMatch[0], '').trim(); | |
| } | |
| const heightMatch = prompt.match(/--height\s+(\d+)/); | |
| if (heightMatch) { | |
| height = parseInt(heightMatch[1]); | |
| prompt = prompt.replace(heightMatch[0], '').trim(); | |
| } | |
| const seedMatch = prompt.match(/--seed\s+(\d+)/); | |
| if (seedMatch) { | |
| seed = parseInt(seedMatch[1]); | |
| prompt = prompt.replace(seedMatch[0], '').trim(); | |
| } | |
| // UI feedback removed (handled by sendMessage) | |
| messageInput.value = ''; | |
| messageInput.style.height = 'auto'; | |
| if (sendBtn) sendBtn.classList.remove('active'); | |
| if (!currentConversationId) { | |
| currentConversationId = await createConversation(fullText); | |
| } else { | |
| addMessageToConversation(currentConversationId, fullText, 'user'); | |
| } | |
| // Show generating status | |
| const statusDiv = document.createElement('div'); | |
| statusDiv.className = 'message ai'; | |
| statusDiv.innerHTML = ` | |
| <div class="message-avatar">AI</div> | |
| <div class="message-content"> | |
| <div class="gen-status"> | |
| <span class="gen-loader"></span> | |
| Generating your masterpiece... | |
| </div> | |
| </div> | |
| `; | |
| chatHistory.appendChild(statusDiv); | |
| smoothScrollToBottom(); | |
| if (stopBtn) stopBtn.classList.add('active'); | |
| if (sendBtn) sendBtn.style.display = 'none'; | |
| try { | |
| const encodedPrompt = encodeURIComponent(prompt); | |
| const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?width=${width}&height=${height}&seed=${seed}&nologo=true`; | |
| // Create image card | |
| const imageCard = document.createElement('div'); | |
| imageCard.className = 'image-card'; | |
| imageCard.innerHTML = ` | |
| <img src="${imageUrl}" alt="${prompt}" onload="this.classList.add('loaded')" onerror="this.parentElement.innerHTML='<div style=\'padding:20px;color:#ef4444;font-size:13px;\'><i data-lucide=\'alert-circle\'></i> Image failed to load from provider.</div>'; lucide.createIcons();"> | |
| <div class="image-card-actions"> | |
| <a href="${imageUrl}" target="_blank" class="img-btn"> | |
| <i data-lucide="external-link"></i> Open | |
| </a> | |
| <a href="${imageUrl}" download="nexa-ai-${Date.now()}.jpg" class="img-btn" onclick="downloadImage(event, '${imageUrl}')"> | |
| <i data-lucide="download"></i> Download | |
| </a> | |
| </div> | |
| `; | |
| // Replace status with image card | |
| const messageContent = statusDiv.querySelector('.message-content'); | |
| messageContent.innerHTML = ''; | |
| messageContent.appendChild(document.createTextNode(`Here is your generated image for: "${prompt}"`)); | |
| messageContent.appendChild(imageCard); | |
| addMessageToConversation(currentConversationId, `Generated image: ${imageUrl}`, 'ai'); | |
| if (window.lucide) lucide.createIcons(); | |
| smoothScrollToBottom(); | |
| } catch (error) { | |
| console.error("Image generation failed:", error); | |
| statusDiv.querySelector('.message-content').innerHTML = `Sorry, image generation failed: ${error.message}. Please try again.`; | |
| } finally { | |
| isGeneratingImage = false; | |
| if (stopBtn) stopBtn.classList.remove('active'); | |
| if (sendBtn) sendBtn.style.display = 'flex'; | |
| } | |
| } | |
| async function handleSearchCommand(fullText) { | |
| if (isSearching || searchCooldown) return; | |
| const query = fullText.replace(/^\/(search|news|youtube)\s*/, '').trim(); | |
| if (!query) { | |
| addMessage('Please provide a search query. Example: `/search latest AI news`', 'ai'); | |
| return; | |
| } | |
| isSearching = true; | |
| searchCooldown = true; | |
| setTimeout(() => searchCooldown = false, 3000); | |
| // Command feedback - Clear input and reset button immediately | |
| messageInput.value = ''; | |
| messageInput.style.height = 'auto'; | |
| if (sendBtn) sendBtn.classList.remove('active'); | |
| if (!currentConversationId) { | |
| currentConversationId = await createConversation(fullText); | |
| } else { | |
| addMessageToConversation(currentConversationId, fullText, 'user'); | |
| } | |
| const thinkingDiv = showThinking(); | |
| const messageContent = thinkingDiv.querySelector('.message-content'); | |
| messageContent.innerHTML = ` | |
| <div class="tool-output-card"> | |
| <div class="tool-output-header"> | |
| <i data-lucide="search"></i> | |
| <span>Web Search</span> | |
| </div> | |
| <div class="gen-status"> | |
| <span class="gen-loader"></span> | |
| Searching the web for "${query}"... | |
| </div> | |
| </div> | |
| `; | |
| if (window.lucide) lucide.createIcons(); | |
| smoothScrollToBottom(); | |
| try { | |
| // Use absolute path relative to current origin for robustness | |
| const searchUrl = window.location.origin + '/api/search'; | |
| const response = await fetch(searchUrl, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ query }) | |
| }); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`Server returned ${response.status}: ${errorText.substring(0, 50)}...`); | |
| } | |
| const data = await response.json(); | |
| if (data.results && data.results.length > 0) { | |
| const count = data.results.length; | |
| addMessage(`🔍 I found **${count} search results** related to your question. Let me visit the most relevant sources to gather accurate information.`, 'ai'); | |
| messageContent.innerHTML = ` | |
| <div class="tool-output-header"> | |
| <i data-lucide="search"></i> | |
| <span>Intelligence Sources Discovered</span> | |
| </div> | |
| <div class="search-results"> | |
| ${data.results.map(res => ` | |
| <a href="${res.href || res.link}" target="_blank" class="search-card"> | |
| <h5><i data-lucide="external-link"></i> ${res.title}</h5> | |
| <p>${res.body || res.snippet}</p> | |
| </a> | |
| `).join('')} | |
| </div> | |
| <div class="compiling-loader" id="compilingLoader"> | |
| <i data-lucide="loader-2"></i> | |
| <span>Synthesizing verified research report...</span> | |
| </div> | |
| `; | |
| if (window.lucide) lucide.createIcons(); | |
| } else { | |
| messageContent.innerHTML = `<p style="font-size:13px; color:var(--text-secondary);">No results found.</p>`; | |
| } | |
| // Now stream the compiled report in the SAME message if possible, or right below | |
| abortController = new AbortController(); | |
| const chatRes = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| signal: abortController.signal, | |
| body: JSON.stringify({ | |
| message: fullText, | |
| username: currentUser ? currentUser.username : 'Guest', | |
| history: getConversationHistorySnapshot(currentConversationId), | |
| memory: userMemory, | |
| mode: currentAiMode, | |
| deep_research: isDeepResearch, | |
| conversation_id: currentConversationId | |
| }) | |
| }); | |
| if (!chatRes.ok) throw new Error('Chat API failed'); | |
| const reader = chatRes.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let fullResponse = ""; | |
| let reportStarted = false; | |
| let buffer = ''; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop(); | |
| for (const line of lines) { | |
| if (!line.trim()) continue; | |
| try { | |
| const chunkData = JSON.parse(line); | |
| if (chunkData.token) { | |
| if (!reportStarted) { | |
| reportStarted = true; | |
| const loader = messageContent.querySelector('#compilingLoader'); | |
| if (loader) loader.remove(); | |
| const badge = document.createElement('div'); | |
| badge.className = 'compiled-search-badge'; | |
| badge.innerHTML = '<i data-lucide="brain"></i> Synthesized Research Report'; | |
| messageContent.appendChild(badge); | |
| const textContainer = document.createElement('div'); | |
| textContainer.className = 'synthesized-report-text'; | |
| messageContent.appendChild(textContainer); | |
| cursorEl = document.createElement('span'); | |
| cursorEl.className = 'typing-cursor-ai'; | |
| textContainer.appendChild(cursorEl); | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| fullResponse += chunkData.token; | |
| const reportText = messageContent.querySelector('.synthesized-report-text'); | |
| const tempSpan = document.createElement('span'); | |
| tempSpan.className = 'streaming-text'; | |
| tempSpan.textContent = chunkData.token; | |
| reportText.insertBefore(tempSpan, cursorEl); | |
| smoothScrollToBottom(); | |
| } | |
| } catch (e) {} | |
| } | |
| } | |
| if (reportStarted) { | |
| if (cursorEl) cursorEl.remove(); | |
| const reportText = messageContent.querySelector('.synthesized-report-text'); | |
| reportText.innerHTML = marked.parse(fullResponse); | |
| addMessageActions(messageContent, fullResponse); | |
| addMessageToConversation(currentConversationId, fullResponse, 'ai'); | |
| if (window.lucide) lucide.createIcons({ attrs: { 'stroke-width': 2 } }); | |
| } | |
| } catch (err) { | |
| console.error("Search error:", err); | |
| hideThinking(thinkingDiv); | |
| addMessage(`Sorry, search failed: ${err.message}. Please check if the backend is running correctly.`, 'ai'); | |
| } finally { | |
| isSearching = false; | |
| abortController = null; | |
| if (stopBtn) stopBtn.classList.remove('active'); | |
| if (sendBtn) sendBtn.style.display = 'flex'; | |
| } | |
| } | |
| function updateSendButtonState() { | |
| if (!sendBtn) return; | |
| const content = messageInput.value.trim(); | |
| if (activeUploadsCount > 0) { | |
| sendBtn.disabled = true; | |
| sendBtn.classList.remove('active'); | |
| sendBtn.style.opacity = '0.5'; | |
| sendBtn.style.cursor = 'not-allowed'; | |
| } else { | |
| if (content !== '') { | |
| sendBtn.disabled = false; | |
| sendBtn.classList.add('active'); | |
| sendBtn.style.opacity = '1'; | |
| sendBtn.style.cursor = 'pointer'; | |
| } else { | |
| sendBtn.disabled = true; | |
| sendBtn.classList.remove('active'); | |
| sendBtn.style.opacity = '0.5'; | |
| sendBtn.style.cursor = 'default'; | |
| } | |
| } | |
| } | |
| async function handleFileAnalysis(file, card) { | |
| activeUploadsCount++; | |
| updateSendButtonState(); | |
| const infoEl = card.querySelector('.file-attachment-info'); | |
| const typeEl = infoEl.querySelector('.file-attachment-type'); | |
| // Add progress bar container to card | |
| const progressContainer = document.createElement('div'); | |
| progressContainer.className = 'upload-progress-container'; | |
| progressContainer.style.display = 'block'; | |
| progressContainer.innerHTML = '<div class="upload-progress-bar"></div>'; | |
| infoEl.appendChild(progressContainer); | |
| const progressBar = progressContainer.querySelector('.upload-progress-bar'); | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| // Use XMLHttpRequest for upload progress tracking | |
| const xhr = new XMLHttpRequest(); | |
| // Task 20: Connect to abortController | |
| if (abortController) { | |
| abortController.signal.addEventListener('abort', () => xhr.abort()); | |
| } | |
| xhr.upload.addEventListener('progress', (e) => { | |
| if (e.lengthComputable) { | |
| const percent = Math.round((e.loaded / e.total) * 100); | |
| progressBar.style.width = percent + '%'; | |
| typeEl.innerHTML = `<i data-lucide="upload-cloud" style="width:10px;"></i> ${percent}%`; | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| }); | |
| xhr.onload = async () => { | |
| activeUploadsCount--; | |
| updateSendButtonState(); | |
| if (xhr.status === 200) { | |
| try { | |
| const data = JSON.parse(xhr.responseText); | |
| if (data.error) throw new Error(data.error); | |
| card.setAttribute('data-analysis', data.content); | |
| card.setAttribute('data-analyzed', 'true'); | |
| typeEl.innerHTML = `<i data-lucide="check-circle" style="width:10px;color:#10a37f;"></i> Ready`; | |
| progressContainer.style.display = 'none'; | |
| } catch (err) { | |
| typeEl.innerHTML = `<i data-lucide="alert-circle" style="width:10px;color:#ef4444;"></i> Failed`; | |
| console.error("Analysis failed:", err); | |
| } | |
| } else { | |
| typeEl.innerHTML = `<i data-lucide="alert-circle" style="width:10px;color:#ef4444;"></i> Error ${xhr.status}`; | |
| } | |
| if (window.lucide) lucide.createIcons(); | |
| }; | |
| xhr.onerror = () => { | |
| activeUploadsCount--; | |
| updateSendButtonState(); | |
| typeEl.innerHTML = `<i data-lucide="alert-circle" style="width:10px;color:#ef4444;"></i> Connection Error`; | |
| if (window.lucide) lucide.createIcons(); | |
| }; | |
| xhr.open('POST', '/api/analyze-file'); | |
| xhr.send(formData); | |
| } | |
| async function downloadImage(e, url) { | |
| e.preventDefault(); | |
| try { | |
| const response = await fetch(url); | |
| const blob = await response.blob(); | |
| const blobUrl = window.URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.href = blobUrl; | |
| // Determine extension from URL or fallback to jpg | |
| let ext = 'jpg'; | |
| if (url.includes('.mp4')) ext = 'mp4'; | |
| else if (url.includes('.png')) ext = 'png'; | |
| else if (url.includes('.webp')) ext = 'webp'; | |
| link.download = `nexa-download-${Date.now()}.${ext}`; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| window.URL.revokeObjectURL(blobUrl); | |
| } catch (err) { | |
| window.open(url, '_blank'); | |
| } | |
| } | |
| async function sendMessage() { | |
| if (!messageInput) return; | |
| let content = messageInput.value.trim(); | |
| if (!content) return; | |
| // Handle very long text (> 4000 characters) by converting to file | |
| if (content.length > 4000) { | |
| const blob = new Blob([content], { type: 'text/plain' }); | |
| const file = new File([blob], `long_message_${Date.now()}.txt`, { type: 'text/plain' }); | |
| const attachContainer = document.getElementById('attachmentsContainer'); | |
| attachContainer.style.display = 'flex'; | |
| const card = document.createElement('div'); | |
| card.className = 'file-attachment-card'; | |
| card.setAttribute('data-filename', file.name); | |
| card.setAttribute('data-preview', content); | |
| card.innerHTML = ` | |
| <div class="file-attachment-icon" style="background: #38a169"> | |
| <i data-lucide="file-text" style="width: 18px; height: 18px;"></i> | |
| </div> | |
| <div class="file-attachment-info"> | |
| <span class="file-attachment-name">${file.name}</span> | |
| <span class="file-attachment-type">TXT (Auto-generated)</span> | |
| </div> | |
| <div class="file-attachment-remove" onclick="this.parentElement.remove(); if(document.getElementById('attachmentsContainer').children.length === 0) document.getElementById('attachmentsContainer').style.display='none';"> | |
| <i data-lucide="x"></i> | |
| </div> | |
| `; | |
| attachContainer.appendChild(card); | |
| if (window.lucide) lucide.createIcons(); | |
| content = "I've uploaded a long message as a text file for analysis. Please process the attached file."; | |
| messageInput.value = ''; | |
| } | |
| // Handle active feature | |
| if (activeFeature) { | |
| const commands = ['/image', '/search', '/video', '/news', '/youtube', '/upload', '/remember', '/forget', '/my memory']; | |
| const hasCommand = commands.some(cmd => content.startsWith(cmd)); | |
| if (!hasCommand) { | |
| if (activeFeature === 'image') content = `/image ${content}`; | |
| else if (activeFeature === 'search') content = `/search ${content}`; | |
| else if (activeFeature === 'video') content = `/video ${content}`; | |
| } | |
| // Deactivate feature after sending | |
| deactivateFeature(); | |
| } | |
| // Task 28: Send debouncing | |
| if (isTyping || isGeneratingImage || isSearching) return; | |
| // UI Feedback | |
| updateMemoryFromText(content); | |
| const { text: attachmentText, html: attachmentHtml } = buildAttachmentContext(); | |
| closeSidebarOnMobile(); | |
| const userMsgDiv = addMessage(content, 'user'); | |
| if (attachmentHtml) { | |
| const contentEl = userMsgDiv.querySelector('.message-content'); | |
| const cardsContainer = document.createElement('div'); | |
| cardsContainer.innerHTML = attachmentHtml; | |
| contentEl.prepend(cardsContainer); | |
| } | |
| messageInput.value = ''; | |
| messageInput.style.height = 'auto'; | |
| if (sendBtn) sendBtn.classList.remove('active'); | |
| // Detect commands | |
| if (content.startsWith('/image')) { handleImageCommand(content); return; } | |
| if (content.startsWith('/video')) { handleVideoCommand(content); return; } | |
| if (content.startsWith('/search') || content.startsWith('/news') || content.startsWith('/youtube')) { handleSearchCommand(content); return; } | |
| if (content.startsWith('/upload')) { triggerUpload('Document'); messageInput.value = ''; return; } | |
| // Task 25: Fix /remember parsing with regex | |
| const rememberMatch = content.match(/^\/remember\s+(.+?)\s+is\s+(.+)$/i); | |
| if (content.startsWith('/remember')) { | |
| if (rememberMatch) { | |
| const key = rememberMatch[1].trim(); | |
| const value = rememberMatch[2].trim(); | |
| saveMemory(key, value); | |
| addMessage(`I'll remember that ${key} is ${value}. 🧠`, 'ai'); | |
| } else { | |
| addMessage("Please use format: `/remember [key] is [value]`", 'ai'); | |
| } | |
| messageInput.value = ''; | |
| return; | |
| } | |
| if (content.startsWith('/forget')) { clearMemory(); addMessage("I've forgotten everything I knew about you. 🗑️", 'ai'); messageInput.value = ''; return; } | |
| if (content.startsWith('/my memory')) { | |
| const mem = Object.entries(userMemory); | |
| if (mem.length === 0) addMessage("My memory is currently empty.", 'ai'); | |
| else { | |
| const memStr = mem.map(([k, v]) => `• **${k}**: ${v}`).join('\n'); | |
| addMessage(`Here's what I remember about you:\n${memStr}`, 'ai'); | |
| } | |
| messageInput.value = ''; | |
| return; | |
| } | |
| const plannerKeywords = ['plan', 'schedule', 'timeline', 'routine', 'study day', 'itinerary']; | |
| const isPlannerRequest = plannerKeywords.some(k => content.toLowerCase().includes(k)) && !content.startsWith('/'); | |
| // Clear attachments IMMEDIATELY after deciding to send | |
| const attachContainer = document.getElementById('attachmentsContainer'); | |
| if (attachContainer) { | |
| attachContainer.innerHTML = ''; | |
| attachContainer.style.display = 'none'; | |
| } | |
| if (isPlannerRequest) { | |
| if (!currentConversationId) currentConversationId = await createConversation(content); | |
| else addMessageToConversation(currentConversationId, content, 'user'); | |
| handlePlannerMode(content); | |
| return; | |
| } | |
| if (sendBtn) sendBtn.disabled = true; // Disable during processing | |
| if (!currentConversationId && currentUser) { | |
| currentConversationId = await createConversation(content); | |
| } else if (currentConversationId) { | |
| addMessageToConversation(currentConversationId, content, 'user'); | |
| } | |
| // Show thinking and switch buttons | |
| const thinkingDiv = showThinking(); | |
| isTyping = true; | |
| if (stopBtn) stopBtn.classList.add('active'); | |
| if (sendBtn) sendBtn.style.display = 'none'; | |
| try { | |
| abortController = new AbortController(); | |
| const history = getConversationHistorySnapshot(currentConversationId); | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| signal: abortController.signal, | |
| body: JSON.stringify({ | |
| message: content, | |
| username: currentUser ? currentUser.username : 'Guest', | |
| history: history, | |
| memory: userMemory, | |
| attachments: attachmentText, | |
| mode: currentAiMode, | |
| deep_research: isDeepResearch, | |
| conversation_id: currentConversationId | |
| }) | |
| }); | |
| hideThinking(thinkingDiv); | |
| if (!response.ok) throw new Error('Network response was not ok'); | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let fullResponse = ""; | |
| let aiMsgDiv = null; | |
| let aiContentEl = null; | |
| let cursorEl = null; | |
| let buffer = ''; | |
| let toolsUsed = []; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop(); // Keep incomplete line in buffer | |
| for (const line of lines) { | |
| if (!line.trim()) continue; | |
| try { | |
| const data = JSON.parse(line); | |
| if (data.error) throw new Error(data.error); | |
| // Task: Handle search results from backend synthesis | |
| if (data.search_results) { | |
| // If we're already in an AI message, put results at the top | |
| if (!aiMsgDiv) { | |
| aiMsgDiv = addMessage("", 'ai'); | |
| aiContentEl = aiMsgDiv.querySelector('.message-content'); | |
| } | |
| const sourcesHtml = ` | |
| <div class="tool-output-header"> | |
| <i data-lucide="search"></i> | |
| <span>Sources Discovered</span> | |
| </div> | |
| <div class="search-results"> | |
| ${data.search_results.results.map(res => ` | |
| <a href="${res.href || res.link}" target="_blank" class="search-card"> | |
| <h5><i data-lucide="external-link"></i> ${res.title}</h5> | |
| <p>${res.body || res.snippet}</p> | |
| </a> | |
| `).join('')} | |
| </div> | |
| <div class="compiling-loader" id="compilingLoader"> | |
| <i data-lucide="loader-2"></i> | |
| <span>Compiling synthesized research report...</span> | |
| </div> | |
| `; | |
| aiContentEl.innerHTML = sourcesHtml; | |
| if (window.lucide) lucide.createIcons(); | |
| smoothScrollToBottom(); | |
| } | |
| if (data.tools_used) { | |
| toolsUsed = data.tools_used; | |
| // If image tool is detected and not yet handled | |
| if (toolsUsed.includes('image') && !aiMsgDiv) { | |
| // We'll wait for the message to finish or start it now | |
| } | |
| } | |
| if (data.token) { | |
| if (!aiMsgDiv) { | |
| aiMsgDiv = addMessage("", 'ai'); | |
| aiContentEl = aiMsgDiv.querySelector('.message-content'); | |
| aiContentEl.innerHTML = ''; | |
| } | |
| // Remove compiling loader once tokens start arriving | |
| const loader = document.getElementById('compilingLoader'); | |
| if (loader) { | |
| loader.remove(); | |
| // Add the badge at the top of the text | |
| const badge = document.createElement('div'); | |
| badge.className = 'compiled-search-badge'; | |
| badge.innerHTML = '<i data-lucide="brain"></i> Synthesized Research Report'; | |
| aiContentEl.appendChild(badge); | |
| if (window.lucide) lucide.createIcons(); | |
| cursorEl = document.createElement('span'); | |
| cursorEl.className = 'typing-cursor-ai'; | |
| aiContentEl.appendChild(cursorEl); | |
| } | |
| if (!cursorEl) { | |
| cursorEl = document.createElement('span'); | |
| cursorEl.className = 'typing-cursor-ai'; | |
| aiContentEl.appendChild(cursorEl); | |
| } | |
| fullResponse += data.token; | |
| const tempSpan = document.createElement('span'); | |
| tempSpan.className = 'streaming-text'; | |
| tempSpan.textContent = data.token; | |
| aiContentEl.insertBefore(tempSpan, cursorEl); | |
| smoothScrollToBottom(); | |
| } | |
| } catch (e) { | |
| console.error("Error parsing stream chunk:", e, line); | |
| } | |
| } | |
| } | |
| // Finalize message with full markdown | |
| if (aiMsgDiv) { | |
| if (cursorEl) cursorEl.remove(); | |
| const badge = (toolsUsed.includes('search')) ? '<div class="compiled-search-badge"><i data-lucide="brain"></i> Synthesized Research Report</div>' : ''; | |
| const processedContent = fullResponse.replace(/\*\*(.*?)\*\*/g, '<span class="important-highlight">$1</span>'); | |
| aiContentEl.innerHTML = badge + marked.parse(processedContent); | |
| // Handle image generation if tool was used | |
| if (toolsUsed.includes('image')) { | |
| const imageContainer = document.createElement('div'); | |
| imageContainer.className = 'image-card'; | |
| imageContainer.innerHTML = `<div class="img-skeleton visible" style="height: 300px; background: rgba(255,255,255,0.05); border-radius: 12px; animation: pulse 1.5s infinite;"></div>`; | |
| aiContentEl.appendChild(imageContainer); | |
| smoothScrollToBottom(); | |
| try { | |
| const imgRes = await fetch('/api/image', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ prompt: content }) | |
| }); | |
| const imgData = await imgRes.json(); | |
| if (imgData.status === 'success') { | |
| imageContainer.innerHTML = ` | |
| <div class="image-card-header"><i data-lucide="image" style="width:14px;color:#10a37f;"></i><span>Generated Image</span></div> | |
| <img src="${imgData.image_url}" class="loaded" onload="this.parentElement.querySelector('.img-skeleton')?.remove();"> | |
| <div class="image-card-actions"> | |
| <a href="${imgData.image_url}" target="_blank" class="img-btn"><i data-lucide="external-link"></i> Open</a> | |
| <a href="${imgData.image_url}" download class="img-btn" onclick="downloadImage(event, '${imgData.image_url}')"><i data-lucide="download"></i> Download</a> | |
| </div> | |
| `; | |
| } else { | |
| imageContainer.innerHTML = ` | |
| <div class="image-card-header"><i data-lucide="alert-circle" style="width:14px;color:#ef4444;"></i><span>Generation Error</span></div> | |
| <div style="padding: 20px; text-align: center; color: var(--text-secondary); font-size: 13px;"> | |
| ${imgData.error || 'The image service is currently busy.'} | |
| </div> | |
| `; | |
| } | |
| if (window.lucide) lucide.createIcons(); | |
| } catch (imgErr) { | |
| console.error("Image generation failed:", imgErr); | |
| imageContainer.innerHTML = `<p style="font-size:12px; color:#ef4444; padding:10px;">Failed to generate image.</p>`; | |
| } | |
| } | |
| addMessageActions(aiContentEl, fullResponse); | |
| addMessageToConversation(currentConversationId, fullResponse, 'ai'); | |
| if (window.lucide) lucide.createIcons({ attrs: { 'stroke-width': 2 } }); | |
| } | |
| messageCount++; | |
| if (currentUser) { | |
| currentUser.messages_used = messageCount; | |
| localStorage.setItem('chatgpt_current_user', JSON.stringify(currentUser)); | |
| } | |
| // Task 17: Clear attachments AFTER successful send | |
| const attachContainer = document.getElementById('attachmentsContainer'); | |
| if (attachContainer) { | |
| attachContainer.innerHTML = ''; | |
| attachContainer.style.display = 'none'; | |
| } | |
| if (isAutoVoiceEnabled && fullResponse) speakText(fullResponse); | |
| } catch (error) { | |
| if (error.name === 'AbortError') { | |
| console.log('Fetch aborted'); | |
| } else { | |
| hideThinking(thinkingDiv); | |
| addMessage('I encountered a problem. Please check your connection and try again.', 'ai'); | |
| } | |
| } finally { | |
| isTyping = false; | |
| abortController = null; | |
| if (stopBtn) stopBtn.classList.remove('active'); | |
| if (sendBtn) { | |
| sendBtn.style.display = 'flex'; | |
| sendBtn.disabled = false; | |
| } | |
| messageInput.focus(); | |
| } | |
| } | |
| function buildAttachmentContext() { | |
| const attachContainer = document.getElementById('attachmentsContainer'); | |
| if (!attachContainer) return { text: '', html: '' }; | |
| const attachedCards = attachContainer.querySelectorAll('.file-attachment-card'); | |
| if (!attachedCards.length) return { text: '', html: '' }; | |
| const lines = ["IMPORTANT: The user has attached the following files for your analysis. Please prioritize this content for your response:"]; | |
| let cardsHtml = '<div class="message-attachments-preview" style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px;">'; | |
| attachedCards.forEach(card => { | |
| const fname = card.getAttribute('data-filename') || 'unknown-file'; | |
| const preview = card.getAttribute('data-preview') || ''; | |
| const analysis = card.getAttribute('data-analysis') || ''; | |
| const type = card.querySelector('.file-attachment-type')?.textContent || 'FILE'; | |
| const iconHtml = card.querySelector('.file-attachment-icon')?.outerHTML || ''; | |
| if (analysis) { | |
| lines.push(`[File: ${fname}] Content Analysis:\n${analysis}`); | |
| } else if (preview) { | |
| lines.push(`[File: ${fname}] Preview:\n${preview}`); | |
| } else { | |
| lines.push(`[Attached File: ${fname}]`); | |
| } | |
| cardsHtml += ` | |
| <div class="user-file-card" style="background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 10px; padding: 8px 12px; display: flex; align-items: center; gap: 10px; font-size: 12px; min-width: 150px;"> | |
| ${iconHtml} | |
| <div style="display: flex; flex-direction: column; overflow: hidden;"> | |
| <span style="font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${fname}</span> | |
| <span style="font-size: 10px; color: var(--text-secondary);">${type}</span> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| cardsHtml += '</div>'; | |
| return { | |
| text: lines.join('\n\n'), | |
| html: cardsHtml | |
| }; | |
| } | |
| function getConversationHistorySnapshot(convId) { | |
| if (!convId) return []; | |
| const conv = conversations.find(c => c.id === convId); | |
| if (!conv || !Array.isArray(conv.messages)) return []; | |
| return conv.messages.slice(-8).map(msg => ({ | |
| role: msg.type === 'ai' ? 'assistant' : 'user', | |
| content: msg.content | |
| })); | |
| } | |
| // Conversation management | |
| async function createConversation(firstMessage) { | |
| if (!currentUser) return null; | |
| try { | |
| const title = firstMessage.substring(0, 30) + (firstMessage.length > 30 ? '...' : ''); | |
| const resp = await fetch('/api/conversations', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ title, model: currentAiMode }) | |
| }); | |
| if (resp.ok) { | |
| const conv = await resp.json(); | |
| conversations.unshift(conv); | |
| currentConversationId = conv.id; | |
| renderConversations(); | |
| return conv.id; | |
| } | |
| } catch (e) { | |
| console.error("Failed to create conversation", e); | |
| } | |
| return null; | |
| } | |
| async function addMessageToConversation(convId, content, role) { | |
| if (!currentUser || !convId) return; | |
| // Local update | |
| const conv = conversations.find(c => c.id === convId); | |
| if (conv) { | |
| if (!conv.messages) conv.messages = []; | |
| conv.messages.push({ content, role: role === 'ai' ? 'assistant' : 'user' }); | |
| } | |
| // Persistence | |
| try { | |
| await fetch('/api/messages', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| conversation_id: convId, | |
| content: content, | |
| role: role === 'ai' ? 'assistant' : 'user' | |
| }) | |
| }); | |
| } catch (e) { | |
| console.error("Failed to persist message", e); | |
| } | |
| } | |
| async function loadConversation(convId) { | |
| if (!currentUser) return; | |
| try { | |
| const resp = await fetch(`/api/conversations/${convId}`); | |
| if (resp.ok) { | |
| const data = await resp.json(); | |
| currentConversationId = convId; | |
| chatHistory.innerHTML = ''; | |
| chatHistory.classList.remove('empty-mode'); | |
| document.querySelector('.main-area').classList.remove('hero-mode'); | |
| data.messages.forEach(msg => { | |
| addMessage(msg.content, msg.role === 'assistant' ? 'ai' : 'user', false); | |
| }); | |
| renderConversations(); | |
| smoothScrollToBottom(); | |
| } | |
| } catch (e) { | |
| console.error("Failed to load conversation", e); | |
| } | |
| } | |
| async function deleteConversation(event, convId) { | |
| event.stopPropagation(); | |
| if (!confirm('Are you sure you want to delete this chat?')) return; | |
| try { | |
| const resp = await fetch(`/api/conversations/${convId}`, { method: 'DELETE' }); | |
| if (resp.ok) { | |
| conversations = conversations.filter(c => c.id !== convId); | |
| if (currentConversationId === convId) { | |
| startNewChat(); | |
| } | |
| renderConversations(); | |
| } | |
| } catch (e) { | |
| console.error("Failed to delete conversation", e); | |
| } | |
| } | |
| // FIXED: New Chat with mobile sidebar closure and state reset | |
| function startNewChat() { | |
| // Close sidebar on mobile first | |
| if (window.innerWidth <= 768) { | |
| const sidebar = document.getElementById('sidebar'); | |
| const overlay = document.getElementById('sidebarOverlay'); | |
| if (sidebar) sidebar.classList.remove('open'); | |
| if (overlay) overlay.classList.remove('active'); | |
| document.body.classList.remove('sidebar-open'); | |
| } | |
| currentConversationId = null; | |
| const messageInput = document.getElementById('messageInput'); | |
| if (messageInput) { | |
| messageInput.placeholder = "Message Nexa AI..."; | |
| messageInput.value = ''; | |
| messageInput.style.height = 'auto'; | |
| } | |
| chatHistory.innerHTML = ` | |
| <div class="empty-state" id="emptyState"> | |
| <div class="blinking-emoji" style="width: 80px; height: 80px; gap: 10px; margin-bottom: 24px;"> | |
| <div class="eye" style="width: 10px; height: 16px; border-radius: 5px;"></div> | |
| <div class="eye" style="width: 10px; height: 16px; border-radius: 5px;"></div> | |
| </div> | |
| <h2>How can I help you today?</h2> | |
| <div class="suggestions-grid"> | |
| <div class="suggestion-card" onclick="document.getElementById('messageInput').value='Help me plan a trip'; document.getElementById('messageInput').focus();"> | |
| <h4>Plan a trip</h4> | |
| <p>To explore new destinations</p> | |
| </div> | |
| <div class="suggestion-card" onclick="document.getElementById('messageInput').value='Explain quantum computing'; document.getElementById('messageInput').focus();"> | |
| <h4>Explain concepts</h4> | |
| <p>Like quantum computing</p> | |
| </div> | |
| <div class="suggestion-card" onclick="document.getElementById('messageInput').value='Write a thank you email'; document.getElementById('messageInput').focus();"> | |
| <h4>Write a message</h4> | |
| <p>A thank you email to my boss</p> | |
| </div> | |
| <div class="suggestion-card" onclick="document.getElementById('messageInput').value='Help me find a bug in my code'; document.getElementById('messageInput').focus();"> | |
| <h4>Debug code</h4> | |
| <p>Find errors in my script</p> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| const mainArea = document.querySelector('.main-area'); | |
| if (mainArea) mainArea.classList.add('hero-mode'); | |
| chatHistory.classList.add('empty-mode'); | |
| document.body.classList.remove('glowing'); | |
| if (messageInput) messageInput.focus(); | |
| renderConversations(); | |
| } | |
| function refreshIcons() { | |
| if (typeof lucide !== 'undefined') { | |
| lucide.createIcons({ | |
| attrs: { | |
| 'stroke-width': 2.5, | |
| 'class': 'lucide-icon' | |
| } | |
| }); | |
| } | |
| } | |
| async function init() { | |
| // Assign Selectors | |
| sidebar = document.getElementById('sidebar'); | |
| chatHistory = document.getElementById('chatHistory'); | |
| messageInput = document.getElementById('messageInput'); | |
| sendBtn = document.getElementById('sendBtn'); | |
| stopBtn = document.getElementById('stopBtn'); | |
| accountDropdown = document.getElementById('accountDropdown'); | |
| conversationList = document.getElementById('conversationList'); | |
| authOverlay = document.getElementById('authOverlay'); | |
| // Character Counter | |
| const charCounter = document.getElementById('charCounter'); | |
| if (messageInput) { | |
| messageInput.addEventListener('input', () => { | |
| const len = messageInput.value.length; | |
| if (charCounter) charCounter.textContent = `${len}/4000`; | |
| }); | |
| } | |
| // Keyboard Shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| // Ctrl+K: New Chat | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'k') { | |
| e.preventDefault(); | |
| startNewChat(); | |
| } | |
| // Ctrl+/: Search History (Focus sidebar search) | |
| if ((e.ctrlKey || e.metaKey) && e.key === '/') { | |
| e.preventDefault(); | |
| const searchInput = document.querySelector('.sidebar-search input'); | |
| if (searchInput) searchInput.focus(); | |
| } | |
| // Esc: Stop Generating | |
| if (e.key === 'Escape' && isTyping) { | |
| stopAI(); | |
| } | |
| }); | |
| window.addEventListener('popstate', handleRouting); | |
| // Global click handler for internal links | |
| document.addEventListener('click', e => { | |
| const link = e.target.closest('a[href^="/"]'); | |
| if (link && !link.target && !e.ctrlKey && !e.metaKey) { | |
| e.preventDefault(); | |
| const url = link.getAttribute('href'); | |
| window.history.pushState({}, '', url); | |
| handleRouting(); | |
| } | |
| }); | |
| // Check if user is logged in (Network call LAST) | |
| try { | |
| const resp = await fetch('/api/auth/me'); | |
| if (resp.ok) { | |
| currentUser = await resp.json(); | |
| localStorage.setItem('chatgpt_current_user', JSON.stringify(currentUser)); | |
| applyUserUI(); | |
| loadConversationsFromStorage(); | |
| } else { | |
| currentUser = null; | |
| localStorage.removeItem('chatgpt_current_user'); | |
| } | |
| } catch (e) { | |
| console.error("Auth check failed", e); | |
| } | |
| handleRouting(); | |
| loadConversationsFromStorage(); | |
| applyUserUI(); | |
| initAiModes(); | |
| messageCount = currentUser?.messages_used || 0; | |
| updateMessageCounter(); | |
| autoResizeTextarea(); | |
| // Restore desktop sidebar state | |
| const savedState = localStorage.getItem('nexa_sidebar_collapsed'); | |
| if (sidebar && savedState === 'true' && window.innerWidth > 768) { | |
| sidebar.classList.add('collapsed'); | |
| } | |
| // Restore conversation on refresh | |
| const savedConvId = localStorage.getItem('nexa_current_conversation_id'); | |
| if (savedConvId && conversations.some(c => c.id === parseInt(savedConvId))) { | |
| loadConversation(parseInt(savedConvId)); | |
| } else { | |
| startNewChat(); | |
| } | |
| refreshIcons(); | |
| setupEventListeners(); | |
| setupDragAndDrop(); | |
| } | |
| // --- SPA Routing Logic --- | |
| const routes = { | |
| '/': renderLanding, | |
| '/chat': renderChat, | |
| '/login': renderLogin, | |
| '/signup': renderSignup, | |
| '/pricing': renderPricing, | |
| '/profile': renderProfile | |
| }; | |
| function handleRouting() { | |
| const path = window.location.pathname; | |
| const mainArea = document.querySelector('.main-area'); | |
| // Add transition effect | |
| if (mainArea) { | |
| mainArea.classList.add('transitioning'); | |
| mainArea.style.opacity = '0'; | |
| mainArea.style.transform = 'scale(0.98) translateZ(-50px)'; | |
| } | |
| setTimeout(() => { | |
| // Protected routes | |
| if (path === '/chat' && !currentUser) { | |
| window.history.pushState({}, '', '/login'); | |
| renderLogin(); | |
| finishTransition(); | |
| return; | |
| } | |
| if ((path === '/login' || path === '/signup') && currentUser) { | |
| window.history.pushState({}, '', '/chat'); | |
| renderChat(); | |
| finishTransition(); | |
| return; | |
| } | |
| const renderFunc = routes[path] || renderLanding; | |
| renderFunc(); | |
| if (window.innerWidth <= 768) { | |
| sidebar?.classList.remove('open'); | |
| } | |
| finishTransition(); | |
| }, 300); | |
| } | |
| function finishTransition() { | |
| const mainArea = document.querySelector('.main-area'); | |
| if (mainArea) { | |
| setTimeout(() => { | |
| mainArea.classList.remove('transitioning'); | |
| mainArea.style.opacity = '1'; | |
| mainArea.style.transform = 'scale(1) translateZ(0)'; | |
| }, 50); | |
| } | |
| } | |
| function renderLanding() { | |
| const landing = document.getElementById('landingPage'); | |
| const appContainer = document.querySelector('.app-container'); | |
| const authOverlay = document.getElementById('authOverlay'); | |
| const pricingPage = document.getElementById('pricingPage'); | |
| if (landing) { | |
| landing.style.display = 'block'; | |
| landing.classList.remove('hidden'); | |
| landing.style.pointerEvents = 'auto'; | |
| } | |
| if (appContainer) appContainer.style.display = 'none'; | |
| if (authOverlay) authOverlay.style.display = 'none'; | |
| if (pricingPage) pricingPage.style.display = 'none'; | |
| } | |
| function renderChat() { | |
| const landing = document.getElementById('landingPage'); | |
| const appContainer = document.querySelector('.app-container'); | |
| const authOverlay = document.getElementById('authOverlay'); | |
| const pricingPage = document.getElementById('pricingPage'); | |
| if (landing) landing.style.display = 'none'; | |
| if (appContainer) appContainer.style.display = 'flex'; | |
| if (authOverlay) authOverlay.style.display = 'none'; | |
| if (pricingPage) pricingPage.style.display = 'none'; | |
| // Initialize chat if not already | |
| if (chatHistory && chatHistory.children.length === 0) { | |
| startNewChat(); | |
| } | |
| } | |
| function renderLogin() { | |
| renderAuth('login'); | |
| } | |
| function renderSignup() { | |
| renderAuth('signup'); | |
| } | |
| function renderAuth(mode) { | |
| const landing = document.getElementById('landingPage'); | |
| const appContainer = document.querySelector('.app-container'); | |
| const authOverlay = document.getElementById('authOverlay'); | |
| const pricingPage = document.getElementById('pricingPage'); | |
| if (landing) landing.style.display = 'none'; | |
| if (appContainer) appContainer.style.display = 'none'; | |
| if (authOverlay) { | |
| authOverlay.style.display = 'flex'; | |
| authOverlay.style.zIndex = '9999'; | |
| } | |
| if (pricingPage) pricingPage.style.display = 'none'; | |
| const title = document.getElementById('authTitle'); | |
| const submitBtn = document.getElementById('authSubmitBtn'); | |
| const switchText = document.getElementById('authSwitchText'); | |
| if (mode === 'signup') { | |
| if (title) title.textContent = 'Create your account'; | |
| if (submitBtn) submitBtn.textContent = 'Sign up'; | |
| if (switchText) switchText.innerHTML = 'Already have an account? <a href="/login" style="color:var(--accent-blue);text-decoration:none;font-weight:600;">Log in</a>'; | |
| } else { | |
| if (title) title.textContent = 'Welcome back'; | |
| if (submitBtn) submitBtn.textContent = 'Log in'; | |
| if (switchText) switchText.innerHTML = 'Don\'t have an account? <a href="/signup" style="color:var(--accent-blue);text-decoration:none;font-weight:600;">Sign up</a>'; | |
| } | |
| } | |
| function renderPricing() { | |
| const landing = document.getElementById('landingPage'); | |
| const appContainer = document.querySelector('.app-container'); | |
| const authOverlay = document.getElementById('authOverlay'); | |
| if (landing) { | |
| landing.style.display = 'block'; | |
| landing.classList.remove('hidden'); | |
| landing.style.pointerEvents = 'auto'; | |
| } | |
| if (appContainer) appContainer.style.display = 'none'; | |
| if (authOverlay) authOverlay.style.display = 'none'; | |
| setTimeout(() => { | |
| const pricingSection = document.getElementById('pricingSection'); | |
| if (pricingSection) { | |
| pricingSection.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| }, 100); | |
| } | |
| function renderProfile() { | |
| const landing = document.getElementById('landingPage'); | |
| const appContainer = document.querySelector('.app-container'); | |
| const authOverlay = document.getElementById('authOverlay'); | |
| if (landing) landing.style.display = 'none'; | |
| if (appContainer) appContainer.style.display = 'none'; | |
| if (authOverlay) authOverlay.style.display = 'none'; | |
| let profileEl = document.getElementById('profilePage'); | |
| if (!profileEl) { | |
| profileEl = document.createElement('div'); | |
| profileEl.id = 'profilePage'; | |
| profileEl.className = 'profile-container'; | |
| profileEl.innerHTML = ` | |
| <div class="profile-card"> | |
| <h2>User Profile</h2> | |
| <div class="profile-avatar-large">${getUserInitial()}</div> | |
| <div class="profile-info"> | |
| <div class="info-item"> | |
| <label>Username</label> | |
| <span>${currentUser?.username || 'Guest'}</span> | |
| </div> | |
| <div class="info-item"> | |
| <label>Email</label> | |
| <span>${currentUser?.email || 'N/A'}</span> | |
| </div> | |
| </div> | |
| <button class="btn-secondary" onclick="window.history.pushState({}, '', '/chat'); handleRouting();">Back to Chat</button> | |
| </div> | |
| `; | |
| document.body.appendChild(profileEl); | |
| } | |
| profileEl.style.display = 'flex'; | |
| } | |
| function renderConversations() { | |
| if (!conversationList) return; | |
| conversationList.innerHTML = ''; | |
| if (!conversations || conversations.length === 0) return; | |
| // Database results are already sorted by backend | |
| const sorted = conversations; | |
| const now = new Date(); | |
| const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); | |
| const yesterday = today - 86400000; | |
| const lastWeek = today - (86400000 * 7); | |
| let lastGroup = ""; | |
| sorted.forEach(conv => { | |
| const convDate = new Date(conv.updated_at || conv.created_at || Date.now()); | |
| const convDay = new Date(convDate.getFullYear(), convDate.getMonth(), convDate.getDate()).getTime(); | |
| let group = "Older"; | |
| if (convDay === today) group = "Today"; | |
| else if (convDay === yesterday) group = "Yesterday"; | |
| else if (convDay > lastWeek) group = "Previous 7 Days"; | |
| if (group !== lastGroup) { | |
| const groupLabel = document.createElement('div'); | |
| groupLabel.className = 'history-date-group'; | |
| groupLabel.textContent = group; | |
| conversationList.appendChild(groupLabel); | |
| lastGroup = group; | |
| } | |
| const item = document.createElement('div'); | |
| item.className = `conversation-item ${conv.id === currentConversationId ? 'active' : ''}`; | |
| item.setAttribute('data-id', conv.id); | |
| if (conv.pinned) item.classList.add('pinned'); | |
| const icon = document.createElement('i'); | |
| icon.setAttribute('data-lucide', conv.pinned ? 'pin' : 'message-square'); | |
| icon.style.width = "14px"; | |
| icon.style.flexShrink = "0"; | |
| const titleSpan = document.createElement('span'); | |
| titleSpan.textContent = conv.title; | |
| titleSpan.style.flex = "1"; | |
| titleSpan.style.overflow = "hidden"; | |
| titleSpan.style.textOverflow = "ellipsis"; | |
| titleSpan.style.whiteSpace = "nowrap"; | |
| const actionsDiv = document.createElement('div'); | |
| actionsDiv.className = 'item-actions'; | |
| actionsDiv.style.display = 'flex'; | |
| actionsDiv.style.gap = '4px'; | |
| const pinBtn = document.createElement('button'); | |
| pinBtn.className = 'action-btn'; | |
| pinBtn.innerHTML = `<i data-lucide="${conv.pinned ? 'pin-off' : 'pin'}" style="width:12px;"></i>`; | |
| pinBtn.onclick = async (e) => { | |
| e.stopPropagation(); | |
| try { | |
| const resp = await fetch(`/api/conversations/${conv.id}`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ pinned: conv.pinned ? 0 : 1 }) | |
| }); | |
| if (resp.ok) loadConversationsFromStorage(); | |
| } catch (e) {} | |
| }; | |
| const deleteBtn = document.createElement('button'); | |
| deleteBtn.className = 'delete-btn'; | |
| deleteBtn.innerHTML = `<i data-lucide="trash-2" style="width:14px;"></i>`; | |
| deleteBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| deleteConversation(e, conv.id); | |
| }; | |
| item.appendChild(icon); | |
| item.appendChild(titleSpan); | |
| actionsDiv.appendChild(pinBtn); | |
| actionsDiv.appendChild(deleteBtn); | |
| item.appendChild(actionsDiv); | |
| item.onclick = () => { | |
| closeSidebarOnMobile(); | |
| loadConversation(conv.id); | |
| }; | |
| conversationList.appendChild(item); | |
| }); | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| // Theme toggle | |
| function toggleTheme() { | |
| isLightMode = !isLightMode; | |
| document.body.className = isLightMode ? 'light-mode' : ''; | |
| localStorage.setItem('chatgpt_theme', isLightMode ? 'light' : 'dark'); | |
| } | |
| // Account dropdown | |
| let isDropdownOpen = false; // FIXED: Track dropdown state globally | |
| function toggleAccountDropdown() { | |
| if (accountDropdown) { | |
| isDropdownOpen = !isDropdownOpen; | |
| accountDropdown.classList.toggle('show', isDropdownOpen); | |
| } | |
| } | |
| // Hidden file input | |
| const fileUploadInput = document.createElement('input'); | |
| fileUploadInput.type = 'file'; | |
| fileUploadInput.style.display = 'none'; | |
| document.body.appendChild(fileUploadInput); | |
| fileUploadInput.addEventListener('change', (e) => { | |
| if (e.target.files && e.target.files.length > 0) { | |
| const file = e.target.files[0]; | |
| const fileName = file.name; | |
| const fileExt = fileName.split('.').pop().toUpperCase(); | |
| let bgColors = '#e53e3e'; | |
| if (['JPG','JPEG','PNG','GIF','WEBP'].includes(fileExt)) bgColors = '#3182ce'; | |
| if (['DOC','DOCX','TXT','RTF'].includes(fileExt)) bgColors = '#38a169'; | |
| const attachContainer = document.getElementById('attachmentsContainer'); | |
| attachContainer.style.display = 'flex'; | |
| const card = document.createElement('div'); | |
| card.className = 'file-attachment-card'; | |
| card.setAttribute('data-filename', fileName); | |
| card.innerHTML = ` | |
| <div class="file-attachment-icon" style="background: ${bgColors}"> | |
| <i data-lucide="file-text" style="width: 18px; height: 18px;"></i> | |
| </div> | |
| <div class="file-attachment-info"> | |
| <span class="file-attachment-name">${fileName}</span> | |
| <span class="file-attachment-type">${fileExt}</span> | |
| </div> | |
| <div class="file-attachment-remove" onclick="this.parentElement.remove(); if(document.getElementById('attachmentsContainer').children.length === 0) document.getElementById('attachmentsContainer').style.display='none';"> | |
| <i data-lucide="x"></i> | |
| </div> | |
| `; | |
| if (window.lucide) lucide.createIcons(); | |
| if (['TXT', 'CSV', 'JSON', 'MD', 'PY', 'JS', 'HTML', 'CSS'].includes(fileExt)) { | |
| const reader = new FileReader(); | |
| reader.onload = () => { | |
| card.setAttribute('data-preview', String(reader.result || '').substring(0, 800)); | |
| attachContainer.appendChild(card); | |
| }; | |
| reader.readAsText(file); | |
| } else if (['PDF', 'DOCX', 'PNG', 'JPG', 'JPEG', 'WEBP'].includes(fileExt)) { | |
| // Auto-analyze without confirm, show in input area | |
| handleFileAnalysis(file, card); | |
| attachContainer.appendChild(card); | |
| } else { | |
| attachContainer.appendChild(card); | |
| } | |
| messageInput.focus(); | |
| fileUploadInput.value = ''; | |
| } | |
| }); | |
| function triggerUpload(type) { | |
| document.getElementById('uploadMenu').classList.remove('show'); | |
| if (type === 'PDF') fileUploadInput.accept = '.pdf'; | |
| else if (type === 'Image') fileUploadInput.accept = 'image/*'; | |
| else if (type === 'Document') fileUploadInput.accept = '.doc,.docx,.txt,.rtf'; | |
| else fileUploadInput.accept = '*/*'; | |
| fileUploadInput.click(); | |
| } | |
| // Voice | |
| let recognition = null; | |
| let isRecording = false; | |
| function toggleSpeechRecognition() { | |
| if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { | |
| alert("Speech recognition is not supported in this browser."); | |
| return; | |
| } | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if (!recognition) { | |
| recognition = new SpeechRecognition(); | |
| recognition.continuous = false; | |
| recognition.interimResults = true; | |
| recognition.lang = 'en-US'; | |
| recognition.onstart = () => { | |
| isRecording = true; | |
| document.getElementById('micBtn').classList.add('recording'); | |
| }; | |
| recognition.onresult = (event) => { | |
| const transcript = Array.from(event.results) | |
| .map(result => result[0]) | |
| .map(result => result.transcript) | |
| .join(''); | |
| document.getElementById('messageInput').value = transcript; | |
| }; | |
| recognition.onend = () => { | |
| isRecording = false; | |
| document.getElementById('micBtn').classList.remove('recording'); | |
| recognition = null; | |
| }; | |
| recognition.onerror = (event) => { | |
| console.error("Speech recognition error:", event.error); | |
| isRecording = false; | |
| document.getElementById('micBtn').classList.remove('recording'); | |
| }; | |
| } | |
| if (isRecording) { | |
| recognition.stop(); | |
| } else { | |
| recognition.start(); | |
| } | |
| } | |
| let currentUtterance = null; | |
| function speakText(text, callbacks = {}) { | |
| if (!('speechSynthesis' in window)) return; | |
| // Stop any ongoing speech | |
| stopSpeech(); | |
| const cleanText = text.replace(/[*_#]/g, '').replace(/\[.*?\]/g, '').replace(/```[\s\S]*?```/g, 'Code block.'); | |
| const utterance = new SpeechSynthesisUtterance(cleanText); | |
| currentUtterance = utterance; | |
| // Get available voices | |
| const voices = window.speechSynthesis.getVoices(); | |
| // Find a high-quality female voice | |
| let femaleVoice = voices.find(v => v.name.includes('Google US English') && v.name.includes('Female')) || | |
| voices.find(v => v.name.includes('Microsoft Zira')) || | |
| voices.find(v => v.name.toLowerCase().includes('female')) || | |
| voices.find(v => v.name.toLowerCase().includes('woman')) || | |
| voices.find(v => v.lang.startsWith('en') && (v.name.includes('Samantha') || v.name.includes('Victoria') || v.name.includes('Google'))); | |
| if (femaleVoice) { | |
| utterance.voice = femaleVoice; | |
| } | |
| utterance.pitch = 1.1; | |
| utterance.rate = 1.0; | |
| utterance.volume = 1.0; | |
| if (callbacks.onStart) utterance.onstart = callbacks.onStart; | |
| if (callbacks.onEnd) { | |
| utterance.onend = callbacks.onEnd; | |
| utterance.onerror = callbacks.onEnd; | |
| } | |
| window.speechSynthesis.speak(utterance); | |
| } | |
| function stopSpeech() { | |
| if ('speechSynthesis' in window) { | |
| window.speechSynthesis.cancel(); | |
| currentUtterance = null; | |
| } | |
| } | |
| // Ensure voices are loaded (some browsers load them asynchronously) | |
| if ('speechSynthesis' in window) { | |
| window.speechSynthesis.onvoiceschanged = () => { | |
| window.speechSynthesis.getVoices(); | |
| }; | |
| } | |
| function addMessageActions(container, text) { | |
| const actionsDiv = document.createElement('div'); | |
| actionsDiv.className = 'message-actions'; | |
| // Copy Button | |
| const copyBtn = document.createElement('button'); | |
| copyBtn.className = 'action-icon-btn'; | |
| copyBtn.innerHTML = '<i data-lucide="copy"></i>'; | |
| copyBtn.title = 'Copy response'; | |
| copyBtn.onclick = () => { | |
| navigator.clipboard.writeText(text); | |
| const originalContent = copyBtn.innerHTML; | |
| copyBtn.innerHTML = '<i data-lucide="check"></i>'; | |
| setTimeout(() => { | |
| copyBtn.innerHTML = originalContent; | |
| refreshIcons(); | |
| }, 2000); | |
| refreshIcons(); | |
| }; | |
| // Like Button | |
| const likeBtn = document.createElement('button'); | |
| likeBtn.className = 'action-icon-btn'; | |
| likeBtn.innerHTML = '<i data-lucide="thumbs-up"></i>'; | |
| likeBtn.title = 'Like'; | |
| likeBtn.onclick = () => { | |
| likeBtn.classList.toggle('active'); | |
| dislikeBtn.classList.remove('active'); | |
| }; | |
| // Dislike Button | |
| const dislikeBtn = document.createElement('button'); | |
| dislikeBtn.className = 'action-icon-btn'; | |
| dislikeBtn.innerHTML = '<i data-lucide="thumbs-down"></i>'; | |
| dislikeBtn.title = 'Bad'; | |
| dislikeBtn.onclick = () => { | |
| dislikeBtn.classList.toggle('active'); | |
| likeBtn.classList.remove('active'); | |
| }; | |
| // Speak Button (Start) | |
| const speakBtn = document.createElement('button'); | |
| speakBtn.className = 'action-icon-btn'; | |
| speakBtn.innerHTML = '<i data-lucide="play"></i>'; | |
| speakBtn.title = 'Start reading'; | |
| // Stop Button | |
| const stopBtn = document.createElement('button'); | |
| stopBtn.className = 'action-icon-btn'; | |
| stopBtn.style.display = 'none'; // Hidden by default | |
| stopBtn.innerHTML = '<i data-lucide="square"></i>'; | |
| stopBtn.title = 'Stop reading'; | |
| speakBtn.onclick = () => { | |
| // Global stop to ensure only one message speaks at a time | |
| document.querySelectorAll('.action-icon-btn.speaking-stop').forEach(btn => { | |
| btn.click(); | |
| }); | |
| speakText(text, { | |
| onStart: () => { | |
| speakBtn.style.display = 'none'; | |
| stopBtn.style.display = 'flex'; | |
| stopBtn.classList.add('speaking-stop'); | |
| }, | |
| onEnd: () => { | |
| speakBtn.style.display = 'flex'; | |
| stopBtn.style.display = 'none'; | |
| stopBtn.classList.remove('speaking-stop'); | |
| refreshIcons(); | |
| } | |
| }); | |
| }; | |
| stopBtn.onclick = () => { | |
| stopSpeech(); | |
| speakBtn.style.display = 'flex'; | |
| stopBtn.style.display = 'none'; | |
| stopBtn.classList.remove('speaking-stop'); | |
| refreshIcons(); | |
| }; | |
| // Export Button | |
| const exportBtn = document.createElement('button'); | |
| exportBtn.className = 'action-icon-btn'; | |
| exportBtn.innerHTML = '<i data-lucide="download"></i>'; | |
| exportBtn.title = 'Export as Markdown'; | |
| exportBtn.onclick = () => { | |
| const blob = new Blob([text], { type: 'text/markdown' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `nexa-response-${Date.now()}.md`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| }; | |
| actionsDiv.appendChild(copyBtn); | |
| actionsDiv.appendChild(likeBtn); | |
| actionsDiv.appendChild(dislikeBtn); | |
| actionsDiv.appendChild(speakBtn); | |
| actionsDiv.appendChild(stopBtn); | |
| actionsDiv.appendChild(exportBtn); | |
| container.appendChild(actionsDiv); | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| // Image Generation | |
| function openImageGenModal() { | |
| document.getElementById('imgModalOverlay').classList.add('open'); | |
| setTimeout(() => document.getElementById('imgPromptInput').focus(), 350); | |
| } | |
| function closeImageGenModal() { | |
| document.getElementById('imgModalOverlay').classList.remove('open'); | |
| } | |
| // Video Overlay Functions | |
| let currentOverlayVideoUrl = ""; | |
| function openVideoOverlayModal(videoUrl) { | |
| currentOverlayVideoUrl = videoUrl; | |
| const modal = document.getElementById('videoOverlayModal'); | |
| modal.style.display = 'flex'; | |
| modal.setAttribute('data-video-url', videoUrl); | |
| document.getElementById('overlayModalStatus').textContent = ""; | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| async function handleImgOverlayClick(e) { | |
| if (e.target === document.getElementById('imgModalOverlay')) closeImageGenModal(); | |
| } | |
| let isGeneratingImageModal = false; // FIXED: Prevent double-clicks in modal | |
| async function generateImage() { | |
| if (isGeneratingImageModal) return; | |
| const promptInput = document.getElementById('imgPromptInput'); | |
| const prompt = promptInput ? promptInput.value.trim() : ''; | |
| if (!prompt) return; | |
| isGeneratingImageModal = true; | |
| const genBtn = document.getElementById('imgGenerateBtn'); | |
| const skeleton = document.getElementById('imgSkeleton'); | |
| const resultArea = document.getElementById('imgResultArea'); | |
| const statusEl = document.getElementById('imgModalStatus'); | |
| const resultImg = document.getElementById('imgResultImg'); | |
| if (genBtn) genBtn.disabled = true; | |
| if (resultArea) resultArea.classList.remove('visible'); | |
| if (skeleton) skeleton.classList.add('visible'); | |
| if (statusEl) statusEl.textContent = '✦ Generating Masterpiece...'; | |
| try { | |
| const seed = Math.floor(Math.random() * 1000000); | |
| const encodedPrompt = encodeURIComponent(prompt); | |
| const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?width=1024&height=1024&seed=${seed}&nologo=true`; | |
| const img = new Image(); | |
| img.onload = () => { | |
| if (resultImg) resultImg.src = imageUrl; | |
| const downloadBtn = document.getElementById('imgDownloadBtn'); | |
| if (downloadBtn) { | |
| downloadBtn.href = imageUrl; | |
| downloadBtn.download = `nexa-${Date.now()}.png`; | |
| } | |
| if (skeleton) skeleton.classList.remove('visible'); | |
| if (resultArea) resultArea.classList.add('visible'); | |
| if (statusEl) statusEl.textContent = '✓ Done!'; | |
| if (genBtn) genBtn.disabled = false; | |
| isGeneratingImageModal = false; | |
| }; | |
| img.onerror = () => { | |
| throw new Error("Image provider error"); | |
| }; | |
| img.src = imageUrl; | |
| } catch (err) { | |
| if (skeleton) skeleton.classList.remove('visible'); | |
| if (statusEl) statusEl.textContent = '✗ Error: ' + err.message; | |
| if (genBtn) genBtn.disabled = false; | |
| isGeneratingImageModal = false; | |
| } | |
| } | |
| // IDE Assistant Logic | |
| let currentIDEPlan = []; | |
| let currentIDEMode = 'plan'; | |
| function switchIDEMode(mode) { | |
| currentIDEMode = mode; | |
| const planBtn = document.getElementById('ideModePlan'); | |
| const debugBtn = document.getElementById('ideModeDebug'); | |
| const planArea = document.getElementById('idePlanInputArea'); | |
| const debugArea = document.getElementById('ideDebugInputArea'); | |
| const planContainer = document.getElementById('idePlanContainer'); | |
| planContainer.innerHTML = ''; | |
| if (mode === 'plan') { | |
| planBtn.style.background = 'var(--accent-blue)'; | |
| debugBtn.style.background = 'rgba(255,255,255,0.05)'; | |
| planArea.style.display = 'block'; | |
| debugArea.style.display = 'none'; | |
| } else { | |
| debugBtn.style.background = 'var(--accent-blue)'; | |
| planBtn.style.background = 'rgba(255,255,255,0.05)'; | |
| planArea.style.display = 'none'; | |
| debugArea.style.display = 'block'; | |
| } | |
| } | |
| async function analyzeIDEError() { | |
| const errorText = document.getElementById('ideErrorInput').value.trim(); | |
| if (!errorText) return; | |
| const debugBtn = document.getElementById('ideDebugBtn'); | |
| const statusArea = document.getElementById('ideStatusArea'); | |
| const planContainer = document.getElementById('idePlanContainer'); | |
| debugBtn.disabled = true; | |
| statusArea.innerHTML = '<i data-lucide="loader-2" class="animate-spin"></i> Analyzing root cause and finding fixes...'; | |
| planContainer.innerHTML = ''; | |
| if (window.lucide) lucide.createIcons(); | |
| try { | |
| const resp = await fetch('/api/ide/debug', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| error: errorText, | |
| stack: errorText, // Use full text as stack | |
| file: 'app.py', // Mock current file | |
| line: 10 // Mock line | |
| }) | |
| }); | |
| const data = await resp.json(); | |
| if (data.error) throw new Error(data.error); | |
| statusArea.innerHTML = `<i data-lucide="check-circle" class="text-green-500"></i> Found ${data.suggestions.length} possible fixes. Seen this ${data.frequency} times.`; | |
| planContainer.innerHTML = ` | |
| <div style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2); padding: 16px; border-radius: 12px; margin-bottom: 20px;"> | |
| <h4 style="font-size: 12px; color: #f87171; font-weight: 700; text-transform: uppercase; margin-bottom: 8px;">Root Cause Analysis</h4> | |
| <div style="font-size: 14px; color: var(--text-primary); line-height: 1.5;">${data.root_cause}</div> | |
| </div> | |
| <div style="display: flex; flex-direction: column; gap: 12px;"> | |
| ${data.suggestions.map((s, idx) => ` | |
| <div class="feature-card" style="padding: 16px; border-radius: 12px; flex-direction: column; align-items: stretch;"> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 12px;"> | |
| <div style="font-size: 11px; font-weight: 700; color: #10a37f;">FIX OPTION #${idx + 1} (${Math.round(s.confidence * 100)}% Confidence)</div> | |
| <div style="font-size: 11px; color: var(--text-secondary);"><i data-lucide="clock" style="width:10px; display:inline;"></i> Saved ~${s.time_saved}</div> | |
| </div> | |
| <p style="font-size: 14px; margin-bottom: 12px; color: var(--text-secondary);">${s.explanation}</p> | |
| <pre style="background: rgba(0,0,0,0.4); padding: 12px; border-radius: 8px; font-size: 12px; color: #60a5fa; overflow-x: auto; margin-bottom: 12px;"><code>${s.fix}</code></pre> | |
| <button class="btn-primary" style="width: 100%; padding: 8px; font-size: 13px;" onclick="applyIDEFix('${btoa(s.fix)}')">Apply Fix</button> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| } catch (err) { | |
| statusArea.innerHTML = `<i data-lucide="alert-circle" class="text-red-500"></i> Debugging failed: ${err.message}`; | |
| } finally { | |
| debugBtn.disabled = false; | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| } | |
| async function applyIDEFix(encodedFix) { | |
| const fix = atob(encodedFix); | |
| try { | |
| // Mock apply (real would write to file) | |
| await fetch('/api/ide/learn', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ fix }) | |
| }); | |
| alert('Fix applied successfully! The AI has learned from this resolution.'); | |
| } catch (e) { | |
| alert('Failed to apply fix.'); | |
| } | |
| } | |
| function openIDEAssistant() { | |
| document.getElementById('ideModalOverlay').classList.add('open'); | |
| fetchFileTree(); | |
| setTimeout(() => document.getElementById('ideCommandInput').focus(), 350); | |
| } | |
| function closeIDEAssistant() { | |
| document.getElementById('ideModalOverlay').classList.remove('open'); | |
| } | |
| function openFeaturesModal() { | |
| document.getElementById('featuresModalOverlay').classList.add('open'); | |
| } | |
| function closeFeaturesModal() { | |
| document.getElementById('featuresModalOverlay').classList.remove('open'); | |
| } | |
| function openSettings() { | |
| document.getElementById('settingsModalOverlay').classList.add('open'); | |
| updateSettingsUI(); | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| function closeSettings() { | |
| document.getElementById('settingsModalOverlay').classList.remove('open'); | |
| } | |
| function handleSettingsOverlayClick(e) { | |
| // FIXED: Improved click-outside detection for settings modal with specific target check | |
| const modal = e.target.closest('.settings-modal'); | |
| if (!modal && e.target.classList.contains('settings-modal-overlay')) { | |
| closeSettings(); | |
| } | |
| } | |
| let isAutoVoiceEnabled = localStorage.getItem('nexa_auto_voice') !== 'false'; | |
| function toggleAutoVoice() { | |
| isAutoVoiceEnabled = !isAutoVoiceEnabled; | |
| localStorage.setItem('nexa_auto_voice', isAutoVoiceEnabled); | |
| updateSettingsUI(); | |
| } | |
| function setFontSize(size) { | |
| const root = document.documentElement; | |
| if (size === 'small') root.style.setProperty('--chat-font-size', '13px'); | |
| else if (size === 'medium') root.style.setProperty('--chat-font-size', '15px'); | |
| else if (size === 'large') root.style.setProperty('--chat-font-size', '18px'); | |
| localStorage.setItem('nexa_font_size', size); | |
| } | |
| // Initialize font size | |
| const savedFontSize = localStorage.getItem('nexa_font_size') || 'medium'; | |
| setFontSize(savedFontSize); | |
| function updateSettingsUI() { | |
| const voiceBtn = document.getElementById('voiceToggleBtn'); | |
| if (voiceBtn) { | |
| voiceBtn.textContent = isAutoVoiceEnabled ? 'Enabled' : 'Disabled'; | |
| voiceBtn.classList.toggle('danger', !isAutoVoiceEnabled); | |
| } | |
| } | |
| function confirmClearMemory() { | |
| openConfirm("Clear Nexa's Memory?", "This will forget everything Nexa has learned about your preferences, name, and style.", () => { | |
| userMemory = {}; | |
| localStorage.removeItem('nexa_ai_memory'); | |
| addMessage("I've cleared my memory. We're starting fresh! 🧠✨", 'ai'); | |
| closeConfirm(); | |
| closeSettings(); | |
| }); | |
| } | |
| function confirmDeleteAllChats() { | |
| openConfirm("Delete All Conversations?", "This will permanently delete your entire chat history. This action cannot be undone.", () => { | |
| conversations = []; | |
| localStorage.setItem(getConversationStorageKey(), JSON.stringify([])); | |
| currentConversationId = null; | |
| renderConversations(); | |
| startNewChat(); | |
| addMessage("All conversations have been deleted permanently.", 'ai'); | |
| closeConfirm(); | |
| closeSettings(); | |
| }); | |
| } | |
| function openConfirm(title, text, onConfirm) { | |
| document.getElementById('confirmTitle').textContent = title; | |
| document.getElementById('confirmText').textContent = text; | |
| const deleteBtn = document.getElementById('confirmDeleteBtn'); | |
| deleteBtn.onclick = onConfirm; | |
| document.getElementById('confirmOverlay').classList.add('open'); | |
| } | |
| function closeConfirm() { | |
| document.getElementById('confirmOverlay').classList.remove('open'); | |
| } | |
| function handleConfirmOverlayClick(e) { | |
| if (e.target === document.getElementById('confirmOverlay')) closeConfirm(); | |
| } | |
| function handleFeaturesOverlayClick(e) { | |
| if (e.target === document.getElementById('featuresModalOverlay')) closeFeaturesModal(); | |
| } | |
| function exportFullChat() { | |
| if (!currentConversationId) { | |
| alert("No active conversation to export."); | |
| return; | |
| } | |
| const conv = conversations.find(c => c.id === currentConversationId); | |
| if (!conv || !conv.messages || conv.messages.length === 0) { | |
| alert("Conversation is empty."); | |
| return; | |
| } | |
| // Task 27: Strip HTML tags from content | |
| const stripHtml = (html) => { | |
| const tmp = document.createElement("DIV"); | |
| tmp.innerHTML = html; | |
| return tmp.textContent || tmp.innerText || ""; | |
| }; | |
| let markdown = `# Nexa AI Conversation: ${conv.title}\n\n`; | |
| conv.messages.forEach(msg => { | |
| const role = msg.type === 'ai' ? 'AI' : 'User'; | |
| const cleanContent = stripHtml(msg.content); | |
| markdown += `### ${role}\n${cleanContent}\n\n---\n\n`; | |
| }); | |
| const blob = new Blob([markdown], { type: 'text/markdown' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `nexa-chat-${conv.id}.md`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| function handleIDEModalClick(e) { | |
| if (e.target === document.getElementById('ideModalOverlay')) closeIDEAssistant(); | |
| } | |
| async function fetchFileTree() { | |
| try { | |
| const resp = await fetch('/api/ide/execute', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ action: 'list_files' }) | |
| }); | |
| const data = await resp.json(); | |
| if (data.files) { | |
| const tree = document.getElementById('ideFileTree'); | |
| tree.innerHTML = data.files.slice(0, 15).map(f => `<div><i data-lucide="file-code" style="width:12px;display:inline;margin-right:4px;"></i>${f}</div>`).join(''); | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| } catch (e) {} | |
| } | |
| async function generateIDEPlan() { | |
| const command = document.getElementById('ideCommandInput').value.trim(); | |
| if (!command) return; | |
| const runBtn = document.getElementById('ideRunBtn'); | |
| const statusArea = document.getElementById('ideStatusArea'); | |
| const planContainer = document.getElementById('idePlanContainer'); | |
| runBtn.disabled = true; | |
| statusArea.innerHTML = '<i data-lucide="loader-2" class="animate-spin"></i> Analyzing project and planning steps...'; | |
| planContainer.innerHTML = ''; | |
| if (window.lucide) lucide.createIcons(); | |
| try { | |
| const resp = await fetch('/api/ide/plan', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ command }) | |
| }); | |
| const data = await resp.json(); | |
| if (data.error) throw new Error(data.error); | |
| currentIDEPlan = data.plan; | |
| document.getElementById('ideCostStats').textContent = `${data.stats.costSavings} Saved`; | |
| statusArea.innerHTML = '<i data-lucide="check-circle" class="text-green-500"></i> Plan generated. Awaiting approval.'; | |
| renderIDEPlan(); | |
| } catch (err) { | |
| statusArea.innerHTML = `<i data-lucide="alert-circle" class="text-red-500"></i> Error: ${err.message}`; | |
| } finally { | |
| runBtn.disabled = false; | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| } | |
| function renderIDEPlan() { | |
| const container = document.getElementById('idePlanContainer'); | |
| container.innerHTML = currentIDEPlan.map(step => ` | |
| <div class="feature-card" style="padding: 16px; border-radius: 12px; display: flex; justify-content: space-between; align-items: center;" id="step-${step.id}"> | |
| <div style="display: flex; gap: 12px;"> | |
| <div style="font-weight: 700; color: var(--text-secondary);">${step.id}</div> | |
| <div> | |
| <div style="font-size: 11px; text-transform: uppercase; font-weight: 700; color: var(--accent-blue);">${step.action.replace('_', ' ')}</div> | |
| <div style="font-size: 14px;">${step.description}</div> | |
| ${step.path ? `<code style="font-size: 11px; background: rgba(0,0,0,0.3); padding: 2px 6px; border-radius: 4px; color: #60a5fa; margin-top: 4px; display: inline-block;">${step.path}</code>` : ''} | |
| </div> | |
| </div> | |
| <div id="status-container-${step.id}"> | |
| <button class="btn-primary" style="padding: 6px 12px; font-size: 12px; border-radius: 8px;" onclick="executeIDEStep(${step.id})">Approve</button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| async function executeIDEStep(stepId) { | |
| const step = currentIDEPlan.find(s => s.id === stepId); | |
| const statusContainer = document.getElementById(`status-container-${stepId}`); | |
| statusContainer.innerHTML = '<i data-lucide="loader-2" class="animate-spin" style="width: 18px;"></i>'; | |
| if (window.lucide) lucide.createIcons(); | |
| try { | |
| const resp = await fetch('/api/ide/execute', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| action: step.action, | |
| payload: step.action === 'create_file' ? { path: step.path, content: step.content } : { command: step.command } | |
| }) | |
| }); | |
| const data = await resp.json(); | |
| if (data.status === 'success') { | |
| statusContainer.innerHTML = '<i data-lucide="check-circle" class="text-green-500"></i>'; | |
| } else { | |
| throw new Error(data.stderr || 'Execution failed'); | |
| } | |
| } catch (err) { | |
| statusContainer.innerHTML = '<i data-lucide="x-circle" class="text-red-500"></i>'; | |
| alert(`Step failed: ${err.message}`); | |
| } finally { | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| } | |
| // --- Landing Page Animations --- | |
| const words = ["Intelligent", "Smart", "Powerful", "Creative"]; | |
| let wordIndex = 0; | |
| let charIndex = 0; | |
| let isDeleting = false; | |
| let typingDelay = 150; | |
| let erasingDelay = 100; | |
| let newWordDelay = 2500; | |
| function typeEffect() { | |
| const typingText = document.getElementById("typing-text"); | |
| if (!typingText) return; | |
| const currentWord = words[wordIndex]; | |
| if (isDeleting) { | |
| if (charIndex > 0) { | |
| typingText.textContent = currentWord.substring(0, charIndex - 1); | |
| charIndex--; | |
| } | |
| } else { | |
| typingText.textContent = currentWord.substring(0, charIndex + 1); | |
| charIndex++; | |
| } | |
| if (!isDeleting && charIndex === currentWord.length) { | |
| isDeleting = true; | |
| setTimeout(typeEffect, newWordDelay); | |
| } else if (isDeleting && charIndex === 0) { | |
| isDeleting = false; | |
| wordIndex = (wordIndex + 1) % words.length; | |
| setTimeout(typeEffect, 500); | |
| } else { | |
| setTimeout(typeEffect, isDeleting ? erasingDelay : typingDelay); | |
| } | |
| } | |
| function triggerLightning() { | |
| const lightningOverlay = document.getElementById("lightningOverlay"); | |
| if (!lightningOverlay) return; | |
| lightningOverlay.classList.remove("lightning-flash"); | |
| void lightningOverlay.offsetWidth; | |
| lightningOverlay.classList.add("lightning-flash"); | |
| setTimeout(triggerLightning, Math.random() * 8000 + 4000); | |
| } | |
| // --- 3D Background System --- | |
| function init3DBackground() { | |
| const canvas = document.getElementById('canvas3d'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let w, h; | |
| const particles = []; | |
| const particleCount = 60; | |
| function resize() { | |
| w = canvas.width = window.innerWidth; | |
| h = canvas.height = window.innerHeight; | |
| } | |
| class Particle { | |
| constructor() { | |
| this.reset(); | |
| } | |
| reset() { | |
| this.x = Math.random() * w; | |
| this.y = Math.random() * h; | |
| this.z = Math.random() * 1000; | |
| this.size = 1 + Math.random() * 2; | |
| this.speed = 0.5 + Math.random() * 1.5; | |
| this.vx = (Math.random() - 0.5) * 0.5; | |
| this.vy = (Math.random() - 0.5) * 0.5; | |
| } | |
| update() { | |
| this.z -= this.speed; | |
| this.x += this.vx; | |
| this.y += this.vy; | |
| if (this.z <= 0) this.reset(); | |
| } | |
| draw() { | |
| const scale = 500 / (500 + this.z); | |
| const x2d = (this.x - w / 2) * scale + w / 2; | |
| const y2d = (this.y - h / 2) * scale + h / 2; | |
| const opacity = Math.min(1, (1000 - this.z) / 800) * 0.3; | |
| ctx.fillStyle = `rgba(16, 163, 127, ${opacity})`; | |
| ctx.beginPath(); | |
| ctx.arc(x2d, y2d, this.size * scale, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| } | |
| for (let i = 0; i < particleCount; i++) particles.push(new Particle()); | |
| function animate() { | |
| ctx.clearRect(0, 0, w, h); | |
| particles.forEach(p => { | |
| p.update(); | |
| p.draw(); | |
| }); | |
| requestAnimationFrame(animate); | |
| } | |
| window.addEventListener('resize', resize); | |
| resize(); | |
| animate(); | |
| } | |
| // --- 3D Card Tilt Effect --- | |
| function init3DTilt() { | |
| document.addEventListener('mousemove', e => { | |
| const cards = document.querySelectorAll('.suggestion-card, .feature-item'); | |
| cards.forEach(card => { | |
| const rect = card.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| if (x > 0 && x < rect.width && y > 0 && y < rect.height) { | |
| const centerX = rect.width / 2; | |
| const centerY = rect.height / 2; | |
| const rotateX = (y - centerY) / 10; | |
| const rotateY = (centerX - x) / 10; | |
| card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) translateZ(10px)`; | |
| } else { | |
| card.style.transform = ''; | |
| } | |
| }); | |
| }); | |
| } | |
| // Initialize | |
| window.addEventListener('DOMContentLoaded', async () => { | |
| try { | |
| // Configure marked | |
| if (typeof marked !== 'undefined') { | |
| marked.setOptions({ breaks: true, gfm: true }); | |
| } | |
| // 1. Run initialization first | |
| try { | |
| await init(); | |
| } catch (initErr) { | |
| console.error("init() failed:", initErr); | |
| } | |
| // 2. Setup event listeners (delegation) | |
| setupEventListeners(); | |
| // 3. Start animations | |
| setTimeout(typeEffect, 1000); | |
| setTimeout(triggerLightning, 3000); | |
| init3DBackground(); | |
| init3DTilt(); | |
| refreshIcons(); | |
| // 4. Server Check | |
| fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ message: "status_check" }) | |
| }) | |
| .then(res => res.ok ? res.json() : { status: 'Offline' }) | |
| .then(data => { | |
| const badge = document.getElementById('serverStatusBadge'); | |
| if (badge) { | |
| if (data.status === 'Offline') { | |
| badge.className = 'status-badge offline'; | |
| badge.innerHTML = `<i data-lucide="alert-triangle" style="width:12px;"></i> Offline Mode`; | |
| } else { | |
| badge.className = 'status-badge online'; | |
| badge.innerHTML = `<i data-lucide="globe" style="width:12px;"></i> Online Mode`; | |
| } | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| }) | |
| .catch(() => { | |
| const badge = document.getElementById('serverStatusBadge'); | |
| if (badge) { | |
| badge.className = 'status-badge offline'; | |
| badge.innerHTML = `<i data-lucide="alert-triangle" style="width:12px;"></i> Connection Error`; | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| }); | |
| } catch (err) { | |
| console.error("Critical initialization error:", err); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |