Skip to content
Open
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: 46 additions & 5 deletions src/routes/Dashboard/DashboardComponentsV2View.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,26 @@ export const DashboardComponentsV2View = () => {
);
})();

const deepAiCandidateMatches: LexicalMatch[] = (() => {
if (trimmedQuery.length === 0) return [];

const candidates: LexicalMatch[] = [];
const seenDigests = new Set<string>();
const allLexicalMatches = lexicalSearch(filteredIndex, query, {
limit: filteredIndex.length,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

[HIGH] Same unbounded-deep-pool concern on the Dashboard surface: lexicalSearch(filteredIndex, ..., { limit: filteredIndex.length }) then every remaining sortedIndex entry is appended, and the whole pool is JSON.stringify-ed into a billed LLM rerank with no input cap or confirmation. Bound the deep pool to a finite N or confirm before sending a very large pool; document the worst-case prompt size.

});
for (const match of allLexicalMatches) {
seenDigests.add(match.digest);
candidates.push(match);
}
for (const entry of sortedIndex) {
if (seenDigests.has(entry.digest)) continue;
seenDigests.add(entry.digest);
candidates.push(indexEntryToLexicalMatch(entry));
}
return candidates;
})();

const {
mutate: rerank,
data: rerankData,
Expand All @@ -760,21 +780,29 @@ export const DashboardComponentsV2View = () => {
}
};

const handleSmartSearch = () => {
const startAiSearch = (matches: LexicalMatch[]) => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

[LOW] startAiSearch passes no scoreAllCandidates for either smart or deep, so both default to false here, whereas the Editor uses true for smart / false for deep. Routing the new deep button through this shared helper cements a smart/deep behavior divergence between the two surfaces. Consider passing a flag (as the Editor’s startRerank does) for parity.

const trimmed = query.trim();
if (trimmed.length === 0 || aiCandidateMatches.length === 0) return;
if (trimmed.length === 0 || matches.length === 0) return;

const candidates = aiCandidateMatches
.map((m) => componentReferenceToCandidate(m.reference))
const candidates = matches
.map((m) => componentReferenceToCandidate(m.reference, m.source))
.filter((c): c is NonNullable<typeof c> => c !== null);

if (candidates.length === 0) return;

setRerankBaseMatches(aiCandidateMatches);
setRerankBaseMatches(matches);
setRerankedFor(trimmed);
rerank({ query: trimmed, candidates });
};

const handleSmartSearch = () => {
startAiSearch(aiCandidateMatches);
};

const handleDeepAiSearch = () => {
startAiSearch(deepAiCandidateMatches);
};

