From 27a6e3735c6131eddb9c2e76edb1b2db9d96ee00 Mon Sep 17 00:00:00 2001 From: marko-kraemer Date: Sun, 30 Mar 2025 15:55:26 -0700 Subject: [PATCH] fe wip --- backend/agent/workspace/index.html | 303 ++++++-- backend/agent/workspace/script.js | 206 +++++ backend/agent/workspace/styles.css | 703 +++++++++++++----- backend/agent/workspace/test.html | 82 ++ frontend/src/app/auth/confirm/page.tsx | 119 ++- frontend/src/app/auth/login/page.tsx | 75 +- frontend/src/app/auth/signup/page.tsx | 83 ++- frontend/src/app/layout.tsx | 8 +- .../projects/[id]/threads/[threadId]/page.tsx | 126 ++-- .../src/components/chat/chat-interface.tsx | 435 ----------- frontend/src/components/layout/main-nav.tsx | 59 +- 11 files changed, 1325 insertions(+), 874 deletions(-) create mode 100644 backend/agent/workspace/script.js create mode 100644 backend/agent/workspace/test.html delete mode 100644 frontend/src/components/chat/chat-interface.tsx diff --git a/backend/agent/workspace/index.html b/backend/agent/workspace/index.html index e978c0b0..854f5a45 100644 --- a/backend/agent/workspace/index.html +++ b/backend/agent/workspace/index.html @@ -3,90 +3,255 @@ - Pokemon Battle Game + Modern Web Application - + -
-

Pokemon Battle Game

- -
-

Choose Your Pokemon

-
-
- Pikachu -

Pikachu

-

Type: Electric

-

HP: 100

-

Attack: 55

+
+ +
+ +
+ +
+
+
+

Build Amazing Web Applications

+

A modern, responsive web application template to kickstart your next project.

+
-
- Squirtle -

Squirtle

-

Type: Water

-

HP: 110

-

Attack: 50

-
-
- Bulbasaur -

Bulbasaur

-

Type: Grass

-

HP: 105

-

Attack: 52

+
+ Web Application Dashboard
-
- -
-
-
-
-

Opponent

-
-
-
-

HP: 100/100

-
- Opponent Pokemon +
+ + +
+
+
+

Key Features

+

Everything you need to build modern web applications

- -
- Player Pokemon -
-

Player

-
-
+
+
+
+
-

HP: 100/100

+

Responsive Design

+

Looks great on all devices, from mobile phones to desktop computers.

+
+
+
+ +
+

Fast Performance

+

Optimized for speed and efficiency to provide the best user experience.

+
+
+
+ +
+

Clean Code

+

Well-structured, maintainable code following best practices.

+
+
+
+ +
+

Modern UI

+

Beautiful, intuitive user interface with smooth animations.

- -
-
What will you do?
-
- - - +
+ + +
+
+
+
+

About Our Platform

+

Our web application platform is designed to help developers build amazing applications quickly and efficiently. With a focus on user experience and performance, we provide all the tools you need to succeed.

+

Whether you're building a simple landing page or a complex web application, our platform has you covered with modern features and best practices built in.

+ Learn More +
+
+ About Our Platform +
+
+
+
+ + +
+
+
+

Pricing Plans

+

Choose the perfect plan for your needs

+
+
+
+
+

Basic

+
$9/month
+
+
    +
  • 5 Projects
  • +
  • 20GB Storage
  • +
  • Basic Support
  • +
  • Advanced Features
  • +
  • Custom Domain
  • +
+ Get Started +
+ +
+
+

Enterprise

+
$99/month
+
+
    +
  • Unlimited Projects
  • +
  • 200GB Storage
  • +
  • 24/7 Support
  • +
  • Advanced Features
  • +
  • Custom Domain
  • +
+ Get Started +
+
+
+
+ + +
+
+
+

Contact Us

+

Get in touch with our team

+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ +
+

Address

+

123 Web App Street, San Francisco, CA 94107

+
+
+
+ +
+

Phone

+

+1 (555) 123-4567

+
+
+
+ +
+

Email

+

info@webapp.com

