feat(ai): add block disable functionality

- Introduced a `disabled` state for context blocks, allowing users to toggle blocks on and off without removing them.
- Updated `ContextBlock` and `CombinedContextBlock` components to handle the new `disabled` prop and associated click behavior.
- Filtered out disabled blocks in the `ChatInterface` to improve user experience.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-10-18 01:11:49 +08:00
parent d282bd8cb2
commit 31a2ae9482
4 changed files with 67 additions and 15 deletions
@@ -14,21 +14,30 @@ const BlockContainer: FC<{
icon: string | null | undefined
label?: string
onRemove?: () => void
disabled?: boolean
onDisableClick?: () => void
content: ReactNode
}> = memo(({ icon, label, onRemove, content }) => {
}> = memo(({ icon, label, onRemove, content, disabled, onDisableClick }) => {
const isStringContent = typeof content === "string"
return (
<div
className={cn(
className={clsx(
"group relative flex h-7 min-w-0 max-w-[calc(50%-0.5rem)] flex-shrink-0 items-center gap-2 overflow-hidden rounded-lg px-2.5",
"bg-fill-tertiary border-border border",
disabled && "cursor-pointer border-dashed opacity-50",
)}
onClick={() => {
if (disabled) {
onDisableClick?.()
}
}}
>
<div
className={clsx(
"min-w-0",
"group-hover:[mask-image:linear-gradient(to_right,black_0%,black_calc(100%-3rem),rgba(0,0,0,0.8)_calc(100%-2rem),rgba(0,0,0,0.3)_calc(100%-1rem),transparent_100%)]",
!disabled &&
"group-hover:[mask-image:linear-gradient(to_right,black_0%,black_calc(100%-3rem),rgba(0,0,0,0.8)_calc(100%-2rem),rgba(0,0,0,0.3)_calc(100%-1rem),transparent_100%)]",
)}
>
<div className="flex min-w-0 flex-1 items-center gap-1.5">
@@ -45,13 +54,15 @@ const BlockContainer: FC<{
</div>
</div>
<button
type="button"
onClick={onRemove}
className="text-text/90 cursor-button hover:text-text absolute inset-y-0 right-2 flex-shrink-0 opacity-0 transition-all ease-in group-hover:opacity-100"
>
<i className="i-mgc-close-cute-re size-3" />
</button>
{onRemove && !disabled && (
<button
type="button"
onClick={onRemove}
className="text-text/90 cursor-button hover:text-text absolute inset-y-0 right-2 flex-shrink-0 opacity-0 transition-all ease-in group-hover:opacity-100"
>
<i className="i-mgc-close-cute-re size-3" />
</button>
)}
</div>
)
})
@@ -70,10 +81,11 @@ export const CombinedContextBlock: FC<{
const blockActions = useChatBlockActions()
const viewIcon = viewBlock && getView(Number(viewBlock.value))?.icon.props.className
const feedIcon = feedBlock && "i-mgc-rss-cute-fi"
const handleRemove = () => {
viewBlock && blockActions.removeBlock(viewBlock.id)
feedBlock && blockActions.removeBlock(feedBlock.id)
viewBlock && blockActions.toggleBlockDisabled(viewBlock.id, true)
feedBlock && blockActions.toggleBlockDisabled(feedBlock.id, true)
unreadOnlyBlock && blockActions.removeBlock(unreadOnlyBlock.id)
}
@@ -94,7 +106,18 @@ export const CombinedContextBlock: FC<{
</span>
)
return <BlockContainer icon={viewIcon} onRemove={handleRemove} content={displayContent} />
return (
<BlockContainer
icon={viewIcon || feedIcon}
disabled={viewBlock?.disabled || feedBlock?.disabled}
onRemove={handleRemove}
onDisableClick={() => {
viewBlock && blockActions.toggleBlockDisabled(viewBlock.id, false)
feedBlock && blockActions.toggleBlockDisabled(feedBlock.id, false)
}}
content={displayContent}
/>
)
})
CombinedContextBlock.displayName = "CombinedContextBlock"
@@ -107,7 +130,19 @@ export const ContextBlock: FC<{ block: AIChatContextBlock }> = memo(({ block })
<BlockContainer
icon={icon}
label={label}
onRemove={() => blockActions.removeBlock(block.id)}
disabled={block.disabled}
onRemove={() => {
if (block.type === "mainEntry") {
blockActions.toggleBlockDisabled(block.id, true)
} else {
blockActions.removeBlock(block.id)
}
}}
onDisableClick={() => {
if (block.type === "mainEntry") {
blockActions.toggleBlockDisabled(block.id, false)
}
}}
content={displayContent}
/>
)
@@ -221,7 +221,7 @@ const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => {
const parts: BizUIMessage["parts"] = [
{
type: "data-block",
data: blocks,
data: blocks.filter((block) => !block.disabled),
},
]
@@ -73,6 +73,22 @@ export class BlockSliceAction {
)
}
toggleBlockDisabled(id: string, disabled?: boolean) {
this.set(
produce((state: BlockSlice) => {
const block = state.blocks.find((block) => block.id === id)
if (block) {
const nextDisabled = disabled ?? !block.disabled
if (!nextDisabled) {
delete block.disabled
} else {
block.disabled = true
}
}
}),
)
}
updateBlock(id: string, updates: Partial<AIChatContextBlock>) {
this.set(
produce((state: BlockSlice) => {
@@ -17,6 +17,7 @@ export interface FileAttachment {
interface BaseContextBlock {
id: string
disabled?: boolean
}
export type ValueContextBlockType =