refactor: merge main view and feed in data block

This commit is contained in:
Stephen Zhou
2025-09-26 16:05:07 +08:00
parent 090ae03b42
commit 1a91d57cf0
4 changed files with 142 additions and 52 deletions
@@ -4,9 +4,9 @@ import { memo, useCallback, useEffect, useMemo, useRef } from "react"
import { useAISettingValue } from "~/atoms/settings/ai"
import { DropdownMenu, DropdownMenuTrigger } from "~/components/ui/dropdown-menu/dropdown-menu"
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
import { useDisplayBlocks } from "~/modules/ai-chat/hooks/useDisplayBlocks"
import { useFileUploadWithDefaults } from "~/modules/ai-chat/hooks/useFileUpload"
import { useAIChatStore } from "~/modules/ai-chat/store/AIChatContext"
import type { AIChatContextBlock, ValueContextBlock } from "~/modules/ai-chat/store/types"
import { SUPPORTED_MIME_ACCEPT } from "~/modules/ai-chat/utils/file-validation"
import { useBlockActions } from "../../store/hooks"
@@ -14,10 +14,6 @@ import { BlockSliceAction } from "../../store/slices/block.slice"
import { ContextBlock, MainViewFeedContextBlock } from "../context-bar/blocks"
import { ContextMenuContent, ShortcutsMenuContent } from "../context-bar/menus"
type ValueBlockOf<Type extends ValueContextBlock["type"]> = Omit<ValueContextBlock, "type"> & {
type: Type
}
export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) => void }> = memo(
({ className, onSendShortcut }) => {
const blocks = useAIChatStore()((s) => s.blocks)
@@ -75,38 +71,7 @@ export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) =>
}
}, [addOrUpdateBlock, feedId, removeBlock])
const displayBlocks = useMemo(() => {
const mainViewBlock = blocks.find(
(block): block is ValueBlockOf<"mainView"> => block.type === "mainView",
)
const mainFeedBlock = blocks.find(
(block): block is ValueBlockOf<"mainFeed"> => block.type === "mainFeed",
)
if (mainViewBlock && mainFeedBlock) {
const items: (
| {
kind: "combined"
viewBlock: ValueBlockOf<"mainView">
feedBlock: ValueBlockOf<"mainFeed">
}
| { kind: "single"; block: AIChatContextBlock }
)[] = []
items.push({ kind: "combined", viewBlock: mainViewBlock, feedBlock: mainFeedBlock })
const otherBlocks = blocks.filter(
(block) => block.id !== mainViewBlock.id && block.id !== mainFeedBlock.id,
)
otherBlocks.forEach((block) => {
items.push({ kind: "single", block })
})
return items
}
return blocks.map((block) => ({ kind: "single" as const, block }))
}, [blocks])
const displayBlocks = useDisplayBlocks(blocks)
return (
<div className={cn("flex flex-wrap items-center gap-2 px-4 py-3", className)}>
@@ -3,7 +3,7 @@ import { cn } from "@follow/utils/utils"
import { t } from "i18next"
import * as React from "react"
import type { AIChatContextBlock } from "~/modules/ai-chat/store/types"
import type { AIChatContextBlock, ValueContextBlock } from "~/modules/ai-chat/store/types"
import {
getBlockIcon,
@@ -20,6 +20,15 @@ interface AIDataBlockItemProps {
index: number
}
type ValueBlockOf<Type extends ValueContextBlock["type"]> = Omit<ValueContextBlock, "type"> & {
type: Type
}
interface MainViewFeedDataBlockItemProps {
viewBlock: ValueBlockOf<"mainView">
feedBlock: ValueBlockOf<"mainFeed">
}
/**
* Gets the display content for a context block
*/
@@ -87,13 +96,15 @@ const BlockIcon: React.FC<{
BlockIcon.displayName = "BlockIcon"
/**
* Individual block item component with optimized rendering and animations
* Shared block container component to ensure consistency
*/
export const AIDataBlockItem: React.FC<AIDataBlockItemProps> = React.memo(({ block }) => {
const styles = React.useMemo(() => getBlockStyles(block.type), [block.type])
const label = React.useMemo(() => getBlockLabel(block.type), [block.type])
const displayContent = React.useMemo(() => getDisplayContent(block), [block])
const BlockContainer: React.FC<{
styles: ReturnType<typeof getBlockStyles>
block: AIChatContextBlock
label?: string
displayContent: React.ReactNode
title?: string
}> = React.memo(({ styles, block, label, displayContent, title }) => {
return (
<div
key={block.id}
@@ -107,11 +118,15 @@ export const AIDataBlockItem: React.FC<AIDataBlockItemProps> = React.memo(({ blo
{/* Label and content */}
<div className="flex min-w-0 items-center gap-1">
<span className={cn("text-xs font-medium", styles.label)}>{label}</span>
<span className="text-text-secondary text-xs">·</span>
{label && (
<>
<span className={cn("text-xs font-medium", styles.label)}>{label}</span>
<span className="text-text-secondary text-xs">·</span>
</>
)}
<span
className="text-text max-w-24 truncate text-xs font-medium"
title={typeof displayContent === "string" ? displayContent : undefined}
title={title || (typeof displayContent === "string" ? displayContent : undefined)}
>
{displayContent}
</span>
@@ -120,4 +135,49 @@ export const AIDataBlockItem: React.FC<AIDataBlockItemProps> = React.memo(({ blo
)
})
BlockContainer.displayName = "BlockContainer"
/**
* Individual block item component with optimized rendering and animations
*/
export const AIDataBlockItem: React.FC<AIDataBlockItemProps> = React.memo(({ block }) => {
const styles = React.useMemo(() => getBlockStyles(block.type), [block.type])
const label = React.useMemo(() => getBlockLabel(block.type), [block.type])
const displayContent = React.useMemo(() => getDisplayContent(block), [block])
return (
<BlockContainer styles={styles} block={block} label={label} displayContent={displayContent} />
)
})
AIDataBlockItem.displayName = "AIDataBlockItem"
/**
* Combined block item component for main view and main feed
* Displays view icon with feed title content
*/
export const MainViewFeedDataBlockItem: React.FC<MainViewFeedDataBlockItemProps> = React.memo(
({ viewBlock, feedBlock }) => {
const { styles, displayContent, title } = React.useMemo(() => {
const view = views.find((v) => v.view === Number(viewBlock.value))
const viewName = view?.name ? t(view.name, { ns: "common" }) : viewBlock.value
return {
styles: getBlockStyles("mainView"),
displayContent: <FeedTitle feedId={feedBlock.value} fallback={feedBlock.value} />,
title: `${viewName} - ${feedBlock.value}`,
}
}, [viewBlock.value, feedBlock.value])
return (
<BlockContainer
styles={styles}
block={viewBlock}
displayContent={displayContent}
title={title}
/>
)
},
)
MainViewFeedDataBlockItem.displayName = "MainViewFeedDataBlockItem"
@@ -1,9 +1,10 @@
import { cn } from "@follow/utils/utils"
import * as React from "react"
import { useDisplayBlocks } from "~/modules/ai-chat/hooks/useDisplayBlocks"
import type { AIChatContextBlock } from "~/modules/ai-chat/store/types"
import { AIDataBlockItem } from "./AIDataBlockItem"
import { AIDataBlockItem, MainViewFeedDataBlockItem } from "./AIDataBlockItem"
interface AIDataBlockPartProps {
blocks: AIChatContextBlock[]
@@ -14,8 +15,10 @@ interface AIDataBlockPartProps {
* Displays various types of context (entries, feeds, text, files) with compact styling
*/
export const AIDataBlockPart: React.FC<AIDataBlockPartProps> = React.memo(({ blocks }) => {
const displayBlocks = useDisplayBlocks(blocks)
// Early return for empty blocks
if (!blocks?.length) {
if (displayBlocks.length === 0) {
return null
}
@@ -34,9 +37,19 @@ export const AIDataBlockPart: React.FC<AIDataBlockPartProps> = React.memo(({ blo
</div>
{/* Render individual block items */}
{blocks.map((block, index) => (
<AIDataBlockItem key={block.id} block={block} index={index} />
))}
{displayBlocks.map((item, index) => {
if (item.kind === "combined") {
return (
<MainViewFeedDataBlockItem
key={item.viewBlock.id}
viewBlock={item.viewBlock}
feedBlock={item.feedBlock}
/>
)
}
return <AIDataBlockItem key={item.block.id} block={item.block} index={index} />
})}
</div>
</div>
)
@@ -0,0 +1,52 @@
import { useMemo } from "react"
import type { AIChatContextBlock, ValueContextBlock } from "~/modules/ai-chat/store/types"
type ValueBlockOf<Type extends ValueContextBlock["type"]> = Omit<ValueContextBlock, "type"> & {
type: Type
}
export type DisplayBlockItem =
| {
kind: "combined"
viewBlock: ValueBlockOf<"mainView">
feedBlock: ValueBlockOf<"mainFeed">
}
| { kind: "single"; block: AIChatContextBlock }
/**
* Custom hook to process blocks and merge mainView + mainFeed when both exist
* Returns an array of display items that can be either combined or single blocks
*/
export const useDisplayBlocks = (blocks: AIChatContextBlock[]): DisplayBlockItem[] => {
return useMemo(() => {
// Early return for empty blocks
if (!blocks?.length) {
return []
}
const mainViewBlock = blocks.find(
(block): block is ValueBlockOf<"mainView"> => block.type === "mainView",
)
const mainFeedBlock = blocks.find(
(block): block is ValueBlockOf<"mainFeed"> => block.type === "mainFeed",
)
if (mainViewBlock && mainFeedBlock) {
const items: DisplayBlockItem[] = []
items.push({ kind: "combined", viewBlock: mainViewBlock, feedBlock: mainFeedBlock })
const otherBlocks = blocks.filter(
(block) => block.id !== mainViewBlock.id && block.id !== mainFeedBlock.id,
)
otherBlocks.forEach((block) => {
items.push({ kind: "single", block })
})
return items
}
return blocks.map((block) => ({ kind: "single" as const, block }))
}, [blocks])
}