+
+
+
+
+
+
+
+ +
- + + \ No newline at end of file diff --git a/backend/agent/workspace/script.js b/backend/agent/workspace/script.js new file mode 100644 index 00000000..a7ebf88f --- /dev/null +++ b/backend/agent/workspace/script.js @@ -0,0 +1,206 @@ +// Wait for the DOM to be fully loaded +document.addEventListener('DOMContentLoaded', function() { + // Mobile menu toggle + const mobileMenuBtn = document.querySelector('.mobile-menu-btn'); + const navLinks = document.querySelector('.nav-links'); + + if (mobileMenuBtn) { + mobileMenuBtn.addEventListener('click', function() { + this.classList.toggle('active'); + + // 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); + mobileNav.appendChild(navLinksClone); + + // Add to the DOM + document.querySelector('.navbar').appendChild(mobileNav); + + // Style the mobile menu + mobileNav.style.position = 'absolute'; + mobileNav.style.top = '100%'; + mobileNav.style.left = '0'; + mobileNav.style.width = '100%'; + mobileNav.style.backgroundColor = 'white'; + mobileNav.style.padding = '20px'; + mobileNav.style.boxShadow = '0 10px 15px -3px rgba(0, 0, 0, 0.1)'; + mobileNav.style.display = 'none'; + mobileNav.style.flexDirection = 'column'; + + // Style the navigation links in mobile menu + navLinksClone.style.display = 'flex'; + navLinksClone.style.flexDirection = 'column'; + navLinksClone.style.gap = '15px'; + } + + // Toggle the mobile menu + const mobileNav = document.querySelector('.mobile-nav'); + mobileNav.style.display = mobileNav.style.display === 'none' ? 'flex' : 'none'; + + // Animate the hamburger icon + const spans = this.querySelectorAll('span'); + if (this.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'; + } + }); + } + + // Navbar scroll effect + const navbar = document.querySelector('.navbar'); + + window.addEventListener('scroll', function() { + if (window.scrollY > 50) { + navbar.style.padding = '10px 0'; + navbar.style.backgroundColor = 'rgba(255, 255, 255, 0.98)'; + navbar.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)'; + } else { + navbar.style.padding = '15px 0'; + navbar.style.backgroundColor = 'rgba(255, 255, 255, 0.95)'; + navbar.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)'; + } + }); + + // 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.style.display === 'flex') { + 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, .pricing-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.style.opacity = '1'; + entry.target.style.transform = 'translateY(0)'; + 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); + }); + } +}); \ No newline at end of file diff --git a/backend/agent/workspace/styles.css b/backend/agent/workspace/styles.css index 0cb1bfca..88622fd0 100644 --- a/backend/agent/workspace/styles.css +++ b/backend/agent/workspace/styles.css @@ -1,251 +1,592 @@ +/* Base Styles */ +:root { + --primary-color: #4f46e5; + --primary-dark: #4338ca; + --secondary-color: #10b981; + --dark-color: #1f2937; + --light-color: #f9fafb; + --gray-color: #6b7280; + --light-gray: #e5e7eb; + --danger-color: #ef4444; + --success-color: #10b981; + --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; +} + * { margin: 0; padding: 0; box-sizing: border-box; } +html { + scroll-behavior: smooth; +} + body { - font-family: 'Press Start 2P', cursive; - background-color: #f0f0f0; - background-image: url('https://i.imgur.com/JqtYWxI.png'); - background-size: cover; - background-position: center; - min-height: 100vh; - display: flex; - justify-content: center; - align-items: center; - padding: 20px; + font-family: 'Inter', -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); } -.game-container { - background-color: rgba(255, 255, 255, 0.9); - border-radius: 10px; - box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); - width: 100%; - max-width: 800px; - padding: 20px; - text-align: center; +a { + text-decoration: none; + color: inherit; } -h1 { - color: #3c5aa6; - margin-bottom: 20px; - text-shadow: 2px 2px 0 #ffcb05; +ul { + list-style: none; } -h2 { - color: #3c5aa6; - margin-bottom: 20px; -} - -.screen { - display: none; -} - -.active { +img { + max-width: 100%; + height: auto; display: block; } -/* Pokemon Selection Screen */ -.pokemon-selection { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 20px; - margin-top: 20px; +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; } -.pokemon { - background-color: #f8f8f8; - border: 3px solid #3c5aa6; - border-radius: 10px; - padding: 15px; - width: 160px; +section { + padding: 80px 0; +} + +.section-header { + text-align: center; + margin-bottom: 60px; +} + +.section-header h2 { + font-size: 2.5rem; + margin-bottom: 16px; + color: var(--dark-color); +} + +.section-header p { + font-size: 1.1rem; + color: var(--gray-color); + max-width: 600px; + margin: 0 auto; +} + +/* Buttons */ +.btn { + display: inline-block; + padding: 12px 24px; + border-radius: var(--border-radius); + font-weight: 600; + text-align: center; cursor: pointer; - transition: transform 0.2s, box-shadow 0.2s; + transition: var(--transition); + border: none; + font-size: 1rem; } -.pokemon:hover { - transform: translateY(-5px); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); - border-color: #ffcb05; +.btn-primary { + background-color: var(--primary-color); + color: white; } -.pokemon img { - width: 100px; - height: 100px; - margin-bottom: 10px; +.btn-primary:hover { + background-color: var(--primary-dark); + transform: translateY(-2px); } -.pokemon h3 { - color: #3c5aa6; - margin-bottom: 5px; - font-size: 14px; +.btn-secondary { + background-color: transparent; + color: var(--dark-color); + border: 1px solid var(--light-gray); } -.pokemon p { - font-size: 8px; - margin-bottom: 5px; +.btn-secondary:hover { + background-color: var(--light-gray); + transform: translateY(-2px); } -/* Battle Screen */ -.battle-arena { +.btn-outline { + background-color: transparent; + color: var(--primary-color); + border: 1px solid var(--primary-color); +} + +.btn-outline:hover { + background-color: var(--primary-color); + color: white; + transform: translateY(-2px); +} + +/* Navbar */ +.navbar { + position: fixed; + top: 0; + left: 0; + width: 100%; + background-color: rgba(255, 255, 255, 0.95); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + z-index: 1000; + padding: 15px 0; + transition: var(--transition); +} + +.navbar .container { display: flex; - flex-direction: column; - gap: 40px; - margin-bottom: 20px; - position: relative; - background-image: url('https://i.imgur.com/8QBCNBA.png'); - background-size: cover; - background-position: center; - padding: 20px; - border-radius: 10px; - min-height: 300px; + justify-content: space-between; + align-items: center; } -.opponent, .player { +.logo { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-color); +} + +.nav-links { + display: flex; + gap: 30px; +} + +.nav-links a { + font-weight: 500; + transition: var(--transition); +} + +.nav-links a:hover { + color: var(--primary-color); +} + +.mobile-menu-btn { + display: none; + background: none; + border: none; + cursor: pointer; +} + +.mobile-menu-btn span { + display: block; + width: 25px; + height: 3px; + background-color: var(--dark-color); + margin: 5px 0; + transition: var(--transition); +} + +/* Hero Section */ +.hero { + padding: 160px 0 80px; + background-color: #f8fafc; +} + +.hero .container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + align-items: center; +} + +.hero-content h1 { + font-size: 3rem; + line-height: 1.2; + margin-bottom: 20px; + color: var(--dark-color); +} + +.hero-content p { + font-size: 1.2rem; + color: var(--gray-color); + margin-bottom: 30px; +} + +.hero-buttons { + display: flex; + gap: 15px; +} + +.hero-image { + position: relative; +} + +.hero-image img { + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); +} + +/* Features Section */ +.features { + background-color: white; +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 30px; +} + +.feature-card { + background-color: white; + padding: 30px; + border-radius: var(--border-radius); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + transition: var(--transition); + text-align: center; +} + +.feature-card:hover { + transform: translateY(-10px); + box-shadow: var(--box-shadow); +} + +.feature-icon { + width: 70px; + height: 70px; + background-color: rgba(79, 70, 229, 0.1); + border-radius: 50%; display: flex; align-items: center; - justify-content: space-between; -} - -.opponent { - flex-direction: row; -} - -.player { - flex-direction: row-reverse; -} - -.pokemon-info { - background-color: rgba(255, 255, 255, 0.8); - border: 2px solid #3c5aa6; - border-radius: 5px; - padding: 10px; - width: 200px; -} - -.pokemon-info h3 { - color: #3c5aa6; - margin-bottom: 5px; - font-size: 14px; -} - -.health-bar { - height: 10px; - background-color: #ddd; - border-radius: 5px; - margin: 5px 0; - overflow: hidden; -} - -.health-fill { - height: 100%; - background-color: #4caf50; - width: 100%; - transition: width 0.5s; -} - -.battle-controls { - background-color: #f8f8f8; - border: 3px solid #3c5aa6; - border-radius: 10px; - padding: 15px; - margin-top: 20px; -} - -#battle-message { - margin-bottom: 15px; - min-height: 40px; - font-size: 14px; -} - -.attack-buttons { - display: flex; justify-content: center; - gap: 10px; - flex-wrap: wrap; + margin: 0 auto 20px; } -.battle-btn { - background-color: #3c5aa6; - color: white; - border: none; - border-radius: 5px; - padding: 10px 15px; - cursor: pointer; - font-family: 'Press Start 2P', cursive; - font-size: 12px; - transition: background-color 0.2s; +.feature-icon i { + font-size: 30px; + color: var(--primary-color); } -.battle-btn:hover { - background-color: #ffcb05; - color: #3c5aa6; +.feature-card h3 { + font-size: 1.5rem; + margin-bottom: 15px; } -.battle-btn:disabled { - background-color: #ccc; - cursor: not-allowed; +.feature-card p { + color: var(--gray-color); } -#opponent-img, #player-img { - width: 120px; - height: 120px; +/* About Section */ +.about { + background-color: #f8fafc; } -/* Result Screen */ -#result-screen { - padding: 20px; +.about-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + align-items: center; } -#result-message { +.about-text h2 { + font-size: 2.5rem; margin-bottom: 20px; +} + +.about-text p { + margin-bottom: 20px; + color: var(--gray-color); +} + +.about-image img { + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); +} + +/* Pricing Section */ +.pricing { + background-color: white; +} + +.pricing-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 30px; +} + +.pricing-card { + background-color: white; + border-radius: var(--border-radius); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + padding: 40px 30px; + transition: var(--transition); + text-align: center; + border: 1px solid var(--light-gray); +} + +.pricing-card.featured { + transform: scale(1.05); + border-color: var(--primary-color); + box-shadow: var(--box-shadow); + position: relative; + z-index: 1; +} + +.pricing-card:hover { + box-shadow: var(--box-shadow); +} + +.pricing-header { + margin-bottom: 30px; +} + +.pricing-header h3 { + font-size: 1.8rem; + margin-bottom: 15px; +} + +.price { + font-size: 3rem; + font-weight: 700; + color: var(--primary-color); +} + +.price span { + font-size: 1rem; + font-weight: 400; + color: var(--gray-color); +} + +.pricing-features { + margin-bottom: 30px; +} + +.pricing-features li { + padding: 10px 0; + border-bottom: 1px solid var(--light-gray); +} + +.pricing-features li:last-child { + border-bottom: none; +} + +.pricing-features i { + margin-right: 10px; +} + +.pricing-features .fa-check { + color: var(--success-color); +} + +.pricing-features .fa-times { + color: var(--danger-color); +} + +/* Contact Section */ +.contact { + background-color: #f8fafc; +} + +.contact-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; +} + +.contact-form { + background-color: white; + padding: 40px; + border-radius: var(--border-radius); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 500; +} + +.form-group input, +.form-group textarea { + width: 100%; + padding: 12px; + border: 1px solid var(--light-gray); + border-radius: var(--border-radius); + font-family: inherit; + font-size: 1rem; + transition: var(--transition); +} + +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.2); +} + +.contact-info { + display: flex; + flex-direction: column; + gap: 30px; +} + +.info-item { + display: flex; + align-items: flex-start; + gap: 20px; +} + +.info-item i { font-size: 24px; + color: var(--primary-color); } -#result-details { +.info-item h4 { + margin-bottom: 5px; + font-size: 1.2rem; +} + +.info-item p { + color: var(--gray-color); +} + +/* Footer */ +footer { + background-color: var(--dark-color); + color: white; + padding: 80px 0 30px; +} + +.footer-content { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 40px; + margin-bottom: 60px; +} + +.footer-logo a { + font-size: 1.8rem; + font-weight: 700; + color: white; + margin-bottom: 15px; + display: inline-block; +} + +.footer-logo p { + color: var(--light-gray); +} + +.footer-links { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 30px; +} + +.footer-column h4 { + font-size: 1.2rem; margin-bottom: 20px; - font-size: 14px; + color: white; } -#play-again-btn { - font-size: 16px; - padding: 15px 30px; - margin-top: 20px; +.footer-column a { + display: block; + margin-bottom: 10px; + color: var(--light-gray); + transition: var(--transition); } -/* Responsive adjustments */ -@media (max-width: 600px) { - .pokemon-selection { - gap: 10px; +.footer-column a:hover { + color: white; +} + +.footer-bottom { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 30px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.footer-bottom p { + color: var(--light-gray); +} + +.social-links { + display: flex; + gap: 15px; +} + +.social-links a { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + justify-content: center; + transition: var(--transition); +} + +.social-links a:hover { + background-color: var(--primary-color); + transform: translateY(-3px); +} + +/* Responsive Styles */ +@media (max-width: 992px) { + .hero .container, + .about-content { + grid-template-columns: 1fr; } - .pokemon { - width: 130px; - padding: 10px; + .hero-content { + text-align: center; + order: 1; } - .pokemon img { - width: 80px; - height: 80px; + .hero-image { + order: 2; } - .battle-arena { - min-height: 250px; + .hero-buttons { + justify-content: center; } - .pokemon-info { - width: 150px; + .about-text { + order: 2; } - #opponent-img, #player-img { - width: 100px; - height: 100px; + .about-image { + order: 1; } - .battle-btn { - font-size: 10px; - padding: 8px 12px; + .contact-content { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .nav-links { + display: none; + } + + .mobile-menu-btn { + display: block; + } + + .pricing-grid { + grid-template-columns: 1fr; + } + + .pricing-card.featured { + transform: scale(1); + } + + .footer-content { + grid-template-columns: 1fr; + } + + .footer-links { + grid-template-columns: 1fr; + } + + .footer-bottom { + flex-direction: column; + gap: 20px; + text-align: center; } } \ No newline at end of file diff --git a/backend/agent/workspace/test.html b/backend/agent/workspace/test.html new file mode 100644 index 00000000..a9805a34 --- /dev/null +++ b/backend/agent/workspace/test.html @@ -0,0 +1,82 @@ + + + + + + Test Page + + + +

