diff --git a/backend/agent/workspace/index.html b/backend/agent/workspace/index.html index 5d9f9a51..0f021113 100644 --- a/backend/agent/workspace/index.html +++ b/backend/agent/workspace/index.html @@ -17,11 +17,17 @@ About Contact - + diff --git a/backend/agent/workspace/script.js b/backend/agent/workspace/script.js new file mode 100644 index 00000000..1fa39901 --- /dev/null +++ b/backend/agent/workspace/script.js @@ -0,0 +1,207 @@ +// Wait for the DOM to be fully loaded +document.addEventListener('DOMContentLoaded', function() { + // Dark mode toggle functionality + const darkModeToggle = document.getElementById('darkModeToggle'); + + // Check for saved theme preference or use device preference + const savedTheme = localStorage.getItem('theme'); + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + + // Set initial theme + if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { + document.documentElement.setAttribute('data-theme', 'dark'); + } + + // Toggle dark mode + darkModeToggle.addEventListener('click', function() { + const currentTheme = document.documentElement.getAttribute('data-theme'); + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + + document.documentElement.setAttribute('data-theme', newTheme); + localStorage.setItem('theme', newTheme); + }); + // Mobile menu toggle + const mobileMenuBtn = document.querySelector('.mobile-menu-btn'); + const navLinks = document.querySelector('.nav-links'); + + if (mobileMenuBtn) { + mobileMenuBtn.addEventListener('click', function() { + // Create mobile menu if it doesn't exist + if (!document.querySelector('.mobile-nav')) { + const mobileNav = document.createElement('div'); + mobileNav.className = 'mobile-nav'; + + // Clone the navigation links + const navLinksClone = navLinks.cloneNode(true); + + // Get all links + const links = navLinksClone.querySelectorAll('a'); + + // Convert to individual links for mobile + links.forEach(link => { + mobileNav.appendChild(link); + }); + + // Add to the DOM + document.querySelector('header').appendChild(mobileNav); + } + + // Toggle the mobile menu + const mobileNav = document.querySelector('.mobile-nav'); + mobileNav.classList.toggle('active'); + + // Animate the hamburger icon + const spans = this.querySelectorAll('span'); + if (mobileNav.classList.contains('active')) { + spans[0].style.transform = 'rotate(45deg) translate(5px, 5px)'; + spans[1].style.opacity = '0'; + spans[2].style.transform = 'rotate(-45deg) translate(7px, -6px)'; + } else { + spans[0].style.transform = 'none'; + spans[1].style.opacity = '1'; + spans[2].style.transform = 'none'; + } + }); + } + + // Smooth scrolling for anchor links + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function(e) { + e.preventDefault(); + + const targetId = this.getAttribute('href'); + if (targetId === '#') return; + + const targetElement = document.querySelector(targetId); + if (targetElement) { + // Close mobile menu if open + const mobileNav = document.querySelector('.mobile-nav'); + if (mobileNav && mobileNav.classList.contains('active')) { + mobileMenuBtn.click(); + } + + // Scroll to the target element + window.scrollTo({ + top: targetElement.offsetTop - 80, + behavior: 'smooth' + }); + } + }); + }); + + // Form validation + const contactForm = document.getElementById('contactForm'); + + if (contactForm) { + contactForm.addEventListener('submit', function(e) { + e.preventDefault(); + + // Get form values + const name = document.getElementById('name').value.trim(); + const email = document.getElementById('email').value.trim(); + const message = document.getElementById('message').value.trim(); + + // Simple validation + if (name === '') { + showError('name', 'Please enter your name'); + return; + } + + if (email === '') { + showError('email', 'Please enter your email'); + return; + } + + if (!isValidEmail(email)) { + showError('email', 'Please enter a valid email'); + return; + } + + if (message === '') { + showError('message', 'Please enter your message'); + return; + } + + // If all validations pass, show success message + contactForm.innerHTML = ` +
+ +

Thank You!

+

Your message has been sent successfully. We'll get back to you soon.

