Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 43 additions & 8 deletions src/routes/Dashboard/DashboardComponentsV2SourceFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { QuickTooltip } from "@/components/ui/tooltip";
import { Paragraph, Text } from "@/components/ui/typography";
import { cn } from "@/lib/utils";
import type {
Expand All @@ -27,6 +28,16 @@ const SOURCE_FILTER_LABEL_BY_KIND: Record<
user: "User generated",
};

const SOURCE_FILTER_TOOLTIP_BY_KIND: Record<
ComponentSearchSource["kind"],
string
> = {
standard: "Standard",
published: "Published",
registered: "Registered",
user: "User",
};

export interface SourceFilterOption {
source: ComponentSearchSource;
count: number;
Expand Down Expand Up @@ -79,13 +90,15 @@ interface SourceFilterBarProps {
disabledSourceKeys: string[];
onToggle: (sourceKey: string) => void;
onEnableAll: () => void;
display?: "full" | "icons";
}

export const SourceFilterBar = ({
options,
disabledSourceKeys,
onToggle,
onEnableAll,
display = "full",
}: SourceFilterBarProps) => {
const disabled = new Set(disabledSourceKeys);
const activeCount = options.filter(
Expand All @@ -103,16 +116,20 @@ export const SourceFilterBar = ({
{options.map(({ source, count }) => {
const key = sourceFilterKey(source);
const active = !disabled.has(key);
return (
const label = `${source.label} source (${count} component${count === 1 ? "" : "s"})`;
const button = (
<Button
key={key}
type="button"
size="xs"
size={display === "icons" ? "min" : "xs"}
variant={active ? "secondary" : "outline"}
aria-pressed={active}
aria-label={`${source.label} source (${count} component${count === 1 ? "" : "s"})`}
aria-label={label}
onClick={() => onToggle(key)}
className={cn(!active && "opacity-60")}
className={cn(
display === "icons" && "h-7 w-7 rounded-full p-1.5",
!active && "opacity-60",
)}
{...tracking("component_library.source_filter", {
source_kind: source.kind,
enabled_after_click: !active,
Expand All @@ -123,12 +140,30 @@ export const SourceFilterBar = ({
size="sm"
className={SOURCE_ICON_TONE_BY_KIND[source.kind]}
/>
{source.label}
<Text as="span" size="xs" tone="subdued">
{count}
</Text>
{display === "full" && (
<>
{source.label}
<Text as="span" size="xs" tone="subdued">
{count}
</Text>
</>
)}
</Button>
);

if (display === "icons") {
return (
<QuickTooltip
key={key}
content={SOURCE_FILTER_TOOLTIP_BY_KIND[source.kind]}
side="bottom"
>
{button}
</QuickTooltip>
);
}

return button;
})}
{activeCount < options.length && (
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ describe("ComponentSearchV2Content", () => {
isRerankActive: false,
rerank: vi.fn(),
clearRerank: vi.fn(),
sourceFilterOptions: [],
disabledSourceKeys: [],
toggleSourceFilter: vi.fn(),
enableAllSources: vi.fn(),
}));
});

Expand Down Expand Up @@ -98,6 +102,10 @@ describe("ComponentSearchV2Content", () => {
isRerankActive: false,
rerank: vi.fn(),
clearRerank: vi.fn(),
sourceFilterOptions: [],
disabledSourceKeys: [],
toggleSourceFilter: vi.fn(),
enableAllSources: vi.fn(),
}));

render(<ComponentSearchV2Content />);
Expand All @@ -115,6 +123,47 @@ describe("ComponentSearchV2Content", () => {
);
});

it("shows source filters and toggles them from editor search", () => {
const toggleSourceFilter = vi.fn();
const enableAllSources = vi.fn();
mocks.useComponentSearchV2State.mockImplementation(() => ({
results: [],
browseFolders: [],
searchSuggestions: [],
isLoading: false,
canRerank: false,
isReranking: false,
isRerankActive: false,
rerank: vi.fn(),
clearRerank: vi.fn(),
sourceFilterOptions: [
{
source: { kind: "standard", id: "standard", label: "Standard" },
count: 2,
},
{
source: { kind: "user", id: "user", label: "User generated" },
count: 1,
},
],
disabledSourceKeys: ["user"],
toggleSourceFilter,
enableAllSources,
}));

render(<ComponentSearchV2Content />);

fireEvent.click(
screen.getByRole("button", {
name: "User generated source (1 component)",
}),
);
fireEvent.click(screen.getByRole("button", { name: "Show all" }));

expect(toggleSourceFilter).toHaveBeenCalledWith("user");
expect(enableAllSources).toHaveBeenCalled();
});

it("tracks editor component search completions without query text", async () => {
render(<ComponentSearchV2Content />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Spinner } from "@/components/ui/spinner";
import { Text } from "@/components/ui/typography";
import { useDebouncedSearchValue } from "@/hooks/useDebouncedSearchValue";
import { useAnalytics } from "@/providers/AnalyticsProvider";
import { SourceFilterBar } from "@/routes/Dashboard/DashboardComponentsV2SourceFilter";
import { useComponentSearchV2State } from "@/routes/v2/pages/Editor/hooks/useComponentSearchV2State";
import { tracking } from "@/utils/tracking";

Expand Down Expand Up @@ -96,6 +97,10 @@ export function ComponentSearchV2Content() {
isRerankActive,
rerank,
clearRerank,
sourceFilterOptions,
disabledSourceKeys,
toggleSourceFilter,
enableAllSources,
} = useComponentSearchV2State(deferredQuery);

const handleQueryCommit = (value: string) => {
Expand Down Expand Up @@ -183,6 +188,13 @@ export function ComponentSearchV2Content() {
</Button>
</InlineStack>
{isReranking && <AiRerankProgress />}
<SourceFilterBar
options={sourceFilterOptions}
disabledSourceKeys={disabledSourceKeys}
onToggle={toggleSourceFilter}
onEnableAll={enableAllSources}
display="icons"
/>
</BlockStack>
<ComponentSearchResults
query={deferredQuery}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,25 @@ export interface ComponentSearchV2Result {
rerankReason?: string;
}

interface ComponentSearchV2SourceFilterOption {
source: ComponentSearchSource;
count: number;
}

export interface ComponentSearchV2State {
results: ComponentSearchV2Result[];
browseFolders: UIComponentFolder[];
searchSuggestions: ComponentSearchSuggestion[];
sourceFilterOptions: ComponentSearchV2SourceFilterOption[];
disabledSourceKeys: string[];
isLoading: boolean;
canRerank: boolean;
isReranking: boolean;
isRerankActive: boolean;
rerank: () => void;
clearRerank: () => void;
toggleSourceFilter: (sourceKey: string) => void;
enableAllSources: () => void;
}

export function registeredSource(
Expand Down
45 changes: 39 additions & 6 deletions src/routes/v2/pages/Editor/hooks/useComponentSearchV2State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import {
LibraryDB,
type StoredLibrary,
} from "@/providers/ComponentLibraryProvider/libraries/storage";
import {
createSourceFilterOptions,
filterIndexByDisabledSourceKeys,
} from "@/routes/Dashboard/DashboardComponentsV2SourceFilter";
import {
buildAiCandidateMatches,
buildLexicalMatches,
Expand Down Expand Up @@ -195,10 +199,20 @@ export function useComponentSearchV2State(
}),
);

const [disabledSourceKeys, setDisabledSourceKeys] = useState<string[]>([]);
const sourceFilterOptions = createSourceFilterOptions(index);
const filteredIndex = filterIndexByDisabledSourceKeys(
index,
disabledSourceKeys,
);

const canUseEmbeddingSearch = aiConfig.apiBase.trim().length > 0;
const trimmedQuery = query.trim();
const lexicalMatches = buildLexicalMatches(index, trimmedQuery);
const aiCandidateMatches = buildAiCandidateMatches(index, trimmedQuery);
const lexicalMatches = buildLexicalMatches(filteredIndex, trimmedQuery);
const aiCandidateMatches = buildAiCandidateMatches(
filteredIndex,
trimmedQuery,
);
const {
mutate,
reset: resetRerank,
Expand All @@ -225,7 +239,7 @@ export function useComponentSearchV2State(
setRerankedFor(null);
setRerankBaseMatches([]);
embeddingAbortControllerRef.current?.abort();
}, [query, resetRerank]);
}, [query, disabledSourceKeys, resetRerank]);

useEffect(() => {
return () => embeddingAbortControllerRef.current?.abort();
Expand Down Expand Up @@ -297,7 +311,7 @@ export function useComponentSearchV2State(
const effectiveLimit = limit || LEXICAL_RESULT_LIMIT;
const embeddingMatches = canUseEmbeddings
? await buildEmbeddingMatches({
sourceIndex: index,
sourceIndex: filteredIndex,
limit: effectiveLimit,
})
: [];
Expand Down Expand Up @@ -335,6 +349,16 @@ export function useComponentSearchV2State(
});
};

const toggleSourceFilter = (sourceKey: string) => {
setDisabledSourceKeys((current) =>
current.includes(sourceKey)
? current.filter((key) => key !== sourceKey)
: [...current, sourceKey],
);
};

const enableAllSources = () => setDisabledSourceKeys([]);

const rerankMatchByDigest = buildRerankMatchByDigest(
rerankData,
isRerankActive,
Expand All @@ -347,11 +371,18 @@ export function useComponentSearchV2State(

return {
results,
browseFolders: buildResultFolders({ results, standardLibrary }),
searchSuggestions: buildComponentSearchSuggestions(index, {
browseFolders: buildResultFolders({
results,
standardLibrary: disabledSourceKeys.includes("standard")
? undefined
: standardLibrary,
}),
searchSuggestions: buildComponentSearchSuggestions(filteredIndex, {
includeSources: false,
query: trimmedQuery,
}),
sourceFilterOptions,
disabledSourceKeys,
isLoading:
isLoadingStandardLibrary ||
isLoadingUserComponents ||
Expand All @@ -367,5 +398,7 @@ export function useComponentSearchV2State(
isRerankActive,
rerank,
clearRerank,
toggleSourceFilter,
enableAllSources,
};
}
Loading