feat(recipe-studio): add block sheet search + clearer icons in sheet
This commit is contained in:
parent
81a7e38aa6
commit
1e3d50f876
1 changed files with 156 additions and 38 deletions
|
|
@ -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<HTMLButtonElement>) => void;
|
||||
showChevron?: boolean;
|
||||
trailing?: "chevron" | "drag" | "none";
|
||||
}): ReactElement {
|
||||
return (
|
||||
<button
|
||||
|
|
@ -154,11 +165,16 @@ function BlockSheetButton({
|
|||
<p className="text-sm font-semibold text-foreground">{title}</p>
|
||||
<p className="text-[11px] text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
{showChevron ? (
|
||||
{trailing === "chevron" ? (
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight01Icon}
|
||||
className="size-3.5 text-muted-foreground"
|
||||
/>
|
||||
) : trailing === "drag" ? (
|
||||
<HugeiconsIcon
|
||||
icon={DragDropVerticalIcon}
|
||||
className="size-5 text-primary/80"
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
|
|
@ -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 (
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
|
|
@ -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"
|
||||
>
|
||||
<SheetHeader className="border-b border-border/60 px-6 py-5">
|
||||
<SheetHeader className="px-6 py-5">
|
||||
<div className="flex items-center gap-2">
|
||||
{sheetView !== "root" && (
|
||||
<Button
|
||||
|
|
@ -252,17 +355,44 @@ export function BlockSheet({
|
|||
)}
|
||||
<SheetTitle>{sheetTitle}</SheetTitle>
|
||||
</div>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="Search blocks..."
|
||||
className="corner-squircle mt-3 h-9"
|
||||
/>
|
||||
</SheetHeader>
|
||||
<div className=" py-4">
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
{sheetView === "root" &&
|
||||
ROOT_GROUPS.map((item, index) => (
|
||||
{isRootView &&
|
||||
hasSearch &&
|
||||
rootSearchBlocks.map((item, index) => (
|
||||
<BlockSheetButton
|
||||
key={`${item.kind}:${item.type}`}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
isActive={index === 0}
|
||||
draggable={true}
|
||||
onDragStart={buildDragStart(item.kind, item.type)}
|
||||
trailing={getTrailing(item.kind)}
|
||||
onClick={() => onBlockClick(item.kind, item.type)}
|
||||
/>
|
||||
))}
|
||||
{isRootView &&
|
||||
!hasSearch &&
|
||||
rootGroups.map((item, index) => (
|
||||
<BlockSheetButton
|
||||
key={item.kind}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
isActive={index === 0}
|
||||
trailing={
|
||||
item.kind === "expression" || item.kind === "note"
|
||||
? "none"
|
||||
: "chevron"
|
||||
}
|
||||
onClick={() => {
|
||||
if (item.kind === "processor") {
|
||||
setSheetOpen(false);
|
||||
|
|
@ -288,18 +418,20 @@ export function BlockSheet({
|
|||
}}
|
||||
/>
|
||||
))}
|
||||
{sheetView === "processor" && (
|
||||
<BlockSheetButton
|
||||
icon={CodeIcon}
|
||||
title="Schema Transform"
|
||||
description="Transform final dataset schema."
|
||||
isActive={true}
|
||||
onClick={onOpenProcessors}
|
||||
/>
|
||||
{isProcessorView && (
|
||||
(!hasSearch ||
|
||||
matchesSearch(PROCESSOR_TITLE, PROCESSOR_DESCRIPTION)) && (
|
||||
<BlockSheetButton
|
||||
icon={CodeIcon}
|
||||
title={PROCESSOR_TITLE}
|
||||
description={PROCESSOR_DESCRIPTION}
|
||||
isActive={true}
|
||||
onClick={onOpenProcessors}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{sheetView !== "root" &&
|
||||
sheetView !== "processor" &&
|
||||
getBlocksForKind(VIEW_KIND[sheetView] ?? "sampler").map(
|
||||
{isScopedBlockView &&
|
||||
scopedBlocks.map(
|
||||
(item, index) => (
|
||||
<BlockSheetButton
|
||||
key={item.type}
|
||||
|
|
@ -309,30 +441,16 @@ export function BlockSheet({
|
|||
isActive={index === 0}
|
||||
draggable={true}
|
||||
onDragStart={buildDragStart(item.kind, item.type)}
|
||||
showChevron={!(sheetView === "expression" || sheetView === "note")}
|
||||
onClick={() => {
|
||||
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 && (
|
||||
<p className="px-3 py-2 text-xs text-muted-foreground">
|
||||
No blocks match.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue