mirror of https://github.com/kortix-ai/suna.git
fe wip
This commit is contained in:
parent
611a52d2c2
commit
27a6e3735c
|
@ -3,90 +3,255 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pokemon Battle Game</title>
|
||||
<title>Modern Web Application</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<h1>Pokemon Battle Game</h1>
|
||||
|
||||
<div id="start-screen" class="screen active">
|
||||
<h2>Choose Your Pokemon</h2>
|
||||
<div class="pokemon-selection">
|
||||
<div class="pokemon" data-pokemon="pikachu">
|
||||
<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png" alt="Pikachu">
|
||||
<h3>Pikachu</h3>
|
||||
<p>Type: Electric</p>
|
||||
<p>HP: 100</p>
|
||||
<p>Attack: 55</p>
|
||||
<header>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="#" class="logo">WebApp</a>
|
||||
<div class="nav-links">
|
||||
<a href="#features">Features</a>
|
||||
<a href="#about">About</a>
|
||||
<a href="#pricing">Pricing</a>
|
||||
<a href="#contact">Contact</a>
|
||||
</div>
|
||||
<div class="pokemon" data-pokemon="charmander">
|
||||
<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/4.png" alt="Charmander">
|
||||
<h3>Charmander</h3>
|
||||
<p>Type: Fire</p>
|
||||
<p>HP: 90</p>
|
||||
<p>Attack: 60</p>
|
||||
<button class="mobile-menu-btn">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
<h1>Build Amazing Web Applications</h1>
|
||||
<p>A modern, responsive web application template to kickstart your next project.</p>
|
||||
<div class="hero-buttons">
|
||||
<a href="#features" class="btn btn-primary">Get Started</a>
|
||||
<a href="#" class="btn btn-secondary">Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pokemon" data-pokemon="squirtle">
|
||||
<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/7.png" alt="Squirtle">
|
||||
<h3>Squirtle</h3>
|
||||
<p>Type: Water</p>
|
||||
<p>HP: 110</p>
|
||||
<p>Attack: 50</p>
|
||||
</div>
|
||||
<div class="pokemon" data-pokemon="bulbasaur">
|
||||
<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png" alt="Bulbasaur">
|
||||
<h3>Bulbasaur</h3>
|
||||
<p>Type: Grass</p>
|
||||
<p>HP: 105</p>
|
||||
<p>Attack: 52</p>
|
||||
<div class="hero-image">
|
||||
<img src="https://via.placeholder.com/600x400" alt="Web Application Dashboard">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="battle-screen" class="screen">
|
||||
<div class="battle-arena">
|
||||
<div class="opponent">
|
||||
<div class="pokemon-info">
|
||||
<h3 id="opponent-name">Opponent</h3>
|
||||
<div class="health-bar">
|
||||
<div id="opponent-health" class="health-fill"></div>
|
||||
</div>
|
||||
<p id="opponent-hp">HP: 100/100</p>
|
||||
</div>
|
||||
<img id="opponent-img" src="" alt="Opponent Pokemon">
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" class="features">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2>Key Features</h2>
|
||||
<p>Everything you need to build modern web applications</p>
|
||||
</div>
|
||||
|
||||
<div class="player">
|
||||
<img id="player-img" src="" alt="Player Pokemon">
|
||||
<div class="pokemon-info">
|
||||
<h3 id="player-name">Player</h3>
|
||||
<div class="health-bar">
|
||||
<div id="player-health" class="health-fill"></div>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-mobile-alt"></i>
|
||||
</div>
|
||||
<p id="player-hp">HP: 100/100</p>
|
||||
<h3>Responsive Design</h3>
|
||||
<p>Looks great on all devices, from mobile phones to desktop computers.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-bolt"></i>
|
||||
</div>
|
||||
<h3>Fast Performance</h3>
|
||||
<p>Optimized for speed and efficiency to provide the best user experience.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-code"></i>
|
||||
</div>
|
||||
<h3>Clean Code</h3>
|
||||
<p>Well-structured, maintainable code following best practices.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-paint-brush"></i>
|
||||
</div>
|
||||
<h3>Modern UI</h3>
|
||||
<p>Beautiful, intuitive user interface with smooth animations.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="battle-controls">
|
||||
<div id="battle-message">What will you do?</div>
|
||||
<div class="attack-buttons">
|
||||
<button id="attack-btn" class="battle-btn">Attack</button>
|
||||
<button id="special-attack-btn" class="battle-btn">Special Attack</button>
|
||||
<button id="heal-btn" class="battle-btn">Heal</button>
|
||||
</section>
|
||||
|
||||
<!-- About Section -->
|
||||
<section id="about" class="about">
|
||||
<div class="container">
|
||||
<div class="about-content">
|
||||
<div class="about-text">
|
||||
<h2>About Our Platform</h2>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<a href="#" class="btn btn-primary">Learn More</a>
|
||||
</div>
|
||||
<div class="about-image">
|
||||
<img src="https://via.placeholder.com/500x400" alt="About Our Platform">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pricing Section -->
|
||||
<section id="pricing" class="pricing">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2>Pricing Plans</h2>
|
||||
<p>Choose the perfect plan for your needs</p>
|
||||
</div>
|
||||
<div class="pricing-grid">
|
||||
<div class="pricing-card">
|
||||
<div class="pricing-header">
|
||||
<h3>Basic</h3>
|
||||
<div class="price">$9<span>/month</span></div>
|
||||
</div>
|
||||
<ul class="pricing-features">
|
||||
<li><i class="fas fa-check"></i> 5 Projects</li>
|
||||
<li><i class="fas fa-check"></i> 20GB Storage</li>
|
||||
<li><i class="fas fa-check"></i> Basic Support</li>
|
||||
<li><i class="fas fa-times"></i> Advanced Features</li>
|
||||
<li><i class="fas fa-times"></i> Custom Domain</li>
|
||||
</ul>
|
||||
<a href="#" class="btn btn-outline">Get Started</a>
|
||||
</div>
|
||||
<div class="pricing-card featured">
|
||||
<div class="pricing-header">
|
||||
<h3>Pro</h3>
|
||||
<div class="price">$29<span>/month</span></div>
|
||||
</div>
|
||||
<ul class="pricing-features">
|
||||
<li><i class="fas fa-check"></i> 15 Projects</li>
|
||||
<li><i class="fas fa-check"></i> 50GB Storage</li>
|
||||
<li><i class="fas fa-check"></i> Priority Support</li>
|
||||
<li><i class="fas fa-check"></i> Advanced Features</li>
|
||||
<li><i class="fas fa-times"></i> Custom Domain</li>
|
||||
</ul>
|
||||
<a href="#" class="btn btn-primary">Get Started</a>
|
||||
</div>
|
||||
<div class="pricing-card">
|
||||
<div class="pricing-header">
|
||||
<h3>Enterprise</h3>
|
||||
<div class="price">$99<span>/month</span></div>
|
||||
</div>
|
||||
<ul class="pricing-features">
|
||||
<li><i class="fas fa-check"></i> Unlimited Projects</li>
|
||||
<li><i class="fas fa-check"></i> 200GB Storage</li>
|
||||
<li><i class="fas fa-check"></i> 24/7 Support</li>
|
||||
<li><i class="fas fa-check"></i> Advanced Features</li>
|
||||
<li><i class="fas fa-check"></i> Custom Domain</li>
|
||||
</ul>
|
||||
<a href="#" class="btn btn-outline">Get Started</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Contact Section -->
|
||||
<section id="contact" class="contact">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2>Contact Us</h2>
|
||||
<p>Get in touch with our team</p>
|
||||
</div>
|
||||
<div class="contact-content">
|
||||
<div class="contact-form">
|
||||
<form id="contactForm">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="message">Message</label>
|
||||
<textarea id="message" name="message" rows="5" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Send Message</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="contact-info">
|
||||
<div class="info-item">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<div>
|
||||
<h4>Address</h4>
|
||||
<p>123 Web App Street, San Francisco, CA 94107</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<i class="fas fa-phone"></i>
|
||||
<div>
|
||||
<h4>Phone</h4>
|
||||
<p>+1 (555) 123-4567</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<div>
|
||||
<h4>Email</h4>
|
||||
<p>info@webapp.com</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-logo">
|
||||
<a href="#">WebApp</a>
|
||||
<p>Building the future of web applications.</p>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<div class="footer-column">
|
||||
<h4>Product</h4>
|
||||
<a href="#">Features</a>
|
||||
<a href="#">Pricing</a>
|
||||
<a href="#">Documentation</a>
|
||||
<a href="#">Updates</a>
|
||||
</div>
|
||||
<div class="footer-column">
|
||||
<h4>Company</h4>
|
||||
<a href="#">About</a>
|
||||
<a href="#">Team</a>
|
||||
<a href="#">Careers</a>
|
||||
<a href="#">Contact</a>
|
||||
</div>
|
||||
<div class="footer-column">
|
||||
<h4>Resources</h4>
|
||||
<a href="#">Blog</a>
|
||||
<a href="#">Newsletter</a>
|
||||
<a href="#">Events</a>
|
||||
<a href="#">Help Center</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2023 WebApp. All rights reserved.</p>
|
||||
<div class="social-links">
|
||||
<a href="#"><i class="fab fa-twitter"></i></a>
|
||||
<a href="#"><i class="fab fa-facebook-f"></i></a>
|
||||
<a href="#"><i class="fab fa-instagram"></i></a>
|
||||
<a href="#"><i class="fab fa-github"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="result-screen" class="screen">
|
||||
<h2 id="result-message">Battle Result</h2>
|
||||
<div id="result-details"></div>
|
||||
<button id="play-again-btn" class="battle-btn">Play Again</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</footer>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -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 = `
|
||||
<div style="text-align: center; padding: 40px 0;">
|
||||
<i class="fas fa-check-circle" style="font-size: 48px; color: #10b981; margin-bottom: 20px;"></i>
|
||||
<h3>Thank You!</h3>
|
||||
<p>Your message has been sent successfully. We'll get back to you soon.</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Page</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
color: #1f2937;
|
||||
}
|
||||
h1 {
|
||||
color: #4f46e5;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.success {
|
||||
color: #10b981;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
background-color: #4f46e5;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.button:hover {
|
||||
background-color: #4338ca;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.file-list {
|
||||
background-color: #f8fafc;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.file-list li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.file-list strong {
|
||||
color: #4f46e5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Web Application Test Page</h1>
|
||||
<p class="success">✅ Success! All files have been created successfully.</p>
|
||||
|
||||
<p>The web application includes a modern, responsive landing page with the following features:</p>
|
||||
|
||||
<ul>
|
||||
<li>Responsive design that works on all devices</li>
|
||||
<li>Modern UI with smooth animations</li>
|
||||
<li>Mobile-friendly navigation with hamburger menu</li>
|
||||
<li>Form validation for the contact form</li>
|
||||
<li>Scroll animations using Intersection Observer API</li>
|
||||
</ul>
|
||||
|
||||
<div class="file-list">
|
||||
<h2>Files in the workspace:</h2>
|
||||
<ul>
|
||||
<li><strong>index.html</strong> - Main HTML structure (11,782 bytes)</li>
|
||||
<li><strong>styles.css</strong> - CSS styling (10,101 bytes)</li>
|
||||
<li><strong>script.js</strong> - JavaScript functionality (7,961 bytes)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a href="index.html" class="button">View Web Application</a>
|
||||
</body>
|
||||
</html>
|
|
@ -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 (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
<Card className="w-full max-w-md border-border">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-2xl font-bold">Confirm your email</CardTitle>
|
||||
<CardDescription>
|
||||
{code
|
||||
? 'Please wait while we confirm your email...'
|
||||
: 'Please check your email for the confirmation link.'}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
If you haven't received the email, please check your spam folder or try signing up again.
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col space-y-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => router.push('/auth/signup')}
|
||||
<div className="flex-1 flex flex-col items-center justify-center h-full py-8">
|
||||
<div className="w-full max-w-md mx-auto px-8">
|
||||
<div className="flex items-center justify-center mb-8">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-2 h-8 w-8"
|
||||
>
|
||||
Try again
|
||||
</Button>
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Already have an account?{' '}
|
||||
<Link href="/auth/login" className="text-primary hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
||||
</svg>
|
||||
<span className="text-2xl font-bold">AgentPress</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-lg border shadow-sm">
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
{code ? 'Email verification' : 'Confirm your email'}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{code
|
||||
? isConfirmed
|
||||
? 'Your email has been verified!'
|
||||
: 'Please wait while we verify your email...'
|
||||
: 'Please check your email for the confirmation link.'}
|
||||
</p>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{!code && (
|
||||
<div className="text-sm text-muted-foreground mb-6">
|
||||
If you haven't received the email, please check your spam folder or try signing up again.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col space-y-3">
|
||||
{isConfirmed ? (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => router.push('/auth/login')}
|
||||
>
|
||||
Continue to sign in
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => router.push('/auth/signup')}
|
||||
>
|
||||
{code ? 'Back to sign up' : 'Try again'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="mt-4 text-center text-sm text-muted-foreground">
|
||||
{isConfirmed ? (
|
||||
'Thank you for verifying your email!'
|
||||
) : (
|
||||
<>
|
||||
Already have an account?{' '}
|
||||
<Link href="/auth/login" className="font-medium text-primary hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
<Card className="w-full max-w-md border-border">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-2xl font-bold">Welcome back</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your credentials to sign in to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex-1 flex flex-col items-center justify-center h-full py-8">
|
||||
<div className="w-full max-w-md mx-auto px-8">
|
||||
<div className="flex items-center justify-center mb-8">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-2 h-8 w-8"
|
||||
>
|
||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
||||
</svg>
|
||||
<span className="text-2xl font-bold">AgentPress</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-lg border shadow-sm">
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Welcome back
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Enter your credentials to sign in to your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
|
@ -62,15 +80,22 @@ export default function LoginPage() {
|
|||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="example@email.com"
|
||||
placeholder="name@example.com"
|
||||
required
|
||||
disabled={isLoading}
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
autoCorrect="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Link href="/auth/forgot-password" className="text-sm text-primary hover:underline">
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
className="text-xs font-medium text-primary hover:underline"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -83,24 +108,24 @@ export default function LoginPage() {
|
|||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col space-y-4">
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
className="w-full mt-1"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign in'}
|
||||
</Button>
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/auth/signup" className="text-primary hover:underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
</form>
|
||||
|
||||
<div className="mt-4 text-center text-sm text-muted-foreground">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/auth/signup" className="font-medium text-primary hover:underline">
|
||||
Create an account
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
<Card className="w-full max-w-md border-border">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-2xl font-bold">Create an account</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to create your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex-1 flex flex-col items-center justify-center h-full py-8">
|
||||
<div className="w-full max-w-md mx-auto px-8">
|
||||
<div className="flex items-center justify-center mb-8">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-2 h-8 w-8"
|
||||
>
|
||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
||||
</svg>
|
||||
<span className="text-2xl font-bold">AgentPress</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-lg border shadow-sm">
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Create an account
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Enter your email below to create your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
|
@ -67,11 +85,15 @@ export default function SignupPage() {
|
|||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="example@email.com"
|
||||
placeholder="name@example.com"
|
||||
required
|
||||
disabled={isLoading}
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
autoCorrect="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
|
@ -83,6 +105,7 @@ export default function SignupPage() {
|
|||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||
<Input
|
||||
|
@ -94,24 +117,36 @@ export default function SignupPage() {
|
|||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col space-y-4">
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
className="w-full mt-1"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Creating account...' : 'Create account'}
|
||||
</Button>
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Already have an account?{' '}
|
||||
<Link href="/auth/login" className="text-primary hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
</form>
|
||||
|
||||
<div className="mt-4 text-center text-sm text-muted-foreground">
|
||||
Already have an account?{' '}
|
||||
<Link href="/auth/login" className="font-medium text-primary hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-center text-xs text-muted-foreground">
|
||||
By clicking continue, you agree to our{' '}
|
||||
<Link href="/terms" className="underline underline-offset-4 hover:text-primary">
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
and{' '}
|
||||
<Link href="/privacy" className="underline underline-offset-4 hover:text-primary">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -57,17 +57,17 @@ export default function RootLayout({
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<html lang="en" className="h-full">
|
||||
<body className={`${inter.className} h-full flex flex-col`}>
|
||||
<AuthProvider>
|
||||
<MainNav />
|
||||
{apiConnected === false && (
|
||||
<div className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 mb-4 mx-4 mt-2" role="alert">
|
||||
<div className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-2 text-sm" role="alert">
|
||||
<p className="font-bold">Warning</p>
|
||||
<p>Could not connect to backend API. Agent and thread features may not work properly.</p>
|
||||
</div>
|
||||
)}
|
||||
<main className="min-h-screen">{children}</main>
|
||||
<main className="flex-1 overflow-hidden">{children}</main>
|
||||
<Toaster position="top-right" />
|
||||
</AuthProvider>
|
||||
</body>
|
||||
|
|
|
@ -36,6 +36,7 @@ export default function ThreadPage({ params }: { params: ThreadParams }) {
|
|||
const messagesContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const streamCleanupRef = useRef<(() => void) | null>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const initialLoadCompleted = useRef<boolean>(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 (
|
||||
<div className="container mx-auto p-6 flex justify-center items-center min-h-[80vh]">
|
||||
<div className="text-center">
|
||||
|
@ -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 (
|
||||
<div className="container mx-auto p-6 flex flex-col h-[calc(100vh-64px)]">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{project.name}</h1>
|
||||
<p className="text-gray-500">Thread {thread.thread_id.slice(0, 8)}</p>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-baseline">
|
||||
<h1 className="text-2xl font-bold">{projectName}</h1>
|
||||
<div className="mx-2 text-zinc-300">•</div>
|
||||
<div className="text-zinc-500 text-sm">Thread {thread?.thread_id ? thread.thread_id.slice(0, 8) : '...'}</div>
|
||||
</div>
|
||||
|
||||
{isStreaming && (
|
||||
<div className="flex items-center text-green-600 text-sm mr-2">
|
||||
<Wifi className="h-4 w-4 mr-1 animate-pulse" />
|
||||
<span>Streaming</span>
|
||||
<div className="flex items-center text-zinc-700 border border-zinc-200 py-1 px-2.5 rounded-full shadow-sm bg-white">
|
||||
<div className="w-1.5 h-1.5 bg-zinc-700 rounded-full animate-pulse mr-2"></div>
|
||||
<span className="text-xs font-medium">Live</span>
|
||||
</div>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => router.push(`/projects/${projectId}`)}>
|
||||
Back to Project
|
||||
</Button>
|
||||
{agentStatus === 'idle' ? (
|
||||
<Button onClick={handleStartAgent}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
Run Agent
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="destructive" onClick={handleStopAgent}>
|
||||
<Square className="h-4 w-4 mr-2" />
|
||||
Stop Agent
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -476,7 +448,7 @@ export default function ThreadPage({ params }: { params: ThreadParams }) {
|
|||
<div
|
||||
className={`max-w-[80%] px-4 py-2 rounded-lg ${
|
||||
message.role === 'user'
|
||||
? 'bg-blue-500 text-white rounded-br-none'
|
||||
? 'bg-zinc-900 text-white rounded-br-none'
|
||||
: 'bg-white border border-gray-200 rounded-bl-none'
|
||||
}`}
|
||||
>
|
||||
|
@ -491,7 +463,7 @@ export default function ThreadPage({ params }: { params: ThreadParams }) {
|
|||
<div className="max-w-[80%] px-4 py-2 rounded-lg bg-white border border-gray-200 rounded-bl-none">
|
||||
<div className="whitespace-pre-wrap">
|
||||
{streamContent}
|
||||
{isStreaming && <span className="animate-pulse">▌</span>}
|
||||
{isStreaming && <span className="inline-block h-4 w-0.5 bg-zinc-800 ml-0.5 animate-pulse"></span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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' ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleStopAgent}
|
||||
className="absolute right-2 bottom-2 h-8 w-8 p-0 rounded-full bg-red-500 hover:bg-red-600"
|
||||
aria-label="Stop agent"
|
||||
>
|
||||
<Button
|
||||
type={agentStatus === 'running' ? 'button' : 'submit'}
|
||||
onClick={agentStatus === 'running' ? handleStopAgent : undefined}
|
||||
className="absolute right-2 bottom-2 h-8 w-8 p-0 rounded-full bg-zinc-900 hover:bg-black"
|
||||
disabled={(!newMessage.trim() && agentStatus !== 'running') || isSending}
|
||||
aria-label={agentStatus === 'running' ? 'Stop agent' : 'Send message'}
|
||||
>
|
||||
{isSending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : agentStatus === 'running' ? (
|
||||
<Square className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="submit"
|
||||
className="absolute right-2 bottom-2 h-8 w-8 p-0 rounded-full"
|
||||
disabled={!newMessage.trim() || isSending}
|
||||
>
|
||||
{isSending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Send className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
) : (
|
||||
<Send className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* Fixed height container to prevent layout shifts */}
|
||||
<div className="h-5 mt-1">
|
||||
{agentStatus === 'running' && (
|
||||
<div className="text-xs text-gray-500 text-center">
|
||||
Agent is responding... Click the stop button to interrupt.
|
||||
{isStreaming ? 'Agent is responding...' : 'Agent is thinking...'}
|
||||
{isStreaming && ' Click the stop button to interrupt.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -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<Message[]>([]);
|
||||
const [userInput, setUserInput] = useState('');
|
||||
const [promptValue, setPromptValue] = useState('Create a modern, responsive landing page');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [agentStatus, setAgentStatus] = useState<'idle' | 'running' | 'stopped' | 'completed'>('idle');
|
||||
const [currentAgentRunId, setCurrentAgentRunId] = useState<string | null>(null);
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const stopStreamRef = useRef<(() => void) | null>(null);
|
||||
const streamingContentRef = useRef<string>('');
|
||||
const lastMessageIdRef = useRef<string | null>(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 (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Task description and controls */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-4 mb-4">
|
||||
<h2 className="text-xl font-semibold mb-2">Task Description</h2>
|
||||
<div className="space-y-4">
|
||||
<Textarea
|
||||
value={promptValue}
|
||||
onChange={(e) => setPromptValue(e.target.value)}
|
||||
className="w-full"
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
onClick={() => startAgentWithCustomPrompt(false)}
|
||||
disabled={agentStatus === 'running' || !promptValue.trim()}
|
||||
>
|
||||
Start Agent
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => startAgentWithCustomPrompt(true)}
|
||||
variant="secondary"
|
||||
disabled={agentStatus === 'running' || !promptValue.trim()}
|
||||
>
|
||||
Start & Stream
|
||||
</Button>
|
||||
{agentStatus === 'running' && (
|
||||
<Button
|
||||
onClick={handleStopAgent}
|
||||
variant="destructive"
|
||||
>
|
||||
⏹️ Stop Agent
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 text-sm text-gray-500">
|
||||
<span>Status:</span>
|
||||
<span className={`inline-block w-2 h-2 rounded-full ${
|
||||
agentStatus === 'running'
|
||||
? 'bg-green-500 animate-pulse'
|
||||
: agentStatus === 'stopped'
|
||||
? 'bg-red-500'
|
||||
: agentStatus === 'completed'
|
||||
? 'bg-blue-500'
|
||||
: 'bg-gray-500'
|
||||
}`}></span>
|
||||
<span>
|
||||
{agentStatus === 'running'
|
||||
? 'Running'
|
||||
: agentStatus === 'stopped'
|
||||
? 'Stopped'
|
||||
: agentStatus === 'completed'
|
||||
? 'Completed'
|
||||
: 'Not Running'}
|
||||
</span>
|
||||
|
||||
{isStreaming && (
|
||||
<span className="ml-2 flex items-center text-indigo-600">
|
||||
<span className="mr-1 inline-block w-3 h-3 border-2 border-t-indigo-600 border-r-indigo-600 border-b-transparent border-l-transparent rounded-full animate-spin"></span>
|
||||
Streaming
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="ml-auto flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="autoScroll"
|
||||
checked={autoScroll}
|
||||
onChange={(e) => setAutoScroll(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<label htmlFor="autoScroll" className="text-sm">Auto-scroll</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 bg-white rounded-lg shadow-lg p-4 mb-4 overflow-hidden flex flex-col">
|
||||
<div className="flex-1 overflow-y-auto space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-4">Loading messages...</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-red-500 mb-2">{error}</p>
|
||||
<button onClick={loadMessages} className="text-blue-500 underline">
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
) : messages.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No messages yet. Start by describing your task.
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message, index) => {
|
||||
if (message.role === 'user') {
|
||||
return (
|
||||
<div key={`user-${index}`} className="bg-blue-100 rounded-lg p-3 ml-12">
|
||||
<div className="whitespace-pre-wrap font-mono">{message.content}</div>
|
||||
</div>
|
||||
);
|
||||
} else if (message.role === 'assistant') {
|
||||
return (
|
||||
<div key={`assistant-${index}`} className="bg-gray-100 rounded-lg p-3 mr-12">
|
||||
<div className="whitespace-pre-wrap font-mono">{message.content}</div>
|
||||
</div>
|
||||
);
|
||||
} else if (message.role === 'tool') {
|
||||
return (
|
||||
<div key={message.tool_call_id || `tool-${index}`} className="bg-yellow-50 rounded-lg p-3 mr-12">
|
||||
<div className="font-medium">Tool Call: {message.name}</div>
|
||||
<pre className="text-xs bg-gray-100 p-2 rounded mt-1 overflow-x-auto">
|
||||
{message.content}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Message input */}
|
||||
<form onSubmit={handleUserInputSubmit} className="mt-4 flex space-x-2">
|
||||
<Input
|
||||
placeholder="Type your message..."
|
||||
value={userInput}
|
||||
onChange={(e) => setUserInput(e.target.value)}
|
||||
disabled={agentStatus === 'running'}
|
||||
/>
|
||||
<Button type="submit" disabled={!userInput.trim() || agentStatus === 'running'}>
|
||||
Send
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -25,18 +25,32 @@ export function MainNav() {
|
|||
};
|
||||
|
||||
return (
|
||||
<nav className="border-b bg-white">
|
||||
<div className="container mx-auto px-4 flex h-16 items-center justify-between">
|
||||
<div className="flex items-center space-x-6">
|
||||
<Link href="/" className="text-xl font-bold">
|
||||
AgentPress
|
||||
<nav className="border-b border-zinc-100 bg-white shadow-sm shrink-0">
|
||||
<div className="container mx-auto px-6 flex h-16 items-center justify-between">
|
||||
<div className="flex items-center space-x-8">
|
||||
<Link href="/" className="flex items-center group">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-2.5 h-7 w-7 text-black transition-transform duration-200 group-hover:scale-110"
|
||||
>
|
||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
||||
</svg>
|
||||
<span className="text-xl font-bold tracking-tight">AgentPress</span>
|
||||
</Link>
|
||||
{user && (
|
||||
<div className="hidden md:flex items-center space-x-4">
|
||||
<div className="hidden md:flex items-center space-x-6">
|
||||
<Link
|
||||
href="/projects"
|
||||
className={`text-sm font-medium ${
|
||||
pathname?.startsWith('/projects') ? 'text-indigo-600' : 'text-gray-600 hover:text-gray-900'
|
||||
className={`relative py-1.5 text-sm font-medium transition-all duration-200 ${
|
||||
pathname?.startsWith('/projects')
|
||||
? 'text-black after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-black'
|
||||
: 'text-zinc-600 hover:text-black'
|
||||
}`}
|
||||
>
|
||||
Projects
|
||||
|
@ -45,30 +59,39 @@ export function MainNav() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-5">
|
||||
{user ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-10 w-10 rounded-full">
|
||||
<Button variant="ghost" className="relative h-10 w-10 rounded-full overflow-hidden border border-zinc-200 hover:bg-zinc-50 transition-colors">
|
||||
<Avatar>
|
||||
<AvatarFallback>{getUserInitials()}</AvatarFallback>
|
||||
<AvatarFallback className="bg-zinc-100 text-zinc-800">{getUserInitials()}</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem className="cursor-default">
|
||||
<span className="font-medium">{user.email}</span>
|
||||
<DropdownMenuContent align="end" className="w-56 p-1.5">
|
||||
<DropdownMenuItem className="cursor-default px-3 py-2 text-zinc-500 font-medium">
|
||||
<span>{user.email}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={logout}
|
||||
className="cursor-pointer px-3 py-2 hover:bg-zinc-100 hover:text-black transition-colors"
|
||||
>
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={logout}>Log out</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex space-x-3">
|
||||
<Link href="/auth/login">
|
||||
<Button variant="outline">Log in</Button>
|
||||
<Button variant="ghost" className="text-zinc-800 hover:text-black hover:bg-zinc-100 border border-transparent">
|
||||
Log in
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/auth/signup">
|
||||
<Button>Sign up</Button>
|
||||
<Button className="bg-zinc-900 text-white hover:bg-black transition-colors">
|
||||
Sign up
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
|
Loading…
Reference in New Issue