Extend time range (contributor submission) (#2484)

* added new options for time range; removed duplicated code

* refactor + remove unused code

---------

Co-authored-by: Zoltan Szabo <zoltan.szabo@eaudeweb.ro>
This commit is contained in:
pablodanswer 2024-09-17 15:36:25 -07:00 committed by GitHub
parent 18fbe9d7e8
commit a9427f190a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 68 additions and 514 deletions

View File

@ -1,453 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import { DocumentSet, Tag, ValidSources } from "@/lib/types";
import { SourceMetadata } from "@/lib/search/interfaces";
import {
FiBook,
FiBookmark,
FiCalendar,
FiFilter,
FiMap,
FiTag,
FiX,
} from "react-icons/fi";
import { DateRangePickerValue } from "@tremor/react";
import { listSourceMetadata } from "@/lib/sources";
import { SourceIcon } from "@/components/SourceIcon";
import { BasicClickable } from "@/components/BasicClickable";
import { ControlledPopup, DefaultDropdownElement } from "@/components/Dropdown";
import { getXDaysAgo } from "@/lib/dateUtils";
import { SourceSelectorProps } from "@/components/search/filtering/Filters";
import { containsObject, objectsAreEquivalent } from "@/lib/contains";
enum FilterType {
Source = "Source",
KnowledgeSet = "Knowledge Set",
TimeRange = "Time Range",
Tag = "Tag",
}
function SelectedBubble({
children,
onClick,
}: {
children: string | JSX.Element;
onClick: () => void;
}) {
return (
<div
className={
"flex text-xs cursor-pointer items-center border border-border " +
"py-1 rounded-lg px-2 w-fit select-none hover:bg-hover"
}
onClick={onClick}
>
{children}
<FiX className="ml-2" size={14} />
</div>
);
}
function SelectFilterType({
onSelect,
hasSources,
hasKnowledgeSets,
hasTags,
}: {
onSelect: (filterType: FilterType) => void;
hasSources: boolean;
hasKnowledgeSets: boolean;
hasTags: boolean;
}) {
return (
<div className="w-64">
{hasSources && (
<DefaultDropdownElement
key={FilterType.Source}
name={FilterType.Source}
icon={FiMap}
onSelect={() => onSelect(FilterType.Source)}
isSelected={false}
/>
)}
{hasKnowledgeSets && (
<DefaultDropdownElement
key={FilterType.KnowledgeSet}
name={FilterType.KnowledgeSet}
icon={FiBook}
onSelect={() => onSelect(FilterType.KnowledgeSet)}
isSelected={false}
/>
)}
{hasTags && (
<DefaultDropdownElement
key={FilterType.Tag}
name={FilterType.Tag}
icon={FiTag}
onSelect={() => onSelect(FilterType.Tag)}
isSelected={false}
/>
)}
<DefaultDropdownElement
key={FilterType.TimeRange}
name={FilterType.TimeRange}
icon={FiCalendar}
onSelect={() => onSelect(FilterType.TimeRange)}
isSelected={false}
/>
</div>
);
}
function SourcesSection({
sources,
selectedSources,
onSelect,
}: {
sources: SourceMetadata[];
selectedSources: string[];
onSelect: (source: SourceMetadata) => void;
}) {
return (
<div className="w-64">
{sources.map((source) => (
<DefaultDropdownElement
key={source.internalName}
name={source.displayName}
icon={source.icon}
onSelect={() => onSelect(source)}
isSelected={selectedSources.includes(source.internalName)}
includeCheckbox
/>
))}
</div>
);
}
function KnowledgeSetsSection({
documentSets,
selectedDocumentSets,
onSelect,
}: {
documentSets: DocumentSet[];
selectedDocumentSets: string[];
onSelect: (documentSetName: string) => void;
}) {
return (
<div className="w-64">
{documentSets.map((documentSet) => (
<DefaultDropdownElement
key={documentSet.name}
name={documentSet.name}
icon={FiBookmark}
onSelect={() => onSelect(documentSet.name)}
isSelected={selectedDocumentSets.includes(documentSet.name)}
includeCheckbox
/>
))}
</div>
);
}
const LAST_30_DAYS = "Last 30 days";
const LAST_7_DAYS = "Last 7 days";
const TODAY = "Today";
function TimeRangeSection({
selectedTimeRange,
onSelect,
}: {
selectedTimeRange: string | null;
onSelect: (timeRange: DateRangePickerValue) => void;
}) {
return (
<div className="w-64">
<DefaultDropdownElement
key={LAST_30_DAYS}
name={LAST_30_DAYS}
onSelect={() =>
onSelect({
to: new Date(),
from: getXDaysAgo(30),
selectValue: LAST_30_DAYS,
})
}
isSelected={selectedTimeRange === LAST_30_DAYS}
/>
<DefaultDropdownElement
key={LAST_7_DAYS}
name={LAST_7_DAYS}
onSelect={() =>
onSelect({
to: new Date(),
from: getXDaysAgo(7),
selectValue: LAST_7_DAYS,
})
}
isSelected={selectedTimeRange === LAST_7_DAYS}
/>
<DefaultDropdownElement
key={TODAY}
name={TODAY}
onSelect={() =>
onSelect({
to: new Date(),
from: getXDaysAgo(1),
selectValue: TODAY,
})
}
isSelected={selectedTimeRange === TODAY}
/>
</div>
);
}
function TagsSection({
availableTags,
selectedTags,
onSelect,
}: {
availableTags: Tag[];
selectedTags: Tag[];
onSelect: (tag: Tag) => void;
}) {
const [filterValue, setFilterValue] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
const filterValueLower = filterValue.toLowerCase();
const filteredTags = filterValueLower
? availableTags.filter(
(tags) =>
tags.tag_value.toLowerCase().startsWith(filterValueLower) ||
tags.tag_key.toLowerCase().startsWith(filterValueLower)
)
: availableTags;
return (
<div className="w-96">
<div className="max-h-48 overflow-y-auto">
{filteredTags.length > 0 ? (
filteredTags.map((tag) => (
<DefaultDropdownElement
key={tag.tag_key + tag.tag_value}
name={
<div className="max-w-full break-all line-clamp-1 text-ellipsis">
{tag.tag_key}
<b>=</b>
{tag.tag_value}
</div>
}
onSelect={() => onSelect(tag)}
isSelected={selectedTags.includes(tag)}
includeCheckbox
/>
))
) : (
<div className="text-sm px-2 py-2">No matching tags found</div>
)}
</div>
<div className="mx-2 mb-2 pt-2 border-t border-border">
<input
ref={inputRef}
className="w-full border border-border py-0.5 px-2 rounded text-sm h-8 "
placeholder="Find a tag"
value={filterValue}
onChange={(event) => setFilterValue(event.target.value)}
/>
</div>
</div>
);
}
export function ChatFilters({
timeRange,
setTimeRange,
selectedSources,
setSelectedSources,
selectedDocumentSets,
setSelectedDocumentSets,
selectedTags,
setSelectedTags,
availableDocumentSets,
existingSources,
availableTags,
}: SourceSelectorProps) {
const [filtersOpen, setFiltersOpen] = useState(false);
const handleFiltersToggle = (value: boolean) => {
setSelectedFilterType(null);
setFiltersOpen(value);
};
const [selectedFilterType, setSelectedFilterType] =
useState<FilterType | null>(null);
const handleSourceSelect = (source: SourceMetadata) => {
setSelectedSources((prev: SourceMetadata[]) => {
const prevSourceNames = prev.map((source) => source.internalName);
if (prevSourceNames.includes(source.internalName)) {
return prev.filter((s) => s.internalName !== source.internalName);
} else {
return [...prev, source];
}
});
};
const handleDocumentSetSelect = (documentSetName: string) => {
setSelectedDocumentSets((prev: string[]) => {
if (prev.includes(documentSetName)) {
return prev.filter((s) => s !== documentSetName);
} else {
return [...prev, documentSetName];
}
});
};
const handleTagToggle = (tag: Tag) => {
setSelectedTags((prev) => {
if (containsObject(prev, tag)) {
return prev.filter((t) => !objectsAreEquivalent(t, tag));
} else {
return [...prev, tag];
}
});
};
const allSources = listSourceMetadata();
const availableSources = allSources.filter((source) =>
existingSources.includes(source.internalName)
);
let popupDisplay = null;
if (selectedFilterType === FilterType.Source) {
popupDisplay = (
<SourcesSection
sources={availableSources}
selectedSources={selectedSources.map((source) => source.internalName)}
onSelect={handleSourceSelect}
/>
);
} else if (selectedFilterType === FilterType.KnowledgeSet) {
popupDisplay = (
<KnowledgeSetsSection
documentSets={availableDocumentSets}
selectedDocumentSets={selectedDocumentSets}
onSelect={handleDocumentSetSelect}
/>
);
} else if (selectedFilterType === FilterType.TimeRange) {
popupDisplay = (
<TimeRangeSection
selectedTimeRange={timeRange?.selectValue || null}
onSelect={(timeRange) => {
setTimeRange(timeRange);
handleFiltersToggle(!filtersOpen);
}}
/>
);
} else if (selectedFilterType === FilterType.Tag) {
popupDisplay = (
<TagsSection
availableTags={availableTags}
selectedTags={selectedTags}
onSelect={handleTagToggle}
/>
);
} else {
popupDisplay = (
<SelectFilterType
onSelect={(filterType) => setSelectedFilterType(filterType)}
hasSources={availableSources.length > 0}
hasKnowledgeSets={availableDocumentSets.length > 0}
hasTags={availableTags.length > 0}
/>
);
}
return (
<div className="flex">
<ControlledPopup
isOpen={filtersOpen}
setIsOpen={handleFiltersToggle}
popupContent={popupDisplay}
>
<div className="flex">
<BasicClickable onClick={() => handleFiltersToggle(!filtersOpen)}>
<div className="flex text-xs">
<FiFilter className="my-auto mr-1" /> Filter
</div>
</BasicClickable>
</div>
</ControlledPopup>
<div className="flex ml-4">
{((timeRange && timeRange.selectValue !== undefined) ||
selectedSources.length > 0 ||
selectedDocumentSets.length > 0) && (
<p className="text-xs my-auto mr-1">Currently applied:</p>
)}
<div className="flex flex-wrap gap-x-2">
{timeRange && timeRange.selectValue && (
<SelectedBubble onClick={() => setTimeRange(null)}>
<div className="flex">{timeRange.selectValue}</div>
</SelectedBubble>
)}
{existingSources.length > 0 &&
selectedSources.map((source) => (
<SelectedBubble
key={source.internalName}
onClick={() => handleSourceSelect(source)}
>
<>
<SourceIcon sourceType={source.internalName} iconSize={16} />
<span className="ml-2">{source.displayName}</span>
</>
</SelectedBubble>
))}
{selectedDocumentSets.length > 0 &&
selectedDocumentSets.map((documentSetName) => (
<SelectedBubble
key={documentSetName}
onClick={() => handleDocumentSetSelect(documentSetName)}
>
<>
<div>
<FiBookmark />
</div>
<span className="ml-2">{documentSetName}</span>
</>
</SelectedBubble>
))}
{selectedTags.length > 0 &&
selectedTags.map((tag) => (
<SelectedBubble
key={tag.tag_key + tag.tag_value}
onClick={() => handleTagToggle(tag)}
>
<>
<div>
<FiTag />
</div>
<span className="ml-1 max-w-[100px] text-ellipsis line-clamp-1 break-all">
{tag.tag_key}
<b>=</b>
{tag.tag_value}
</span>
</>
</SelectedBubble>
))}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import { getXDaysAgo, getXYearsAgo } from "@/lib/dateUtils";
export const timeRangeValues = [
{ label: "Last 2 years", value: getXYearsAgo(2) },
{ label: "Last year", value: getXYearsAgo(1) },
{ label: "Last 30 days", value: getXDaysAgo(30) },
{ label: "Last 7 days", value: getXDaysAgo(7) },
{ label: "Today", value: getXDaysAgo(1) },
];

View File

@ -0,0 +1,32 @@
import { DefaultDropdownElement } from "../Dropdown";
export function TimeRangeSelector({
value,
onValueChange,
className,
timeRangeValues,
}: {
value: any;
onValueChange: any;
className: any;
timeRangeValues: { label: string; value: Date }[];
}) {
return (
<div className={className}>
{timeRangeValues.map((timeRangeValue) => (
<DefaultDropdownElement
key={timeRangeValue.label}
name={timeRangeValue.label}
onSelect={() =>
onValueChange({
to: new Date(),
from: timeRangeValue.value,
selectValue: timeRangeValue.label,
})
}
isSelected={value?.selectValue === timeRangeValue.label}
/>
))}
</div>
);
}

View File

@ -1,11 +1,8 @@
import { getXDaysAgo } from "@/lib/dateUtils";
import { DateRangePickerValue } from "@tremor/react";
import { FiCalendar, FiChevronDown, FiXCircle } from "react-icons/fi";
import { CustomDropdown, DefaultDropdownElement } from "../Dropdown";
export const LAST_30_DAYS = "Last 30 days";
export const LAST_7_DAYS = "Last 7 days";
export const TODAY = "Today";
import { CustomDropdown } from "../Dropdown";
import { timeRangeValues } from "@/app/config/timeRange";
import { TimeRangeSelector } from "@/components/filters/TimeRangeSelector";
export function DateRangeSelector({
value,
@ -20,59 +17,23 @@ export function DateRangeSelector({
<div>
<CustomDropdown
dropdown={
<div
<TimeRangeSelector
value={value}
className={`
border
border-border
bg-background
rounded-lg
flex
flex-col
w-64
max-h-96
overflow-y-auto
flex
overscroll-contain`}
>
<DefaultDropdownElement
key={LAST_30_DAYS}
name={LAST_30_DAYS}
onSelect={() =>
onValueChange({
to: new Date(),
from: getXDaysAgo(30),
selectValue: LAST_30_DAYS,
})
}
isSelected={value?.selectValue === LAST_30_DAYS}
/>
<DefaultDropdownElement
key={LAST_7_DAYS}
name={LAST_7_DAYS}
onSelect={() =>
onValueChange({
to: new Date(),
from: getXDaysAgo(7),
selectValue: LAST_7_DAYS,
})
}
isSelected={value?.selectValue === LAST_7_DAYS}
/>
<DefaultDropdownElement
key={TODAY}
name={TODAY}
onSelect={() =>
onValueChange({
to: new Date(),
from: getXDaysAgo(1),
selectValue: TODAY,
})
}
isSelected={value?.selectValue === TODAY}
/>
</div>
border
border-border
bg-background
rounded-lg
flex
flex-col
w-64
max-h-96
overflow-y-auto
flex
overscroll-contain`}
timeRangeValues={timeRangeValues}
onValueChange={onValueChange}
/>
}
>
<div

View File

@ -3,7 +3,6 @@
import {
DanswerDocument,
DocumentRelevance,
Relevance,
SearchDanswerDocument,
} from "@/lib/search/interfaces";
import { DocumentFeedbackBlock } from "./DocumentFeedbackBlock";
@ -12,11 +11,10 @@ import { PopupSpec } from "../admin/connectors/Popup";
import { DocumentUpdatedAtBadge } from "./DocumentUpdatedAtBadge";
import { SourceIcon } from "../SourceIcon";
import { MetadataBadge } from "../MetadataBadge";
import { BookIcon, CheckmarkIcon, LightBulbIcon, XIcon } from "../icons/icons";
import { BookIcon, LightBulbIcon } from "../icons/icons";
import { FaStar } from "react-icons/fa";
import { FiTag } from "react-icons/fi";
import { DISABLE_LLM_DOC_RELEVANCE } from "@/lib/constants";
import { SettingsContext } from "../settings/SettingsProvider";
import { CustomTooltip, TooltipGroup } from "../tooltip/CustomTooltip";
import { WarningCircle } from "@phosphor-icons/react";

View File

@ -5,6 +5,13 @@ export function getXDaysAgo(daysAgo: number) {
return daysAgoDate;
}
export function getXYearsAgo(yearsAgo: number) {
const today = new Date();
const yearsAgoDate = new Date(today);
yearsAgoDate.setFullYear(yearsAgoDate.getFullYear() - yearsAgo);
return yearsAgoDate;
}
export const timestampToDateString = (timestamp: string) => {
const date = new Date(timestamp);
const year = date.getFullYear();