diff --git a/web/src/app/app/message/messageComponents/renderers/MessageTextRenderer.tsx b/web/src/app/app/message/messageComponents/renderers/MessageTextRenderer.tsx index 42aee88585..ff6afbb23c 100644 --- a/web/src/app/app/message/messageComponents/renderers/MessageTextRenderer.tsx +++ b/web/src/app/app/message/messageComponents/renderers/MessageTextRenderer.tsx @@ -28,6 +28,7 @@ import { CodeBlock } from "@/app/app/message/CodeBlock"; import { InMessageImage } from "@/app/app/components/files/images/InMessageImage"; import { extractChatImageFileId } from "@/app/app/components/files/images/utils"; import { cn, transformLinkUri } from "@/lib/utils"; +import { useSmoothStreaming } from "@/hooks/useSmoothStreaming"; /** Maps a visible-char count to a markdown index (skips formatting chars, * extends to word boundary). Used by the voice-sync reveal path only. */ @@ -96,6 +97,8 @@ export const MessageTextRenderer: MessageRenderer< stopReason, children, }) => { + const { enabled: smoothStreamingEnabled } = useSmoothStreaming(); + const lastStableSyncedContentRef = useRef(""); const lastVisibleContentRef = useRef(""); @@ -247,7 +250,8 @@ export const MessageTextRenderer: MessageRenderer< const isStreamingAnimationEnabled = animate && !shouldUseAutoPlaybackSync && - stopReason !== StopReason.USER_CANCELLED; + stopReason !== StopReason.USER_CANCELLED && + smoothStreamingEnabled; const isStreamFinished = isFinalAnswerComplete(packets); diff --git a/web/src/hooks/useSmoothStreaming.ts b/web/src/hooks/useSmoothStreaming.ts new file mode 100644 index 0000000000..61b56ed752 --- /dev/null +++ b/web/src/hooks/useSmoothStreaming.ts @@ -0,0 +1,42 @@ +import Cookies from "js-cookie"; +import { useCallback, useEffect, useState } from "react"; + +import { SMOOTH_STREAMING_COOKIE_NAME } from "@/lib/constants"; + +const CHANGE_EVENT = "smoothStreaming:change"; +const DEFAULT_ENABLED = true; +const COOKIE_EXPIRES_DAYS = 365; + +function readCookie(): boolean { + const value = Cookies.get(SMOOTH_STREAMING_COOKIE_NAME); + if (value === undefined) return DEFAULT_ENABLED; + return value !== "false"; +} + +interface UseSmoothStreamingResult { + enabled: boolean; + setEnabled: (next: boolean) => void; +} + +export function useSmoothStreaming(): UseSmoothStreamingResult { + const [enabled, setEnabledState] = useState(() => + typeof window === "undefined" ? DEFAULT_ENABLED : readCookie() + ); + + useEffect(() => { + setEnabledState(readCookie()); + const handler = () => setEnabledState(readCookie()); + window.addEventListener(CHANGE_EVENT, handler); + return () => window.removeEventListener(CHANGE_EVENT, handler); + }, []); + + const setEnabled = useCallback((next: boolean) => { + Cookies.set(SMOOTH_STREAMING_COOKIE_NAME, String(next), { + expires: COOKIE_EXPIRES_DAYS, + }); + setEnabledState(next); + window.dispatchEvent(new Event(CHANGE_EVENT)); + }, []); + + return { enabled, setEnabled }; +} diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 305802a54c..f1407cdf25 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -81,6 +81,10 @@ export const NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED = export const NEXT_PUBLIC_TEST_ENV = process.env.NEXT_PUBLIC_TEST_ENV?.toLowerCase() === "true"; +// Cookie controlling the per-character typewriter reveal in chat. +// "false" disables smooth streaming — chunks render as they arrive. +export const SMOOTH_STREAMING_COOKIE_NAME = "smoothStreamingEnabled"; + export const NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK = process.env.NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK?.toLowerCase() === "true"; diff --git a/web/src/refresh-pages/SettingsPage.tsx b/web/src/refresh-pages/SettingsPage.tsx index 325d8d628c..9ca3962542 100644 --- a/web/src/refresh-pages/SettingsPage.tsx +++ b/web/src/refresh-pages/SettingsPage.tsx @@ -63,6 +63,7 @@ import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidE import { useSettingsContext } from "@/providers/SettingsProvider"; import SimpleTooltip from "@/refresh-components/SimpleTooltip"; import { useCloudSubscription } from "@/hooks/useCloudSubscription"; +import { useSmoothStreaming } from "@/hooks/useSmoothStreaming"; interface PAT { id: number; @@ -755,6 +756,10 @@ function ChatPreferencesSettings() { const settings = useSettingsContext(); const { isSearchModeAvailable: searchUiEnabled } = settings; const llmManager = useLlmManager(); + const { + enabled: smoothStreamingEnabled, + setEnabled: setSmoothStreamingEnabled, + } = useSmoothStreaming(); const { personalizationValues, @@ -850,6 +855,16 @@ function ChatPreferencesSettings() { /> + + + + {isPaidEnterpriseFeaturesEnabled && (