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 (