const handleSourceToggle = (sourceKey: string) => {
setDisabledSourceKeys((current) =>
current.includes(sourceKey)
Expand Down Expand Up @@ -1014,6 +1042,19 @@ export const DashboardComponentsV2View = () => {
>
{isReranking ? <Spinner size={16} /> : <Icon name="Sparkles" />}
</Button>
<Button
variant="outline"
onClick={handleDeepAiSearch}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

[MEDIUM] This "Deep AI search" Button gives no busy/in-progress feedback while isReranking, unlike the sibling Sparkles button which sets a dynamic aria-label. Add e.g. aria-label={isReranking ? "Deep AI search in progress" : "Deep AI search"} (and consider a busy indicator) so the disabled-while-reranking state is announced to assistive tech.

disabled={
isReranking ||
isEmpty ||
deepAiCandidateMatches.length === 0 ||
!isConfigured
}
title="Deep AI search — rerank all components in selected sources"
>
Deep AI search
</Button>
</InlineStack>
<SourceFilterBar
options={sourceFilterOptions}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,16 @@ import { ComponentSearchResults } from "./ComponentSearchResults";
export function ComponentSearchV2Content() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const { results, browseFolders, isLoading, canRerank, isReranking, rerank } =
useComponentSearchV2State(deferredQuery);
const {
results,
browseFolders,
isLoading,
canRerank,
canDeepRerank,
isReranking,
rerank,
deepRerank,
} = useComponentSearchV2State(deferredQuery);

const handleQueryChange = (event: ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
Expand Down Expand Up @@ -73,6 +81,16 @@ export function ComponentSearchV2Content() {
>
{isReranking ? <Spinner size={14} /> : <Icon name="Sparkles" />}
</Button>
<Button
variant="outline"
size="sm"
className="h-8 shrink-0 px-2"
title="Deep AI search — rerank all components"
onClick={deepRerank}
disabled={!canDeepRerank || isReranking}
>
Deep AI
</Button>
</InlineStack>
</BlockStack>
<ComponentSearchResults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {

import {
buildAiCandidateMatches,
buildDeepAiCandidateMatches,
buildLexicalMatches,
buildResultFolders,
buildResults,
Expand Down Expand Up @@ -243,6 +244,7 @@ describe("buildLexicalMatches / buildAiCandidateMatches", () => {

it("returns no AI candidates for an empty query", () => {
expect(buildAiCandidateMatches(index, "")).toEqual([]);
expect(buildDeepAiCandidateMatches(index, "")).toEqual([]);
});

it("falls back to a browse pool when literal search finds nothing", () => {
Expand Down Expand Up @@ -271,4 +273,22 @@ describe("buildLexicalMatches / buildAiCandidateMatches", () => {
"user-upload",
);
});

it("builds deep AI candidates from all searchable components", () => {
const broadIndex = buildSearchIndex([
...Array.from({ length: 100 }, (_, i) => ({
reference: ref(`train-${i}`, `train_${i}`),
source: source("standard"),
})),
{ reference: ref("z-upload", "upload_file"), source: USER_SOURCE },
]);

const candidates = buildDeepAiCandidateMatches(broadIndex, "train");

expect(candidates).toHaveLength(101);
expect(candidates.at(0)?.digest).toBe("train-0");
expect(candidates.map((candidate) => candidate.digest)).toContain(
"z-upload",
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ export interface ComponentSearchV2State {
browseFolders: UIComponentFolder[];
isLoading: boolean;
canRerank: boolean;
canDeepRerank: boolean;
isReranking: boolean;
rerank: () => void;
deepRerank: () => void;
}

export function registeredSource(
Expand Down Expand Up @@ -294,12 +296,13 @@ function appendUniqueMatches(
target: LexicalMatch[],
seenDigests: Set<string>,
matches: LexicalMatch[],
limit: number,
) {
for (const match of matches) {
if (seenDigests.has(match.digest)) continue;
seenDigests.add(match.digest);
target.push(match);
if (target.length >= AI_CANDIDATE_LIMIT) return;
if (target.length >= limit) return;
}
}

Expand Down Expand Up @@ -344,19 +347,56 @@ export function buildAiCandidateMatches(
limit: AI_LEXICAL_CANDIDATE_LIMIT,
minLength: 1,
}),
AI_CANDIDATE_LIMIT,
);

appendUniqueMatches(
candidates,
seenDigests,
buildSourceDiverseBrowseMatches(index),
AI_CANDIDATE_LIMIT,
);

const sortedIndex = [...index].sort((a, b) => a.name.localeCompare(b.name));
appendUniqueMatches(
candidates,
seenDigests,
sampleEvenly(sortedIndex, AI_CANDIDATE_LIMIT).map(indexEntryToLexicalMatch),
AI_CANDIDATE_LIMIT,
);

return candidates;
}

/**
* Explicit deep AI search candidate pool. Sends every searchable component,
* ordered with lexical hits first so truncating providers still see likely
* matches early.
*/
export function buildDeepAiCandidateMatches(
index: IndexEntry[],
trimmedQuery: string,
): LexicalMatch[] {
if (trimmedQuery.length === 0) return [];

const candidates: LexicalMatch[] = [];
const seenDigests = new Set<string>();
appendUniqueMatches(
candidates,
seenDigests,
lexicalSearch(index, trimmedQuery, {
limit: index.length,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

[HIGH] Deep search builds an unbounded candidate pool: lexicalSearch(index, ..., { limit: index.length }) followed by appending every remaining entry with Number.MAX_SAFE_INTEGER, then JSON.stringify-ing each candidate into a billed LLM rerank prompt. Output is bounded (scoreAllCandidates: false → ≤20) but input is not — there is no cap, truncation, or confirmation. Cap the deep pool at a high-but-finite N (and/or surface a confirmation when the pool is very large). At minimum, make Number.MAX_SAFE_INTEGER a deliberate, documented bound rather than effectively unlimited.

minLength: 1,
}),
Number.MAX_SAFE_INTEGER,
);

const sortedIndex = [...index].sort((a, b) => a.name.localeCompare(b.name));
appendUniqueMatches(
candidates,
seenDigests,
sortedIndex.map(indexEntryToLexicalMatch),
Number.MAX_SAFE_INTEGER,
);

return candidates;
Expand Down
38 changes: 29 additions & 9 deletions src/routes/v2/pages/Editor/hooks/useComponentSearchV2State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from "@/providers/ComponentLibraryProvider/libraries/storage";
import {
buildAiCandidateMatches,
buildDeepAiCandidateMatches,
buildLexicalMatches,
buildRerankScoreByDigest,
buildResultFolders,
Expand Down Expand Up @@ -175,6 +176,10 @@ export function useComponentSearchV2State(
const trimmedQuery = query.trim();
const lexicalMatches = buildLexicalMatches(index, trimmedQuery);
const aiCandidateMatches = buildAiCandidateMatches(index, trimmedQuery);
const deepAiCandidateMatches = buildDeepAiCandidateMatches(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This is an AI-generated code review comment.

[LOW] buildDeepAiCandidateMatches (full-index lexicalSearch + full [...index].sort()) runs every render though it is only needed at click time. React Compiler memoizes this, so it is not a correctness bug. Optionally, compute only .length/enabled-state during render and build the ordered pool lazily inside the deep handler.

index,
trimmedQuery,
);

const {
mutate,
Expand Down Expand Up @@ -202,23 +207,33 @@ export function useComponentSearchV2State(
? rerankedMatches(rerankData, rerankBaseMatches)
: lexicalMatches;

const rerank = () => {
if (!trimmedQuery || aiCandidateMatches.length === 0 || !isConfigured) {
return;
}
const startRerank = (
matches: LexicalMatch[],
{ scoreAllCandidates }: { scoreAllCandidates: boolean },
) => {
if (!trimmedQuery || matches.length === 0 || !isConfigured) return;

const candidates = aiCandidateMatches
.map((match) => componentReferenceToCandidate(match.reference))
const candidates = matches
.map((match) =>
componentReferenceToCandidate(match.reference, match.source),
)
.filter((candidate): candidate is NonNullable<typeof candidate> =>
Boolean(candidate),
);

if (candidates.length === 0) return;

setRerankBaseMatches(aiCandidateMatches);
setRerankBaseMatches(matches);
setRerankedFor(trimmedQuery);
// Score every candidate so each displayed result shows a relevance %.
mutate({ query: trimmedQuery, candidates, scoreAllCandidates: true });
mutate({ query: trimmedQuery, candidates, scoreAllCandidates });
};

const rerank = () => {
startRerank(aiCandidateMatches, { scoreAllCandidates: true });
};

const deepRerank = () => {
startRerank(deepAiCandidateMatches, { scoreAllCandidates: false });
};

const rerankScoreByDigest = buildRerankScoreByDigest(
Expand All @@ -243,7 +258,12 @@ export function useComponentSearchV2State(
isHydrating,
canRerank:
trimmedQuery.length > 0 && aiCandidateMatches.length > 0 && isConfigured,
canDeepRerank:
trimmedQuery.length > 0 &&
deepAiCandidateMatches.length > 0 &&
isConfigured,
isReranking,
rerank,
deepRerank,
};
}
43 changes: 37 additions & 6 deletions src/services/naturalLanguageComponentSearchService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,54 @@ describe("componentReferenceToCandidate", () => {
});
});

it("includes input/output names when present", () => {
it("includes input/output types, descriptions, and source when present", () => {
const ref: ComponentReference = {
digest: "abc",
spec: {
name: "train",
description: "",
inputs: [{ name: "dataset" }],
outputs: [{ name: "model" }],
inputs: [
{
name: "dataset",
type: "Dataset",
description: "Training data",
},
],
outputs: [
{
name: "model",
type: { Model: { format: "xgboost" } },
description: "Trained model",
},
],
implementation: { container: { image: "x" } },
},
};
expect(componentReferenceToCandidate(ref)).toEqual({
expect(
componentReferenceToCandidate(ref, {
kind: "published",
label: "Published",
id: "published",
}),
).toEqual({
id: "abc",
name: "train",
description: "",
inputs: ["dataset"],
outputs: ["model"],
source: { kind: "published", label: "Published" },
inputs: [
{
name: "dataset",
type: "Dataset",
description: "Training data",
},
],
outputs: [
{
name: "model",
type: '{"Model":{"format":"xgboost"}}',
description: "Trained model",
},
],
});
});
});
Expand Down
Loading
Loading