mirror of
https://github.com/RSSNext/Folo.git
synced 2026-10-29 12:12:59 +08:00
refactor: merge main view and feed in data block
This commit is contained in:
+2
-37
@@ -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)}>
|
||||
|
||||
+70
-10
@@ -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"
|
||||
|
||||
+18
-5
@@ -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])
|
||||
}
|
||||
Reference in New Issue
Block a user