This commit is contained in:
marko-kraemer 2025-03-30 15:55:26 -07:00
parent 611a52d2c2
commit 27a6e3735c
11 changed files with 1325 additions and 874 deletions

View File

@ -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>&copy; 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>

View File

@ -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);
});
}
});

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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&apos;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&apos;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>
);
}

View File

@ -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&apos;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&apos;t have an account?{' '}
<Link href="/auth/signup" className="font-medium text-primary hover:underline">
Create an account
</Link>
</div>
</div>
</div>
</div>
);
}

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>
)}