fe thread ux improvements

This commit is contained in:
marko-kraemer 2025-03-30 17:12:21 -07:00
parent 47001a0d91
commit 1b4f216c6a
4 changed files with 326 additions and 18 deletions

View File

@ -17,11 +17,17 @@
<a href="#about">About</a>
<a href="#contact">Contact</a>
</div>
<button class="mobile-menu-btn">
<span></span>
<span></span>
<span></span>
</button>
<div class="nav-actions">
<button id="darkModeToggle" class="dark-mode-toggle" aria-label="Toggle dark mode">
<i class="fas fa-moon"></i>
<i class="fas fa-sun"></i>
</button>
<button class="mobile-menu-btn">
<span></span>
<span></span>
<span></span>
</button>
</div>
</nav>
</header>

View File

@ -0,0 +1,207 @@
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Dark mode toggle functionality
const darkModeToggle = document.getElementById('darkModeToggle');
// Check for saved theme preference or use device preference
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Set initial theme
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
document.documentElement.setAttribute('data-theme', 'dark');
}
// Toggle dark mode
darkModeToggle.addEventListener('click', function() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
});
// Mobile menu toggle
const mobileMenuBtn = document.querySelector('.mobile-menu-btn');
const navLinks = document.querySelector('.nav-links');
if (mobileMenuBtn) {
mobileMenuBtn.addEventListener('click', function() {
// Create mobile menu if it doesn't exist
if (!document.querySelector('.mobile-nav')) {
const mobileNav = document.createElement('div');
mobileNav.className = 'mobile-nav';
// Clone the navigation links
const navLinksClone = navLinks.cloneNode(true);
// Get all links
const links = navLinksClone.querySelectorAll('a');
// Convert to individual links for mobile
links.forEach(link => {
mobileNav.appendChild(link);
});
// Add to the DOM
document.querySelector('header').appendChild(mobileNav);
}
// Toggle the mobile menu
const mobileNav = document.querySelector('.mobile-nav');
mobileNav.classList.toggle('active');
// Animate the hamburger icon
const spans = this.querySelectorAll('span');
if (mobileNav.classList.contains('active')) {
spans[0].style.transform = 'rotate(45deg) translate(5px, 5px)';
spans[1].style.opacity = '0';
spans[2].style.transform = 'rotate(-45deg) translate(7px, -6px)';
} else {
spans[0].style.transform = 'none';
spans[1].style.opacity = '1';
spans[2].style.transform = 'none';
}
});
}
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
if (targetId === '#') return;
const targetElement = document.querySelector(targetId);
if (targetElement) {
// Close mobile menu if open
const mobileNav = document.querySelector('.mobile-nav');
if (mobileNav && mobileNav.classList.contains('active')) {
mobileMenuBtn.click();
}
// Scroll to the target element
window.scrollTo({
top: targetElement.offsetTop - 80,
behavior: 'smooth'
});
}
});
});
// Form validation
const contactForm = document.getElementById('contactForm');
if (contactForm) {
contactForm.addEventListener('submit', function(e) {
e.preventDefault();
// Get form values
const name = document.getElementById('name').value.trim();
const email = document.getElementById('email').value.trim();
const message = document.getElementById('message').value.trim();
// Simple validation
if (name === '') {
showError('name', 'Please enter your name');
return;
}
if (email === '') {
showError('email', 'Please enter your email');
return;
}
if (!isValidEmail(email)) {
showError('email', 'Please enter a valid email');
return;
}
if (message === '') {
showError('message', 'Please enter your message');
return;
}
// If all validations pass, show success message
contactForm.innerHTML = `
<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, .about-image, .about-text');
// Check if IntersectionObserver is supported
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animated');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
animateElements.forEach(element => {
element.style.opacity = '0';
element.style.transform = 'translateY(20px)';
element.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
observer.observe(element);
});
}
// Add animated class to elements
document.addEventListener('scroll', function() {
document.querySelectorAll('.animated').forEach(element => {
element.style.opacity = '1';
element.style.transform = 'translateY(0)';
});
});
});

View File

@ -10,6 +10,29 @@
--border-radius: 8px;
--box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--transition: all 0.3s ease;
/* Background and text colors */
--bg-color: #f9fafb;
--text-color: #1f2937;
--card-bg: #ffffff;
--header-bg: #ffffff;
--footer-bg: #1f2937;
--footer-text: #ffffff;
}
/* Dark mode colors */
[data-theme="dark"] {
--primary-color: #6366f1;
--primary-dark: #4f46e5;
--bg-color: #111827;
--text-color: #f9fafb;
--card-bg: #1f2937;
--gray-color: #9ca3af;
--light-gray: #374151;
--header-bg: #1f2937;
--footer-bg: #111827;
--footer-text: #f9fafb;
--box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.15);
}
* {
@ -25,8 +48,9 @@ html {
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.6;
color: var(--dark-color);
background-color: var(--light-color);
color: var(--text-color);
background-color: var(--bg-color);
transition: background-color 0.3s ease, color 0.3s ease;
}
a {
@ -92,11 +116,12 @@ section {
/* Header Styles */
header {
background-color: white;
background-color: var(--header-bg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
position: sticky;
top: 0;
z-index: 100;
transition: background-color 0.3s ease;
}
.navbar {
@ -106,6 +131,58 @@ header {
padding: 20px 0;
}
.nav-actions {
display: flex;
align-items: center;
gap: 20px;
}
.dark-mode-toggle {
background: none;
border: none;
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
color: var(--text-color);
background-color: var(--light-gray);
transition: var(--transition);
position: relative;
overflow: hidden;
}
.dark-mode-toggle:hover {
background-color: var(--gray-color);
}
.dark-mode-toggle .fa-sun {
position: absolute;
opacity: 0;
transform: translateY(20px);
transition: var(--transition);
}
.dark-mode-toggle .fa-moon {
position: absolute;
opacity: 1;
transform: translateY(0);
transition: var(--transition);
}
[data-theme="dark"] .dark-mode-toggle .fa-sun {
opacity: 1;
transform: translateY(0);
}
[data-theme="dark"] .dark-mode-toggle .fa-moon {
opacity: 0;
transform: translateY(-20px);
}
.logo {
font-size: 1.5rem;
font-weight: 700;
@ -200,7 +277,8 @@ header {
/* Features Section */
.features {
background-color: white;
background-color: var(--card-bg);
transition: background-color 0.3s ease;
}
.features-grid {
@ -210,11 +288,11 @@ header {
}
.feature-card {
background-color: white;
background-color: var(--card-bg);
padding: 30px;
border-radius: var(--border-radius);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: var(--transition);
transition: transform 0.3s ease, box-shadow 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
text-align: center;
border: 1px solid var(--light-gray);
}
@ -278,7 +356,8 @@ header {
/* Contact Section */
.contact {
background-color: white;
background-color: var(--card-bg);
transition: background-color 0.3s ease;
}
.contact-form {
@ -320,9 +399,10 @@ header {
/* Footer */
footer {
background-color: var(--dark-color);
color: white;
background-color: var(--footer-bg);
color: var(--footer-text);
padding: 80px 0 30px;
transition: background-color 0.3s ease;
}
.footer-content {
@ -455,10 +535,11 @@ footer {
top: 100%;
left: 0;
width: 100%;
background-color: white;
background-color: var(--card-bg);
padding: 20px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
z-index: 99;
transition: background-color 0.3s ease;
}
.mobile-nav.active {

View File

@ -146,6 +146,9 @@ export default function ThreadPage({ params }: { params: ThreadParams }) {
// Clear the input
setNewMessage('');
// Scroll to bottom immediately when user sends a message
scrollToBottom();
// Send to the API
await addMessage(threadId, userMessage);
@ -159,6 +162,9 @@ export default function ThreadPage({ params }: { params: ThreadParams }) {
setAgentRunId(result.agent_run_id);
setAgentStatus('running');
// Scroll to bottom when agent starts responding
scrollToBottom();
// Start streaming the agent's responses
handleStreamAgent(result.agent_run_id);
} catch (err: any) {
@ -423,11 +429,11 @@ export default function ThreadPage({ params }: { params: ThreadParams }) {
// Auto-scroll when messages change
useEffect(() => {
// Only auto-scroll if already at bottom or is a new message
if (!showScrollButton || messages.length > 0 && messages[messages.length - 1]?.role === 'assistant') {
// Only auto-scroll if user hasn't manually scrolled up
if (isLatestMessageVisible) {
scrollToBottom();
}
}, [messages, streamContent]);
}, [messages, streamContent, isLatestMessageVisible]);
// Initial scroll to bottom with animation
useEffect(() => {
@ -441,6 +447,14 @@ export default function ThreadPage({ params }: { params: ThreadParams }) {
}
}, [isLoading]);
// Update UI states when agent status changes
useEffect(() => {
// Scroll to bottom when agent starts responding
if (agentStatus === 'running') {
scrollToBottom();
}
}, [agentStatus]);
// Only show a full-screen loader on the very first load
if (isAuthLoading || (isLoading && !initialLoadCompleted.current)) {
return (