Skip to content
43 changes: 15 additions & 28 deletions apps/sim/app/(auth)/login/login-form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
Expand Down Expand Up @@ -99,15 +99,21 @@ export default function LoginPage({
const router = useRouter()
const searchParams = useSearchParams()
const [isLoading, setIsLoading] = useState(false)
const [_mounted, setMounted] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [password, setPassword] = useState('')
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
const [showValidationError, setShowValidationError] = useState(false)
const buttonClass = useBrandedButtonClass()

const [callbackUrl, setCallbackUrl] = useState('/workspace')
const [isInviteFlow, setIsInviteFlow] = useState(false)
const callbackUrlParam = searchParams?.get('callbackUrl')
const invalidCallbackRef = useRef(false)
if (callbackUrlParam && !validateCallbackUrl(callbackUrlParam) && !invalidCallbackRef.current) {
invalidCallbackRef.current = true
logger.warn('Invalid callback URL detected and blocked:', { url: callbackUrlParam })
}
const callbackUrl =
callbackUrlParam && validateCallbackUrl(callbackUrlParam) ? callbackUrlParam : '/workspace'
const isInviteFlow = searchParams?.get('invite_flow') === 'true'

const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
const [forgotPasswordEmail, setForgotPasswordEmail] = useState('')
Expand All @@ -120,30 +126,11 @@ export default function LoginPage({
const [email, setEmail] = useState('')
const [emailErrors, setEmailErrors] = useState<string[]>([])
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
const [resetSuccessMessage, setResetSuccessMessage] = useState<string | null>(null)

useEffect(() => {
setMounted(true)

if (searchParams) {
const callback = searchParams.get('callbackUrl')
if (callback) {
if (validateCallbackUrl(callback)) {
setCallbackUrl(callback)
} else {
logger.warn('Invalid callback URL detected and blocked:', { url: callback })
}
}

const inviteFlow = searchParams.get('invite_flow') === 'true'
setIsInviteFlow(inviteFlow)

const resetSuccess = searchParams.get('resetSuccess') === 'true'
if (resetSuccess) {
setResetSuccessMessage('Password reset successful. Please sign in with your new password.')
}
}
}, [searchParams])
const [resetSuccessMessage, setResetSuccessMessage] = useState<string | null>(() =>
searchParams?.get('resetSuccess') === 'true'
? 'Password reset successful. Please sign in with your new password.'
: null
)

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
Expand Down
17 changes: 6 additions & 11 deletions apps/sim/app/(auth)/reset-password/reset-password-content.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { Suspense, useEffect, useState } from 'react'
import { Suspense, useState } from 'react'
import { createLogger } from '@sim/logger'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
Expand All @@ -22,14 +22,9 @@ function ResetPasswordContent() {
text: '',
})

useEffect(() => {
if (!token) {
setStatusMessage({
type: 'error',
text: 'Invalid or missing reset token. Please request a new password reset link.',
})
}
}, [token])
const tokenError = !token
? 'Invalid or missing reset token. Please request a new password reset link.'
: null

const handleResetPassword = async (password: string) => {
try {
Expand Down Expand Up @@ -87,8 +82,8 @@ function ResetPasswordContent() {
token={token}
onSubmit={handleResetPassword}
isSubmitting={isSubmitting}
statusType={statusMessage.type}
statusMessage={statusMessage.text}
statusType={tokenError ? 'error' : statusMessage.type}
statusMessage={tokenError ?? statusMessage.text}
/>
</div>

Expand Down
45 changes: 14 additions & 31 deletions apps/sim/app/(auth)/signup/signup-form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { Suspense, useEffect, useState } from 'react'
import { Suspense, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
Expand Down Expand Up @@ -82,49 +82,32 @@ function SignupFormContent({
const searchParams = useSearchParams()
const { refetch: refetchSession } = useSession()
const [isLoading, setIsLoading] = useState(false)
const [, setMounted] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [password, setPassword] = useState('')
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
const [showValidationError, setShowValidationError] = useState(false)
const [email, setEmail] = useState('')
const [email, setEmail] = useState(() => searchParams.get('email') ?? '')
const [emailError, setEmailError] = useState('')
const [emailErrors, setEmailErrors] = useState<string[]>([])
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
const [redirectUrl, setRedirectUrl] = useState('')
const [isInviteFlow, setIsInviteFlow] = useState(false)
const buttonClass = useBrandedButtonClass()

const redirectUrl = useMemo(
() => searchParams.get('redirect') || searchParams.get('callbackUrl') || '',
[searchParams]
)
const isInviteFlow = useMemo(
() =>
searchParams.get('invite_flow') === 'true' ||
redirectUrl.startsWith('/invite/') ||
redirectUrl.startsWith('/credential-account/'),
[searchParams, redirectUrl]
)

const [name, setName] = useState('')
const [nameErrors, setNameErrors] = useState<string[]>([])
const [showNameValidationError, setShowNameValidationError] = useState(false)

useEffect(() => {
setMounted(true)
const emailParam = searchParams.get('email')
if (emailParam) {
setEmail(emailParam)
}

// Check both 'redirect' and 'callbackUrl' params (login page uses callbackUrl)
const redirectParam = searchParams.get('redirect') || searchParams.get('callbackUrl')
if (redirectParam) {
setRedirectUrl(redirectParam)

if (
redirectParam.startsWith('/invite/') ||
redirectParam.startsWith('/credential-account/')
) {
setIsInviteFlow(true)
}
}

const inviteFlowParam = searchParams.get('invite_flow')
if (inviteFlowParam === 'true') {
setIsInviteFlow(true)
}
}, [searchParams])

const validatePassword = (passwordValue: string): string[] => {
const errors: string[] = []

Expand Down
21 changes: 7 additions & 14 deletions apps/sim/app/chat/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,6 @@ export const ChatInput: React.FC<{
}
}

// Adjust height on input change
useEffect(() => {
adjustTextareaHeight()
}, [inputValue])

// Close the input when clicking outside (only when empty)
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
Expand All @@ -94,17 +89,14 @@ export const ChatInput: React.FC<{
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [inputValue])

// Handle focus and initial height when activated
useEffect(() => {
if (isActive && textareaRef.current) {
textareaRef.current.focus()
adjustTextareaHeight() // Adjust height when becoming active
}
}, [isActive])

const handleActivate = () => {
setIsActive(true)
// Focus is now handled by the useEffect above
requestAnimationFrame(() => {
if (textareaRef.current) {
textareaRef.current.focus()
adjustTextareaHeight()
}
})
}

// Handle file selection
Expand Down Expand Up @@ -186,6 +178,7 @@ export const ChatInput: React.FC<{

const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(e.target.value)
adjustTextareaHeight()
}

// Handle voice start with smooth transition to voice-first mode
Expand Down
50 changes: 26 additions & 24 deletions apps/sim/app/chat/components/voice-interface/voice-interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ export function VoiceInterface({
const currentStateRef = useRef<'idle' | 'listening' | 'agent_speaking'>('idle')
const isCallEndedRef = useRef(false)

useEffect(() => {
currentStateRef.current = state
}, [state])
const updateState = useCallback((next: 'idle' | 'listening' | 'agent_speaking') => {
setState(next)
currentStateRef.current = next
}, [])

const recognitionRef = useRef<SpeechRecognition | null>(null)
const mediaStreamRef = useRef<MediaStream | null>(null)
Expand All @@ -97,9 +98,10 @@ export function VoiceInterface({
(window as WindowWithSpeech).webkitSpeechRecognition
)

useEffect(() => {
isMutedRef.current = isMuted
}, [isMuted])
const updateIsMuted = useCallback((next: boolean) => {
setIsMuted(next)
isMutedRef.current = next
}, [])

const setResponseTimeout = useCallback(() => {
if (responseTimeoutRef.current) {
Expand All @@ -108,7 +110,7 @@ export function VoiceInterface({

responseTimeoutRef.current = setTimeout(() => {
if (currentStateRef.current === 'listening') {
setState('idle')
updateState('idle')
}
}, 5000)
}, [])
Expand All @@ -123,10 +125,10 @@ export function VoiceInterface({
useEffect(() => {
if (isPlayingAudio && state !== 'agent_speaking') {
clearResponseTimeout()
setState('agent_speaking')
updateState('agent_speaking')
setCurrentTranscript('')

setIsMuted(true)
updateIsMuted(true)
if (mediaStreamRef.current) {
mediaStreamRef.current.getAudioTracks().forEach((track) => {
track.enabled = false
Expand All @@ -141,17 +143,17 @@ export function VoiceInterface({
}
}
} else if (!isPlayingAudio && state === 'agent_speaking') {
setState('idle')
updateState('idle')
setCurrentTranscript('')

setIsMuted(false)
updateIsMuted(false)
if (mediaStreamRef.current) {
mediaStreamRef.current.getAudioTracks().forEach((track) => {
track.enabled = true
})
}
}
}, [isPlayingAudio, state, clearResponseTimeout])
}, [isPlayingAudio, state, clearResponseTimeout, updateState, updateIsMuted])

const setupAudio = useCallback(async () => {
try {
Expand Down Expand Up @@ -310,7 +312,7 @@ export function VoiceInterface({
return
}

setState('listening')
updateState('listening')
setCurrentTranscript('')

if (recognitionRef.current) {
Expand All @@ -320,10 +322,10 @@ export function VoiceInterface({
logger.error('Error starting recognition:', error)
}
}
}, [isInitialized, isMuted, state])
}, [isInitialized, isMuted, state, updateState])

const stopListening = useCallback(() => {
setState('idle')
updateState('idle')
setCurrentTranscript('')

if (recognitionRef.current) {
Expand All @@ -333,15 +335,15 @@ export function VoiceInterface({
// Ignore
}
}
}, [])
}, [updateState])

const handleInterrupt = useCallback(() => {
if (state === 'agent_speaking') {
onInterrupt?.()
setState('listening')
updateState('listening')
setCurrentTranscript('')

setIsMuted(false)
updateIsMuted(false)
if (mediaStreamRef.current) {
mediaStreamRef.current.getAudioTracks().forEach((track) => {
track.enabled = true
Expand All @@ -356,14 +358,14 @@ export function VoiceInterface({
}
}
}
}, [state, onInterrupt])
}, [state, onInterrupt, updateState, updateIsMuted])

const handleCallEnd = useCallback(() => {
isCallEndedRef.current = true

setState('idle')
updateState('idle')
setCurrentTranscript('')
setIsMuted(false)
updateIsMuted(false)

if (recognitionRef.current) {
try {
Expand All @@ -376,7 +378,7 @@ export function VoiceInterface({
clearResponseTimeout()
onInterrupt?.()
onCallEnd?.()
}, [onCallEnd, onInterrupt, clearResponseTimeout])
}, [onCallEnd, onInterrupt, clearResponseTimeout, updateState, updateIsMuted])

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
Expand All @@ -397,7 +399,7 @@ export function VoiceInterface({
}

const newMutedState = !isMuted
setIsMuted(newMutedState)
updateIsMuted(newMutedState)

if (mediaStreamRef.current) {
mediaStreamRef.current.getAudioTracks().forEach((track) => {
Expand All @@ -410,7 +412,7 @@ export function VoiceInterface({
} else if (state === 'idle') {
startListening()
}
}, [isMuted, state, handleInterrupt, stopListening, startListening])
}, [isMuted, state, handleInterrupt, stopListening, startListening, updateIsMuted])

useEffect(() => {
if (isSupported) {
Expand Down
Loading
Loading