feat: implement scroll threshold state management
- Introduced AIRootStateContext to manage the scroll threshold state across components. - Updated AIChatRoot, ChatHeader, ChatInterface, and WelcomeScreen to utilize the new context for scroll behavior. - Added SCROLLED_BEYOND_THRESHOLD constant to define the scroll limit for triggering state changes. These changes enhance the chat interface by allowing components to react to scroll position, improving user experience and interaction dynamics. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
b05f49fe84
commit
682771cd4e
|
|
@ -1,3 +1,4 @@
|
|||
import { atom } from "jotai"
|
||||
import type { FC, PropsWithChildren } from "react"
|
||||
import { useEffect, useMemo, useRef } from "react"
|
||||
|
||||
|
|
@ -5,7 +6,11 @@ import { Focusable } from "~/components/common/Focusable"
|
|||
import { HotkeyScope } from "~/constants"
|
||||
|
||||
import type { AIPanelRefs } from "../../store/AIChatContext"
|
||||
import { AIChatStoreContext, AIPanelRefsContext } from "../../store/AIChatContext"
|
||||
import {
|
||||
AIChatStoreContext,
|
||||
AIPanelRefsContext,
|
||||
AIRootStateContext,
|
||||
} from "../../store/AIChatContext"
|
||||
import { ChatSliceActions } from "../../store/chat-core/chat-actions"
|
||||
import { useChatActions, useCurrentChatId } from "../../store/hooks"
|
||||
import { createAIChatStore } from "../../store/store"
|
||||
|
|
@ -58,7 +63,16 @@ export const AIChatRoot: FC<AIChatRootProps> = ({
|
|||
|
||||
const Element = (
|
||||
<AIChatStoreContext value={useAiContextStore}>
|
||||
<AIChatRootInner chatId={externalChatId}>{children}</AIChatRootInner>
|
||||
<AIRootStateContext
|
||||
value={useMemo(
|
||||
() => ({
|
||||
isScrolledBeyondThreshold: atom(false),
|
||||
}),
|
||||
[],
|
||||
)}
|
||||
>
|
||||
<AIChatRootInner chatId={externalChatId}>{children}</AIChatRootInner>
|
||||
</AIRootStateContext>
|
||||
</AIChatStoreContext>
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { ActionButton } from "@follow/components/ui/button/index.js"
|
||||
import { cn } from "@follow/utils"
|
||||
import { useAtomValue } from "jotai"
|
||||
import type { ReactNode } from "react"
|
||||
import { useCallback } from "react"
|
||||
|
||||
import { AIChatPanelStyle, setAIPanelVisibility, useAIChatPanelStyle } from "~/atoms/settings/ai"
|
||||
import { useBlockActions, useChatActions, useCurrentTitle } from "~/modules/ai-chat/store/hooks"
|
||||
|
||||
import { useAIRootState } from "../../store/AIChatContext"
|
||||
import { ChatMoreDropdown } from "./ChatMoreDropdown"
|
||||
import { EditableTitle } from "./EditableTitle"
|
||||
import { TaskReportDropdown } from "./TaskReportDropdown"
|
||||
|
|
@ -43,8 +45,17 @@ const ChatHeaderLayout = ({
|
|||
|
||||
const isFloating = useAIChatPanelStyle() === AIChatPanelStyle.Floating
|
||||
|
||||
const { isScrolledBeyondThreshold } = useAIRootState()
|
||||
const isScrolledBeyondThresholdValue = useAtomValue(isScrolledBeyondThreshold)
|
||||
|
||||
return (
|
||||
<div className={cn("absolute inset-x-0 top-0 z-[1]", !isFloating && "bg-background border-b")}>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-x-0 top-0 z-[1] duration-200",
|
||||
!isFloating && "bg-background data-[scrolled-beyond-threshold=true]:border-b",
|
||||
)}
|
||||
data-scrolled-beyond-threshold={isScrolledBeyondThresholdValue}
|
||||
>
|
||||
<div className="h-entry-header">
|
||||
{isFloating && (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ import { tracker } from "@follow/tracker"
|
|||
import { clsx, cn, nextFrame } from "@follow/utils"
|
||||
import type { BizUIMessage } from "@folo-services/ai-tools"
|
||||
import { ErrorBoundary } from "@sentry/react"
|
||||
import { useSetAtom } from "jotai"
|
||||
import type { EditorState, LexicalEditor } from "lexical"
|
||||
import { AnimatePresence } from "motion/react"
|
||||
import { nanoid } from "nanoid"
|
||||
import type { FC, Ref } from "react"
|
||||
import { Suspense, useEffect, useRef, useState } from "react"
|
||||
import { Suspense, useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useEventCallback } from "usehooks-ts"
|
||||
|
||||
import { useAISettingKey } from "~/atoms/settings/ai"
|
||||
|
|
@ -31,6 +32,8 @@ import {
|
|||
useMessages,
|
||||
} from "~/modules/ai-chat/store/hooks"
|
||||
|
||||
import { SCROLLED_BEYOND_THRESHOLD } from "../../constants"
|
||||
import { useAIRootState } from "../../store/AIChatContext"
|
||||
import type { AIChatContextBlock } from "../../store/types"
|
||||
import { convertLexicalToMarkdown } from "../../utils/lexical-markdown"
|
||||
import { GlobalFileDropZone } from "../file/GlobalFileDropZone"
|
||||
|
|
@ -225,6 +228,15 @@ const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => {
|
|||
|
||||
const shouldShowScrollToBottom = hasMessages && !isAtBottom && !isLoadingHistory
|
||||
|
||||
const { isScrolledBeyondThreshold } = useAIRootState()
|
||||
const setIsScrolledBeyondThreshold = useSetAtom(isScrolledBeyondThreshold)
|
||||
const handleScroll = useCallback(
|
||||
(event: React.UIEvent<HTMLDivElement>) => {
|
||||
const { scrollTop } = event.currentTarget
|
||||
setIsScrolledBeyondThreshold(scrollTop > SCROLLED_BEYOND_THRESHOLD)
|
||||
},
|
||||
[setIsScrolledBeyondThreshold],
|
||||
)
|
||||
return (
|
||||
<GlobalFileDropZone className="@container flex size-full flex-col">
|
||||
<div className="flex min-h-0 flex-1 flex-col" ref={scrollContainerParentRef}>
|
||||
|
|
@ -234,6 +246,7 @@ const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => {
|
|||
) : (
|
||||
<>
|
||||
<ScrollArea
|
||||
onScroll={handleScroll}
|
||||
flex
|
||||
scrollbarClassName="mb-40 mt-12"
|
||||
ref={setScrollAreaRef}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
import { Folo } from "@follow/components/icons/folo.js"
|
||||
import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js"
|
||||
import { clsx } from "@follow/utils"
|
||||
import { useSetAtom } from "jotai"
|
||||
import type { EditorState, LexicalEditor } from "lexical"
|
||||
import { AnimatePresence, m } from "motion/react"
|
||||
import { useCallback } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useAISettingValue } from "~/atoms/settings/ai"
|
||||
import { AISpline } from "~/modules/ai-chat/components/3d-models/AISpline"
|
||||
|
||||
import { SCROLLED_BEYOND_THRESHOLD } from "../../constants"
|
||||
import { useMainEntryId } from "../../hooks/useMainEntryId"
|
||||
import { useAIRootState } from "../../store/AIChatContext"
|
||||
import { DefaultWelcomeContent, EntrySummaryCard } from "../welcome"
|
||||
|
||||
interface WelcomeScreenProps {
|
||||
|
|
@ -24,12 +28,22 @@ export const WelcomeScreen = ({ onSend, centerInputOnEmpty }: WelcomeScreenProps
|
|||
const hasEntryContext = !!mainEntryId
|
||||
const enabledShortcuts = aiSettings.shortcuts?.filter((shortcut) => shortcut.enabled) || []
|
||||
|
||||
const { isScrolledBeyondThreshold } = useAIRootState()
|
||||
const setIsScrolledBeyondThreshold = useSetAtom(isScrolledBeyondThreshold)
|
||||
const handleScroll = useCallback(
|
||||
(event: React.UIEvent<HTMLDivElement>) => {
|
||||
const { scrollTop } = event.currentTarget
|
||||
setIsScrolledBeyondThreshold(scrollTop > SCROLLED_BEYOND_THRESHOLD)
|
||||
},
|
||||
[setIsScrolledBeyondThreshold],
|
||||
)
|
||||
return (
|
||||
<ScrollArea
|
||||
rootClassName="flex min-h-0 flex-1"
|
||||
viewportClassName="px-6 pt-24 flex min-h-0 grow"
|
||||
scrollbarClassName="mb-40 mt-12"
|
||||
flex
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<div className="mx-auto flex w-full flex-1 flex-col justify-center space-y-8 pb-52">
|
||||
{/* Header Section - Always Present */}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export const SCROLLED_BEYOND_THRESHOLD = 100
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import type { PrimitiveAtom } from "jotai"
|
||||
import { createContext, use } from "react"
|
||||
import type { StoreApi } from "zustand"
|
||||
import type { UseBoundStoreWithEqualityFn } from "zustand/traditional"
|
||||
|
|
@ -22,3 +23,17 @@ export const useAIChatStore = () => {
|
|||
}
|
||||
return store
|
||||
}
|
||||
|
||||
export type AIRootStateContext = {
|
||||
isScrolledBeyondThreshold: PrimitiveAtom<boolean>
|
||||
}
|
||||
|
||||
export const AIRootStateContext = createContext<AIRootStateContext>(null!)
|
||||
|
||||
export const useAIRootState = () => {
|
||||
const context = use(AIRootStateContext)
|
||||
if (!context) {
|
||||
throw new Error("useAIRootState must be used within a AIRootStateContext")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue