mirror of https://github.com/kortix-ai/suna.git
fe thread ux improvements
This commit is contained in:
parent
47001a0d91
commit
1b4f216c6a
|
@ -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>
|
||||
|
||||
|
|
|
@ -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)';
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
|
|
Loading…
Reference in New Issue