+
+ `; + }); + } + + // Helper function to show error messages + function showError(fieldId, message) { + const field = document.getElementById(fieldId); + + // Remove any existing error message + const existingError = field.parentElement.querySelector('.error-message'); + if (existingError) { + existingError.remove(); + } + + // Create and add error message + const errorDiv = document.createElement('div'); + errorDiv.className = 'error-message'; + errorDiv.textContent = message; + errorDiv.style.color = '#ef4444'; + errorDiv.style.fontSize = '0.875rem'; + errorDiv.style.marginTop = '5px'; + + field.parentElement.appendChild(errorDiv); + + // Highlight the field + field.style.borderColor = '#ef4444'; + + // Focus on the field + field.focus(); + + // Remove error when user starts typing + field.addEventListener('input', function() { + const error = this.parentElement.querySelector('.error-message'); + if (error) { + error.remove(); + } + this.style.borderColor = ''; + }); + } + + // Helper function to validate email + function isValidEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } + + // Add animation on scroll + const animateElements = document.querySelectorAll('.feature-card, .about-image, .about-text'); + + // Check if IntersectionObserver is supported + if ('IntersectionObserver' in window) { + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('animated'); + observer.unobserve(entry.target); + } + }); + }, { threshold: 0.1 }); + + animateElements.forEach(element => { + element.style.opacity = '0'; + element.style.transform = 'translateY(20px)'; + element.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; + observer.observe(element); + }); + } + + // Add animated class to elements + document.addEventListener('scroll', function() { + document.querySelectorAll('.animated').forEach(element => { + element.style.opacity = '1'; + element.style.transform = 'translateY(0)'; + }); + }); +}); \ No newline at end of file diff --git a/backend/agent/workspace/styles.css b/backend/agent/workspace/styles.css index 35dacf3a..aa48f836 100644 --- a/backend/agent/workspace/styles.css +++ b/backend/agent/workspace/styles.css @@ -10,6 +10,29 @@ --border-radius: 8px; --box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); --transition: all 0.3s ease; + + /* Background and text colors */ + --bg-color: #f9fafb; + --text-color: #1f2937; + --card-bg: #ffffff; + --header-bg: #ffffff; + --footer-bg: #1f2937; + --footer-text: #ffffff; +} + +/* Dark mode colors */ +[data-theme="dark"] { + --primary-color: #6366f1; + --primary-dark: #4f46e5; + --bg-color: #111827; + --text-color: #f9fafb; + --card-bg: #1f2937; + --gray-color: #9ca3af; + --light-gray: #374151; + --header-bg: #1f2937; + --footer-bg: #111827; + --footer-text: #f9fafb; + --box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.15); } * { @@ -25,8 +48,9 @@ html { body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; line-height: 1.6; - color: var(--dark-color); - background-color: var(--light-color); + color: var(--text-color); + background-color: var(--bg-color); + transition: background-color 0.3s ease, color 0.3s ease; } a { @@ -92,11 +116,12 @@ section { /* Header Styles */ header { - background-color: white; + background-color: var(--header-bg); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); position: sticky; top: 0; z-index: 100; + transition: background-color 0.3s ease; } .navbar { @@ -106,6 +131,58 @@ header { padding: 20px 0; } +.nav-actions { + display: flex; + align-items: center; + gap: 20px; +} + +.dark-mode-toggle { + background: none; + border: none; + cursor: pointer; + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + color: var(--text-color); + background-color: var(--light-gray); + transition: var(--transition); + position: relative; + overflow: hidden; +} + +.dark-mode-toggle:hover { + background-color: var(--gray-color); +} + +.dark-mode-toggle .fa-sun { + position: absolute; + opacity: 0; + transform: translateY(20px); + transition: var(--transition); +} + +.dark-mode-toggle .fa-moon { + position: absolute; + opacity: 1; + transform: translateY(0); + transition: var(--transition); +} + +[data-theme="dark"] .dark-mode-toggle .fa-sun { + opacity: 1; + transform: translateY(0); +} + +[data-theme="dark"] .dark-mode-toggle .fa-moon { + opacity: 0; + transform: translateY(-20px); +} + .logo { font-size: 1.5rem; font-weight: 700; @@ -200,7 +277,8 @@ header { /* Features Section */ .features { - background-color: white; + background-color: var(--card-bg); + transition: background-color 0.3s ease; } .features-grid { @@ -210,11 +288,11 @@ header { } .feature-card { - background-color: white; + background-color: var(--card-bg); padding: 30px; border-radius: var(--border-radius); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); - transition: var(--transition); + transition: transform 0.3s ease, box-shadow 0.3s ease, background-color 0.3s ease, border-color 0.3s ease; text-align: center; border: 1px solid var(--light-gray); } @@ -278,7 +356,8 @@ header { /* Contact Section */ .contact { - background-color: white; + background-color: var(--card-bg); + transition: background-color 0.3s ease; } .contact-form { @@ -320,9 +399,10 @@ header { /* Footer */ footer { - background-color: var(--dark-color); - color: white; + background-color: var(--footer-bg); + color: var(--footer-text); padding: 80px 0 30px; + transition: background-color 0.3s ease; } .footer-content { @@ -455,10 +535,11 @@ footer { top: 100%; left: 0; width: 100%; - background-color: white; + background-color: var(--card-bg); padding: 20px; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); z-index: 99; + transition: background-color 0.3s ease; } .mobile-nav.active { diff --git a/frontend/src/app/projects/[id]/threads/[threadId]/page.tsx b/frontend/src/app/projects/[id]/threads/[threadId]/page.tsx index fa37066e..09a0d2a3 100644 --- a/frontend/src/app/projects/[id]/threads/[threadId]/page.tsx +++ b/frontend/src/app/projects/[id]/threads/[threadId]/page.tsx @@ -146,6 +146,9 @@ export default function ThreadPage({ params }: { params: ThreadParams }) { // Clear the input setNewMessage(''); + // Scroll to bottom immediately when user sends a message + scrollToBottom(); + // Send to the API await addMessage(threadId, userMessage); @@ -159,6 +162,9 @@ export default function ThreadPage({ params }: { params: ThreadParams }) { setAgentRunId(result.agent_run_id); setAgentStatus('running'); + // Scroll to bottom when agent starts responding + scrollToBottom(); + // Start streaming the agent's responses handleStreamAgent(result.agent_run_id); } catch (err: any) { @@ -423,11 +429,11 @@ export default function ThreadPage({ params }: { params: ThreadParams }) { // Auto-scroll when messages change useEffect(() => { - // Only auto-scroll if already at bottom or is a new message - if (!showScrollButton || messages.length > 0 && messages[messages.length - 1]?.role === 'assistant') { + // Only auto-scroll if user hasn't manually scrolled up + if (isLatestMessageVisible) { scrollToBottom(); } - }, [messages, streamContent]); + }, [messages, streamContent, isLatestMessageVisible]); // Initial scroll to bottom with animation useEffect(() => { @@ -441,6 +447,14 @@ export default function ThreadPage({ params }: { params: ThreadParams }) { } }, [isLoading]); + // Update UI states when agent status changes + useEffect(() => { + // Scroll to bottom when agent starts responding + if (agentStatus === 'running') { + scrollToBottom(); + } + }, [agentStatus]); + // Only show a full-screen loader on the very first load if (isAuthLoading || (isLoading && !initialLoadCompleted.current)) { return (