From 1e3d50f8767c50ed2f653df154677c9ffbbb0f42 Mon Sep 17 00:00:00 2001 From: Shine1i Date: Thu, 26 Feb 2026 12:55:09 +0100 Subject: [PATCH] feat(recipe-studio): add block sheet search + clearer icons in sheet --- .../recipe-studio/components/block-sheet.tsx | 194 ++++++++++++++---- 1 file changed, 156 insertions(+), 38 deletions(-) diff --git a/studio/frontend/src/features/recipe-studio/components/block-sheet.tsx b/studio/frontend/src/features/recipe-studio/components/block-sheet.tsx index 281eccd810..976de9f9a4 100644 --- a/studio/frontend/src/features/recipe-studio/components/block-sheet.tsx +++ b/studio/frontend/src/features/recipe-studio/components/block-sheet.tsx @@ -1,4 +1,5 @@ import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { Sheet, SheetContent, @@ -12,6 +13,7 @@ import { CodeIcon, Copy02Icon, type Database02Icon, + DragDropVerticalIcon, PlusSignIcon, Tick02Icon, Upload01Icon, @@ -115,6 +117,15 @@ const ROOT_GROUPS: RootGroup[] = [ icon: CodeIcon, }, ]; +const SEARCHABLE_KINDS: SheetKind[] = [ + "sampler", + "seed", + "llm", + "expression", + "note", +]; +const PROCESSOR_TITLE = "Schema Transform"; +const PROCESSOR_DESCRIPTION = "Transform final dataset schema."; function BlockSheetButton({ icon, @@ -124,7 +135,7 @@ function BlockSheetButton({ isActive = false, draggable = false, onDragStart, - showChevron = true, + trailing = "chevron", }: { icon: typeof Database02Icon; title: string; @@ -133,7 +144,7 @@ function BlockSheetButton({ isActive?: boolean; draggable?: boolean; onDragStart?: (event: ReactDragEvent) => void; - showChevron?: boolean; + trailing?: "chevron" | "drag" | "none"; }): ReactElement { return ( ); @@ -184,11 +200,17 @@ export function BlockSheet({ }: BlockSheetProps): ReactElement { const sheetTitle = getSheetTitle(sheetView); const [uncontrolledOpen, setUncontrolledOpen] = useState(false); + const [search, setSearch] = useState(""); const expressionBlocks = useMemo(() => getBlocksForKind("expression"), []); const noteBlocks = useMemo(() => getBlocksForKind("note"), []); const seedBlocks = useMemo(() => getBlocksForKind("seed"), []); const isControlled = typeof open === "boolean"; const sheetOpen = isControlled ? (open as boolean) : uncontrolledOpen; + const normalizedSearch = search.trim().toLowerCase(); + const hasSearch = normalizedSearch.length > 0; + const isProcessorView = sheetView === "processor"; + const isRootView = sheetView === "root"; + const isScopedBlockView = !isRootView && !isProcessorView; const setSheetOpen = (nextOpen: boolean) => { if (!isControlled) { @@ -196,6 +218,56 @@ export function BlockSheet({ } onOpenChange?.(nextOpen); }; + const matchesSearch = (title: string, description: string) => + title.toLowerCase().includes(normalizedSearch) || + description.toLowerCase().includes(normalizedSearch); + + const searchableBlocks = useMemo( + () => SEARCHABLE_KINDS.flatMap((kind) => getBlocksForKind(kind)), + [], + ); + const rootSearchBlocks = useMemo(() => { + if (!hasSearch) { + return []; + } + return searchableBlocks.filter((item) => + matchesSearch(item.title, item.description), + ); + }, [hasSearch, searchableBlocks, normalizedSearch]); + + const scopedBlocks = useMemo(() => { + if (!isScopedBlockView) { + return []; + } + const blocks = getBlocksForKind(VIEW_KIND[sheetView] ?? "sampler"); + if (!hasSearch) { + return blocks; + } + return blocks.filter((item) => matchesSearch(item.title, item.description)); + }, [hasSearch, isScopedBlockView, normalizedSearch, sheetView]); + + const rootGroups = useMemo(() => { + if (!hasSearch) { + return ROOT_GROUPS; + } + return ROOT_GROUPS.filter((group) => { + if (matchesSearch(group.title, group.description)) { + return true; + } + if (group.kind === "processor") { + return matchesSearch(PROCESSOR_TITLE, PROCESSOR_DESCRIPTION); + } + return getBlocksForKind(group.kind).some((item) => + matchesSearch(item.title, item.description), + ); + }); + }, [hasSearch, normalizedSearch]); + const showNoMatches = + (isRootView && hasSearch && rootSearchBlocks.length === 0) || + (isScopedBlockView && scopedBlocks.length === 0) || + (isProcessorView && + hasSearch && + !matchesSearch(PROCESSOR_TITLE, PROCESSOR_DESCRIPTION)); const buildDragStart = (kind: SheetKind, type: BlockType) => @@ -206,6 +278,36 @@ export function BlockSheet({ event.dataTransfer.setData("text/plain", serialized); event.dataTransfer.effectAllowed = "copy"; }; + const getTrailing = (kind: SheetKind): "drag" | "none" => + kind === "sampler" || kind === "seed" || kind === "llm" ? "drag" : "none"; + const onBlockClick = (kind: SheetKind, type: BlockType) => { + setSheetOpen(false); + if (kind === "sampler") { + onAddSampler(type as SamplerType); + return; + } + if (kind === "seed") { + onAddSeed(type as SeedBlockType); + return; + } + if (kind === "llm") { + if (type === "model_provider") { + onAddModelProvider(); + return; + } + if (type === "model_config") { + onAddModelConfig(); + return; + } + onAddLlm(type as LlmType); + return; + } + if (kind === "expression") { + onAddExpression(); + return; + } + onAddMarkdownNote(); + }; return (
@@ -215,6 +317,7 @@ export function BlockSheet({ setSheetOpen(nextOpen); if (nextOpen) { onViewChange("root"); + setSearch(""); } }} > @@ -238,7 +341,7 @@ export function BlockSheet({ className="absolute gap-0 p-0 shadow-none" overlayClassName="bg-transparent pointer-events-none backdrop-blur-none supports-backdrop-filter:backdrop-blur-none" > - +
{sheetView !== "root" && (
+ setSearch(event.target.value)} + placeholder="Search blocks..." + className="corner-squircle mt-3 h-9" + />
- {sheetView === "root" && - ROOT_GROUPS.map((item, index) => ( + {isRootView && + hasSearch && + rootSearchBlocks.map((item, index) => ( + onBlockClick(item.kind, item.type)} + /> + ))} + {isRootView && + !hasSearch && + rootGroups.map((item, index) => ( { if (item.kind === "processor") { setSheetOpen(false); @@ -288,18 +418,20 @@ export function BlockSheet({ }} /> ))} - {sheetView === "processor" && ( - + {isProcessorView && ( + (!hasSearch || + matchesSearch(PROCESSOR_TITLE, PROCESSOR_DESCRIPTION)) && ( + + ) )} - {sheetView !== "root" && - sheetView !== "processor" && - getBlocksForKind(VIEW_KIND[sheetView] ?? "sampler").map( + {isScopedBlockView && + scopedBlocks.map( (item, index) => ( { - setSheetOpen(false); - if (item.kind === "sampler") { - onAddSampler(item.type as SamplerType); - } else if (item.kind === "seed") { - onAddSeed(item.type as SeedBlockType); - } else if (item.kind === "llm") { - if (item.type === "model_provider") { - onAddModelProvider(); - } else if (item.type === "model_config") { - onAddModelConfig(); - } else { - onAddLlm(item.type as LlmType); - } - } else if (item.kind === "expression") { - onAddExpression(); - } else { - onAddMarkdownNote(); - } - }} + trailing={getTrailing(item.kind)} + onClick={() => onBlockClick(item.kind, item.type)} /> ), )} + {showNoMatches && ( +

+ No blocks match. +

+ )}