Nexa.ai / index.html
umar8902's picture
Update index.html
f8871f2 verified
Raw
History Blame Contribute Delete
348 kB
<!DOCTYPE html>
<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) !important;
}
.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% !important;
}
.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 !important;
width: 18px !important;
height: 18px !important;
stroke-width: 2.5px !important;
}
.send-button svg, .stop-button svg, .send-button i, .stop-button i {
color: white !important;
stroke: white !important;
}
.action-btn svg, .action-btn i {
color: var(--text-secondary) !important;
stroke: var(--text-secondary) !important;
}
.action-btn:hover svg, .action-btn:hover i {
color: var(--text-primary) !important;
stroke: var(--text-primary) !important;
}
/* 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 !important;
cursor: not-allowed !important;
transform: none !important;
}
.btn-primary:disabled {
opacity: 0.6 !important;
cursor: wait !important;
}
/* FIXED: Ensure auth overlay is always on top */
#authOverlay {
z-index: 9999 !important;
}
/* 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">
&copy; 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>