|
1 | | -import React, { useMemo } from "react"; |
| 1 | +import React, { useEffect, useMemo } from "react"; |
2 | 2 | import { useState } from "react"; |
3 | 3 | import { CheckIcon, ChevronDownIcon, ChevronUpicon, XIcon } from "./icons"; |
4 | 4 | import { |
|
7 | 7 | useModalState, |
8 | 8 | } from "../utils/hooks/modal-context"; |
9 | 9 | import { TwoThumbInputRange } from "react-two-thumb-input-range"; |
| 10 | +import { GetToolFunctionParamsReqPayload } from "trieve-ts-sdk"; |
10 | 11 |
|
11 | 12 | function getCssVar(varName: string) { |
12 | 13 | // Get the root element (or any other element that has the variable) |
@@ -210,6 +211,12 @@ export const Accordion = ({ |
210 | 211 | return count; |
211 | 212 | }, [activeTagFilters, activeRangeFilters]); |
212 | 213 |
|
| 214 | + useEffect(() => { |
| 215 | + if (numberOfSelectedFilters > 0) { |
| 216 | + setOpen(true); |
| 217 | + } |
| 218 | + }, [numberOfSelectedFilters]); |
| 219 | + |
213 | 220 | return ( |
214 | 221 | <div |
215 | 222 | className="trieve-accordion-container" |
@@ -496,11 +503,190 @@ export const FilterButton = ({ |
496 | 503 | ); |
497 | 504 | }; |
498 | 505 |
|
| 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 | + |
499 | 518 | 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 | + |
500 | 659 | return ( |
501 | 660 | <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> |
502 | 689 | <div className="trieve-filter-sidebar-section"> |
503 | | - <div className="">Filters</div> |
504 | 690 | {sections.map((section) => ( |
505 | 691 | <Accordion |
506 | 692 | key={section.key} |
|
0 commit comments