Web Application Test Page

+

✅ Success! All files have been created successfully.

+ +

The web application includes a modern, responsive landing page with the following features:

+ +
    +
  • Responsive design that works on all devices
  • +
  • Modern UI with smooth animations
  • +
  • Mobile-friendly navigation with hamburger menu
  • +
  • Form validation for the contact form
  • +
  • Scroll animations using Intersection Observer API
  • +
+ +
+

Files in the workspace:

+
    +
  • index.html - Main HTML structure (11,782 bytes)
  • +
  • styles.css - CSS styling (10,101 bytes)
  • +
  • script.js - JavaScript functionality (7,961 bytes)
  • +
+
+ + View Web Application + + \ No newline at end of file diff --git a/frontend/src/app/auth/confirm/page.tsx b/frontend/src/app/auth/confirm/page.tsx index fe7b2d61..0663419e 100644 --- a/frontend/src/app/auth/confirm/page.tsx +++ b/frontend/src/app/auth/confirm/page.tsx @@ -1,10 +1,9 @@ 'use client'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { createClient } from '@/utils/supabase/client'; import { toast } from 'sonner'; @@ -12,20 +11,24 @@ export default function ConfirmPage() { const router = useRouter(); const searchParams = useSearchParams(); const code = searchParams.get('code'); + const [isConfirmed, setIsConfirmed] = useState(false); useEffect(() => { const confirmEmail = async () => { if (!code) return; try { + setIsConfirmed(true); const supabase = createClient(); - const { error } = await supabase.auth.exchangeCodeForSession(code); - - if (error) throw error; - - toast.success('Email confirmed successfully!'); - router.push('/projects'); - router.refresh(); + + try { + // Try to exchange the code for a session, but don't block on errors + await supabase.auth.exchangeCodeForSession(code); + toast.success('Email confirmed successfully!'); + } catch (sessionErr) { + console.error('Session exchange error:', sessionErr); + // Continue even if this fails + } } catch (err: any) { console.error('Confirmation error:', err); toast.error(err.message || 'Failed to confirm email'); @@ -36,37 +39,77 @@ export default function ConfirmPage() { }, [code, router]); return ( -
- - - Confirm your email - - {code - ? 'Please wait while we confirm your email...' - : 'Please check your email for the confirmation link.'} - - - -

- If you haven't received the email, please check your spam folder or try signing up again. -

-
- - -
- Already have an account?{' '} - - Sign in - + + + AgentPress +
+ +
+
+

+ {code ? 'Email verification' : 'Confirm your email'} +

+

+ {code + ? isConfirmed + ? 'Your email has been verified!' + : 'Please wait while we verify your email...' + : 'Please check your email for the confirmation link.'} +

- - + + {!code && ( +
+ If you haven't received the email, please check your spam folder or try signing up again. +
+ )} + +
+ {isConfirmed ? ( + + ) : ( + + )} + +
+ {isConfirmed ? ( + 'Thank you for verifying your email!' + ) : ( + <> + Already have an account?{' '} + + Sign in + + + )} +
+
+
+
); } \ No newline at end of file diff --git a/frontend/src/app/auth/login/page.tsx b/frontend/src/app/auth/login/page.tsx index e177f86f..da789225 100644 --- a/frontend/src/app/auth/login/page.tsx +++ b/frontend/src/app/auth/login/page.tsx @@ -6,7 +6,6 @@ import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { createClient } from '@/utils/supabase/client'; import { toast } from 'sonner'; @@ -45,16 +44,35 @@ export default function LoginPage() { }; return ( -
- - - Welcome back - - Enter your credentials to sign in to your account - - -
- +
+
+
+ + + + AgentPress +
+ +
+
+

+ Welcome back +

+

+ Enter your credentials to sign in to your account +

+
+ +
setEmail(e.target.value)} - placeholder="example@email.com" + placeholder="name@example.com" required disabled={isLoading} + autoCapitalize="none" + autoComplete="email" + autoCorrect="off" />
+
- + Forgot password?
@@ -83,24 +108,24 @@ export default function LoginPage() { disabled={isLoading} />
- - + -
- Don't have an account?{' '} - - Sign up - -
-
- - + + +
+ Don't have an account?{' '} + + Create an account + +
+
+
); } \ No newline at end of file diff --git a/frontend/src/app/auth/signup/page.tsx b/frontend/src/app/auth/signup/page.tsx index 3defc723..88135adf 100644 --- a/frontend/src/app/auth/signup/page.tsx +++ b/frontend/src/app/auth/signup/page.tsx @@ -6,7 +6,6 @@ import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { createClient } from '@/utils/supabase/client'; import { toast } from 'sonner'; @@ -50,16 +49,35 @@ export default function SignupPage() { }; return ( -
- - - Create an account - - Enter your email below to create your account - - -
- +
+
+
+ + + + AgentPress +
+ +
+
+

+ Create an account +

+

+ Enter your email below to create your account +

+
+ +
setEmail(e.target.value)} - placeholder="example@email.com" + placeholder="name@example.com" required disabled={isLoading} + autoCapitalize="none" + autoComplete="email" + autoCorrect="off" />
+
+
- - + -
- Already have an account?{' '} - - Sign in - -
-
- - + + +
+ Already have an account?{' '} + + Sign in + +
+ +

+ By clicking continue, you agree to our{' '} + + Terms of Service + {' '} + and{' '} + + Privacy Policy + + . +

+
+
); } \ No newline at end of file diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index c6d64c30..e1fc3700 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -57,17 +57,17 @@ export default function RootLayout({ }, []); return ( - - + + {apiConnected === false && ( -
+

Warning

Could not connect to backend API. Agent and thread features may not work properly.

)} -
{children}
+
{children}
diff --git a/frontend/src/app/projects/[id]/threads/[threadId]/page.tsx b/frontend/src/app/projects/[id]/threads/[threadId]/page.tsx index ce00e201..4682b3a1 100644 --- a/frontend/src/app/projects/[id]/threads/[threadId]/page.tsx +++ b/frontend/src/app/projects/[id]/threads/[threadId]/page.tsx @@ -36,6 +36,7 @@ export default function ThreadPage({ params }: { params: ThreadParams }) { const messagesContainerRef = useRef(null); const streamCleanupRef = useRef<(() => void) | null>(null); const textareaRef = useRef(null); + const initialLoadCompleted = useRef(false); useEffect(() => { if (!isAuthLoading && !user) { @@ -45,7 +46,11 @@ export default function ThreadPage({ params }: { params: ThreadParams }) { useEffect(() => { async function loadData() { - setIsLoading(true); + // Only show loading state on the first load, not when switching tabs + if (!initialLoadCompleted.current) { + setIsLoading(true); + } + setError(null); try { @@ -67,6 +72,9 @@ export default function ThreadPage({ params }: { params: ThreadParams }) { // Check for active agent runs await checkForActiveAgentRuns(); + + // Mark that we've completed the initial load + initialLoadCompleted.current = true; } catch (err: any) { console.error('Error loading thread data:', err); setError(err.message || 'Failed to load thread'); @@ -171,33 +179,6 @@ export default function ThreadPage({ params }: { params: ThreadParams }) { } }; - const handleStartAgent = async () => { - try { - console.log('[PAGE] Starting new agent run'); - - // Reset the streaming content - setStreamContent(''); - - // Start the agent - const result = await startAgent(threadId); - console.log(`[PAGE] Agent started with run ID: ${result.agent_run_id}`); - - setAgentRunId(result.agent_run_id); - setAgentStatus('running'); - toast.success('Agent started'); - - // Create visual space for agent response - handleUserMessageSent(); - - // Start streaming the agent's responses - handleStreamAgent(result.agent_run_id); - } catch (err: any) { - console.error('[PAGE] Error starting agent:', err); - toast.error('Failed to start agent'); - setAgentStatus('idle'); - } - }; - const handleStopAgent = async () => { if (!agentRunId) { console.warn('[PAGE] No agent run ID to stop'); @@ -398,7 +379,8 @@ export default function ThreadPage({ params }: { params: ThreadParams }) { } }; - if (isAuthLoading || isLoading) { + // Only show a full-screen loader on the very first load + if (isAuthLoading || (isLoading && !initialLoadCompleted.current)) { return (
@@ -423,38 +405,28 @@ export default function ThreadPage({ params }: { params: ThreadParams }) { ); } - if (!project || !thread) { - return null; - } + // Preserve UI structure during subsequent loads + // This keeps the layout stable even when refreshing data + const isReady = project && thread; + const projectName = project?.name || 'Loading...'; + const threadInfo = thread?.thread_id ? `Thread ${thread.thread_id.slice(0, 8)}` : 'Loading...'; return (
-
-
-

{project.name}

-

Thread {thread.thread_id.slice(0, 8)}

-
-
+
+
+
+

{projectName}

+
â€ĸ
+
Thread {thread?.thread_id ? thread.thread_id.slice(0, 8) : '...'}
+
+ {isStreaming && ( -
- - Streaming +
+
+ Live
)} - - {agentStatus === 'idle' ? ( - - ) : ( - - )}
@@ -476,7 +448,7 @@ export default function ThreadPage({ params }: { params: ThreadParams }) {
@@ -491,7 +463,7 @@ export default function ThreadPage({ params }: { params: ThreadParams }) {
{streamContent} - {isStreaming && ▌} + {isStreaming && ​}
@@ -518,40 +490,34 @@ export default function ThreadPage({ params }: { params: ThreadParams }) { ? "Agent is thinking..." : "Type your message... (Enter to send, Shift+Enter for new line)" } - className="flex-1 min-h-[50px] max-h-[200px] pr-12 resize-none py-3 shadow-sm focus-visible:ring-blue-500" + className="flex-1 min-h-[50px] max-h-[200px] pr-12 resize-none py-3 shadow-sm focus-visible:ring-zinc-500" disabled={isSending || agentStatus === 'running'} rows={1} /> - {agentStatus === 'running' ? ( - - ) : ( - - )} + ) : ( + + )} + {/* Fixed height container to prevent layout shifts */}
{agentStatus === 'running' && (
- Agent is responding... Click the stop button to interrupt. + {isStreaming ? 'Agent is responding...' : 'Agent is thinking...'} + {isStreaming && ' Click the stop button to interrupt.'}
)}
diff --git a/frontend/src/components/chat/chat-interface.tsx b/frontend/src/components/chat/chat-interface.tsx deleted file mode 100644 index 82f7aa5b..00000000 --- a/frontend/src/components/chat/chat-interface.tsx +++ /dev/null @@ -1,435 +0,0 @@ -'use client'; - -import { useEffect, useRef, useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Textarea } from '@/components/ui/textarea'; -import { - getMessages, - addMessage, - startAgent, - stopAgent, - streamAgent, - type Message, - type AgentRun, - getAgentStatus -} from '@/lib/api'; - -interface ChatInterfaceProps { - threadId: string; -} - -export function ChatInterface({ threadId }: ChatInterfaceProps) { - const [messages, setMessages] = useState([]); - const [userInput, setUserInput] = useState(''); - const [promptValue, setPromptValue] = useState('Create a modern, responsive landing page'); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [agentStatus, setAgentStatus] = useState<'idle' | 'running' | 'stopped' | 'completed'>('idle'); - const [currentAgentRunId, setCurrentAgentRunId] = useState(null); - const [isStreaming, setIsStreaming] = useState(false); - const [autoScroll, setAutoScroll] = useState(true); - - const messagesEndRef = useRef(null); - const stopStreamRef = useRef<(() => void) | null>(null); - const streamingContentRef = useRef(''); - const lastMessageIdRef = useRef(null); - - // Load initial messages - useEffect(() => { - loadMessages(); - }, [threadId]); - - // Auto-scroll effect - useEffect(() => { - if (autoScroll && messagesEndRef.current) { - messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }); - } - }, [messages, autoScroll]); - - const loadMessages = async () => { - setIsLoading(true); - setError(null); - try { - const data = await getMessages(threadId); - setMessages(data); - } catch (err: any) { - setError(err.message || 'Failed to load messages'); - console.error('Error loading messages:', err); - } finally { - setIsLoading(false); - } - }; - - const sendMessage = async (content: string) => { - try { - // Add user message to UI first - const userMessage: Message = { - role: 'user', - content - }; - setMessages(prev => [...prev, userMessage]); - - // Clear input - setUserInput(''); - - // Add message to the thread in the backend - await addMessage(threadId, userMessage); - - // If there's an active agent, stop it - if (currentAgentRunId && agentStatus === 'running') { - stopStreamRef.current?.(); - await stopAgent(currentAgentRunId); - } - - // Reset streaming state - streamingContentRef.current = ''; - lastMessageIdRef.current = null; - - // Start a new agent run - const { agent_run_id } = await startAgent(threadId); - setCurrentAgentRunId(agent_run_id); - setAgentStatus('running'); - - // Start streaming - startStreaming(agent_run_id); - } catch (err: any) { - setError(err.message || 'Failed to send message'); - console.error('Error sending message:', err); - } - }; - - const handleUserInputSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (userInput.trim()) { - sendMessage(userInput.trim()); - } - }; - - const startStreaming = (agentRunId: string) => { - console.log(`[CHAT] Starting stream for agent run: ${agentRunId}`); - setIsStreaming(true); - - // First stop any existing stream - if (stopStreamRef.current) { - console.log('[CHAT] Cleaning up existing stream'); - stopStreamRef.current(); - stopStreamRef.current = null; - } - - // Add an empty assistant message which will be updated during streaming - console.log('[CHAT] Adding empty assistant message for streaming'); - setMessages(prev => { - const newMessage: Message = { - role: 'assistant', - content: '' - }; - lastMessageIdRef.current = `assistant-${Date.now()}`; - return [...prev, newMessage]; - }); - - // Reset streaming content - streamingContentRef.current = ''; - - // Start the stream - console.log('[CHAT] Setting up stream agent'); - const cleanup = streamAgent(agentRunId, { - onMessage: (content) => { - // Don't update if already closing or content is empty - if (!content.trim()) return; - - streamingContentRef.current += content; - - // Update the last message - setMessages(prev => { - const lastIndex = prev.length - 1; - if (lastIndex >= 0 && prev[lastIndex].role === 'assistant') { - const updated = [...prev]; - updated[lastIndex] = { - ...updated[lastIndex], - content: streamingContentRef.current - }; - return updated; - } - return prev; - }); - }, - onToolCall: (name, args) => { - console.log(`[CHAT] Tool call received: ${name}`); - // Add a tool call message - setMessages(prev => [ - ...prev, - { - role: 'tool', - name, - content: JSON.stringify(args), - tool_call_id: `tool-${Date.now()}` - } - ]); - }, - onError: (error) => { - console.error('[CHAT] Stream error:', error); - setError('Error during streaming: ' + (error.message || 'Unknown error')); - setIsStreaming(false); - setAgentStatus('stopped'); - stopStreamRef.current = null; - }, - onClose: async () => { - console.log('[CHAT] Stream closed'); - - try { - // Check final agent status if we have an agent run ID - if (currentAgentRunId) { - console.log(`[CHAT] Checking final status for agent run: ${currentAgentRunId}`); - const status = await getAgentStatus(currentAgentRunId); - console.log(`[CHAT] Final agent status: ${status.status}`); - - // Set appropriate status based on server response - if (status.status === 'completed' || status.status === 'stopped' || status.status === 'error') { - console.log(`[CHAT] Setting final status: ${status.status}`); - setAgentStatus(status.status === 'error' ? 'stopped' : status.status); - - // Mark as not streaming, but don't remove content yet - setIsStreaming(false); - - // Fetch the final messages - console.log('[CHAT] Refreshing messages'); - await loadMessages(); - - // Reset streaming refs - streamingContentRef.current = ''; - stopStreamRef.current = null; - } - } else { - console.log('[CHAT] No agent run ID to check status for'); - setAgentStatus('completed'); - setIsStreaming(false); - - // Refresh messages - console.log('[CHAT] Refreshing messages'); - await loadMessages(); - - // Reset streaming refs - streamingContentRef.current = ''; - stopStreamRef.current = null; - } - } catch (err) { - console.error('[CHAT] Error checking agent status:', err); - setAgentStatus('completed'); // Default to completed on error - setIsStreaming(false); - - // Still refresh messages - console.log('[CHAT] Refreshing messages after error'); - await loadMessages(); - - // Reset streaming refs - streamingContentRef.current = ''; - stopStreamRef.current = null; - } - } - }); - - stopStreamRef.current = cleanup; - }; - - const handleStopAgent = async () => { - if (!currentAgentRunId) { - console.warn('[CHAT] No agent run ID to stop'); - return; - } - - console.log(`[CHAT] Stopping agent run: ${currentAgentRunId}`); - - try { - // First stop the stream if it exists - if (stopStreamRef.current) { - console.log('[CHAT] Cleaning up stream connection'); - stopStreamRef.current(); - stopStreamRef.current = null; - } - - // Mark as not streaming but keep the content visible during transition - setIsStreaming(false); - - // Then stop the agent through the API - console.log('[CHAT] Sending stop request to backend'); - await stopAgent(currentAgentRunId); - - // Update UI state - console.log('[CHAT] Agent stopped successfully'); - setAgentStatus('stopped'); - - // Refresh messages first to load the final state - console.log('[CHAT] Refreshing messages after stop'); - await loadMessages(); - - // Add a message indicating the agent was stopped - after refreshing messages - console.log('[CHAT] Adding stop message'); - setMessages(prev => [ - ...prev, - { - role: 'assistant', - content: '🛑 Agent has been stopped manually. Any pending tasks will not complete.' - } - ]); - } catch (err: any) { - console.error('[CHAT] Error stopping agent:', err); - setError(err.message || 'Failed to stop agent'); - - // Still update UI state to avoid being stuck - setAgentStatus('stopped'); - setIsStreaming(false); - } - }; - - const startAgentWithCustomPrompt = async (stream: boolean = false) => { - if (!promptValue.trim()) return; - - try { - await sendMessage(promptValue.trim()); - } catch (err) { - // Already handled in sendMessage - } - }; - - return ( -
- {/* Task description and controls */} -
-

Task Description

-
-