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="#about">About</a>
|
||||||
<a href="#contact">Contact</a>
|
<a href="#contact">Contact</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="mobile-menu-btn">
|
<div class="nav-actions">
|
||||||
<span></span>
|
<button id="darkModeToggle" class="dark-mode-toggle" aria-label="Toggle dark mode">
|
||||||
<span></span>
|
<i class="fas fa-moon"></i>
|
||||||
<span></span>
|
<i class="fas fa-sun"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="mobile-menu-btn">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</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;
|
--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);
|
--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;
|
--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 {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: var(--dark-color);
|
color: var(--text-color);
|
||||||
background-color: var(--light-color);
|
background-color: var(--bg-color);
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -92,11 +116,12 @@ section {
|
||||||
|
|
||||||
/* Header Styles */
|
/* Header Styles */
|
||||||
header {
|
header {
|
||||||
background-color: white;
|
background-color: var(--header-bg);
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
|
@ -106,6 +131,58 @@ header {
|
||||||
padding: 20px 0;
|
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 {
|
.logo {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
@ -200,7 +277,8 @@ header {
|
||||||
|
|
||||||
/* Features Section */
|
/* Features Section */
|
||||||
.features {
|
.features {
|
||||||
background-color: white;
|
background-color: var(--card-bg);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.features-grid {
|
.features-grid {
|
||||||
|
@ -210,11 +288,11 @@ header {
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-card {
|
.feature-card {
|
||||||
background-color: white;
|
background-color: var(--card-bg);
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
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;
|
text-align: center;
|
||||||
border: 1px solid var(--light-gray);
|
border: 1px solid var(--light-gray);
|
||||||
}
|
}
|
||||||
|
@ -278,7 +356,8 @@ header {
|
||||||
|
|
||||||
/* Contact Section */
|
/* Contact Section */
|
||||||
.contact {
|
.contact {
|
||||||
background-color: white;
|
background-color: var(--card-bg);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-form {
|
.contact-form {
|
||||||
|
@ -320,9 +399,10 @@ header {
|
||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
footer {
|
footer {
|
||||||
background-color: var(--dark-color);
|
background-color: var(--footer-bg);
|
||||||
color: white;
|
color: var(--footer-text);
|
||||||
padding: 80px 0 30px;
|
padding: 80px 0 30px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-content {
|
.footer-content {
|
||||||
|
@ -455,10 +535,11 @@ footer {
|
||||||
top: 100%;
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: white;
|
background-color: var(--card-bg);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-nav.active {
|
.mobile-nav.active {
|
||||||
|
|
|
@ -146,6 +146,9 @@ export default function ThreadPage({ params }: { params: ThreadParams }) {
|
||||||
// Clear the input
|
// Clear the input
|
||||||
setNewMessage('');
|
setNewMessage('');
|
||||||
|
|
||||||
|
// Scroll to bottom immediately when user sends a message
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
// Send to the API
|
// Send to the API
|
||||||
await addMessage(threadId, userMessage);
|
await addMessage(threadId, userMessage);
|
||||||
|
|
||||||
|
@ -159,6 +162,9 @@ export default function ThreadPage({ params }: { params: ThreadParams }) {
|
||||||
setAgentRunId(result.agent_run_id);
|
setAgentRunId(result.agent_run_id);
|
||||||
setAgentStatus('running');
|
setAgentStatus('running');
|
||||||
|
|
||||||
|
// Scroll to bottom when agent starts responding
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
// Start streaming the agent's responses
|
// Start streaming the agent's responses
|
||||||
handleStreamAgent(result.agent_run_id);
|
handleStreamAgent(result.agent_run_id);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
@ -423,11 +429,11 @@ export default function ThreadPage({ params }: { params: ThreadParams }) {
|
||||||
|
|
||||||
// Auto-scroll when messages change
|
// Auto-scroll when messages change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only auto-scroll if already at bottom or is a new message
|
// Only auto-scroll if user hasn't manually scrolled up
|
||||||
if (!showScrollButton || messages.length > 0 && messages[messages.length - 1]?.role === 'assistant') {
|
if (isLatestMessageVisible) {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
}, [messages, streamContent]);
|
}, [messages, streamContent, isLatestMessageVisible]);
|
||||||
|
|
||||||
// Initial scroll to bottom with animation
|
// Initial scroll to bottom with animation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -441,6 +447,14 @@ export default function ThreadPage({ params }: { params: ThreadParams }) {
|
||||||
}
|
}
|
||||||
}, [isLoading]);
|
}, [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
|
// Only show a full-screen loader on the very first load
|
||||||
if (isAuthLoading || (isLoading && !initialLoadCompleted.current)) {
|
if (isAuthLoading || (isLoading && !initialLoadCompleted.current)) {
|
||||||
return (
|
return (
|
||||||
|
|
Loading…
Reference in New Issue