diff --git a/apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx b/apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx
index 0685f106822..f066279140d 100644
--- a/apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx
+++ b/apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx
@@ -1,6 +1,6 @@
'use client'
-import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { type RefObject, useCallback, useMemo, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import {
@@ -26,6 +26,8 @@ import { useGitHubStars } from '@/hooks/queries/github-stars'
const logger = createLogger('ChatClient')
+const NEAR_BOTTOM_THRESHOLD_PX = 100
+
interface ChatRequestFile {
name: string
size: number
@@ -87,13 +89,11 @@ export default function ChatClient({ identifier }: { identifier: string }) {
const { isStreamingResponse, abortControllerRef, stopStreaming, handleStreamedResponse } =
useChatStreaming()
- const NEAR_BOTTOM_THRESHOLD_PX = 100
-
/**
* ChatGPT-style scroll. Without `force`, no-ops when the user has scrolled away.
* With `force` (jump button), re-pins to bottom.
*/
- const scrollToBottom = useCallback((options?: { behavior?: ScrollBehavior; force?: boolean }) => {
+ const scrollToBottom = (options?: { behavior?: ScrollBehavior; force?: boolean }) => {
const behavior = options?.behavior ?? 'smooth'
const force = options?.force === true
if (!force && !stickToBottomRef.current) return
@@ -112,52 +112,46 @@ export default function ChatClient({ identifier }: { identifier: string }) {
},
behavior === 'smooth' ? 400 : 50
)
- }, [])
+ }
- const scrollToMessage = useCallback(
- (messageId: string, scrollToShowOnlyMessage = false) => {
- const messageElement = document.querySelector(`[data-message-id="${messageId}"]`)
- if (messageElement && messagesContainerRef.current) {
- const container = messagesContainerRef.current
- const containerRect = container.getBoundingClientRect()
- const messageRect = messageElement.getBoundingClientRect()
-
- if (scrollToShowOnlyMessage) {
- const scrollTop = container.scrollTop + messageRect.top - containerRect.top
-
- container.scrollTo({
- top: scrollTop,
- behavior: 'smooth',
- })
- } else {
- const scrollTop = container.scrollTop + messageRect.top - containerRect.top - 80
-
- container.scrollTo({
- top: scrollTop,
- behavior: 'smooth',
- })
- }
- }
- },
- [messagesContainerRef]
- )
+ const scrollToMessage = (messageId: string) => {
+ const messageElement = document.querySelector(`[data-message-id="${messageId}"]`)
+ if (!messageElement || !messagesContainerRef.current) return
- useEffect(() => {
const container = messagesContainerRef.current
- if (!container) return
+ const containerRect = container.getBoundingClientRect()
+ const messageRect = messageElement.getBoundingClientRect()
+
+ container.scrollTo({
+ top: container.scrollTop + messageRect.top - containerRect.top,
+ behavior: 'smooth',
+ })
+ }
+
+ /**
+ * Attaches on mount via a ref callback rather than an effect: the container
+ * renders only after the auth/loading early returns, so an effect would need
+ * unrelated render values as a stand-in for "the node exists yet".
+ */
+ const attachMessagesContainer = useCallback((node: HTMLDivElement | null) => {
+ messagesContainerRef.current = node
+ if (!node) return
const handleScroll = () => {
if (ignoreScrollRef.current) return
- const { scrollTop, scrollHeight, clientHeight } = container
+ const { scrollTop, scrollHeight, clientHeight } = node
const distanceFromBottom = scrollHeight - scrollTop - clientHeight
const nearBottom = distanceFromBottom <= NEAR_BOTTOM_THRESHOLD_PX
stickToBottomRef.current = nearBottom
setShowScrollButton(!nearBottom)
}
- container.addEventListener('scroll', handleScroll, { passive: true })
- return () => container.removeEventListener('scroll', handleScroll)
- }, [chatConfig, authRequired])
+ node.addEventListener('scroll', handleScroll, { passive: true })
+ return () => {
+ node.removeEventListener('scroll', handleScroll)
+ messagesContainerRef.current = null
+ }
+ }, [])
const handleSendMessage = async (
messageToSend: string,
@@ -199,7 +193,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
setIsLoading(true)
setTimeout(() => {
- scrollToMessage(userMessage.id, true)
+ scrollToMessage(userMessage.id)
}, 100)
// One AbortController for fetch + SSE body reads so Stop cancels server work too.
@@ -314,7 +308,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
}
return (
-
+
@@ -322,10 +316,9 @@ export default function ChatClient({ identifier }: { identifier: string }) {
messages={displayMessages}
isLoading={isLoading}
showScrollButton={showScrollButton}
- messagesContainerRef={messagesContainerRef as RefObject
}
+ messagesContainerRef={attachMessagesContainer}
messagesEndRef={messagesEndRef as RefObject}
scrollToBottom={() => scrollToBottom({ behavior: 'smooth', force: true })}
- scrollToMessage={scrollToMessage}
chatConfig={chatConfig}
/>
diff --git a/apps/sim/app/(interfaces)/chat/[identifier]/loading.tsx b/apps/sim/app/(interfaces)/chat/[identifier]/loading.tsx
index a964d796cb0..405a06bc0e7 100644
--- a/apps/sim/app/(interfaces)/chat/[identifier]/loading.tsx
+++ b/apps/sim/app/(interfaces)/chat/[identifier]/loading.tsx
@@ -3,7 +3,7 @@ import { DesktopTitleBarLane } from '@/app/_shell/desktop-title-bar'
export default function ChatLoading() {
return (
-
+
diff --git a/apps/sim/app/(interfaces)/chat/components/auth/email/email-auth.tsx b/apps/sim/app/(interfaces)/chat/components/auth/email/email-auth.tsx
index f5cb4767fc8..392106c515a 100644
--- a/apps/sim/app/(interfaces)/chat/components/auth/email/email-auth.tsx
+++ b/apps/sim/app/(interfaces)/chat/components/auth/email/email-auth.tsx
@@ -35,7 +35,7 @@ export default function EmailAuth({ identifier }: EmailAuthProps) {
const [email, setEmail] = useState('')
const [authError, setAuthError] = useState
(null)
const [emailErrors, setEmailErrors] = useState([])
- const [showEmailValidationError, setShowEmailValidationError] = useState(false)
+ const hasEmailError = emailErrors.length > 0
const [showOtpVerification, setShowOtpVerification] = useState(false)
const [otpValue, setOtpValue] = useState('')
@@ -53,15 +53,12 @@ export default function EmailAuth({ identifier }: EmailAuthProps) {
const handleEmailChange = (e: React.ChangeEvent) => {
const newEmail = e.target.value
setEmail(newEmail)
- const errors = validateEmailField(newEmail)
- setEmailErrors(errors)
- setShowEmailValidationError(false)
+ setEmailErrors([])
}
const handleSendOtp = async () => {
const emailValidationErrors = validateEmailField(email)
setEmailErrors(emailValidationErrors)
- setShowEmailValidationError(emailValidationErrors.length > 0)
if (emailValidationErrors.length > 0) {
return
@@ -75,7 +72,6 @@ export default function EmailAuth({ identifier }: EmailAuthProps) {
} catch (error) {
logger.error('Error sending OTP:', error)
setEmailErrors([toError(error).message || 'Failed to send verification code'])
- setShowEmailValidationError(true)
}
}
@@ -149,12 +145,10 @@ export default function EmailAuth({ identifier }: EmailAuthProps) {
value={email}
onChange={handleEmailChange}
className={cn(
- showEmailValidationError &&
- emailErrors.length > 0 &&
- 'border-[var(--text-error)] focus:border-[var(--text-error)]'
+ hasEmailError && 'border-[var(--text-error)] focus:border-[var(--text-error)]'
)}
/>
- {showEmailValidationError && emailErrors.length > 0 && (
+ {hasEmailError && (
{emailErrors.map((error) => (
{error}
diff --git a/apps/sim/app/(interfaces)/chat/components/auth/password/password-auth.tsx b/apps/sim/app/(interfaces)/chat/components/auth/password/password-auth.tsx
index 0d6a1841e9c..bbf1471f8b9 100644
--- a/apps/sim/app/(interfaces)/chat/components/auth/password/password-auth.tsx
+++ b/apps/sim/app/(interfaces)/chat/components/auth/password/password-auth.tsx
@@ -17,21 +17,19 @@ interface PasswordAuthProps {
export default function PasswordAuth({ identifier }: PasswordAuthProps) {
const [password, setPassword] = useState('')
const [showPassword, setShowPassword] = useState(false)
- const [showValidationError, setShowValidationError] = useState(false)
const [passwordErrors, setPasswordErrors] = useState
([])
+ const hasPasswordError = passwordErrors.length > 0
const authenticate = useChatPasswordAuth(identifier)
const handlePasswordChange = (e: React.ChangeEvent) => {
const newPassword = e.target.value
setPassword(newPassword)
- setShowValidationError(false)
setPasswordErrors([])
}
const handleAuthenticate = async () => {
if (!password.trim()) {
setPasswordErrors(['Password is required'])
- setShowValidationError(true)
return
}
@@ -41,7 +39,6 @@ export default function PasswordAuth({ identifier }: PasswordAuthProps) {
} catch (error) {
logger.error('Authentication error:', error)
setPasswordErrors([toError(error).message || 'Invalid password. Please try again.'])
- setShowValidationError(true)
}
}
@@ -84,15 +81,14 @@ export default function PasswordAuth({ identifier }: PasswordAuthProps) {
onChange={handlePasswordChange}
className={cn(
'pr-10',
- showValidationError &&
- passwordErrors.length > 0 &&
+ hasPasswordError &&
'border-[var(--text-error)] focus:border-[var(--text-error)]'
)}
/>