From ae97aa2b1a8c24aee12ca7aa35c53ed840a62fd5 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Thu, 6 Nov 2025 09:09:06 +0800 Subject: [PATCH] refactor(ai): drop floating chat panel and simplify timeline layout --- .../layer/renderer/src/atoms/settings/ai.ts | 15 - .../ui/ai-summary-card/AISummaryCardBase.tsx | 39 +- .../hooks/biz/useShowEntryDetailsColumn.ts | 7 +- .../ai-chat/components/layouts/ChatHeader.tsx | 16 +- .../components/layouts/ChatInterface.tsx | 3 +- .../components/layouts/ChatMoreDropdown.tsx | 49 +-- .../modules/ai-chat/hooks/useAIShortcut.ts | 13 +- .../hooks/useAutoTimelineSummaryShortcut.ts | 2 +- .../hooks/useTimelineSummaryAutoContext.ts | 10 +- .../AIEnhancedTimelineLayout.tsx | 377 +++++++----------- .../app-layout/ai/AIChatFixedPanel.tsx | 2 +- .../modules/app-layout/ai/AIChatLayout.tsx | 42 -- .../modules/app-layout/ai/AISplineButton.tsx | 17 +- .../entry-content/components/AISummary.tsx | 20 +- .../accessories/ContainerToc.tsx | 29 +- .../components/layouts/ArticleLayout.tsx | 8 +- .../renderer/src/modules/settings/tabs/ai.tsx | 2 - .../settings/tabs/ai/PanelStyleSection.tsx | 34 -- .../src/modules/settings/tabs/ai/index.ts | 1 - 19 files changed, 204 insertions(+), 482 deletions(-) delete mode 100644 apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatLayout.tsx delete mode 100644 apps/desktop/layer/renderer/src/modules/settings/tabs/ai/PanelStyleSection.tsx diff --git a/apps/desktop/layer/renderer/src/atoms/settings/ai.ts b/apps/desktop/layer/renderer/src/atoms/settings/ai.ts index 06be487f9..87ad418b2 100644 --- a/apps/desktop/layer/renderer/src/atoms/settings/ai.ts +++ b/apps/desktop/layer/renderer/src/atoms/settings/ai.ts @@ -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) } diff --git a/apps/desktop/layer/renderer/src/components/ui/ai-summary-card/AISummaryCardBase.tsx b/apps/desktop/layer/renderer/src/components/ui/ai-summary-card/AISummaryCardBase.tsx index e3ba7486f..37eb6a914 100644 --- a/apps/desktop/layer/renderer/src/components/ui/ai-summary-card/AISummaryCardBase.tsx +++ b/apps/desktop/layer/renderer/src/components/ui/ai-summary-card/AISummaryCardBase.tsx @@ -177,26 +177,6 @@ export const AISummaryCardBase: React.FC = ({ )}
- {aiEnabled && showAskAIButton && hasContent && onAskAI && ( - - - Ask AI - - )} - {showCopyButton && hasContent && ( = ({ )} /> )} + {aiEnabled && showAskAIButton && hasContent && onAskAI && ( + + + Ask AI + + )}
diff --git a/apps/desktop/layer/renderer/src/hooks/biz/useShowEntryDetailsColumn.ts b/apps/desktop/layer/renderer/src/hooks/biz/useShowEntryDetailsColumn.ts index 6aed66cd3..b0eb80b37 100644 --- a/apps/desktop/layer/renderer/src/hooks/biz/useShowEntryDetailsColumn.ts +++ b/apps/desktop/layer/renderer/src/hooks/biz/useShowEntryDetailsColumn.ts @@ -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 } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx index f045b1830..0a669521e 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx @@ -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 = ({
- {isFloating && ( + {!isFixedMode && (
{ - const panelStyle = useAIChatPanelStyle() +export const ChatHeader = ({ isFixedMode = false }: { isFixedMode?: boolean }) => { const { t } = useTranslation("ai") return ( ( <> @@ -130,7 +130,7 @@ export const ChatHeader = () => { } /> - {panelStyle === AIChatPanelStyle.Floating && ( + {!isFixedMode && ( <>
setAIPanelVisibility(false)}> diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx index 49641271d..37d3ba244 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx @@ -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", diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx index 26bb9024b..ec747db7f 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx @@ -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 = ({ {isGenerating ? t("common.generating_title") : t("common.generate_title")} - - {canToggleMode && ( - <> - - - - {panelStyle === AIChatPanelStyle.Fixed - ? "Switch to Floating Panel" - : "Switch to Fixed Panel"} - - - - - )} - settingModalPresent("ai")}> AI Settings - - {panelStyle !== AIChatPanelStyle.Floating && ( - <> - - - - Close Sidebar - - - )} ) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useAIShortcut.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useAIShortcut.ts index 562fc2ad1..5dc3eab37 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useAIShortcut.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useAIShortcut.ts @@ -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) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useAutoTimelineSummaryShortcut.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useAutoTimelineSummaryShortcut.ts index 69c4902bd..e1667a5c7 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useAutoTimelineSummaryShortcut.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useAutoTimelineSummaryShortcut.ts @@ -106,7 +106,7 @@ export const useAutoTimelineSummaryShortcut = () => { }) const previousContextKeyRef = useRef(null) - const isAllTimeline = isTimelineSummaryAutoContext({ view, entryId }) + const isAllTimeline = isTimelineSummaryAutoContext({ entryId }) const defaultShortcut = useMemo(() => { const shortcuts = aiSettings.shortcuts ?? [] diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useTimelineSummaryAutoContext.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useTimelineSummaryAutoContext.ts index 6353f649b..fb547c18b 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useTimelineSummaryAutoContext.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useTimelineSummaryAutoContext.ts @@ -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 +export type TimelineSummaryContextParams = Pick -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 })) diff --git a/apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx b/apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx index 0dc3d072a..bc00d0b7c 100644 --- a/apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx +++ b/apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx @@ -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(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, @@ -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, 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) => { - 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 ? ( + + ) : shouldShowFixedAI ? ( + + ) : null + + return ( +
-
+
-
- {/* Entry list - always rendered to prevent animation */} - - {!showEntryDetailsColumn && ( - <> - - {realEntryId && ( + + + {showEntryContentOnLeft && ( + <> + + {realEntryId && ( + + + + )} + + + + {realEntryId && ( +
- + - )} - - {/* Entry content overlay with exit animation */} - - {realEntryId && ( -
- - - -
- )} -
- - )} -
+
+ )} + + + )}
- {showEntryDetailsColumn && shouldRenderDetailsColumn && ( + {shouldRenderRightColumn && ( <> -
- { - setUISetting("entryColWidth", defaultUISettings.entryColWidth) - setTimelineColumnWidth(defaultUISettings.entryColWidth) - window.dispatchEvent(new Event("resize")) - }} - /> -
+ {splitter} +
- {realEntryId ? ( - <> + {showEntryContentOnRight && realEntryId ? ( +
-
-
- -
+
+
- +
+ ) : shouldShowFixedAI ? ( +
+ +
) : (
@@ -284,93 +281,15 @@ const AIEnhancedTimelineLayoutImpl = () => {
- ), - [ - aiPanelStyle, - handleTimelineTransitionEnd, - isAllView, - isTimelineDragging, - realEntryId, - setTimelineColumnWidth, - shouldRenderDetailsColumn, - showEntryDetailsColumn, - timelineColumnWidth, - timelineSeparatorCursor, - timelineSeparatorProps, - ], - ) - - const aiChatLayoutNode = useMemo( - () => ( -
- - {isAllView && ( -
- -
- )} -
- ), - [aiPanelWidth, isAllView], - ) - - return ( -
- {isAllView ? ( - <> - {aiChatLayoutNode} - {timelineSection} - - ) : ( - <> - {timelineSection} - - {/* Fixed panel layout */} - {aiSidebarVisible && ( - <> - { - 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 && } - - )} + {!shouldShowFixedAI && }
) } export const AIEnhancedTimelineLayout = memo(function AIEnhancedTimelineLayout() { - const aiEnabled = useFeature("ai") return ( - {aiEnabled && } ) diff --git a/apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatFixedPanel.tsx b/apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatFixedPanel.tsx index bb61e1569..b3ae3bb20 100644 --- a/apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatFixedPanel.tsx +++ b/apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatFixedPanel.tsx @@ -17,7 +17,7 @@ export const AIChatFixedPanel: FC = ({ className, ...prop className={cn("relative flex h-full flex-col overflow-hidden bg-background", className)} {...props} > - + ) diff --git a/apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatLayout.tsx b/apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatLayout.tsx deleted file mode 100644 index 4d87de006..000000000 --- a/apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatLayout.tsx +++ /dev/null @@ -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, 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 - * - */ -export const AIChatLayout: FC = ({ ...props }) => { - const panelStyle = useAIChatPanelStyle() - const { isAllView } = useRouteParamsSelector((s) => ({ - isAllView: s.view === FeedViewType.All, - })) - - if (panelStyle === AIChatPanelStyle.Floating && !isAllView) { - return - } - return -} diff --git a/apps/desktop/layer/renderer/src/modules/app-layout/ai/AISplineButton.tsx b/apps/desktop/layer/renderer/src/modules/app-layout/ai/AISplineButton.tsx index 108d367b6..705ae5865 100644 --- a/apps/desktop/layer/renderer/src/modules/app-layout/ai/AISplineButton.tsx +++ b/apps/desktop/layer/renderer/src/modules/app-layout/ai/AISplineButton.tsx @@ -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 ( - {shouldShow && !showSplineButton && } - {shouldShow && showSplineButton && ( + {!showSplineButton && } + {showSplineButton && ( { )} + ) } diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/AISummary.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/AISummary.tsx index 21c0d1827..de4864e04 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/AISummary.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/AISummary.tsx @@ -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} /> diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/accessories/ContainerToc.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/accessories/ContainerToc.tsx index 54bbd9283..623357bae 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/accessories/ContainerToc.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/accessories/ContainerToc.tsx @@ -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 ( { className, )} > -
- - {readPercent}% -
-
- {aiEnabled && !isAiPanelOpen && ( - { - setAIPanelVisibility(true) - }} - className={cn( - "mt-1 flex flex-nowrap items-center gap-2 text-sm opacity-50 transition-all duration-500 hover:opacity-100", - )} - > - - Ask AI - + {!!readPercent && ( +
+ + {readPercent}% +
+
)} { diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx index 8929ccc6b..a019f1ffd 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx @@ -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 = ({ 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 = ({
- {shouldShowAISummary && } + {showTranscript ? ( diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx index 8fb7a22ed..67e2d74d8 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx @@ -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"), diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/PanelStyleSection.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/PanelStyleSection.tsx deleted file mode 100644 index d093a81ca..000000000 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/PanelStyleSection.tsx +++ /dev/null @@ -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 ( - , - }, - { - value: AIChatPanelStyle.Floating, - label: t("settings.panel_style.floating"), - icon: , - }, - ]} - onValueChanged={(value) => { - setAIChatPanelStyle(value as AIChatPanelStyle) - }} - /> - ) -} diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/index.ts b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/index.ts index 7b8eeff0f..a4ac41aee 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/index.ts +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/index.ts @@ -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"