refactor(ai): drop floating chat panel and simplify timeline layout
This commit is contained in:
parent
f6a8691e3a
commit
ae97aa2b1a
|
|
@ -15,7 +15,6 @@ import { atom, useAtomValue } from "jotai"
|
|||
import { getFeature } from "~/hooks/biz/useFeature"
|
||||
|
||||
export interface WebAISettings extends AISettings {
|
||||
panelStyle: AIChatPanelStyle
|
||||
showSplineButton: boolean
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +110,6 @@ export const isServerShortcut = (shortcut: AIShortcut) => !!shortcut.defaultProm
|
|||
export const createDefaultSettings = (): WebAISettings => ({
|
||||
...defaultAISettings,
|
||||
shortcuts: normalizeShortcuts(defaultAISettings.shortcuts),
|
||||
panelStyle: AIChatPanelStyle.Fixed,
|
||||
showSplineButton: true,
|
||||
})
|
||||
|
||||
|
|
@ -137,18 +135,6 @@ export const syncServerShortcuts = (
|
|||
setAISetting("shortcuts", mergedShortcuts)
|
||||
}
|
||||
|
||||
////////// AI Panel Style
|
||||
export enum AIChatPanelStyle {
|
||||
Fixed = "fixed",
|
||||
Floating = "floating",
|
||||
}
|
||||
|
||||
export const useAIChatPanelStyle = () => useAISettingKey("panelStyle")
|
||||
export const setAIChatPanelStyle = (style: AIChatPanelStyle) => {
|
||||
setAISetting("panelStyle", style)
|
||||
}
|
||||
export const getAIChatPanelStyle = () => getAISettings().panelStyle
|
||||
|
||||
// Floating panel state atoms
|
||||
interface FloatingPanelState {
|
||||
width: number
|
||||
|
|
@ -224,5 +210,4 @@ export const removeMCPService = (id: string) => {
|
|||
//// Enhance Init Ai Settings
|
||||
export const initializeDefaultAISettings = () => {
|
||||
initializeDefaultSettings()
|
||||
if (getAISettings().panelStyle === AIChatPanelStyle.Fixed) setAIPanelVisibility(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,26 +177,6 @@ export const AISummaryCardBase: React.FC<AISummaryCardBaseProps> = ({
|
|||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{aiEnabled && showAskAIButton && hasContent && onAskAI && (
|
||||
<MotionButtonBase
|
||||
onClick={onAskAI}
|
||||
className={cn(
|
||||
"flex h-7 items-center gap-1.5 rounded-lg px-3 text-sm font-medium",
|
||||
"bg-gradient-to-r from-purple-500/10 to-blue-500/10",
|
||||
"border border-purple-200/30 dark:border-purple-800/30",
|
||||
"text-purple-600 dark:text-purple-400",
|
||||
"hover:from-purple-500/20 hover:to-blue-500/20",
|
||||
"hover:border-purple-300/50 dark:hover:border-purple-700/50",
|
||||
"transition-all duration-200",
|
||||
"backdrop-blur-sm",
|
||||
"sm:opacity-0 sm:duration-300 sm:group-hover:translate-y-0 sm:group-hover:opacity-100",
|
||||
)}
|
||||
>
|
||||
<i className="i-mingcute-ai-line text-base" />
|
||||
<span>Ask AI</span>
|
||||
</MotionButtonBase>
|
||||
)}
|
||||
|
||||
{showCopyButton && hasContent && (
|
||||
<CopyButton
|
||||
value={content}
|
||||
|
|
@ -210,6 +190,25 @@ export const AISummaryCardBase: React.FC<AISummaryCardBaseProps> = ({
|
|||
)}
|
||||
/>
|
||||
)}
|
||||
{aiEnabled && showAskAIButton && hasContent && onAskAI && (
|
||||
<MotionButtonBase
|
||||
onClick={onAskAI}
|
||||
className={cn(
|
||||
"flex h-7 items-center gap-1.5 rounded-lg px-3 text-sm font-medium",
|
||||
"bg-gradient-to-r from-purple-500/10 to-blue-500/10",
|
||||
"border border-purple-200/30 dark:border-purple-800/30",
|
||||
"text-purple-600 dark:text-purple-400",
|
||||
"hover:from-purple-500/20 hover:to-blue-500/20",
|
||||
"hover:border-purple-300/50 dark:hover:border-purple-700/50",
|
||||
"transition-all duration-200",
|
||||
"backdrop-blur-sm",
|
||||
"sm:duration-300 sm:group-hover:translate-y-0",
|
||||
)}
|
||||
>
|
||||
<i className="i-mingcute-ai-line text-base" />
|
||||
<span>Ask AI</span>
|
||||
</MotionButtonBase>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
import { getView } from "@follow/constants"
|
||||
|
||||
import { AIChatPanelStyle, useAIChatPanelStyle, useAIPanelVisibility } from "~/atoms/settings/ai"
|
||||
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
|
||||
export const useShowEntryDetailsColumn = () => {
|
||||
const { view } = useRouteParamsSelector((s) => ({
|
||||
view: s.view,
|
||||
}))
|
||||
const aiPanelStyle = useAIChatPanelStyle()
|
||||
const isAIPanelVisible = useAIPanelVisibility()
|
||||
|
||||
return (
|
||||
!getView(view).wideMode && (aiPanelStyle === AIChatPanelStyle.Floating || !isAIPanelVisible)
|
||||
)
|
||||
return !getView(view).wideMode
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type { ReactNode } from "react"
|
|||
import { useCallback } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { AIChatPanelStyle, setAIPanelVisibility, useAIChatPanelStyle } from "~/atoms/settings/ai"
|
||||
import { setAIPanelVisibility } from "~/atoms/settings/ai"
|
||||
import { useTimelineSummaryAutoContext } from "~/modules/ai-chat/hooks/useTimelineSummaryAutoContext"
|
||||
import {
|
||||
useBlockActions,
|
||||
|
|
@ -25,12 +25,14 @@ import { TaskReportDropdown } from "./TaskReportDropdown"
|
|||
// Base header layout with shared logic inside
|
||||
const ChatHeaderLayout = ({
|
||||
renderActions,
|
||||
isFixedMode = false,
|
||||
}: {
|
||||
renderActions: (ctx: {
|
||||
onNewChatClick: () => void
|
||||
currentTitle: string | undefined
|
||||
displayTitle: string | undefined
|
||||
}) => ReactNode
|
||||
isFixedMode?: boolean
|
||||
}) => {
|
||||
const hasMessages = useHasMessages()
|
||||
const currentTitle = useCurrentTitle()
|
||||
|
|
@ -57,8 +59,6 @@ const ChatHeaderLayout = ({
|
|||
blockActions.clearBlocks({ keepSpecialTypes: true })
|
||||
}, [chatActions, blockActions, shouldDisableTimelineSummary])
|
||||
|
||||
const isFloating = useAIChatPanelStyle() === AIChatPanelStyle.Floating
|
||||
|
||||
const { isScrolledBeyondThreshold } = useAIRootState()
|
||||
const isScrolledBeyondThresholdValue = useAtomValue(isScrolledBeyondThreshold)
|
||||
|
||||
|
|
@ -66,12 +66,12 @@ const ChatHeaderLayout = ({
|
|||
<div
|
||||
className={cn(
|
||||
"absolute inset-x-0 top-0 z-[1] border-b border-transparent duration-200",
|
||||
!isFloating && "bg-background data-[scrolled-beyond-threshold=true]:border-b-border",
|
||||
isFixedMode && "bg-background data-[scrolled-beyond-threshold=true]:border-b-border",
|
||||
)}
|
||||
data-scrolled-beyond-threshold={isScrolledBeyondThresholdValue}
|
||||
>
|
||||
<div className="h-top-header">
|
||||
{isFloating && (
|
||||
{!isFixedMode && (
|
||||
<div
|
||||
className="absolute inset-0 bg-background/70 backdrop-blur-background"
|
||||
style={{
|
||||
|
|
@ -108,12 +108,12 @@ const ChatHeaderLayout = ({
|
|||
)
|
||||
}
|
||||
|
||||
export const ChatHeader = () => {
|
||||
const panelStyle = useAIChatPanelStyle()
|
||||
export const ChatHeader = ({ isFixedMode = false }: { isFixedMode?: boolean }) => {
|
||||
const { t } = useTranslation("ai")
|
||||
|
||||
return (
|
||||
<ChatHeaderLayout
|
||||
isFixedMode={isFixedMode}
|
||||
renderActions={({ onNewChatClick }) => (
|
||||
<>
|
||||
<ActionButton tooltip={t("common.new_chat")} onClick={onNewChatClick}>
|
||||
|
|
@ -130,7 +130,7 @@ export const ChatHeader = () => {
|
|||
}
|
||||
/>
|
||||
|
||||
{panelStyle === AIChatPanelStyle.Floating && (
|
||||
{!isFixedMode && (
|
||||
<>
|
||||
<div className="h-5 w-px bg-border" />
|
||||
<ActionButton tooltip="Close" onClick={() => setAIPanelVisibility(false)}>
|
||||
|
|
|
|||
|
|
@ -411,8 +411,7 @@ const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => {
|
|||
data-testid="chat-input-container"
|
||||
className={cn(
|
||||
"absolute z-10 mx-auto duration-500 ease-in-out",
|
||||
hasMessages && "inset-x-0 bottom-0 max-w-4xl px-4 pb-4",
|
||||
!hasMessages && "inset-x-0 bottom-0 max-w-3xl px-4 pb-4 duration-200",
|
||||
"inset-x-0 bottom-0 max-w-4xl px-4 pb-4",
|
||||
centerInputOnEmpty &&
|
||||
!hasMessages &&
|
||||
"bottom-1/2 translate-y-[calc(100%+1rem)] duration-200",
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
import type { ReactNode } from "react"
|
||||
import { useCallback, useRef, useState } from "react"
|
||||
import { useCallback, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
AIChatPanelStyle,
|
||||
setAIChatPanelStyle,
|
||||
setAIPanelVisibility,
|
||||
useAIChatPanelStyle,
|
||||
} from "~/atoms/settings/ai"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu/dropdown-menu"
|
||||
import { useSettingModal } from "~/modules/settings/modal/use-setting-modal-hack"
|
||||
|
|
@ -23,13 +16,10 @@ import { generateAndUpdateChatTitle } from "../../utils/titleGeneration"
|
|||
export const ChatMoreDropdown = ({
|
||||
triggerElement,
|
||||
asChild = true,
|
||||
canToggleMode = true,
|
||||
}: {
|
||||
triggerElement: ReactNode
|
||||
asChild?: boolean
|
||||
canToggleMode?: boolean
|
||||
}) => {
|
||||
const panelStyle = useAIChatPanelStyle()
|
||||
const settingModalPresent = useSettingModal()
|
||||
const chatActions = useChatActions()
|
||||
const currentChatId = useCurrentChatId()
|
||||
|
|
@ -38,16 +28,6 @@ export const ChatMoreDropdown = ({
|
|||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const { t } = useTranslation("ai")
|
||||
|
||||
const handleToggleMode = useCallback(() => {
|
||||
const newStyle =
|
||||
panelStyle === AIChatPanelStyle.Fixed ? AIChatPanelStyle.Floating : AIChatPanelStyle.Fixed
|
||||
setAIChatPanelStyle(newStyle)
|
||||
}, [panelStyle])
|
||||
|
||||
const handleCloseSidebar = useRef(() => {
|
||||
setAIPanelVisibility(false)
|
||||
}).current
|
||||
|
||||
const handleGenerateTitle = useCallback(
|
||||
async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
|
|
@ -81,37 +61,10 @@ export const ChatMoreDropdown = ({
|
|||
<span>{isGenerating ? t("common.generating_title") : t("common.generate_title")}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
{canToggleMode && (
|
||||
<>
|
||||
<DropdownMenuItem onClick={handleToggleMode}>
|
||||
<i
|
||||
className={`mr-2 size-4 ${panelStyle === AIChatPanelStyle.Fixed ? "i-mingcute-rectangle-vertical-line" : "i-mingcute-layout-right-line"}`}
|
||||
/>
|
||||
<span>
|
||||
{panelStyle === AIChatPanelStyle.Fixed
|
||||
? "Switch to Floating Panel"
|
||||
: "Switch to Fixed Panel"}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem onClick={() => settingModalPresent("ai")}>
|
||||
<i className="i-mgc-settings-1-cute-re mr-2 size-4" />
|
||||
<span>AI Settings</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{panelStyle !== AIChatPanelStyle.Floating && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleCloseSidebar}>
|
||||
<i className="i-mgc-close-cute-re mr-2 size-4" />
|
||||
<span>Close Sidebar</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,7 @@ import { useGlobalFocusableScopeSelector } from "@follow/components/common/Focus
|
|||
import { useEffect } from "react"
|
||||
import { tinykeys } from "tinykeys"
|
||||
|
||||
import {
|
||||
AIChatPanelStyle,
|
||||
getAIPanelVisibility,
|
||||
getAISettings,
|
||||
setAIPanelVisibility,
|
||||
} from "~/atoms/settings/ai"
|
||||
import { getAIPanelVisibility, setAIPanelVisibility } from "~/atoms/settings/ai"
|
||||
import { FocusablePresets } from "~/components/common/Focusable"
|
||||
import { getRouteParams } from "~/hooks/biz/useRouteParams"
|
||||
|
||||
|
|
@ -24,14 +19,14 @@ export const useAIShortcut = () => {
|
|||
"$mod+n": (e) => {
|
||||
e.preventDefault()
|
||||
// New chat
|
||||
const { view, entryId } = getRouteParams()
|
||||
if (isTimelineSummaryAutoContext({ view, entryId })) {
|
||||
const { entryId } = getRouteParams()
|
||||
if (isTimelineSummaryAutoContext({ entryId })) {
|
||||
chatActions.setTimelineSummaryManualOverride(true)
|
||||
}
|
||||
chatActions.newChat()
|
||||
},
|
||||
"$mod+w": (e) => {
|
||||
if (getAISettings().panelStyle === AIChatPanelStyle.Floating && getAIPanelVisibility()) {
|
||||
if (getAIPanelVisibility()) {
|
||||
e.preventDefault()
|
||||
// close AI chat
|
||||
setAIPanelVisibility(false)
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export const useAutoTimelineSummaryShortcut = () => {
|
|||
})
|
||||
const previousContextKeyRef = useRef<string | null>(null)
|
||||
|
||||
const isAllTimeline = isTimelineSummaryAutoContext({ view, entryId })
|
||||
const isAllTimeline = isTimelineSummaryAutoContext({ entryId })
|
||||
|
||||
const defaultShortcut = useMemo(() => {
|
||||
const shortcuts = aiSettings.shortcuts ?? []
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import { FeedViewType } from "@follow/constants"
|
||||
|
||||
import { ROUTE_ENTRY_PENDING } from "~/constants"
|
||||
import type { BizRouteParams } from "~/hooks/biz/useRouteParams"
|
||||
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
|
||||
export type TimelineSummaryContextParams = Pick<BizRouteParams, "view" | "entryId">
|
||||
export type TimelineSummaryContextParams = Pick<BizRouteParams, "entryId">
|
||||
|
||||
export const isTimelineSummaryAutoContext = ({ view, entryId }: TimelineSummaryContextParams) => {
|
||||
return view === FeedViewType.All && (!entryId || entryId === ROUTE_ENTRY_PENDING)
|
||||
export const isTimelineSummaryAutoContext = ({ entryId }: TimelineSummaryContextParams) => {
|
||||
return !entryId || entryId === ROUTE_ENTRY_PENDING
|
||||
}
|
||||
|
||||
export const useTimelineSummaryAutoContext = () =>
|
||||
useRouteParamsSelector(({ view, entryId }) => isTimelineSummaryAutoContext({ view, entryId }))
|
||||
useRouteParamsSelector(({ entryId }) => isTimelineSummaryAutoContext({ entryId }))
|
||||
|
|
|
|||
|
|
@ -5,19 +5,17 @@ import { defaultUISettings } from "@follow/shared/settings/defaults"
|
|||
import { cn } from "@follow/utils"
|
||||
import { isSafari } from "@follow/utils/utils"
|
||||
import { AnimatePresence } from "motion/react"
|
||||
import type { TransitionEvent } from "react"
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import type { CSSProperties } from "react"
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from "react"
|
||||
import { useResizable } from "react-resizable-layout"
|
||||
|
||||
import { AIChatPanelStyle, useAIChatPanelStyle, useAIPanelVisibility } from "~/atoms/settings/ai"
|
||||
import { getUISettings, setUISetting, useUISettingKey } from "~/atoms/settings/ui"
|
||||
import { m } from "~/components/common/Motion"
|
||||
import { ROUTE_ENTRY_PENDING } from "~/constants"
|
||||
import { useFeature } from "~/hooks/biz/useFeature"
|
||||
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
import { useShowEntryDetailsColumn } from "~/hooks/biz/useShowEntryDetailsColumn"
|
||||
import { AIChatRoot } from "~/modules/ai-chat/components/layouts/AIChatRoot"
|
||||
import { AIChatLayout } from "~/modules/app-layout/ai/AIChatLayout"
|
||||
import { AIChatFixedPanel } from "~/modules/app-layout/ai/AIChatFixedPanel"
|
||||
import { AIIndicator } from "~/modules/app-layout/ai/AISplineButton"
|
||||
import { EntryContentPlaceholder } from "~/modules/app-layout/entry-content/EntryContentPlaceholder"
|
||||
import { EntryColumn } from "~/modules/entry-column"
|
||||
|
|
@ -26,61 +24,29 @@ import { AIEntryHeader } from "~/modules/entry-content/components/entry-header"
|
|||
import { AppLayoutGridContainerProvider } from "~/providers/app-grid-layout-container-provider"
|
||||
import { MainViewHotkeysProvider } from "~/providers/main-view-hotkeys-provider"
|
||||
|
||||
/**
|
||||
* AIEnhancedTimelineLayout Component
|
||||
*
|
||||
* An advanced timeline layout that integrates AI chat functionality with entry browsing.
|
||||
* This layout provides:
|
||||
* - Entry list display with animated content overlay
|
||||
* - Integrated AI chat panel (Fixed/Floating modes)
|
||||
* - Dynamic subscription column toggling
|
||||
* - Smooth animations for entry transitions
|
||||
* - Resizable AI panel with persistent settings
|
||||
*
|
||||
* Layout Structure:
|
||||
* ```
|
||||
* AIEnhancedTimelineLayout
|
||||
* ├── Main Content Area
|
||||
* │ ├── EntryColumn (entry list)
|
||||
* │ ├── AIEntryHeader (animated overlay)
|
||||
* │ └── EntryContent (animated overlay)
|
||||
* ├── AI Chat Panel (resizable, optional)
|
||||
* │ ├── Fixed Panel (docked to right)
|
||||
* │ └── Floating Panel (draggable overlay)
|
||||
* └── Subscription Column Toggler
|
||||
* ```
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* // Used in AI-enabled timeline routes
|
||||
* // Provides enhanced timeline experience with AI assistance
|
||||
*/
|
||||
const AIEnhancedTimelineLayoutImpl = () => {
|
||||
const aiPanelStyle = useAIChatPanelStyle()
|
||||
const isAIPanelVisible = useAIPanelVisibility()
|
||||
const aiSidebarVisible = aiPanelStyle === AIChatPanelStyle.Fixed && isAIPanelVisible
|
||||
const MIN_ENTRY_WIDTH = isSafari() ? 356 : 300
|
||||
|
||||
const { view, entryId } = useRouteParamsSelector((s) => ({
|
||||
view: s.view,
|
||||
entryId: s.entryId,
|
||||
const AIEnhancedTimelineLayoutImpl = () => {
|
||||
const { view, entryId } = useRouteParamsSelector((state) => ({
|
||||
view: state.view,
|
||||
entryId: state.entryId,
|
||||
}))
|
||||
|
||||
const realEntryId = entryId === ROUTE_ENTRY_PENDING ? "" : entryId
|
||||
const showEntryDetailsColumn = useShowEntryDetailsColumn()
|
||||
const hasSelectedEntry = Boolean(realEntryId)
|
||||
|
||||
const layoutContainerRef = useRef<HTMLDivElement>(null)
|
||||
const hasMountedRef = useRef(false)
|
||||
|
||||
// Delay the details column until the timeline width animation finishes
|
||||
const [shouldRenderDetailsColumn, setShouldRenderDetailsColumn] = useState(showEntryDetailsColumn)
|
||||
const feedColumnWidth = useUISettingKey("feedColWidth")
|
||||
|
||||
// Timeline column resizable configuration (used when entry details column is visible)
|
||||
const entryColumnInitialWidth = useMemo(() => getUISettings().entryColWidth, [])
|
||||
const timelineMaxWidth = useMemo(() => {
|
||||
if (typeof window === "undefined") return 600
|
||||
return Math.max((window.innerWidth - feedColumnWidth) / 2, 600)
|
||||
}, [feedColumnWidth])
|
||||
|
||||
const entryColumnInitialWidth = useMemo(() => getUISettings().entryColWidth, [])
|
||||
const timelineStartDragPosition = useRef(0)
|
||||
|
||||
const {
|
||||
position: timelineColumnWidth,
|
||||
separatorProps: timelineSeparatorProps,
|
||||
|
|
@ -89,7 +55,7 @@ const AIEnhancedTimelineLayoutImpl = () => {
|
|||
setPosition: setTimelineColumnWidth,
|
||||
} = useResizable({
|
||||
axis: "x",
|
||||
min: isSafari() ? 356 : 300,
|
||||
min: MIN_ENTRY_WIDTH,
|
||||
max: timelineMaxWidth,
|
||||
initial: entryColumnInitialWidth,
|
||||
containerRef: layoutContainerRef as React.RefObject<HTMLElement>,
|
||||
|
|
@ -99,12 +65,10 @@ const AIEnhancedTimelineLayoutImpl = () => {
|
|||
onResizeEnd({ position }) {
|
||||
if (position === timelineStartDragPosition.current) return
|
||||
setUISetting("entryColWidth", position)
|
||||
// TODO: Remove this after useMeasure can get bounds in time
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
},
|
||||
})
|
||||
|
||||
// AI chat resizable panel configuration
|
||||
const isAllView = view === FeedViewType.All
|
||||
const widthRange: [number, number] = isAllView ? [500, timelineMaxWidth] : [300, timelineMaxWidth]
|
||||
const [minWidth, maxWidth] = widthRange
|
||||
|
|
@ -116,14 +80,11 @@ const AIEnhancedTimelineLayoutImpl = () => {
|
|||
|
||||
const resolvePreferredWidth = useCallback(() => {
|
||||
const ui = getUISettings()
|
||||
const preferredWidth = ui.aiColWidth ?? defaultUISettings.aiColWidth
|
||||
return clampWidth(preferredWidth)
|
||||
const preferred = ui.aiColWidth ?? defaultUISettings.aiColWidth
|
||||
return clampWidth(preferred)
|
||||
}, [clampWidth])
|
||||
|
||||
// Calculate initial width depending on view
|
||||
const initialAiWidth = useMemo(() => resolvePreferredWidth(), [resolvePreferredWidth])
|
||||
const aiPanelStartDragPosition = useRef(0)
|
||||
|
||||
const {
|
||||
position: aiPanelWidth,
|
||||
separatorProps: aiSeparatorProps,
|
||||
|
|
@ -134,145 +95,181 @@ const AIEnhancedTimelineLayoutImpl = () => {
|
|||
axis: "x",
|
||||
min: minWidth,
|
||||
max: maxWidth,
|
||||
initial: initialAiWidth,
|
||||
initial: resolvePreferredWidth(),
|
||||
reverse: true,
|
||||
containerRef: layoutContainerRef as React.RefObject<HTMLElement>,
|
||||
onResizeStart({ position }) {
|
||||
aiPanelStartDragPosition.current = position
|
||||
},
|
||||
onResizeEnd({ position }) {
|
||||
if (position === aiPanelStartDragPosition.current) return
|
||||
// Persist per-view width
|
||||
setUISetting("aiColWidth", position)
|
||||
// TODO: Remove this after useMeasure can get bounds in time
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
},
|
||||
})
|
||||
|
||||
// When view changes, switch to the saved width for that view
|
||||
useEffect(() => {
|
||||
const width = resolvePreferredWidth()
|
||||
setAiPanelWidth(width)
|
||||
// Trigger layout update
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
}, [resolvePreferredWidth, setAiPanelWidth])
|
||||
|
||||
const handleTimelineTransitionEnd = useCallback(
|
||||
(event: TransitionEvent<HTMLDivElement>) => {
|
||||
if (
|
||||
event.propertyName !== "width" ||
|
||||
event.target !== event.currentTarget ||
|
||||
!showEntryDetailsColumn
|
||||
) {
|
||||
return
|
||||
const showEntryContentOnRight = showEntryDetailsColumn && hasSelectedEntry
|
||||
const shouldShowFixedAI = !showEntryContentOnRight
|
||||
const showEntryContentOnLeft = !showEntryDetailsColumn && hasSelectedEntry
|
||||
|
||||
const shouldRenderRightColumn = showEntryDetailsColumn || shouldShowFixedAI
|
||||
const shouldShowEntryBorder = showEntryDetailsColumn || shouldShowFixedAI
|
||||
|
||||
const entryColumnStyle: CSSProperties = showEntryDetailsColumn
|
||||
? {
|
||||
flexBasis: timelineColumnWidth,
|
||||
minWidth: MIN_ENTRY_WIDTH,
|
||||
}
|
||||
: {
|
||||
minWidth: MIN_ENTRY_WIDTH,
|
||||
}
|
||||
setShouldRenderDetailsColumn(true)
|
||||
},
|
||||
[showEntryDetailsColumn],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!showEntryDetailsColumn) {
|
||||
setShouldRenderDetailsColumn(false)
|
||||
}
|
||||
}, [showEntryDetailsColumn])
|
||||
const rightColumnStyle: CSSProperties = showEntryDetailsColumn
|
||||
? {
|
||||
minWidth: 0,
|
||||
}
|
||||
: {
|
||||
width: aiPanelWidth,
|
||||
minWidth: 0,
|
||||
flexBasis: aiPanelWidth,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasMountedRef.current) {
|
||||
hasMountedRef.current = true
|
||||
setShouldRenderDetailsColumn(showEntryDetailsColumn)
|
||||
return
|
||||
}
|
||||
}, [showEntryDetailsColumn])
|
||||
const resetTimelineWidth = useCallback(() => {
|
||||
setUISetting("entryColWidth", defaultUISettings.entryColWidth)
|
||||
setTimelineColumnWidth(defaultUISettings.entryColWidth)
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
}, [setTimelineColumnWidth])
|
||||
|
||||
const timelineSection = useMemo(
|
||||
() => (
|
||||
const resetAiWidth = useCallback(() => {
|
||||
const resetWidth = clampWidth(defaultUISettings.aiColWidth)
|
||||
setUISetting("aiColWidth", resetWidth)
|
||||
setAiPanelWidth(resetWidth)
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
}, [clampWidth, setAiPanelWidth])
|
||||
|
||||
const splitter =
|
||||
shouldRenderRightColumn && showEntryDetailsColumn ? (
|
||||
<PanelSplitter
|
||||
{...timelineSeparatorProps}
|
||||
cursor={timelineSeparatorCursor}
|
||||
isDragging={isTimelineDragging}
|
||||
onDoubleClick={resetTimelineWidth}
|
||||
/>
|
||||
) : shouldShowFixedAI ? (
|
||||
<PanelSplitter
|
||||
{...aiSeparatorProps}
|
||||
cursor={aiSeparatorCursor}
|
||||
isDragging={isAiPanelDragging}
|
||||
onDoubleClick={resetAiWidth}
|
||||
/>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-full min-w-0 grow",
|
||||
isAllView ? "flex flex-col overflow-y-auto scroll-smooth" : "flex",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-full min-w-0",
|
||||
isAllView ? "min-h-full w-full flex-none snap-start snap-always" : "flex-1",
|
||||
isAllView ? "min-h-full w-full flex-none" : "flex-1",
|
||||
)}
|
||||
>
|
||||
<AppLayoutGridContainerProvider>
|
||||
<div className="relative h-full">
|
||||
<div ref={layoutContainerRef} className="flex h-full min-w-0">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-y-0 left-0 min-w-[300px] transition-[width] duration-200 ease-out",
|
||||
showEntryDetailsColumn && "border-r will-change-[width]",
|
||||
isTimelineDragging && "transition-none",
|
||||
aiPanelStyle === AIChatPanelStyle.Fixed && !showEntryDetailsColumn && "border-r",
|
||||
"relative flex h-full flex-col overflow-hidden",
|
||||
shouldShowEntryBorder && "border-r",
|
||||
showEntryDetailsColumn
|
||||
? "flex-none transition-[flex-basis] duration-200 ease-out will-change-[flex-basis]"
|
||||
: "min-w-0 flex-1",
|
||||
showEntryDetailsColumn && isTimelineDragging && "transition-none",
|
||||
)}
|
||||
onTransitionEnd={handleTimelineTransitionEnd}
|
||||
style={{ width: showEntryDetailsColumn ? timelineColumnWidth : "100%" }}
|
||||
style={entryColumnStyle}
|
||||
>
|
||||
<div className="relative h-full">
|
||||
{/* Entry list - always rendered to prevent animation */}
|
||||
<EntryColumn key="entry-list" />
|
||||
{!showEntryDetailsColumn && (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{realEntryId && (
|
||||
<EntryColumn />
|
||||
|
||||
{showEntryContentOnLeft && (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{realEntryId && (
|
||||
<m.div
|
||||
key="entry-header"
|
||||
className="absolute inset-x-0 top-0 z-10"
|
||||
initial={{ translateY: "-50px", opacity: 0 }}
|
||||
animate={{ translateY: 0, opacity: 1 }}
|
||||
exit={{ translateY: "-50px", opacity: 0 }}
|
||||
transition={Spring.smooth(0.3)}
|
||||
>
|
||||
<AIEntryHeader entryId={realEntryId} />
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{realEntryId && (
|
||||
<div className="pointer-events-none absolute inset-0 z-[9] flex flex-col overflow-hidden">
|
||||
<m.div
|
||||
className="absolute inset-x-0 top-0 z-10"
|
||||
initial={{ translateY: "-50px", opacity: 0 }}
|
||||
animate={{ translateY: 0, opacity: 1 }}
|
||||
exit={{ translateY: "-50px", opacity: 0 }}
|
||||
key="entry-content"
|
||||
lcpOptimization
|
||||
initial={{ translateY: "50px", opacity: 0, scale: 0.98 }}
|
||||
animate={{ translateY: 0, opacity: 1, scale: 1 }}
|
||||
exit={{ translateY: "50px", opacity: 0, scale: 0.98 }}
|
||||
transition={Spring.smooth(0.3)}
|
||||
className="pointer-events-auto relative flex h-0 flex-1 flex-col bg-theme-background"
|
||||
>
|
||||
<AIEntryHeader entryId={realEntryId} />
|
||||
<EntryContent entryId={realEntryId} className="h-full" />
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/* Entry content overlay with exit animation */}
|
||||
<AnimatePresence>
|
||||
{realEntryId && (
|
||||
<div className="pointer-events-none absolute inset-0 z-[9] flex flex-col overflow-hidden">
|
||||
<m.div
|
||||
lcpOptimization
|
||||
initial={{ translateY: "50px", opacity: 0, scale: 0.98 }}
|
||||
animate={{ translateY: 0, opacity: 1, scale: 1 }}
|
||||
exit={{ translateY: "50px", opacity: 0, scale: 0.98 }}
|
||||
transition={Spring.smooth(0.3)}
|
||||
className="pointer-events-auto relative flex h-0 flex-1 flex-col bg-theme-background"
|
||||
>
|
||||
<EntryContent entryId={realEntryId} className="h-full" />
|
||||
</m.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showEntryDetailsColumn && shouldRenderDetailsColumn && (
|
||||
{shouldRenderRightColumn && (
|
||||
<>
|
||||
<div className="absolute inset-y-0 z-[2]" style={{ left: timelineColumnWidth }}>
|
||||
<PanelSplitter
|
||||
{...timelineSeparatorProps}
|
||||
cursor={timelineSeparatorCursor}
|
||||
isDragging={isTimelineDragging}
|
||||
onDoubleClick={() => {
|
||||
setUISetting("entryColWidth", defaultUISettings.entryColWidth)
|
||||
setTimelineColumnWidth(defaultUISettings.entryColWidth)
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{splitter}
|
||||
|
||||
<div
|
||||
className="absolute inset-y-0 right-0 flex min-w-0 flex-1 flex-col overflow-hidden bg-theme-background"
|
||||
style={{ left: timelineColumnWidth }}
|
||||
className={cn(
|
||||
"relative flex h-full min-w-0 flex-col overflow-hidden bg-theme-background",
|
||||
showEntryDetailsColumn ? "flex-1" : "flex-none",
|
||||
)}
|
||||
style={rightColumnStyle}
|
||||
>
|
||||
{realEntryId ? (
|
||||
<>
|
||||
{showEntryContentOnRight && realEntryId ? (
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<div className="absolute inset-x-0 top-0 z-10">
|
||||
<AIEntryHeader entryId={realEntryId} />
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-0 z-[9] flex flex-col overflow-hidden">
|
||||
<div className="pointer-events-auto relative flex h-0 flex-1 flex-col bg-theme-background">
|
||||
<EntryContent entryId={realEntryId} className="h-full" />
|
||||
</div>
|
||||
<div className="flex h-0 flex-1 flex-col overflow-hidden">
|
||||
<EntryContent entryId={realEntryId} className="h-full" />
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
) : shouldShowFixedAI ? (
|
||||
<div className="flex h-full flex-1 items-center justify-center">
|
||||
<AIChatFixedPanel
|
||||
key="ai-chat-layout"
|
||||
style={
|
||||
{
|
||||
width: showEntryDetailsColumn ? "100%" : aiPanelWidth,
|
||||
"--ai-chat-layout-width": showEntryDetailsColumn
|
||||
? "100%"
|
||||
: `${aiPanelWidth}px`,
|
||||
} as CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center px-8">
|
||||
<EntryContentPlaceholder />
|
||||
|
|
@ -284,93 +281,15 @@ const AIEnhancedTimelineLayoutImpl = () => {
|
|||
</div>
|
||||
</AppLayoutGridContainerProvider>
|
||||
</div>
|
||||
),
|
||||
[
|
||||
aiPanelStyle,
|
||||
handleTimelineTransitionEnd,
|
||||
isAllView,
|
||||
isTimelineDragging,
|
||||
realEntryId,
|
||||
setTimelineColumnWidth,
|
||||
shouldRenderDetailsColumn,
|
||||
showEntryDetailsColumn,
|
||||
timelineColumnWidth,
|
||||
timelineSeparatorCursor,
|
||||
timelineSeparatorProps,
|
||||
],
|
||||
)
|
||||
|
||||
const aiChatLayoutNode = useMemo(
|
||||
() => (
|
||||
<div className={cn(isAllView && "size-full flex-none snap-start snap-always border-b")}>
|
||||
<AIChatLayout
|
||||
key="ai-chat-layout"
|
||||
style={
|
||||
{
|
||||
width: isAllView ? "100%" : aiPanelWidth,
|
||||
"--ai-chat-layout-width": `${aiPanelWidth}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
{isAllView && (
|
||||
<div className="center absolute inset-x-0 bottom-0 z-10">
|
||||
<i className="i-mingcute-down-line size-4 shrink-0 text-text-secondary" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
[aiPanelWidth, isAllView],
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={layoutContainerRef}
|
||||
className={cn(
|
||||
"relative h-full min-w-0 grow",
|
||||
isAllView ? "flex snap-y snap-mandatory flex-col overflow-y-auto scroll-smooth" : "flex",
|
||||
)}
|
||||
>
|
||||
{isAllView ? (
|
||||
<>
|
||||
{aiChatLayoutNode}
|
||||
{timelineSection}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{timelineSection}
|
||||
|
||||
{/* Fixed panel layout */}
|
||||
{aiSidebarVisible && (
|
||||
<>
|
||||
<PanelSplitter
|
||||
{...aiSeparatorProps}
|
||||
cursor={aiSeparatorCursor}
|
||||
isDragging={isAiPanelDragging}
|
||||
onDoubleClick={() => {
|
||||
const resetWidth = clampWidth(defaultUISettings.aiColWidth)
|
||||
setUISetting("aiColWidth", resetWidth)
|
||||
setAiPanelWidth(resetWidth)
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
}}
|
||||
/>
|
||||
{aiChatLayoutNode}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Floating panel - renders outside layout flow */}
|
||||
{aiPanelStyle === AIChatPanelStyle.Floating && <AIChatLayout key="ai-chat-layout" />}
|
||||
</>
|
||||
)}
|
||||
{!shouldShowFixedAI && <AIIndicator />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const AIEnhancedTimelineLayout = memo(function AIEnhancedTimelineLayout() {
|
||||
const aiEnabled = useFeature("ai")
|
||||
return (
|
||||
<AIChatRoot wrapFocusable={false}>
|
||||
<AIEnhancedTimelineLayoutImpl />
|
||||
{aiEnabled && <AIIndicator />}
|
||||
<MainViewHotkeysProvider />
|
||||
</AIChatRoot>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const AIChatFixedPanel: FC<AIChatFixedPanelProps> = ({ className, ...prop
|
|||
className={cn("relative flex h-full flex-col overflow-hidden bg-background", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChatHeader />
|
||||
<ChatHeader isFixedMode />
|
||||
<ChatInterface />
|
||||
</Focusable>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
import { FeedViewType } from "@follow/constants"
|
||||
import type { FC } from "react"
|
||||
|
||||
import { AIChatPanelStyle, useAIChatPanelStyle } from "~/atoms/settings/ai"
|
||||
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
|
||||
import { AIChatFixedPanel } from "./AIChatFixedPanel"
|
||||
import { AIChatFloatingPanel } from "./AIChatFloatingPanel"
|
||||
|
||||
export interface AIChatLayoutProps
|
||||
extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {}
|
||||
|
||||
/**
|
||||
* AIChatLayout Component
|
||||
*
|
||||
* A dynamic layout component for AI chat functionality that switches between
|
||||
* different panel styles based on user preferences.
|
||||
*
|
||||
* Supported Layouts:
|
||||
* - Fixed Panel: Chat interface integrated as a fixed sidebar/panel
|
||||
* - Floating Panel: Chat interface as a draggable floating window
|
||||
*
|
||||
* The layout style is controlled by the AIChatPanelStyle setting and automatically
|
||||
* renders the appropriate panel implementation.
|
||||
*
|
||||
* @component
|
||||
* @param props - Standard div HTML attributes passed to the selected panel
|
||||
* @example
|
||||
* // Renders based on user's AI chat panel style preference
|
||||
* <AIChatLayout className="custom-class" />
|
||||
*/
|
||||
export const AIChatLayout: FC<AIChatLayoutProps> = ({ ...props }) => {
|
||||
const panelStyle = useAIChatPanelStyle()
|
||||
const { isAllView } = useRouteParamsSelector((s) => ({
|
||||
isAllView: s.view === FeedViewType.All,
|
||||
}))
|
||||
|
||||
if (panelStyle === AIChatPanelStyle.Floating && !isAllView) {
|
||||
return <AIChatFloatingPanel {...props} />
|
||||
}
|
||||
return <AIChatFixedPanel {...props} />
|
||||
}
|
||||
|
|
@ -3,18 +3,14 @@ import { clsx } from "@follow/utils"
|
|||
import { AnimatePresence, m } from "motion/react"
|
||||
import type { FC } from "react"
|
||||
|
||||
import { setAIPanelVisibility, useAIPanelVisibility, useAISettingKey } from "~/atoms/settings/ai"
|
||||
import { setAIPanelVisibility, useAISettingKey } from "~/atoms/settings/ai"
|
||||
import { AISpline } from "~/modules/ai-chat/components/3d-models/AISpline"
|
||||
import { AISmartSidebar } from "~/modules/ai-chat/components/layouts/AISmartSidebar"
|
||||
|
||||
export const AIIndicator: FC = () => {
|
||||
const isVisible = useAIPanelVisibility()
|
||||
const showSplineButton = useAISettingKey("showSplineButton")
|
||||
import { AIChatFloatingPanel } from "./AIChatFloatingPanel"
|
||||
|
||||
// Only show the spline button when:
|
||||
// 1. Panel style is floating
|
||||
// 2. Panel is currently not visible
|
||||
const shouldShow = !isVisible
|
||||
export const AIIndicator: FC = () => {
|
||||
const showSplineButton = useAISettingKey("showSplineButton")
|
||||
|
||||
const handleClick = () => {
|
||||
setAIPanelVisibility(true)
|
||||
|
|
@ -22,8 +18,8 @@ export const AIIndicator: FC = () => {
|
|||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{shouldShow && !showSplineButton && <AISmartSidebar />}
|
||||
{shouldShow && showSplineButton && (
|
||||
{!showSplineButton && <AISmartSidebar />}
|
||||
{showSplineButton && (
|
||||
<m.button
|
||||
key="ai-spline-button"
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
|
|
@ -46,6 +42,7 @@ export const AIIndicator: FC = () => {
|
|||
<AISpline />
|
||||
</m.button>
|
||||
)}
|
||||
<AIChatFloatingPanel />
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ import { useTranslation } from "react-i18next"
|
|||
|
||||
import { useShowAISummary } from "~/atoms/ai-summary"
|
||||
import { useEntryIsInReadabilitySuccess } from "~/atoms/readability"
|
||||
import {
|
||||
AIChatPanelStyle,
|
||||
setAIPanelVisibility,
|
||||
useAIChatPanelStyle,
|
||||
useAIPanelVisibility,
|
||||
} from "~/atoms/settings/ai"
|
||||
import { setAIPanelVisibility } from "~/atoms/settings/ai"
|
||||
import { useActionLanguage } from "~/atoms/settings/general"
|
||||
import { AISummaryCardBase } from "~/components/ui/ai-summary-card"
|
||||
|
||||
|
|
@ -21,10 +16,6 @@ export function AISummary({ entryId }: { entryId: string }) {
|
|||
|
||||
const actionLanguage = useActionLanguage()
|
||||
|
||||
// AI Chat panel state
|
||||
const aiChatPanelStyle = useAIChatPanelStyle()
|
||||
const isAIPanelVisible = useAIPanelVisibility()
|
||||
|
||||
const summary = usePrefetchSummary({
|
||||
actionLanguage,
|
||||
entryId,
|
||||
|
|
@ -32,13 +23,6 @@ export function AISummary({ entryId }: { entryId: string }) {
|
|||
enabled: showAISummary,
|
||||
})
|
||||
|
||||
// Show Ask AI button when:
|
||||
// 1. Panel style is floating AND panel is not visible
|
||||
// 2. OR panel style is fixed (since fixed panel can be toggled)
|
||||
const shouldShowAskAI =
|
||||
(aiChatPanelStyle === AIChatPanelStyle.Floating && !isAIPanelVisible) ||
|
||||
aiChatPanelStyle === AIChatPanelStyle.Fixed
|
||||
|
||||
const handleAskAI = () => {
|
||||
setAIPanelVisibility(true)
|
||||
}
|
||||
|
|
@ -53,7 +37,7 @@ export function AISummary({ entryId }: { entryId: string }) {
|
|||
isLoading={summary.isLoading}
|
||||
className="my-8"
|
||||
title={t("entry_content.ai_summary")}
|
||||
showAskAIButton={shouldShowAskAI}
|
||||
showAskAIButton={true}
|
||||
onAskAI={handleAskAI}
|
||||
error={summary.error}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,8 @@ import { cn } from "@follow/utils/utils"
|
|||
import { useStore } from "jotai"
|
||||
import { memo, useEffect, useMemo, useState } from "react"
|
||||
|
||||
import { AIChatPanelStyle, setAIPanelVisibility, useAIChatPanelStyle } from "~/atoms/settings/ai"
|
||||
import type { TocRef } from "~/components/ui/markdown/components/Toc"
|
||||
import { Toc } from "~/components/ui/markdown/components/Toc"
|
||||
import { useFeature } from "~/hooks/biz/useFeature"
|
||||
import { useWrappedElement, useWrappedElementSize } from "~/providers/wrapped-element-provider"
|
||||
|
||||
const useReadPercent = () => {
|
||||
|
|
@ -48,10 +46,6 @@ const useReadPercent = () => {
|
|||
const BackTopIndicator: Component = memo(({ className }) => {
|
||||
const [readPercent] = useReadPercent()
|
||||
const scrollElement = useScrollViewElement()
|
||||
const aiEnabled = useFeature("ai")
|
||||
|
||||
const panelStyle = useAIChatPanelStyle()
|
||||
const isAiPanelOpen = panelStyle === AIChatPanelStyle.Fixed
|
||||
|
||||
return (
|
||||
<span
|
||||
|
|
@ -60,23 +54,12 @@ const BackTopIndicator: Component = memo(({ className }) => {
|
|||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 tabular-nums">
|
||||
<CircleProgress percent={readPercent!} size={14} strokeWidth={2} />
|
||||
<span>{readPercent}%</span>
|
||||
<br />
|
||||
</div>
|
||||
{aiEnabled && !isAiPanelOpen && (
|
||||
<MotionButtonBase
|
||||
onClick={() => {
|
||||
setAIPanelVisibility(true)
|
||||
}}
|
||||
className={cn(
|
||||
"mt-1 flex flex-nowrap items-center gap-2 text-sm opacity-50 transition-all duration-500 hover:opacity-100",
|
||||
)}
|
||||
>
|
||||
<i className="i-mgc-ai-cute-re" />
|
||||
<span>Ask AI</span>
|
||||
</MotionButtonBase>
|
||||
{!!readPercent && (
|
||||
<div className="flex items-center gap-2 tabular-nums">
|
||||
<CircleProgress percent={readPercent!} size={14} strokeWidth={2} />
|
||||
<span>{readPercent}%</span>
|
||||
<br />
|
||||
</div>
|
||||
)}
|
||||
<MotionButtonBase
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { cn } from "@follow/utils"
|
|||
import { ErrorBoundary } from "@sentry/react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
|
||||
import { AIChatPanelStyle, useAIChatPanelStyle, useAIPanelVisibility } from "~/atoms/settings/ai"
|
||||
import { useUISettingKey } from "~/atoms/settings/ui"
|
||||
import { ShadowDOM } from "~/components/common/ShadowDOM"
|
||||
import type { TocRef } from "~/components/ui/markdown/components/Toc"
|
||||
|
|
@ -65,11 +64,6 @@ export const ArticleLayout: React.FC<EntryLayoutProps> = ({
|
|||
removeBlock(BlockSliceAction.SPECIAL_TYPES.selectedText)
|
||||
}, [removeBlock])
|
||||
|
||||
const aiChatPanelStyle = useAIChatPanelStyle()
|
||||
const isAIPanelVisible = useAIPanelVisibility()
|
||||
|
||||
const shouldShowAISummary = aiChatPanelStyle === AIChatPanelStyle.Floating || !isAIPanelVisible
|
||||
|
||||
if (!entry) return null
|
||||
|
||||
return (
|
||||
|
|
@ -87,7 +81,7 @@ export const ArticleLayout: React.FC<EntryLayoutProps> = ({
|
|||
|
||||
<WrappedElementProvider boundingDetection>
|
||||
<div className="mx-auto mb-32 mt-6 max-w-full cursor-auto text-[0.94rem]">
|
||||
{shouldShowAISummary && <AISummary entryId={entryId} />}
|
||||
<AISummary entryId={entryId} />
|
||||
<ErrorBoundary fallback={EntryRenderError}>
|
||||
<ReadabilityNotice entryId={entryId} />
|
||||
{showTranscript ? (
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { createDefineSettingItem } from "../helper/builder"
|
|||
import { createSettingBuilder } from "../helper/setting-builder"
|
||||
import { MCPServicesSection } from "./ai/mcp/MCPServicesSection"
|
||||
import { UserMemorySection } from "./ai/memory"
|
||||
import { PanelStyleSection } from "./ai/PanelStyleSection"
|
||||
import { PersonalizePromptSection } from "./ai/PersonalizePromptSection"
|
||||
import { AIShortcutsSection } from "./ai/shortcuts/AIShortcutsSection"
|
||||
import { TaskSchedulingSection } from "./ai/tasks"
|
||||
|
|
@ -33,7 +32,6 @@ export const SettingAI = () => {
|
|||
value: t("features.title"),
|
||||
},
|
||||
|
||||
PanelStyleSection,
|
||||
defineSettingItem("showSplineButton", {
|
||||
label: t("settings.showSplineButton.label"),
|
||||
description: t("settings.showSplineButton.description"),
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { AIChatPanelStyle, setAIChatPanelStyle, useAIChatPanelStyle } from "~/atoms/settings/ai"
|
||||
|
||||
import { SettingTabbedSegment } from "../../control"
|
||||
|
||||
export const PanelStyleSection = () => {
|
||||
const { t } = useTranslation("ai")
|
||||
const panelStyle = useAIChatPanelStyle()
|
||||
|
||||
return (
|
||||
<SettingTabbedSegment
|
||||
key="panel-style"
|
||||
label={t("settings.panel_style.label")}
|
||||
description={t("settings.panel_style.description")}
|
||||
value={panelStyle}
|
||||
values={[
|
||||
{
|
||||
value: AIChatPanelStyle.Fixed,
|
||||
label: t("settings.panel_style.fixed"),
|
||||
icon: <i className="i-mingcute-rectangle-vertical-line" />,
|
||||
},
|
||||
{
|
||||
value: AIChatPanelStyle.Floating,
|
||||
label: t("settings.panel_style.floating"),
|
||||
icon: <i className="i-mingcute-layout-right-line" />,
|
||||
},
|
||||
]}
|
||||
onValueChanged={(value) => {
|
||||
setAIChatPanelStyle(value as AIChatPanelStyle)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
export { UserMemorySection } from "./memory/UserMemorySection"
|
||||
export { PanelStyleSection } from "./PanelStyleSection"
|
||||
export { PersonalizePromptSection } from "./PersonalizePromptSection"
|
||||
export { AIShortcutsSection } from "./shortcuts"
|
||||
export { UsageAnalysisSection } from "./usage"
|
||||
|
|
|
|||
Loading…
Reference in New Issue