Skip to content

Commit b34678d

Browse files
densumeshcdxker
authored andcommitted
feature: add ai filter bar
1 parent 9d2587d commit b34678d

8 files changed

Lines changed: 240 additions & 5 deletions

File tree

clients/search-component/src/TrieveModal/FilterSidebarComponents.tsx

Lines changed: 188 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo } from "react";
1+
import React, { useEffect, useMemo } from "react";
22
import { useState } from "react";
33
import { CheckIcon, ChevronDownIcon, ChevronUpicon, XIcon } from "./icons";
44
import {
@@ -7,6 +7,7 @@ import {
77
useModalState,
88
} from "../utils/hooks/modal-context";
99
import { TwoThumbInputRange } from "react-two-thumb-input-range";
10+
import { GetToolFunctionParamsReqPayload } from "trieve-ts-sdk";
1011

1112
function getCssVar(varName: string) {
1213
// Get the root element (or any other element that has the variable)
@@ -210,6 +211,12 @@ export const Accordion = ({
210211
return count;
211212
}, [activeTagFilters, activeRangeFilters]);
212213

214+
useEffect(() => {
215+
if (numberOfSelectedFilters > 0) {
216+
setOpen(true);
217+
}
218+
}, [numberOfSelectedFilters]);
219+
213220
return (
214221
<div
215222
className="trieve-accordion-container"
@@ -496,11 +503,190 @@ export const FilterButton = ({
496503
);
497504
};
498505

506+
const SendIcon = () => {
507+
return (
508+
<svg fill="currentColor" strokeWidth="0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" height="1em" width="1em" style={{overflow: "visible", color: "currentColor"}}><path d="m1 1.91.78-.41L15 7.449v.95L1.78 14.33 1 13.91 2.583 8 1 1.91ZM3.612 8.5 2.33 13.13 13.5 7.9 2.33 2.839l1.282 4.6L9 7.5v1H3.612Z"></path></svg>
509+
);
510+
};
511+
512+
const LoadingIcon = () => {
513+
return (
514+
<svg fill="currentColor" stroke-width="0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" height="1em" width="1em" style={{overflow: "visible", color: "currentColor"}}><path d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 0 0-94.3-139.9 437.71 437.71 0 0 0-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"></path></svg>
515+
);
516+
};
517+
499518
export const FilterSidebar = ({ sections }: FilterSidebarProps) => {
519+
const [sidebarText, setSidebarText] = useState("");
520+
const [isLoading, setIsLoading] = useState(false);
521+
const { trieveSDK, setSelectedSidebarFilters } = useModalState();
522+
523+
const handleSubmit = async() => {
524+
if (sidebarText.trim() === "") return;
525+
526+
try {
527+
setIsLoading(true);
528+
const toolCallPromises = sections.map((section) => {
529+
let toolCallData: GetToolFunctionParamsReqPayload;
530+
531+
if (section.filterType === "match_any" || section.filterType === "match_all") {
532+
toolCallData = {
533+
user_message_text: sidebarText,
534+
tool_function: {
535+
name: "infer_filters",
536+
description: `
537+
Analyze the user's query to determine relevant filters for "${section.title}".
538+
${section.selectionType === "single"
539+
? "Select only the single most relevant filter that best matches the query intent."
540+
: "Select all filters that directly relate to the query's explicit or implied needs."}
541+
If the query contains no clear relation to these filters, return all as false.
542+
Consider synonyms and related concepts when matching filters to query terms.
543+
`,
544+
parameters: section.options.map((option) => ({
545+
name: option.tag,
546+
parameter_type: "boolean",
547+
description: `${option.label}: ${option.description}
548+
Select this filter only if the user's query explicitly mentions or strongly implies a need for content related to this specific category.`,
549+
})),
550+
},
551+
};
552+
return trieveSDK.getToolCallFunctionParams(toolCallData);
553+
} else if (section.filterType === "range" && section.selectionType === "range") {
554+
toolCallData = {
555+
user_message_text: sidebarText,
556+
tool_function: {
557+
name: "infer_filters",
558+
description: `
559+
Analyze the user's query for numerical range preferences related to "${section.title}".
560+
Valid range: ${section.options[0].range?.min} to ${section.options[0].range?.max}.
561+
If the query specifies or implies a numerical range (e.g., "under 50", "between 100-200", "at least 300"):
562+
- Extract the minimum and maximum values that satisfy the user's intent
563+
- Keep values within allowed bounds
564+
If no range is specified or implied, don't apply this filter (return null for both values).
565+
Interpret qualitative terms appropriately (e.g., "affordable" = lower range, "premium" = higher range).
566+
`,
567+
parameters: [
568+
{
569+
name: "min_value",
570+
parameter_type: "number",
571+
description: `Minimum value for ${section.title}.
572+
Extract from explicit values ("over 50") or implied ranges ("affordable").
573+
Return null if the query doesn't specify or imply a minimum.`,
574+
},
575+
{
576+
name: "max_value",
577+
parameter_type: "number",
578+
description: `Maximum value for ${section.title}.
579+
Extract from explicit values ("under 100") or implied ranges ("budget-friendly").
580+
Return null if the query doesn't specify or imply a maximum.`,
581+
},
582+
],
583+
},
584+
};
585+
return trieveSDK.getToolCallFunctionParams(toolCallData);
586+
}
587+
588+
return Promise.resolve(null);
589+
});
590+
591+
const toolCalls = await Promise.all(toolCallPromises);
592+
593+
setSelectedSidebarFilters((prev) => {
594+
if (prev.length !== 0) {
595+
return prev.map((filter, index) => {
596+
const toolCall = toolCalls[index];
597+
598+
if (!toolCall) return filter;
599+
if (sections[index].filterType === "range" && sections[index].selectionType === "range") {
600+
const parameters = toolCall.parameters as { min_value: number, max_value: number };
601+
const minValue = Math.max(parameters.min_value, sections[index].options[0].range?.min || 0);
602+
const maxValue = Math.min(parameters.max_value, sections[index].options[0].range?.max || Infinity);
603+
if (filter.section.key === sections[index].key) {
604+
return { ...filter, range: { min: minValue, max: maxValue } };
605+
}
606+
} else {
607+
const selectedTags = Object.entries(toolCall.parameters as Record<string, boolean>)
608+
.filter(([, isSelected]) => isSelected)
609+
.map(([tag]) => tag);
610+
611+
if (filter.section.key === sections[index].key) {
612+
return { ...filter, tags: selectedTags };
613+
}
614+
}
615+
616+
return filter;
617+
});
618+
} else {
619+
return sections.map((section, index) => {
620+
const toolCall = toolCalls[index];
621+
622+
if (!toolCall) return { section: section };
623+
if (sections[index].filterType === "range" && sections[index].selectionType === "range") {
624+
const parameters = toolCall.parameters as { min_value: number, max_value: number };
625+
const minValue = Math.max(parameters.min_value, sections[index].options[0].range?.min || 0);
626+
const maxValue = Math.min(parameters.max_value, sections[index].options[0].range?.max || Infinity);
627+
if (section.key === sections[index].key) {
628+
return { section, range: { min: minValue, max: maxValue } };
629+
}
630+
} else {
631+
const selectedTags = Object.entries(toolCall.parameters as Record<string, boolean>)
632+
.filter(([, isSelected]) => isSelected)
633+
.map(([tag]) => tag);
634+
635+
if (section.key === sections[index].key) {
636+
return { section, tags: selectedTags };
637+
}
638+
}
639+
640+
return { section: section };
641+
})
642+
}
643+
});
644+
} catch (error) {
645+
console.error("Error processing filters:", error);
646+
} finally {
647+
setIsLoading(false);
648+
}
649+
};
650+
651+
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
652+
if (event.key === 'Enter' && !event.shiftKey) {
653+
event.preventDefault();
654+
handleSubmit();
655+
}
656+
};
657+
658+
500659
return (
501660
<aside className="trieve-filter-sidebar">
661+
<div className="trieve-filter-sidebar-textarea-container tv-p-2.5 tv-border-b tv-border-gray-200">
662+
<div className="tv-flex tv-flex-col tv-mb-2">
663+
<div className="tv-text-sm tv-text-black-500">
664+
Choose your filters with AI
665+
</div>
666+
<div className="tv-relative tv-flex tv-items-center">
667+
<textarea
668+
value={sidebarText}
669+
onChange={(e) => setSidebarText(e.target.value)}
670+
onKeyDown={handleKeyDown}
671+
placeholder="blue or red under 100"
672+
className="tv-w-full tv-min-h-[40px] tv-p-1 tv-pr-10 tv-border tv-border-gray-300 tv-rounded-md focus:tv-outline-none focus:tv-ring-1 focus:tv-ring-blue-500 focus:tv-border-blue-500 tv-resize-none"
673+
/>
674+
{isLoading ? (
675+
<div className="tv-absolute tv-right-2 tv-p-2 tv-h-[40px] tv-flex tv-items-center tv-justify-center tv-text-gray-500 tv-animate-spin">
676+
<LoadingIcon />
677+
</div>
678+
) : (
679+
<button
680+
onClick={handleSubmit}
681+
className="tv-absolute tv-right-2 tv-p-2 tv-h-[40px] tv-flex tv-items-center tv-justify-center tv-text-gray-500 hover:tv-text-blue-500 focus:tv-outline-none"
682+
>
683+
<SendIcon />
684+
</button>
685+
)}
686+
</div>
687+
</div>
688+
</div>
502689
<div className="trieve-filter-sidebar-section">
503-
<div className="">Filters</div>
504690
{sections.map((section) => (
505691
<Accordion
506692
key={section.key}

clients/search-component/src/utils/hooks/modal-context.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -448,9 +448,7 @@ const ModalProvider = ({
448448
) {
449449
const url = new URL(window.location.href);
450450
url.searchParams.set("q", query);
451-
if (selectedSidebarFilters.length > 0) {
452-
url.searchParams.set("filters", JSON.stringify(selectedSidebarFilters));
453-
}
451+
url.searchParams.set("filters", JSON.stringify(selectedSidebarFilters));
454452
window.history.replaceState({}, "", url.toString());
455453
}
456454

clients/trieve-shopify-extension/app/components/FIlterBlock.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,17 @@ export function FilterBlock({ section, onChange, onDelete }: {
167167
helpText="Visible label to users"
168168
/>
169169
</FormLayout.Group>
170+
171+
<FormLayout.Group condensed>
172+
<TextField
173+
label="Description"
174+
value={option.description}
175+
onChange={(value) => handleOptionChange(index, "description", value)}
176+
autoComplete="off"
177+
helpText="Description of this option. This will used to help the AI filter the options."
178+
multiline={2}
179+
/>
180+
</FormLayout.Group>
170181

171182

172183
{section.selectionType === 'range' && (
@@ -228,6 +239,17 @@ export function FilterBlock({ section, onChange, onDelete }: {
228239
/>
229240
</FormLayout.Group>
230241

242+
<FormLayout.Group condensed>
243+
<TextField
244+
label="Description"
245+
value={newOption.description}
246+
onChange={(value) => setNewOption({...newOption, description: value})}
247+
autoComplete="off"
248+
helpText="Description of this option. This will used to help the AI filter the options."
249+
multiline={2}
250+
/>
251+
</FormLayout.Group>
252+
231253
{section.selectionType === 'range' && (
232254
<FormLayout.Group condensed>
233255
<TextField

clients/trieve-shopify-extension/app/routes/app._dashboard.filters.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface TagProp {
1616
min?: number;
1717
max?: number;
1818
};
19+
description?: string;
1920
}
2021

2122
export interface FilterSidebarSection {

clients/ts-sdk/openapi.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21076,6 +21076,10 @@
2107621076
"TagProp": {
2107721077
"type": "object",
2107821078
"properties": {
21079+
"description": {
21080+
"type": "string",
21081+
"nullable": true
21082+
},
2107921083
"label": {
2108021084
"type": "string",
2108121085
"nullable": true

clients/ts-sdk/src/types.gen.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4261,6 +4261,7 @@ export type SuggestedQueriesResponse = {
42614261
};
42624262

42634263
export type TagProp = {
4264+
description?: (string) | null;
42644265
label?: (string) | null;
42654266
range?: ((RangeSliderConfig) | null);
42664267
tag?: (string) | null;

frontends/dashboard/src/components/FilterSidebarBuilder.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,27 @@ const FilterSidebarBuilder = () => {
323323
/>
324324
</div>
325325
</Show>
326+
<div>
327+
<label class="block text-sm">
328+
Description (This will be used to help the AI filter the
329+
options)
330+
</label>
331+
<textarea
332+
placeholder="Description"
333+
value={option.description || ""}
334+
onInput={(e) => {
335+
setSections(
336+
props.index,
337+
"options",
338+
optionIndex(),
339+
"description",
340+
e.currentTarget.value,
341+
);
342+
updateSerpPageOptions();
343+
}}
344+
class="block w-full rounded border border-neutral-300 px-3 py-1.5 shadow-sm placeholder:text-neutral-400 focus:outline-magenta-500 sm:text-sm sm:leading-6"
345+
/>
346+
</div>
326347
</div>
327348
</div>
328349
)}

server/src/handlers/page_handler.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ pub struct TagProp {
206206
pub label: Option<String>,
207207
#[serde(skip_serializing_if = "Option::is_none")]
208208
pub range: Option<RangeSliderConfig>,
209+
#[serde(skip_serializing_if = "Option::is_none")]
210+
pub description: Option<String>,
209211
}
210212

211213
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema, Default)]

0 commit comments

Comments
 (0)