improvement: branding; auth; chat-deploy (#1351)
* improvement: branding; auth; chat-deploy * improvement: docs favicon
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 521 B |
|
Before Width: | Height: | Size: 897 B After Width: | Height: | Size: 1.0 KiB |
BIN
apps/docs/public/favicon/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
3
apps/docs/public/favicon/favicon.svg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
@@ -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",
|
||||
|
||||
BIN
apps/docs/public/favicon/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
apps/docs/public/favicon/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
@@ -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'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -529,7 +529,7 @@ export default function LoginPage({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`${inter.className} auth-text-muted fixed right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[13px] leading-relaxed`}
|
||||
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] leading-relaxed sm:px-8 md:px-[44px]`}
|
||||
>
|
||||
By signing in, you agree to our{' '}
|
||||
<Link
|
||||
|
||||
@@ -406,7 +406,7 @@ function SignupFormContent({
|
||||
<div className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label htmlFor='name'>Full Name</Label>
|
||||
<Label htmlFor='name'>Full name</Label>
|
||||
</div>
|
||||
<Input
|
||||
id='name'
|
||||
@@ -511,7 +511,7 @@ function SignupFormContent({
|
||||
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 ? 'Creating account...' : 'Create Account'}
|
||||
{isLoading ? 'Creating account...' : 'Create account'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -544,7 +544,7 @@ function SignupFormContent({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`${inter.className} auth-text-muted fixed right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[13px] leading-relaxed`}
|
||||
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] leading-relaxed sm:px-8 md:px-[44px]`}
|
||||
>
|
||||
By creating an account, you agree to our{' '}
|
||||
<Link
|
||||
|
||||
@@ -1,82 +1,14 @@
|
||||
/* Force light mode for chat subdomain by overriding dark mode utilities */
|
||||
/* This file uses CSS variables from globals.css light mode theme */
|
||||
/**
|
||||
* Chat Subdomain Light Mode Overrides
|
||||
*
|
||||
* This file overrides dark mode utility classes to force light mode appearance
|
||||
* in the chat subdomain. It uses CSS variables defined in globals.css.
|
||||
*
|
||||
* The layout.tsx already applies the 'light' class which sets all the light
|
||||
* theme CSS variables from globals.css, so we don't need to redefine them here.
|
||||
*/
|
||||
|
||||
/* When inside the chat layout, force all light mode CSS variables */
|
||||
.chat-light-wrapper {
|
||||
/* Core Colors - from globals.css light mode */
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
|
||||
/* Card Colors */
|
||||
--card: 0 0% 99.2%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
|
||||
/* Popover Colors */
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
|
||||
/* Primary Colors */
|
||||
--primary: 0 0% 11.2%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
/* Secondary Colors */
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 11.2%;
|
||||
|
||||
/* Muted Colors */
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 46.9%;
|
||||
|
||||
/* Accent Colors */
|
||||
--accent: 0 0% 92.5%;
|
||||
--accent-foreground: 0 0% 11.2%;
|
||||
|
||||
/* Destructive Colors */
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
/* Border & Input Colors */
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
|
||||
/* Border Radius */
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* Scrollbar Properties */
|
||||
--scrollbar-track: 0 0% 85%;
|
||||
--scrollbar-thumb: 0 0% 65%;
|
||||
--scrollbar-thumb-hover: 0 0% 55%;
|
||||
--scrollbar-size: 8px;
|
||||
|
||||
/* Workflow Properties */
|
||||
--workflow-background: 0 0% 100%;
|
||||
--workflow-dots: 0 0% 94.5%;
|
||||
--card-background: 0 0% 99.2%;
|
||||
--card-border: 0 0% 89.8%;
|
||||
--card-text: 0 0% 3.9%;
|
||||
--card-hover: 0 0% 96.1%;
|
||||
|
||||
/* Base Component Properties */
|
||||
--base-muted-foreground: #737373;
|
||||
|
||||
/* Gradient Colors */
|
||||
--gradient-primary: 263 85% 70%;
|
||||
--gradient-secondary: 336 95% 65%;
|
||||
|
||||
/* Brand Colors */
|
||||
--brand-primary-hex: #701ffc;
|
||||
--brand-primary-hover-hex: #802fff;
|
||||
--brand-secondary-hex: #6518e6;
|
||||
--brand-accent-hex: #9d54ff;
|
||||
--brand-accent-hover-hex: #a66fff;
|
||||
--brand-background-hex: #0c0c0c;
|
||||
|
||||
/* UI Surface Colors */
|
||||
--surface-elevated: #202020;
|
||||
}
|
||||
|
||||
/* Override dark mode utility classes using CSS variables */
|
||||
/* Background Color Overrides */
|
||||
.chat-light-wrapper :is(.dark\:bg-black) {
|
||||
background-color: hsl(var(--secondary));
|
||||
}
|
||||
@@ -101,7 +33,7 @@
|
||||
background-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
/* Text color overrides using CSS variables */
|
||||
/* Text Color Overrides */
|
||||
.chat-light-wrapper :is(.dark\:text-gray-100) {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
@@ -126,7 +58,7 @@
|
||||
color: var(--brand-accent-hex);
|
||||
}
|
||||
|
||||
/* Border color overrides using CSS variables */
|
||||
/* Border Color Overrides */
|
||||
.chat-light-wrapper :is(.dark\:border-gray-700) {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
@@ -143,12 +75,12 @@
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
/* Hover state overrides */
|
||||
/* Hover State Overrides */
|
||||
.chat-light-wrapper :is(.dark\:hover\:bg-gray-800\/60:hover) {
|
||||
background-color: hsl(var(--card-hover));
|
||||
}
|
||||
|
||||
/* Code blocks specific overrides using CSS variables */
|
||||
/* Code Block Overrides */
|
||||
.chat-light-wrapper pre:is(.dark\:bg-black) {
|
||||
background-color: hsl(var(--workflow-dots));
|
||||
}
|
||||
@@ -161,13 +93,14 @@
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
/* Tooltip overrides - keep tooltips black with white text for consistency */
|
||||
/* Special Components */
|
||||
/* Tooltip overrides - keep tooltips dark with light text for consistency */
|
||||
.chat-light-wrapper [data-radix-tooltip-content] {
|
||||
background-color: hsl(0 0% 3.9%) !important;
|
||||
color: hsl(0 0% 98%) !important;
|
||||
}
|
||||
|
||||
/* Force color scheme */
|
||||
/* Force light color scheme */
|
||||
.chat-light-wrapper {
|
||||
color-scheme: light !important;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { type KeyboardEvent, useState } from 'react'
|
||||
import { type KeyboardEvent, useEffect, useState } from 'react'
|
||||
import { Loader2 } 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 { OTPInputForm } from '@/components/ui/input-otp-form'
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { quickValidateEmail } from '@/lib/email/validation'
|
||||
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('EmailAuth')
|
||||
|
||||
interface EmailAuthProps {
|
||||
subdomain: string
|
||||
@@ -14,6 +22,22 @@ interface EmailAuthProps {
|
||||
primaryColor?: string
|
||||
}
|
||||
|
||||
const validateEmailField = (emailValue: string): string[] => {
|
||||
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<string | null>(null)
|
||||
const [isSendingOtp, setIsSendingOtp] = useState(false)
|
||||
const [isVerifyingOtp, setIsVerifyingOtp] = useState(false)
|
||||
const [emailErrors, setEmailErrors] = useState<string[]>([])
|
||||
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<HTMLInputElement>) => {
|
||||
@@ -38,8 +107,28 @@ export default function EmailAuth({
|
||||
}
|
||||
}
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<Dialog open={true} onOpenChange={() => {}}>
|
||||
<DialogContent
|
||||
className='flex flex-col gap-0 overflow-hidden p-0 sm:max-w-[450px]'
|
||||
hideCloseButton
|
||||
>
|
||||
<DialogHeader className='border-b px-6 py-4'>
|
||||
<div className='flex items-center justify-center'>
|
||||
<a href='https://sim.ai' target='_blank' rel='noopener noreferrer' className='mb-2'>
|
||||
<svg
|
||||
width='40'
|
||||
height='40'
|
||||
viewBox='0 0 50 50'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className='rounded-[6px]'
|
||||
<div className='bg-white'>
|
||||
<Nav variant='auth' />
|
||||
<div className='flex min-h-[calc(100vh-120px)] items-center justify-center px-4'>
|
||||
<div className='w-full max-w-[410px]'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
{/* Header */}
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1
|
||||
className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}
|
||||
>
|
||||
<rect width='50' height='50' fill='var(--brand-primary-hex)' />
|
||||
<path
|
||||
d='M34.1455 20.0728H16.0364C12.7026 20.0728 10 22.7753 10 26.1091V35.1637C10 38.4975 12.7026 41.2 16.0364 41.2H34.1455C37.4792 41.2 40.1818 38.4975 40.1818 35.1637V26.1091C40.1818 22.7753 37.4792 20.0728 34.1455 20.0728Z'
|
||||
fill='var(--brand-primary-hex)'
|
||||
stroke='white'
|
||||
strokeWidth='3.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M25.0919 14.0364C26.7588 14.0364 28.1101 12.6851 28.1101 11.0182C28.1101 9.35129 26.7588 8 25.0919 8C23.425 8 22.0737 9.35129 22.0737 11.0182C22.0737 12.6851 23.425 14.0364 25.0919 14.0364Z'
|
||||
fill='var(--brand-primary-hex)'
|
||||
stroke='white'
|
||||
strokeWidth='4'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M25.0915 14.856V19.0277V14.856ZM20.5645 32.1398V29.1216V32.1398ZM29.619 29.1216V32.1398V29.1216Z'
|
||||
fill='var(--brand-primary-hex)'
|
||||
/>
|
||||
<path
|
||||
d='M25.0915 14.856V19.0277M20.5645 32.1398V29.1216M29.619 29.1216V32.1398'
|
||||
stroke='white'
|
||||
strokeWidth='4'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<circle cx='25' cy='11' r='2' fill='var(--brand-primary-hex)' />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<DialogTitle className='text-center font-medium text-lg'>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className='p-6'>
|
||||
{!showOtpVerification ? (
|
||||
<>
|
||||
<div className='mb-4 text-center'>
|
||||
<p className='text-muted-foreground'>
|
||||
This chat requires email verification. Please enter your email to continue.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{authError && (
|
||||
<div className='mb-4 rounded-md border border-red-200 bg-red-50 p-3 text-red-600 text-sm'>
|
||||
{authError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
handleSendOtp()
|
||||
}}
|
||||
className='space-y-4'
|
||||
>
|
||||
<div className='space-y-2'>
|
||||
<Input
|
||||
id='email'
|
||||
type='email'
|
||||
placeholder='Email address'
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onKeyDown={handleEmailKeyDown}
|
||||
disabled={isSendingOtp}
|
||||
className='w-full'
|
||||
autoFocus
|
||||
autoComplete='off'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
onClick={handleSendOtp}
|
||||
disabled={!email || isSendingOtp}
|
||||
className='w-full'
|
||||
style={{ backgroundColor: primaryColor }}
|
||||
>
|
||||
{isSendingOtp ? (
|
||||
<div className='flex items-center justify-center'>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Sending Code...
|
||||
</div>
|
||||
) : (
|
||||
'Continue'
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<div className='space-y-4'>
|
||||
<div className='text-center'>
|
||||
<p className='mb-1 text-muted-foreground text-sm'>
|
||||
Enter the verification code sent to
|
||||
</p>
|
||||
<p className='break-all font-medium text-sm'>{email}</p>
|
||||
</div>
|
||||
|
||||
{authError && (
|
||||
<div className='rounded-md border border-red-200 bg-red-50 p-3 text-red-600 text-sm'>
|
||||
{authError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<OTPInputForm
|
||||
onSubmit={(value) => {
|
||||
setOtpValue(value)
|
||||
handleVerifyOtp(value)
|
||||
}}
|
||||
isLoading={isVerifyingOtp}
|
||||
error={null}
|
||||
/>
|
||||
|
||||
<div className='flex items-center justify-center pt-2'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleResendOtp}
|
||||
disabled={isSendingOtp}
|
||||
className='text-muted-foreground text-sm hover:underline disabled:opacity-50'
|
||||
>
|
||||
{isSendingOtp ? 'Sending...' : 'Resend code'}
|
||||
</button>
|
||||
<span className='mx-2 text-neutral-300 dark:text-neutral-600'>•</span>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => {
|
||||
setShowOtpVerification(false)
|
||||
setOtpValue('')
|
||||
setAuthError(null)
|
||||
}}
|
||||
className='text-muted-foreground text-sm hover:underline'
|
||||
>
|
||||
Change email
|
||||
</button>
|
||||
</div>
|
||||
{showOtpVerification ? 'Verify Your Email' : 'Email Verification'}
|
||||
</h1>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
{showOtpVerification
|
||||
? `A verification code has been sent to ${email}`
|
||||
: 'This chat requires email verification'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form */}
|
||||
<div className={`${inter.className} mt-8 w-full`}>
|
||||
{!showOtpVerification ? (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
handleSendOtp()
|
||||
}}
|
||||
className='space-y-8'
|
||||
>
|
||||
<div className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label htmlFor='email'>Email</Label>
|
||||
</div>
|
||||
<Input
|
||||
id='email'
|
||||
name='email'
|
||||
placeholder='Enter your email'
|
||||
required
|
||||
autoCapitalize='none'
|
||||
autoComplete='email'
|
||||
autoCorrect='off'
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
onKeyDown={handleEmailKeyDown}
|
||||
className={cn(
|
||||
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
|
||||
showEmailValidationError &&
|
||||
emailErrors.length > 0 &&
|
||||
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
|
||||
)}
|
||||
autoFocus
|
||||
/>
|
||||
{showEmailValidationError && emailErrors.length > 0 && (
|
||||
<div className='mt-1 space-y-1 text-red-400 text-xs'>
|
||||
{emailErrors.map((error, index) => (
|
||||
<p key={index}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
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={isSendingOtp}
|
||||
>
|
||||
{isSendingOtp ? (
|
||||
<>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Sending Code...
|
||||
</>
|
||||
) : (
|
||||
'Continue'
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
) : (
|
||||
<div className='space-y-8'>
|
||||
<div className='space-y-6'>
|
||||
<p className='text-center text-muted-foreground text-sm'>
|
||||
Enter the 6-digit code to verify your account. If you don't see it in your
|
||||
inbox, check your spam folder.
|
||||
</p>
|
||||
|
||||
<div className='flex justify-center'>
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={otpValue}
|
||||
onChange={(value) => {
|
||||
setOtpValue(value)
|
||||
if (value.length === 6) {
|
||||
handleVerifyOtp(value)
|
||||
}
|
||||
}}
|
||||
disabled={isVerifyingOtp}
|
||||
className={cn('gap-2', authError && 'otp-error')}
|
||||
>
|
||||
<InputOTPGroup className='[&>div]:!rounded-[10px] gap-2'>
|
||||
{[0, 1, 2, 3, 4, 5].map((index) => (
|
||||
<InputOTPSlot
|
||||
key={index}
|
||||
index={index}
|
||||
className={cn(
|
||||
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
|
||||
'border-gray-300 hover:border-gray-400',
|
||||
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
|
||||
authError &&
|
||||
'border-red-500 focus:border-red-500 focus:ring-red-100'
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{authError && (
|
||||
<div className='mt-1 space-y-1 text-center text-red-400 text-xs'>
|
||||
<p>{authError}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => handleVerifyOtp()}
|
||||
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={otpValue.length !== 6 || isVerifyingOtp}
|
||||
>
|
||||
{isVerifyingOtp ? 'Verifying...' : 'Verify Email'}
|
||||
</Button>
|
||||
|
||||
<div className='text-center'>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
Didn't receive a code?{' '}
|
||||
{countdown > 0 ? (
|
||||
<span>
|
||||
Resend in{' '}
|
||||
<span className='font-medium text-foreground'>{countdown}s</span>
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
|
||||
onClick={handleResendOtp}
|
||||
disabled={isVerifyingOtp || isResendDisabled}
|
||||
>
|
||||
Resend
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='text-center font-light text-[14px]'>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowOtpVerification(false)
|
||||
setOtpValue('')
|
||||
setAuthError(null)
|
||||
}}
|
||||
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
|
||||
>
|
||||
Change email
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showValidationError, setShowValidationError] = useState(false)
|
||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||
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<HTMLInputElement>) => {
|
||||
@@ -32,10 +73,18 @@ export default function PasswordAuth({
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<Dialog open={true} onOpenChange={() => {}}>
|
||||
<DialogContent
|
||||
className='flex flex-col gap-0 overflow-hidden p-0 sm:max-w-[450px]'
|
||||
hideCloseButton
|
||||
>
|
||||
<DialogHeader className='border-b px-6 py-4'>
|
||||
<div className='flex items-center justify-center'>
|
||||
<a href='https://sim.ai' target='_blank' rel='noopener noreferrer' className='mb-2'>
|
||||
<svg
|
||||
width='40'
|
||||
height='40'
|
||||
viewBox='0 0 50 50'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className='rounded-[6px]'
|
||||
<div className='bg-white'>
|
||||
<Nav variant='auth' />
|
||||
<div className='flex min-h-[calc(100vh-120px)] items-center justify-center px-4'>
|
||||
<div className='w-full max-w-[410px]'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
{/* Header */}
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1
|
||||
className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}
|
||||
>
|
||||
<rect width='50' height='50' fill='var(--brand-primary-hex)' />
|
||||
<path
|
||||
d='M34.1455 20.0728H16.0364C12.7026 20.0728 10 22.7753 10 26.1091V35.1637C10 38.4975 12.7026 41.2 16.0364 41.2H34.1455C37.4792 41.2 40.1818 38.4975 40.1818 35.1637V26.1091C40.1818 22.7753 37.4792 20.0728 34.1455 20.0728Z'
|
||||
fill='var(--brand-primary-hex)'
|
||||
stroke='white'
|
||||
strokeWidth='3.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M25.0919 14.0364C26.7588 14.0364 28.1101 12.6851 28.1101 11.0182C28.1101 9.35129 26.7588 8 25.0919 8C23.425 8 22.0737 9.35129 22.0737 11.0182C22.0737 12.6851 23.425 14.0364 25.0919 14.0364Z'
|
||||
fill='var(--brand-primary-hex)'
|
||||
stroke='white'
|
||||
strokeWidth='4'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M25.0915 14.856V19.0277V14.856ZM20.5645 32.1398V29.1216V32.1398ZM29.619 29.1216V32.1398V29.1216Z'
|
||||
fill='var(--brand-primary-hex)'
|
||||
/>
|
||||
<path
|
||||
d='M25.0915 14.856V19.0277M20.5645 32.1398V29.1216M29.619 29.1216V32.1398'
|
||||
stroke='white'
|
||||
strokeWidth='4'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<circle cx='25' cy='11' r='2' fill='var(--brand-primary-hex)' />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<DialogTitle className='text-center font-medium text-lg'>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className='p-6'>
|
||||
<div className='mb-4 text-center'>
|
||||
<p className='text-muted-foreground'>
|
||||
This chat is password-protected. Please enter the password to continue.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{authError && (
|
||||
<div className='mb-4 rounded-md border border-red-200 bg-red-50 p-3 text-red-600 text-sm'>
|
||||
{authError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
handleAuthenticate()
|
||||
}}
|
||||
className='space-y-4'
|
||||
>
|
||||
<div className='space-y-2'>
|
||||
<Input
|
||||
id='password'
|
||||
type='password'
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder='Enter password'
|
||||
disabled={isAuthenticating}
|
||||
autoComplete='new-password'
|
||||
className='w-full'
|
||||
autoFocus
|
||||
/>
|
||||
Password Required
|
||||
</h1>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
This chat is password-protected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={!password || isAuthenticating}
|
||||
className='w-full'
|
||||
style={{ backgroundColor: primaryColor }}
|
||||
{/* Form */}
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
handleAuthenticate()
|
||||
}}
|
||||
className={`${inter.className} mt-8 w-full space-y-8`}
|
||||
>
|
||||
{isAuthenticating ? (
|
||||
<div className='flex items-center justify-center'>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Authenticating...
|
||||
<div className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label htmlFor='password'>Password</Label>
|
||||
</div>
|
||||
<div className='relative'>
|
||||
<Input
|
||||
id='password'
|
||||
name='password'
|
||||
required
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoCapitalize='none'
|
||||
autoComplete='new-password'
|
||||
autoCorrect='off'
|
||||
placeholder='Enter password'
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn(
|
||||
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
|
||||
showValidationError &&
|
||||
passwordErrors.length > 0 &&
|
||||
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
|
||||
)}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700'
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
{showValidationError && passwordErrors.length > 0 && (
|
||||
<div className='mt-1 space-y-1 text-red-400 text-xs'>
|
||||
{passwordErrors.map((error, index) => (
|
||||
<p key={index}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
'Continue'
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
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={isAuthenticating}
|
||||
>
|
||||
{isAuthenticating ? 'Authenticating...' : 'Continue'}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className='flex min-h-screen items-center justify-center bg-gray-50'>
|
||||
<div className='mx-auto max-w-md rounded-xl bg-white p-6 shadow-md'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<a href='https://sim.ai' target='_blank' rel='noopener noreferrer'>
|
||||
<svg
|
||||
width='32'
|
||||
height='32'
|
||||
viewBox='0 0 50 50'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className='rounded-[6px]'
|
||||
>
|
||||
<rect width='50' height='50' fill='var(--brand-primary-hex)' />
|
||||
<path
|
||||
d='M34.1455 20.0728H16.0364C12.7026 20.0728 10 22.7753 10 26.1091V35.1637C10 38.4975 12.7026 41.2 16.0364 41.2H34.1455C37.4792 41.2 40.1818 38.4975 40.1818 35.1637V26.1091C40.1818 22.7753 37.4792 20.0728 34.1455 20.0728Z'
|
||||
fill='var(--brand-primary-hex)'
|
||||
stroke='white'
|
||||
strokeWidth='3.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M25.0919 14.0364C26.7588 14.0364 28.1101 12.6851 28.1101 11.0182C28.1101 9.35129 26.7588 8 25.0919 8C23.425 8 22.0737 9.35129 22.0737 11.0182C22.0737 12.6851 23.425 14.0364 25.0919 14.0364Z'
|
||||
fill='var(--brand-primary-hex)'
|
||||
stroke='white'
|
||||
strokeWidth='4'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M25.0915 14.856V19.0277V14.856ZM20.5645 32.1398V29.1216V32.1398ZM29.619 29.1216V32.1398V29.1216Z'
|
||||
fill='var(--brand-primary-hex)'
|
||||
/>
|
||||
<path
|
||||
d='M25.0915 14.856V19.0277M20.5645 32.1398V29.1216M29.619 29.1216V32.1398'
|
||||
stroke='white'
|
||||
strokeWidth='4'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<circle cx='25' cy='11' r='2' fill='var(--brand-primary-hex)' />
|
||||
</svg>
|
||||
</a>
|
||||
<ChatHeader chatConfig={null} starCount={starCount} />
|
||||
<div className='bg-white'>
|
||||
<Nav variant='auth' />
|
||||
<div className='flex min-h-[calc(100vh-120px)] items-center justify-center px-4'>
|
||||
<div className='w-full max-w-[410px]'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
{/* Error content */}
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1
|
||||
className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}
|
||||
>
|
||||
Chat Unavailable
|
||||
</h1>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action button - matching login form */}
|
||||
<div className='mt-8 w-full'>
|
||||
<Button
|
||||
type='button'
|
||||
onClick={() => router.push('/workspace')}
|
||||
className={`${buttonClass} flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200`}
|
||||
>
|
||||
Return to Workspace
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className='mb-2 font-bold text-red-500 text-xl'>Error</h2>
|
||||
<p className='text-gray-700'>{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<div className='flex items-center justify-between bg-background/95 px-6 py-4 pt-6 backdrop-blur supports-[backdrop-filter]:bg-background/60 md:px-8 md:pt-4'>
|
||||
<div className='flex items-center gap-4'>
|
||||
{customImage && (
|
||||
<img
|
||||
src={customImage}
|
||||
alt={`${chatConfig?.title || 'Chat'} logo`}
|
||||
className='h-8 w-8 rounded-md object-cover'
|
||||
/>
|
||||
)}
|
||||
<h2 className='font-medium text-foreground text-lg'>
|
||||
{chatConfig?.customizations?.headerText || chatConfig?.title || 'Chat'}
|
||||
</h2>
|
||||
<nav
|
||||
aria-label='Chat navigation'
|
||||
className={`flex w-full items-center justify-between px-4 pt-[12px] pb-[21px] sm:px-8 sm:pt-[8.5px] md:px-[44px] md:pt-[16px]`}
|
||||
>
|
||||
<div className='flex items-center gap-[34px]'>
|
||||
<div className='flex items-center gap-3'>
|
||||
{customImage && (
|
||||
<Image
|
||||
src={customImage}
|
||||
alt={`${chatConfig?.title || 'Chat'} logo`}
|
||||
width={24}
|
||||
height={24}
|
||||
className='h-6 w-6 rounded-md object-cover'
|
||||
/>
|
||||
)}
|
||||
<h2 className={`${inter.className} font-medium text-[18px] text-foreground`}>
|
||||
{chatConfig?.customizations?.headerText || chatConfig?.title || 'Chat'}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<a
|
||||
href='https://github.com/simstudioai/sim'
|
||||
className='flex items-center gap-1 text-foreground'
|
||||
aria-label='GitHub'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<GithubIcon className='h-5 w-5' />
|
||||
<span className='hidden font-medium text-sm sm:inline-block'>{starCount}</span>
|
||||
</a>
|
||||
<a
|
||||
href='https://sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center rounded-md p-1 text-foreground/80 transition-colors duration-200 hover:text-foreground/100'
|
||||
>
|
||||
<div
|
||||
className='flex h-7 w-7 items-center justify-center rounded-md'
|
||||
style={{ backgroundColor: primaryColor }}
|
||||
|
||||
{!brand.logoUrl && (
|
||||
<div className='flex items-center gap-[16px]'>
|
||||
<a
|
||||
href='https://github.com/simstudioai/sim'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='flex items-center gap-2 text-[16px] text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label={`GitHub repository - ${starCount} stars`}
|
||||
>
|
||||
<svg
|
||||
width='16'
|
||||
height='16'
|
||||
viewBox='0 0 50 50'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M34.1455 20.0728H16.0364C12.7026 20.0728 10 22.7753 10 26.1091V35.1637C10 38.4975 12.7026 41.2 16.0364 41.2H34.1455C37.4792 41.2 40.1818 38.4975 40.1818 35.1637V26.1091C40.1818 22.7753 37.4792 20.0728 34.1455 20.0728Z'
|
||||
fill={primaryColor}
|
||||
stroke='white'
|
||||
strokeWidth='3.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M25.0919 14.0364C26.7588 14.0364 28.1101 12.6851 28.1101 11.0182C28.1101 9.35129 26.7588 8 25.0919 8C23.425 8 22.0737 9.35129 22.0737 11.0182C22.0737 12.6851 23.425 14.0364 25.0919 14.0364Z'
|
||||
fill={primaryColor}
|
||||
stroke='white'
|
||||
strokeWidth='4'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M25.0915 14.856V19.0277M20.5645 32.1398V29.1216M29.619 29.1216V32.1398'
|
||||
stroke='white'
|
||||
strokeWidth='4'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<circle cx='25' cy='11' r='2' fill={primaryColor} />
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<GithubIcon className='h-[16px] w-[16px]' aria-hidden='true' />
|
||||
<span className={`${inter.className}`} aria-live='polite'>
|
||||
{starCount}
|
||||
</span>
|
||||
</a>
|
||||
{/* Only show Sim logo if no custom branding is set */}
|
||||
|
||||
<Link
|
||||
href='https://sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
aria-label='Sim home'
|
||||
>
|
||||
<Image
|
||||
src='/logo/b&w/text/small.png'
|
||||
alt='Sim - Workflows for LLMs'
|
||||
width={29.869884}
|
||||
height={14.5656}
|
||||
className='h-[14.5656px] w-auto pb-[1px]'
|
||||
priority
|
||||
loading='eager'
|
||||
quality={100}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
export function ChatLoadingState() {
|
||||
return (
|
||||
<div className='flex min-h-screen items-center justify-center bg-background text-foreground'>
|
||||
<div className='animate-pulse text-center'>
|
||||
<div className='mx-auto mb-4 h-8 w-48 rounded bg-muted' />
|
||||
<div className='mx-auto h-4 w-64 rounded bg-muted' />
|
||||
<div className='bg-white'>
|
||||
<div className='flex min-h-[calc(100vh-120px)] items-center justify-center px-4'>
|
||||
<div className='w-full max-w-[410px]'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
{/* Title skeleton */}
|
||||
<div className='space-y-2 text-center'>
|
||||
<Skeleton className='mx-auto h-8 w-32' />
|
||||
<Skeleton className='mx-auto h-4 w-48' />
|
||||
</div>
|
||||
|
||||
{/* Form skeleton */}
|
||||
<div className='mt-8 w-full space-y-8'>
|
||||
<div className='space-y-2'>
|
||||
<Skeleton className='h-4 w-16' />
|
||||
<Skeleton className='h-10 w-full rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='h-10 w-full rounded-[10px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ export function CreateChunkModal({
|
||||
<Button
|
||||
onClick={handleCreateChunk}
|
||||
disabled={!isFormValid || isCreating}
|
||||
className='bg-[var(--brand-primary-hex)] font-[480] text-primary-foreground shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-secondary-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
|
||||
className='bg-[var(--brand-primary-hex)] font-[480] text-primary-foreground shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
|
||||
@@ -685,7 +685,7 @@ export function Document({
|
||||
onClick={() => setIsCreateChunkModalOpen(true)}
|
||||
disabled={documentData?.processingStatus === 'failed' || !userPermissions.canEdit}
|
||||
size='sm'
|
||||
className='flex items-center gap-1 bg-[var(--brand-primary-hex)] font-[480] text-white shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-secondary-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
className='flex items-center gap-1 bg-[var(--brand-primary-hex)] font-[480] text-white shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
>
|
||||
<Plus className='h-3.5 w-3.5' />
|
||||
<span>Create Chunk</span>
|
||||
|
||||
@@ -65,7 +65,7 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
|
||||
'The Agent block is a core workflow block that is a wrapper around an LLM. It takes in system/user prompts and calls an LLM provider. It can also make tool calls by directly containing tools inside of its tool input. It can additionally return structured output.',
|
||||
docsLink: 'https://docs.sim.ai/blocks/agent',
|
||||
category: 'blocks',
|
||||
bgColor: 'var(--brand-primary-hover-hex)',
|
||||
bgColor: 'var(--brand-primary-hex)',
|
||||
icon: AgentIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ export function LoadingAgent({ size = 'md' }: LoadingAgentProps) {
|
||||
>
|
||||
<path
|
||||
d='M15.6667 9.25H4.66667C2.64162 9.25 1 10.8916 1 12.9167V18.4167C1 20.4417 2.64162 22.0833 4.66667 22.0833H15.6667C17.6917 22.0833 19.3333 20.4417 19.3333 18.4167V12.9167C19.3333 10.8916 17.6917 9.25 15.6667 9.25Z'
|
||||
stroke='var(--brand-primary-hover-hex)'
|
||||
stroke='var(--brand-primary-hex)'
|
||||
strokeWidth='1.8'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
@@ -41,7 +41,7 @@ export function LoadingAgent({ size = 'md' }: LoadingAgentProps) {
|
||||
/>
|
||||
<path
|
||||
d='M10.1663 5.58464C11.1789 5.58464 11.9997 4.76382 11.9997 3.7513C11.9997 2.73878 11.1789 1.91797 10.1663 1.91797C9.15382 1.91797 8.33301 2.73878 8.33301 3.7513C8.33301 4.76382 9.15382 5.58464 10.1663 5.58464Z'
|
||||
stroke='var(--brand-primary-hover-hex)'
|
||||
stroke='var(--brand-primary-hex)'
|
||||
strokeWidth='1.8'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
@@ -54,7 +54,7 @@ export function LoadingAgent({ size = 'md' }: LoadingAgentProps) {
|
||||
/>
|
||||
<path
|
||||
d='M10.167 5.58594V9.2526M7.41699 16.5859V14.7526M12.917 14.7526V16.5859'
|
||||
stroke='var(--brand-primary-hover-hex)'
|
||||
stroke='var(--brand-primary-hex)'
|
||||
strokeWidth='1.8'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
|
||||
@@ -3,7 +3,6 @@ import { getEnv } from '@/lib/env'
|
||||
export interface ThemeColors {
|
||||
primaryColor?: string
|
||||
primaryHoverColor?: string
|
||||
secondaryColor?: string
|
||||
accentColor?: string
|
||||
accentHoverColor?: string
|
||||
backgroundColor?: string
|
||||
@@ -36,7 +35,6 @@ const defaultConfig: BrandConfig = {
|
||||
theme: {
|
||||
primaryColor: '#701ffc',
|
||||
primaryHoverColor: '#802fff',
|
||||
secondaryColor: '#6518e6',
|
||||
accentColor: '#9d54ff',
|
||||
accentHoverColor: '#a66fff',
|
||||
backgroundColor: '#0c0c0c',
|
||||
@@ -48,8 +46,6 @@ const getThemeColors = (): ThemeColors => {
|
||||
primaryColor: getEnv('NEXT_PUBLIC_BRAND_PRIMARY_COLOR') || defaultConfig.theme?.primaryColor,
|
||||
primaryHoverColor:
|
||||
getEnv('NEXT_PUBLIC_BRAND_PRIMARY_HOVER_COLOR') || defaultConfig.theme?.primaryHoverColor,
|
||||
secondaryColor:
|
||||
getEnv('NEXT_PUBLIC_BRAND_SECONDARY_COLOR') || defaultConfig.theme?.secondaryColor,
|
||||
accentColor: getEnv('NEXT_PUBLIC_BRAND_ACCENT_COLOR') || defaultConfig.theme?.accentColor,
|
||||
accentHoverColor:
|
||||
getEnv('NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR') || defaultConfig.theme?.accentHoverColor,
|
||||
|
||||
@@ -19,10 +19,6 @@ export function generateThemeCSS(): string {
|
||||
cssVars.push(`--brand-primary-hover-hex: ${process.env.NEXT_PUBLIC_BRAND_PRIMARY_HOVER_COLOR};`)
|
||||
}
|
||||
|
||||
if (process.env.NEXT_PUBLIC_BRAND_SECONDARY_COLOR) {
|
||||
cssVars.push(`--brand-secondary-hex: ${process.env.NEXT_PUBLIC_BRAND_SECONDARY_COLOR};`)
|
||||
}
|
||||
|
||||
if (process.env.NEXT_PUBLIC_BRAND_ACCENT_COLOR) {
|
||||
cssVars.push(`--brand-accent-hex: ${process.env.NEXT_PUBLIC_BRAND_ACCENT_COLOR};`)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export function generateBrandedMetadata(override: Partial<Metadata> = {}): Metad
|
||||
const brand = getBrandConfig()
|
||||
|
||||
const defaultTitle = brand.name
|
||||
const summaryFull = `Sim is an open-source AI agent workflow builder. Developers at trail-blazing startups to Fortune 500 companies deploy agentic workflows on the Sim platform. 30,000+ developers are already using Sim to build and deploy AI agent workflows. Sim lets developers integrate with 100+ apps to streamline workflows with AI agents. Sim is SOC2 and HIPAA compliant, ensuring enterprise-level security.`
|
||||
const summaryFull = `Sim is an open-source AI agent workflow builder. Developers at trail-blazing startups to Fortune 500 companies deploy agentic workflows on the Sim platform. 35,000+ developers are already using Sim to build and deploy AI agent workflows. Sim lets developers integrate with 100+ apps to streamline workflows with AI agents. Sim is SOC2 and HIPAA compliant, ensuring enterprise-level security.`
|
||||
const summaryShort = `Sim is an open-source AI agent workflow builder.`
|
||||
|
||||
return {
|
||||
|
||||
@@ -240,7 +240,6 @@ export const env = createEnv({
|
||||
// Theme Customization
|
||||
NEXT_PUBLIC_BRAND_PRIMARY_COLOR: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(), // Primary brand color (hex format, e.g., "#701ffc")
|
||||
NEXT_PUBLIC_BRAND_PRIMARY_HOVER_COLOR: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(), // Primary brand hover state (hex format)
|
||||
NEXT_PUBLIC_BRAND_SECONDARY_COLOR: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(), // Secondary brand color (hex format)
|
||||
NEXT_PUBLIC_BRAND_ACCENT_COLOR: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(), // Accent brand color (hex format)
|
||||
NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(), // Accent brand hover state (hex format)
|
||||
NEXT_PUBLIC_BRAND_BACKGROUND_COLOR: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(), // Brand background color (hex format)
|
||||
@@ -275,7 +274,6 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_PRIVACY_URL: process.env.NEXT_PUBLIC_PRIVACY_URL,
|
||||
NEXT_PUBLIC_BRAND_PRIMARY_COLOR: process.env.NEXT_PUBLIC_BRAND_PRIMARY_COLOR,
|
||||
NEXT_PUBLIC_BRAND_PRIMARY_HOVER_COLOR: process.env.NEXT_PUBLIC_BRAND_PRIMARY_HOVER_COLOR,
|
||||
NEXT_PUBLIC_BRAND_SECONDARY_COLOR: process.env.NEXT_PUBLIC_BRAND_SECONDARY_COLOR,
|
||||
NEXT_PUBLIC_BRAND_ACCENT_COLOR: process.env.NEXT_PUBLIC_BRAND_ACCENT_COLOR,
|
||||
NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR: process.env.NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR,
|
||||
NEXT_PUBLIC_BRAND_BACKGROUND_COLOR: process.env.NEXT_PUBLIC_BRAND_BACKGROUND_COLOR,
|
||||
|
||||
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 443 B |
|
Before Width: | Height: | Size: 897 B |
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "Sim",
|
||||
"short_name": "Sim",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||