diff --git a/apps/docs/public/favicon/apple-touch-icon.png b/apps/docs/public/favicon/apple-touch-icon.png index df95b36c7..373fdb092 100644 Binary files a/apps/docs/public/favicon/apple-touch-icon.png and b/apps/docs/public/favicon/apple-touch-icon.png differ diff --git a/apps/docs/public/favicon/favicon-16x16.png b/apps/docs/public/favicon/favicon-16x16.png index c6351b188..f7db82fa3 100644 Binary files a/apps/docs/public/favicon/favicon-16x16.png and b/apps/docs/public/favicon/favicon-16x16.png differ diff --git a/apps/docs/public/favicon/favicon-32x32.png b/apps/docs/public/favicon/favicon-32x32.png index 7b280256d..c246b2672 100644 Binary files a/apps/docs/public/favicon/favicon-32x32.png and b/apps/docs/public/favicon/favicon-32x32.png differ diff --git a/apps/docs/public/favicon/favicon-96x96.png b/apps/docs/public/favicon/favicon-96x96.png new file mode 100644 index 000000000..1c8b8b09a Binary files /dev/null and b/apps/docs/public/favicon/favicon-96x96.png differ diff --git a/apps/docs/public/favicon/favicon.ico b/apps/docs/public/favicon/favicon.ico index 976844415..9aa82bcf0 100644 Binary files a/apps/docs/public/favicon/favicon.ico and b/apps/docs/public/favicon/favicon.ico differ diff --git a/apps/docs/public/favicon/favicon.svg b/apps/docs/public/favicon/favicon.svg new file mode 100644 index 000000000..9d6298768 --- /dev/null +++ b/apps/docs/public/favicon/favicon.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/apps/docs/public/favicon/site.webmanifest b/apps/docs/public/favicon/site.webmanifest index cddc55115..981d97f15 100644 --- a/apps/docs/public/favicon/site.webmanifest +++ b/apps/docs/public/favicon/site.webmanifest @@ -1,16 +1,18 @@ { - "name": "Sim", - "short_name": "Sim", + "name": "MyWebSite", + "short_name": "MySite", "icons": [ { - "src": "/favicon/android-chrome-192x192.png", + "src": "/web-app-manifest-192x192.png", "sizes": "192x192", - "type": "image/png" + "type": "image/png", + "purpose": "maskable" }, { - "src": "/favicon/android-chrome-512x512.png", + "src": "/web-app-manifest-512x512.png", "sizes": "512x512", - "type": "image/png" + "type": "image/png", + "purpose": "maskable" } ], "theme_color": "#ffffff", diff --git a/apps/docs/public/favicon/web-app-manifest-192x192.png b/apps/docs/public/favicon/web-app-manifest-192x192.png new file mode 100644 index 000000000..21fd84c96 Binary files /dev/null and b/apps/docs/public/favicon/web-app-manifest-192x192.png differ diff --git a/apps/docs/public/favicon/web-app-manifest-512x512.png b/apps/docs/public/favicon/web-app-manifest-512x512.png new file mode 100644 index 000000000..a9c92b264 Binary files /dev/null and b/apps/docs/public/favicon/web-app-manifest-512x512.png differ diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index bc4769c8a..f48b599ba 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -496,7 +496,7 @@ export default function LoginPage({ className={`${buttonClass} flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200`} disabled={isLoading} > - {isLoading ? 'Signing in...' : 'Sign In'} + {isLoading ? 'Signing in...' : 'Sign in'} @@ -529,7 +529,7 @@ export default function LoginPage({
By signing in, you agree to our{' '}
- +
- {isLoading ? 'Creating account...' : 'Create Account'} + {isLoading ? 'Creating account...' : 'Create account'} @@ -544,7 +544,7 @@ function SignupFormContent({
By creating an account, you agree to our{' '} { + const errors: string[] = [] + + if (!emailValue || !emailValue.trim()) { + errors.push('Email is required.') + return errors + } + + const validation = quickValidateEmail(emailValue.trim().toLowerCase()) + if (!validation.isValid) { + errors.push(validation.reason || 'Please enter a valid email address.') + } + + return errors +} + export default function EmailAuth({ subdomain, onAuthSuccess, @@ -25,10 +49,55 @@ export default function EmailAuth({ const [authError, setAuthError] = useState(null) const [isSendingOtp, setIsSendingOtp] = useState(false) const [isVerifyingOtp, setIsVerifyingOtp] = useState(false) + const [emailErrors, setEmailErrors] = useState([]) + const [showEmailValidationError, setShowEmailValidationError] = useState(false) + const [buttonClass, setButtonClass] = useState('auth-button-gradient') // OTP verification state const [showOtpVerification, setShowOtpVerification] = useState(false) const [otpValue, setOtpValue] = useState('') + const [countdown, setCountdown] = useState(0) + const [isResendDisabled, setIsResendDisabled] = useState(false) + + useEffect(() => { + // Check if CSS variable has been customized + const checkCustomBrand = () => { + const computedStyle = getComputedStyle(document.documentElement) + const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() + + // Check if the CSS variable exists and is different from the default + if (brandAccent && brandAccent !== '#6f3dfa') { + setButtonClass('auth-button-custom') + } else { + setButtonClass('auth-button-gradient') + } + } + + checkCustomBrand() + + // Also check on window resize or theme changes + window.addEventListener('resize', checkCustomBrand) + const observer = new MutationObserver(checkCustomBrand) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['style', 'class'], + }) + + return () => { + window.removeEventListener('resize', checkCustomBrand) + observer.disconnect() + } + }, []) + + useEffect(() => { + if (countdown > 0) { + const timer = setTimeout(() => setCountdown(countdown - 1), 1000) + return () => clearTimeout(timer) + } + if (countdown === 0 && isResendDisabled) { + setIsResendDisabled(false) + } + }, [countdown, isResendDisabled]) // Handle email input key down const handleEmailKeyDown = (e: KeyboardEvent) => { @@ -38,8 +107,28 @@ export default function EmailAuth({ } } + const handleEmailChange = (e: React.ChangeEvent) => { + const newEmail = e.target.value + setEmail(newEmail) + + // Silently validate but don't show errors until submit + const errors = validateEmailField(newEmail) + setEmailErrors(errors) + setShowEmailValidationError(false) + } + // Handle sending OTP const handleSendOtp = async () => { + // Validate email on submit + const emailValidationErrors = validateEmailField(email) + setEmailErrors(emailValidationErrors) + setShowEmailValidationError(emailValidationErrors.length > 0) + + // If there are validation errors, stop submission + if (emailValidationErrors.length > 0) { + return + } + setAuthError(null) setIsSendingOtp(true) @@ -55,14 +144,16 @@ export default function EmailAuth({ if (!response.ok) { const errorData = await response.json() - setAuthError(errorData.error || 'Failed to send verification code') + setEmailErrors([errorData.error || 'Failed to send verification code']) + setShowEmailValidationError(true) return } setShowOtpVerification(true) } catch (error) { - console.error('Error sending OTP:', error) - setAuthError('An error occurred while sending the verification code') + logger.error('Error sending OTP:', error) + setEmailErrors(['An error occurred while sending the verification code']) + setShowEmailValidationError(true) } finally { setIsSendingOtp(false) } @@ -96,7 +187,7 @@ export default function EmailAuth({ onAuthSuccess() } catch (error) { - console.error('Error verifying OTP:', error) + logger.error('Error verifying OTP:', error) setAuthError('An error occurred during verification') } finally { setIsVerifyingOtp(false) @@ -106,6 +197,8 @@ export default function EmailAuth({ const handleResendOtp = async () => { setAuthError(null) setIsSendingOtp(true) + setIsResendDisabled(true) + setCountdown(30) try { const response = await fetch(`/api/chat/${subdomain}/otp`, { @@ -120,175 +213,195 @@ export default function EmailAuth({ if (!response.ok) { const errorData = await response.json() setAuthError(errorData.error || 'Failed to resend verification code') + setIsResendDisabled(false) + setCountdown(0) return } - setAuthError('Verification code sent. Please check your email.') + // Don't show success message in error state, just reset OTP + setOtpValue('') } catch (error) { - console.error('Error resending OTP:', error) + logger.error('Error resending OTP:', error) setAuthError('An error occurred while resending the verification code') + setIsResendDisabled(false) + setCountdown(0) } finally { setIsSendingOtp(false) } } return ( - {}}> - - -
- - + +
+
+
+ {/* Header */} + - {title} - - -
- {!showOtpVerification ? ( - <> -
-

- This chat requires email verification. Please enter your email to continue. -

-
- - {authError && ( -
- {authError} -
- )} - -
{ - e.preventDefault() - handleSendOtp() - }} - className='space-y-4' - > -
- setEmail(e.target.value)} - onKeyDown={handleEmailKeyDown} - disabled={isSendingOtp} - className='w-full' - autoFocus - autoComplete='off' - /> -
- - -
- - ) : ( -
-
-

- Enter the verification code sent to -

-

{email}

-
- - {authError && ( -
- {authError} -
- )} - - { - setOtpValue(value) - handleVerifyOtp(value) - }} - isLoading={isVerifyingOtp} - error={null} - /> - -
- - - -
+ {showOtpVerification ? 'Verify Your Email' : 'Email Verification'} + +

+ {showOtpVerification + ? `A verification code has been sent to ${email}` + : 'This chat requires email verification'} +

- )} + + {/* Form */} +
+ {!showOtpVerification ? ( +
{ + e.preventDefault() + handleSendOtp() + }} + className='space-y-8' + > +
+
+
+ +
+ 0 && + 'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500' + )} + autoFocus + /> + {showEmailValidationError && emailErrors.length > 0 && ( +
+ {emailErrors.map((error, index) => ( +

{error}

+ ))} +
+ )} +
+
+ + +
+ ) : ( +
+
+

+ Enter the 6-digit code to verify your account. If you don't see it in your + inbox, check your spam folder. +

+ +
+ { + setOtpValue(value) + if (value.length === 6) { + handleVerifyOtp(value) + } + }} + disabled={isVerifyingOtp} + className={cn('gap-2', authError && 'otp-error')} + > + + {[0, 1, 2, 3, 4, 5].map((index) => ( + + ))} + + +
+ + {/* Error message */} + {authError && ( +
+

{authError}

+
+ )} +
+ + + +
+

+ Didn't receive a code?{' '} + {countdown > 0 ? ( + + Resend in{' '} + {countdown}s + + ) : ( + + )} +

+
+ +
+ +
+
+ )} +
+
- -
+
+
) } diff --git a/apps/sim/app/chat/components/auth/password/password-auth.tsx b/apps/sim/app/chat/components/auth/password/password-auth.tsx index 6b65972ea..cf0c2f8dc 100644 --- a/apps/sim/app/chat/components/auth/password/password-auth.tsx +++ b/apps/sim/app/chat/components/auth/password/password-auth.tsx @@ -1,10 +1,17 @@ 'use client' -import { type KeyboardEvent, useState } from 'react' -import { Loader2 } from 'lucide-react' +import { type KeyboardEvent, useEffect, useState } from 'react' +import { Eye, EyeOff } from 'lucide-react' import { Button } from '@/components/ui/button' -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { createLogger } from '@/lib/logs/console/logger' +import { cn } from '@/lib/utils' +import Nav from '@/app/(landing)/components/nav/nav' +import { inter } from '@/app/fonts/inter' +import { soehne } from '@/app/fonts/soehne/soehne' + +const logger = createLogger('PasswordAuth') interface PasswordAuthProps { subdomain: string @@ -23,6 +30,40 @@ export default function PasswordAuth({ const [password, setPassword] = useState('') const [authError, setAuthError] = useState(null) const [isAuthenticating, setIsAuthenticating] = useState(false) + const [showPassword, setShowPassword] = useState(false) + const [showValidationError, setShowValidationError] = useState(false) + const [passwordErrors, setPasswordErrors] = useState([]) + const [buttonClass, setButtonClass] = useState('auth-button-gradient') + + useEffect(() => { + // Check if CSS variable has been customized + const checkCustomBrand = () => { + const computedStyle = getComputedStyle(document.documentElement) + const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() + + // Check if the CSS variable exists and is different from the default + if (brandAccent && brandAccent !== '#6f3dfa') { + setButtonClass('auth-button-custom') + } else { + setButtonClass('auth-button-gradient') + } + } + + checkCustomBrand() + + // Also check on window resize or theme changes + window.addEventListener('resize', checkCustomBrand) + const observer = new MutationObserver(checkCustomBrand) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['style', 'class'], + }) + + return () => { + window.removeEventListener('resize', checkCustomBrand) + observer.disconnect() + } + }, []) // Handle keyboard input for auth forms const handleKeyDown = (e: KeyboardEvent) => { @@ -32,10 +73,18 @@ export default function PasswordAuth({ } } + const handlePasswordChange = (e: React.ChangeEvent) => { + const newPassword = e.target.value + setPassword(newPassword) + setShowValidationError(false) + setPasswordErrors([]) + } + // Handle authentication const handleAuthenticate = async () => { if (!password.trim()) { - setAuthError('Password is required') + setPasswordErrors(['Password is required']) + setShowValidationError(true) return } @@ -57,7 +106,8 @@ export default function PasswordAuth({ if (!response.ok) { const errorData = await response.json() - setAuthError(errorData.error || 'Authentication failed') + setPasswordErrors([errorData.error || 'Invalid password. Please try again.']) + setShowValidationError(true) return } @@ -67,118 +117,96 @@ export default function PasswordAuth({ // Reset auth state setPassword('') } catch (error) { - console.error('Authentication error:', error) - setAuthError('An error occurred during authentication') + logger.error('Authentication error:', error) + setPasswordErrors(['An error occurred during authentication']) + setShowValidationError(true) } finally { setIsAuthenticating(false) } } return ( - {}}> - - -
- - + +
+
+
+ {/* Header */} + - {title} - - -
-
-

- This chat is password-protected. Please enter the password to continue. -

-
- - {authError && ( -
- {authError} -
- )} - -
{ - e.preventDefault() - handleAuthenticate() - }} - className='space-y-4' - > -
- setPassword(e.target.value)} - onKeyDown={handleKeyDown} - placeholder='Enter password' - disabled={isAuthenticating} - autoComplete='new-password' - className='w-full' - autoFocus - /> + Password Required + +

+ This chat is password-protected +

- +
+ {showValidationError && passwordErrors.length > 0 && ( +
+ {passwordErrors.map((error, index) => ( +

{error}

+ ))} +
+ )}
- ) : ( - 'Continue' - )} - - +
+ + + +
-
-
+ + ) } diff --git a/apps/sim/app/chat/components/error-state/error-state.tsx b/apps/sim/app/chat/components/error-state/error-state.tsx index b5e1b1795..f6bba593f 100644 --- a/apps/sim/app/chat/components/error-state/error-state.tsx +++ b/apps/sim/app/chat/components/error-state/error-state.tsx @@ -1,6 +1,11 @@ 'use client' -import { ChatHeader } from '../' +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' +import { Button } from '@/components/ui/button' +import Nav from '@/app/(landing)/components/nav/nav' +import { inter } from '@/app/fonts/inter' +import { soehne } from '@/app/fonts/soehne/soehne' interface ChatErrorStateProps { error: string @@ -8,54 +13,69 @@ interface ChatErrorStateProps { } export function ChatErrorState({ error, starCount }: ChatErrorStateProps) { + const router = useRouter() + const [buttonClass, setButtonClass] = useState('auth-button-gradient') + + useEffect(() => { + // Check if CSS variable has been customized + const checkCustomBrand = () => { + const computedStyle = getComputedStyle(document.documentElement) + const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() + + // Check if the CSS variable exists and is different from the default + if (brandAccent && brandAccent !== '#6f3dfa') { + setButtonClass('auth-button-custom') + } else { + setButtonClass('auth-button-gradient') + } + } + + checkCustomBrand() + + // Also check on window resize or theme changes + window.addEventListener('resize', checkCustomBrand) + const observer = new MutationObserver(checkCustomBrand) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['style', 'class'], + }) + + return () => { + window.removeEventListener('resize', checkCustomBrand) + observer.disconnect() + } + }, []) + return ( -
-
-
- - - - - - - - - - - +
+
) diff --git a/apps/sim/app/chat/components/header/header.tsx b/apps/sim/app/chat/components/header/header.tsx index d99f57f9e..7c7e8b414 100644 --- a/apps/sim/app/chat/components/header/header.tsx +++ b/apps/sim/app/chat/components/header/header.tsx @@ -1,6 +1,10 @@ 'use client' +import Image from 'next/image' +import Link from 'next/link' import { GithubIcon } from '@/components/icons' +import { useBrandConfig } from '@/lib/branding/branding' +import { inter } from '@/app/fonts/inter' interface ChatHeaderProps { chatConfig: { @@ -16,79 +20,67 @@ interface ChatHeaderProps { } export function ChatHeader({ chatConfig, starCount }: ChatHeaderProps) { + const brand = useBrandConfig() const primaryColor = chatConfig?.customizations?.primaryColor || 'var(--brand-primary-hex)' const customImage = chatConfig?.customizations?.imageUrl || chatConfig?.customizations?.logoUrl return ( -
-
- {customImage && ( - {`${chatConfig?.title - )} -

- {chatConfig?.customizations?.headerText || chatConfig?.title || 'Chat'} -

+
+
+ )} + ) } diff --git a/apps/sim/app/chat/components/loading-state/loading-state.tsx b/apps/sim/app/chat/components/loading-state/loading-state.tsx index 7d99d8481..cc292a7f6 100644 --- a/apps/sim/app/chat/components/loading-state/loading-state.tsx +++ b/apps/sim/app/chat/components/loading-state/loading-state.tsx @@ -1,11 +1,29 @@ 'use client' +import { Skeleton } from '@/components/ui/skeleton' + export function ChatLoadingState() { return ( -
-
-
-
+
+
+
+
+ {/* Title skeleton */} +
+ + +
+ + {/* Form skeleton */} +
+
+ + +
+ +
+
+
) diff --git a/apps/sim/app/globals.css b/apps/sim/app/globals.css index bef5339c3..e857c8bce 100644 --- a/apps/sim/app/globals.css +++ b/apps/sim/app/globals.css @@ -120,11 +120,10 @@ --gradient-secondary: 336 95% 65%; /* More vibrant pink */ /* Brand Colors (Default Sim Theme) */ - --brand-primary-hex: #8357ff; /* Primary brand purple - matches Get Started gradient start */ - --brand-primary-hover-hex: #9266ff; /* Primary brand purple hover - matches Get Started hover */ - --brand-secondary-hex: #6f3dfa; /* Secondary brand purple - matches Get Started gradient end */ + --brand-primary-hex: #6f3dfa; /* Primary brand purple - matches Get Started gradient start */ + --brand-primary-hover-hex: #6338d9; /* Primary brand purple hover - matches Get Started hover */ --brand-accent-hex: #6f3dfa; /* Accent purple for links - matches sign in button */ - --brand-accent-hover-hex: #8357ff; /* Accent purple hover - matches sign in gradient start */ + --brand-accent-hover-hex: #6f3dfa; /* Accent purple hover - matches sign in gradient start */ --brand-background-hex: #ffffff; /* Primary light background */ /* UI Surface Colors */ @@ -193,7 +192,6 @@ /* Brand Colors (Keep dark background for actual dark mode) */ --brand-primary-hex: #701ffc; /* Primary brand purple */ --brand-primary-hover-hex: #802fff; /* Primary brand purple hover */ - --brand-secondary-hex: #6518e6; /* Secondary brand purple */ --brand-accent-hex: #9d54ff; /* Accent purple for links */ --brand-accent-hover-hex: #a66fff; /* Accent purple hover */ --brand-background-hex: #0c0c0c; /* Primary dark background */ @@ -399,10 +397,6 @@ input[type="search"]::-ms-clear { background-color: var(--brand-primary-hover-hex); } - .hover\:bg-brand-secondary:hover { - background-color: var(--brand-secondary-hex); - } - .hover\:text-brand-accent-hover:hover { color: var(--brand-accent-hover-hex); } diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/create-chunk-modal/create-chunk-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/create-chunk-modal/create-chunk-modal.tsx index 50d1100c9..9e8a49bca 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/create-chunk-modal/create-chunk-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/create-chunk-modal/create-chunk-modal.tsx @@ -192,7 +192,7 @@ export function CreateChunkModal({