diff --git a/clients/ts-sdk/openapi.json b/clients/ts-sdk/openapi.json index f7dfa5686..c81759075 100644 --- a/clients/ts-sdk/openapi.json +++ b/clients/ts-sdk/openapi.json @@ -10198,6 +10198,11 @@ "field" ], "properties": { + "boolean": { + "type": "boolean", + "description": "Boolean is a true false value for a field. This only works for boolean fields. You can specify this if you want values to be true or false.", + "nullable": true + }, "date_range": { "allOf": [ { diff --git a/clients/ts-sdk/package.json b/clients/ts-sdk/package.json index a9f0d544f..e8ec57404 100644 --- a/clients/ts-sdk/package.json +++ b/clients/ts-sdk/package.json @@ -6,7 +6,7 @@ "files": [ "dist" ], - "version": "0.0.37", + "version": "0.0.38", "license": "MIT", "scripts": { "lint": "eslint 'src/**/*.ts'", diff --git a/clients/ts-sdk/src/types.gen.ts b/clients/ts-sdk/src/types.gen.ts index 60b4eb095..92f972c93 100644 --- a/clients/ts-sdk/src/types.gen.ts +++ b/clients/ts-sdk/src/types.gen.ts @@ -1338,6 +1338,10 @@ export type EventTypesFilter = 'add_to_cart' | 'purchase' | 'view' | 'click' | ' * FieldCondition is a JSON object which can be used to filter chunks by a field. This is useful for when you want to filter chunks by arbitrary metadata. To access fields inside of the metadata that you provide with the card, prefix the field name with `metadata.`. */ export type FieldCondition = { + /** + * Boolean is a true false value for a field. This only works for boolean fields. You can specify this if you want values to be true or false. + */ + boolean?: (boolean) | null; date_range?: ((DateRange) | null); /** * Field is the name of the field to filter on. Commonly used fields are `timestamp`, `link`, `tag_set`, `location`, `num_value`, `group_ids`, and `group_tracking_ids`. The field value will be used to check for an exact substring match on the metadata values for each existing chunk. This is useful for when you want to filter chunks by arbitrary metadata. To access fields inside of the metadata that you provide with the card, prefix the field name with `metadata.`. diff --git a/frontends/chat/src/components/FilterModal.tsx b/frontends/chat/src/components/FilterModal.tsx index 8f7a368e0..e659aa0d2 100644 --- a/frontends/chat/src/components/FilterModal.tsx +++ b/frontends/chat/src/components/FilterModal.tsx @@ -11,7 +11,7 @@ import { } from "solid-js"; import { UserContext } from "./contexts/UserContext"; -export interface Filter { +export interface FieldFilter { field: string; geo_radius?: { center?: { @@ -20,13 +20,15 @@ export interface Filter { } | null; radius?: number | null; } | null; - match?: (string | number)[] | null; + match_any?: (string | number)[] | null; + match_all?: (string | number)[] | null; range?: { gte?: number | null; lte?: number | null; gt?: number | null; lt?: number | null; } | null; + boolean?: boolean | null; date_range?: { gte?: string | null; lte?: string | null; @@ -35,6 +37,13 @@ export interface Filter { } | null; } +export interface HasIdFilter { + ids: string[] | null; + tracking_ids: string[] | null; +} + +export type Filter = FieldFilter | HasIdFilter; + export interface Filters { must: Filter[]; must_not: Filter[]; @@ -265,63 +274,153 @@ export const FilterModal = (props: FilterModalProps) => { ); }; +export function filterIsHasIdFilter(filter: Filter): filter is HasIdFilter { + return ( + filter != null && + ((filter as HasIdFilter)["ids"] != null || + (filter as HasIdFilter)["tracking_ids"] != null) + ); +} + +export function filterAsHasIdFilter(filter: Filter): HasIdFilter | null { + return filterIsHasIdFilter(filter) ? filter : null; +} + +export function filterIsFieldFilter(filter: Filter): filter is FieldFilter { + return filter != null && !filterIsHasIdFilter(filter); +} + +export function filterAsFieldFilter(filter: Filter): FieldFilter | null { + return filterIsFieldFilter(filter) ? filter : null; +} + export interface FilterItemProps { - initialFilter?: Filter; + initialFilter: Filter; onFilterChange: (filter: Filter) => void; } export const FilterItem = (props: FilterItemProps) => { - const [curFilter, setCurFilter] = createSignal( - props.initialFilter ?? defaultFilter, - ); + const [curFilter, setCurFilter] = createSignal(props.initialFilter); + + let initialTempFilterMode = "match_any"; + let initialTempFilterField = "tag_set"; + + if (filterIsFieldFilter(props.initialFilter) && props.initialFilter != null) { + const fieldMode: ["geo_radius", "range", "boolean", "date_range"] = [ + "geo_radius", + "range", + "boolean", + "date_range", + ]; + + for (const attempt of fieldMode) + if (props.initialFilter[attempt] != null) { + initialTempFilterMode = attempt; + break; + } + } else if ( + filterIsHasIdFilter(props.initialFilter) && + props.initialFilter != null + ) { + const fieldMode: ["tracking_ids", "ids"] = ["tracking_ids", "ids"]; + + for (const attempt of fieldMode) + if (props.initialFilter[attempt] != null) { + initialTempFilterMode = "has_id_filter"; + initialTempFilterField = attempt; + break; + } + } + const [tempFilterMode, setTempFilterMode] = createSignal( - props.initialFilter?.geo_radius - ? "geo_radius" - : props.initialFilter?.range - ? "range" - : props.initialFilter?.date_range - ? "date_range" - : props.initialFilter?.match - ? "match" - : "match", + initialTempFilterMode, ); + const [tempFilterField, setTempFilterField] = createSignal( - props.initialFilter?.field ?? "", + filterAsFieldFilter(props.initialFilter)?.field ?? initialTempFilterField, ); const [location, setLocation] = createSignal({ - lat: props.initialFilter?.geo_radius?.center?.lat ?? null, - lon: props.initialFilter?.geo_radius?.center?.lon ?? null, - radius: props.initialFilter?.geo_radius?.radius ?? null, + lat: + filterAsFieldFilter(props.initialFilter)?.geo_radius?.center?.lat ?? null, + lon: + filterAsFieldFilter(props.initialFilter)?.geo_radius?.center?.lon ?? null, + radius: + filterAsFieldFilter(props.initialFilter)?.geo_radius?.radius ?? null, }); const [range, setRange] = createSignal({ - gt: props.initialFilter?.range?.gt ?? null, - lt: props.initialFilter?.range?.lt ?? null, - gte: props.initialFilter?.range?.gte ?? null, - lte: props.initialFilter?.range?.lte ?? null, + gt: filterAsFieldFilter(props.initialFilter)?.range?.gt ?? null, + lt: filterAsFieldFilter(props.initialFilter)?.range?.lt ?? null, + gte: filterAsFieldFilter(props.initialFilter)?.range?.gte ?? null, + lte: filterAsFieldFilter(props.initialFilter)?.range?.lte ?? null, }); const [dateRange, setDateRange] = createSignal({ - gt: props.initialFilter?.date_range?.gt ?? null, - lt: props.initialFilter?.date_range?.lt ?? null, - gte: props.initialFilter?.date_range?.gte ?? null, - lte: props.initialFilter?.date_range?.lte ?? null, + gt: filterAsFieldFilter(props.initialFilter)?.date_range?.gt ?? null, + lt: filterAsFieldFilter(props.initialFilter)?.date_range?.lt ?? null, + gte: filterAsFieldFilter(props.initialFilter)?.date_range?.gte ?? null, + lte: filterAsFieldFilter(props.initialFilter)?.date_range?.lte ?? null, }); - const [match, setMatch] = createSignal<(string | number)[] | null>( - props.initialFilter?.match ?? null, + const [boolean, setBoolean] = createSignal( + filterAsFieldFilter(props.initialFilter)?.boolean ?? null, + ); + + const [matchAny, setMatchAny] = createSignal<(string | number)[] | null>( + filterAsFieldFilter(props.initialFilter)?.match_any ?? null, + ); + + const [matchAll, setMatchAll] = createSignal<(string | number)[] | null>( + filterAsFieldFilter(props.initialFilter)?.match_all ?? null, + ); + + const [idFilterText, setIdFilterText] = createSignal( + filterAsHasIdFilter(props.initialFilter)?.tracking_ids ?? + filterAsHasIdFilter(props.initialFilter)?.ids ?? + [], ); createEffect(() => { - const changedMode = tempFilterMode(); - setCurFilter({ - field: tempFilterField(), - [changedMode]: {}, - }); + const changedField = tempFilterField(); + const curFieldFilter = filterAsFieldFilter(props.initialFilter); + + if (changedField === "ids") { + setCurFilter({ + ids: idFilterText(), + tracking_ids: null, + } as HasIdFilter); + } else if (changedField === "tracking_ids") { + setCurFilter({ + ids: null, + tracking_ids: idFilterText(), + } as HasIdFilter); + } else { + if (curFieldFilter?.match_all?.length) { + setTempFilterMode("match_all"); + } else if (curFieldFilter?.match_any?.length) { + setTempFilterMode("match_any"); + } else if ( + curFieldFilter?.geo_radius?.center || + curFieldFilter?.geo_radius?.radius + ) { + setTempFilterMode("geo_radius"); + } else if (curFieldFilter?.range?.gt || curFieldFilter?.range?.lt) { + setTempFilterMode("range"); + } else if (curFieldFilter?.boolean != null) { + setTempFilterMode("boolean"); + } else if ( + curFieldFilter?.date_range?.gt || + curFieldFilter?.date_range?.lt + ) { + setTempFilterMode("date_range"); + } + return; + } }); createEffect(() => { const changedMode = tempFilterMode(); + if (changedMode === "geo_radius") { setCurFilter({ field: tempFilterField(), @@ -332,31 +431,148 @@ export const FilterItem = (props: FilterItemProps) => { }, radius: location().radius, }, + match_any: null, + match_all: null, + range: null, + date_range: null, + }); + setMatchAll(null); + setMatchAny(null); + setRange({ + gt: null, + lt: null, + gte: null, + lte: null, + }); + setDateRange({ + gt: null, + lt: null, + gte: null, + lte: null, }); } if (changedMode === "range") { setCurFilter({ field: tempFilterField(), + match_any: null, + match_all: null, range: { gt: range().gt, lt: range().lt, gte: range().gte, lte: range().lte, }, + date_range: null, + }); + setMatchAll(null); + setMatchAny(null); + setDateRange({ + gt: null, + lt: null, + gte: null, + lte: null, + }); + setLocation({ + lat: null, + lon: null, + radius: null, }); } - if (changedMode === "match") { + if (changedMode === "boolean") { setCurFilter({ field: tempFilterField(), - match: match(), + match_any: null, + match_all: null, + range: null, + date_range: null, + boolean: boolean(), + }); + setMatchAll(null); + setMatchAny(null); + setRange({ + gt: null, + lt: null, + gte: null, + lte: null, + }); + setDateRange({ + gt: null, + lt: null, + gte: null, + lte: null, + }); + setLocation({ + lat: null, + lon: null, + radius: null, + }); + } + + if (changedMode === "match_any") { + setCurFilter({ + field: tempFilterField(), + match_any: matchAny(), + match_all: null, + range: null, + date_range: null, + }); + setMatchAll(null); + setRange({ + gt: null, + lt: null, + gte: null, + lte: null, + }); + setDateRange({ + gt: null, + lt: null, + gte: null, + lte: null, + }); + setLocation({ + lat: null, + lon: null, + radius: null, + }); + } + + if (changedMode === "match_all") { + console.log("match_all"); + setCurFilter({ + field: tempFilterField(), + match_any: null, + match_all: matchAll(), + range: null, + date_range: null, + } satisfies FieldFilter); + setMatchAny(null); + setRange({ + gt: null, + lt: null, + gte: null, + lte: null, + }); + setDateRange({ + gt: null, + lt: null, + gte: null, + lte: null, + }); + setLocation({ + lat: null, + lon: null, + radius: null, }); } if (changedMode === "date_range") { setCurFilter({ field: tempFilterField(), + match_any: null, + match_all: null, + range: null, date_range: { gt: dateRange().gt, lt: dateRange().lt, @@ -364,6 +580,19 @@ export const FilterItem = (props: FilterItemProps) => { lte: dateRange().lte, }, }); + setMatchAll(null); + setMatchAny(null); + setRange({ + gt: null, + lt: null, + gte: null, + lte: null, + }); + setLocation({ + lat: null, + lon: null, + radius: null, + }); } }); @@ -378,24 +607,40 @@ export const FilterItem = (props: FilterItemProps) => { Filter Field: { setTempFilterField("metadata." + e.currentTarget.value); }} @@ -419,214 +664,298 @@ export const FilterItem = (props: FilterItemProps) => { /> - -
- - -
- -
-
- - { - setLocation({ - ...location(), - lat: parseFloat(e.currentTarget.value), - }); - }} - value={location().lat ?? ""} - /> -
-
- + +
+ . { - setLocation({ - ...location(), - lon: parseFloat(e.currentTarget.value), - }); + setTempFilterField("group_metadata." + e.currentTarget.value); }} - value={location().lon ?? ""} + value={tempFilterField() + .replace("group_metadata", "") + .replace(".", "")} />
-
- - { - setLocation({ - ...location(), - radius: parseFloat(e.currentTarget.value), - }); + +
+ +
+ +
-
- -
-
- - { - setRange({ ...range(), gt: parseFloat(e.currentTarget.value) }); - }} - value={range().gt ?? ""} - /> - - { - setRange({ ...range(), lt: parseFloat(e.currentTarget.value) }); - }} - value={range().lt ?? ""} - /> + +
+
+ + { + setLocation({ + ...location(), + lat: parseFloat(e.currentTarget.value), + }); + }} + value={location().lat ?? ""} + /> +
+
+ + { + setLocation({ + ...location(), + lon: parseFloat(e.currentTarget.value), + }); + }} + value={location().lon ?? ""} + /> +
+
+ + { + setLocation({ + ...location(), + radius: parseFloat(e.currentTarget.value), + }); + }} + value={location().radius ?? ""} + /> +
-
- - { - setRange({ - ...range(), - gte: parseFloat(e.currentTarget.value), - }); - }} - value={range().gte ?? ""} - /> - - { - setRange({ - ...range(), - lte: parseFloat(e.currentTarget.value), - }); - }} - value={range().lte ?? ""} - /> + + +
+
+ + { + setRange({ + ...range(), + gt: parseFloat(e.currentTarget.value), + }); + }} + value={range().gt ?? ""} + /> + + { + setRange({ + ...range(), + lt: parseFloat(e.currentTarget.value), + }); + }} + value={range().lt ?? ""} + /> +
+
+ + { + setRange({ + ...range(), + gte: parseFloat(e.currentTarget.value), + }); + }} + value={range().gte ?? ""} + /> + + { + setRange({ + ...range(), + lte: parseFloat(e.currentTarget.value), + }); + }} + value={range().lte ?? ""} + /> +
-
-
- -
-
- - { - setDateRange({ - ...dateRange(), - gt: e.currentTarget.value + " 00:00:00", - }); - }} - value={dateRange().gt?.replace(" 00:00:00", "") ?? ""} - /> - - { - setDateRange({ - ...dateRange(), - lt: e.currentTarget.value + " 00:00:00", - }); - }} - value={dateRange().lt?.replace(" 00:00:00", "") ?? ""} - /> + + +
+
+ + +
-
- - { - setDateRange({ - ...dateRange(), - gte: e.currentTarget.value + " 00:00:00", - }); - }} - value={dateRange().gte?.replace(" 00:00:00", "") ?? ""} - /> - - { - setDateRange({ - ...dateRange(), - lte: e.currentTarget.value + " 00:00:00", - }); - }} - value={dateRange().lte?.replace(" 00:00:00", "") ?? ""} - /> + + +
+
+ + { + setDateRange({ + ...dateRange(), + gt: e.currentTarget.value + " 00:00:00", + }); + }} + value={dateRange().gt?.replace(" 00:00:00", "") ?? ""} + /> + + { + setDateRange({ + ...dateRange(), + lt: e.currentTarget.value + " 00:00:00", + }); + }} + value={dateRange().lt?.replace(" 00:00:00", "") ?? ""} + /> +
+
+ + { + setDateRange({ + ...dateRange(), + gte: e.currentTarget.value + " 00:00:00", + }); + }} + value={dateRange().gte?.replace(" 00:00:00", "") ?? ""} + /> + + { + setDateRange({ + ...dateRange(), + lte: e.currentTarget.value + " 00:00:00", + }); + }} + value={dateRange().lte?.replace(" 00:00:00", "") ?? ""} + /> +
-
+
+ +
+
+ + { + setMatchAny(e.currentTarget.value.split(",")); + }} + value={(matchAny() ?? []).join(",")} + /> +
+
+
+ +
+
+ + { + setMatchAll(e.currentTarget.value.split(",")); + }} + value={(matchAll() ?? []).join(",")} + /> +
+
+
- + +
- + { - setMatch(e.currentTarget.value.split(",")); + console.log("setIdFilterText"); + setIdFilterText(e.currentTarget.value.split(",")); }} - value={(match() ?? []).join(",")} + value={idFilterText()} />
diff --git a/frontends/chat/src/components/ScoreChunk.tsx b/frontends/chat/src/components/ScoreChunk.tsx index 4feb8377a..c24ff2939 100644 --- a/frontends/chat/src/components/ScoreChunk.tsx +++ b/frontends/chat/src/components/ScoreChunk.tsx @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ @@ -128,6 +130,58 @@ const ScoreChunk = (props: ScoreChunkProps) => { return props.chunk.chunk_html.split(" ").length > 20 * 15; }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const renderMetadataElements = (value: any) => { + if (Array.isArray(value)) { + // Determine if the array consists solely of objects + const allObjects = value.every( + (item) => typeof item === "object" && item !== null, + ); + + return ( +
+ + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {(item: any, itemIndex: () => number) => ( + + {typeof item === "object" + ? renderMetadataElements(item) + : item.toString()} + {itemIndex() < value.length - 1 && + (allObjects ? ( +
+ ) : ( + , + ))} +
+ )} +
+
+ ); + } else if (typeof value === "object" && value !== null) { + return ( +
+ + {(subKey: string) => ( +
+
+ + {subKey}: + + + {renderMetadataElements(value[subKey])} + +
+
+ )} +
+
+ ); + } else { + return value !== null && value !== undefined ? value.toString() : "null"; + } + }; + return (
{ -
+
- {(key) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any - const value = (props.chunk.metadata as any)[key]; - return ( - + {(key) => ( + +
{key}:{" "} - 0 ? ( -
- - {(item, index) => ( - - {item} - {index() < value.length - 1 && ( - , - )} - - )} - -
- ) : ( - value - ) - } - > -
- - {(subKey) => ( -
- - {subKey}: - - - {typeof value[subKey] === "object" - ? JSON.stringify(value[subKey], null, 2) - : value[subKey]} - -
- )} -
-
-
+ {props.chunk.metadata && + renderMetadataElements(props.chunk.metadata[key])}
- - ); - }} +
+
+ )}
diff --git a/frontends/search/src/components/FilterModal.tsx b/frontends/search/src/components/FilterModal.tsx index 02c54d3ee..693219116 100644 --- a/frontends/search/src/components/FilterModal.tsx +++ b/frontends/search/src/components/FilterModal.tsx @@ -17,6 +17,7 @@ export interface FieldFilter { gt?: number | null; lt?: number | null; } | null; + boolean?: boolean | null; date_range?: { gte?: string | null; lte?: string | null; @@ -70,9 +71,10 @@ export const FilterItem = (props: FilterItemProps) => { let initialTempFilterField = "tag_set"; if (filterIsFieldFilter(props.initialFilter) && props.initialFilter != null) { - const fieldMode: ["geo_radius", "range", "date_range"] = [ + const fieldMode: ["geo_radius", "range", "boolean", "date_range"] = [ "geo_radius", "range", + "boolean", "date_range", ]; @@ -125,6 +127,10 @@ export const FilterItem = (props: FilterItemProps) => { lte: filterAsFieldFilter(props.initialFilter)?.date_range?.lte ?? null, }); + const [boolean, setBoolean] = createSignal( + filterAsFieldFilter(props.initialFilter)?.boolean ?? null, + ); + const [matchAny, setMatchAny] = createSignal<(string | number)[] | null>( filterAsFieldFilter(props.initialFilter)?.match_any ?? null, ); @@ -141,8 +147,7 @@ export const FilterItem = (props: FilterItemProps) => { createEffect(() => { const changedField = tempFilterField(); - const curMatchAll = - filterAsFieldFilter(props.initialFilter)?.match_all ?? []; + const curFieldFilter = filterAsFieldFilter(props.initialFilter); if (changedField === "ids") { setCurFilter({ @@ -155,9 +160,25 @@ export const FilterItem = (props: FilterItemProps) => { tracking_ids: idFilterText(), } as HasIdFilter); } else { - setTempFilterMode( - (curMatchAll?.length ?? 0) > 0 ? "match_all" : "match_any", - ); + if (curFieldFilter?.match_all?.length) { + setTempFilterMode("match_all"); + } else if (curFieldFilter?.match_any?.length) { + setTempFilterMode("match_any"); + } else if ( + curFieldFilter?.geo_radius?.center || + curFieldFilter?.geo_radius?.radius + ) { + setTempFilterMode("geo_radius"); + } else if (curFieldFilter?.range?.gt || curFieldFilter?.range?.lt) { + setTempFilterMode("range"); + } else if (curFieldFilter?.boolean != null) { + setTempFilterMode("boolean"); + } else if ( + curFieldFilter?.date_range?.gt || + curFieldFilter?.date_range?.lt + ) { + setTempFilterMode("date_range"); + } return; } }); @@ -224,6 +245,36 @@ export const FilterItem = (props: FilterItemProps) => { }); } + if (changedMode === "boolean") { + setCurFilter({ + field: tempFilterField(), + match_any: null, + match_all: null, + range: null, + date_range: null, + boolean: boolean(), + }); + setMatchAll(null); + setMatchAny(null); + setRange({ + gt: null, + lt: null, + gte: null, + lte: null, + }); + setDateRange({ + gt: null, + lt: null, + gte: null, + lte: null, + }); + setLocation({ + lat: null, + lon: null, + radius: null, + }); + } + if (changedMode === "match_any") { setCurFilter({ field: tempFilterField(), @@ -328,7 +379,9 @@ export const FilterItem = (props: FilterItemProps) => { value={ tempFilterField().startsWith("metadata") ? "metadata" - : tempFilterField() + : tempFilterField().startsWith("group_metadata") + ? "group_metadata" + : tempFilterField() } > { "num_value", "group_tracking_ids", "group_ids", + "group_metadata", "tracking_ids", "ids", ]} @@ -375,6 +429,22 @@ export const FilterItem = (props: FilterItemProps) => { />
+ +
+ . + { + setTempFilterField("group_metadata." + e.currentTarget.value); + }} + value={tempFilterField() + .replace("group_metadata", "") + .replace(".", "")} + /> +
+
@@ -394,20 +464,21 @@ export const FilterItem = (props: FilterItemProps) => { "match_all", "geo_radius", "range", + "boolean", "date_range", ]} > - {(filter_mode) => { + {(filterMode) => { return ( ); }} @@ -525,6 +596,23 @@ export const FilterItem = (props: FilterItemProps) => {
+ +
+
+ + +
+
+
diff --git a/frontends/search/src/components/GroupPage.tsx b/frontends/search/src/components/GroupPage.tsx index e2a2a9845..00eb7f319 100644 --- a/frontends/search/src/components/GroupPage.tsx +++ b/frontends/search/src/components/GroupPage.tsx @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-return */ @@ -7,7 +9,6 @@ import { createEffect, createSignal, For, - onMount, onCleanup, Switch, Match, @@ -49,11 +50,16 @@ import { } from "../hooks/useSearch"; import { downloadFile } from "../utils/downloadFile"; import ScoreChunk from "./ScoreChunk"; -import { BiRegularXCircle } from "solid-icons/bi"; +import { + BiRegularChevronDown, + BiRegularChevronUp, + BiRegularXCircle, +} from "solid-icons/bi"; import { createToast } from "./ShowToasts"; import { FiEye } from "solid-icons/fi"; import { useCtrClickForChunk } from "../hooks/useCtrAnalytics"; import { ChunkGroupAndFileId } from "trieve-ts-sdk"; +import { JsonInput } from "shared/ui"; export interface GroupPageProps { groupID: string; @@ -86,6 +92,8 @@ export const GroupPage = (props: GroupPageProps) => { const [fetchingGroups, setFetchingGroups] = createSignal(false); const [deleting, setDeleting] = createSignal(false); const [editing, setEditing] = createSignal(false); + const [descendTagSet, setDescendTagSet] = createSignal(false); + const [expandGroupMetadata, setExpandGroupMetadata] = createSignal(false); const $currentUser = datasetAndUserContext.user; const [totalPages, setTotalPages] = createSignal(0); const [loadingRecommendations, setLoadingRecommendations] = @@ -118,12 +126,17 @@ export const GroupPage = (props: GroupPageProps) => { note: "", }); - onMount(() => { + createEffect(() => { fetchBookmarks(); + const urlParams = new URLSearchParams(window.location.search); + const editSearch = urlParams.get("edit") === "true"; + setEditing(editSearch); }); createEffect((prevGroupId) => { const curGroupId = props.groupID; + const urlParams = new URLSearchParams(window.location.search); + const editSearch = urlParams.get("edit") === "true"; if (curGroupId !== prevGroupId) { setPage(1); setGroupRecommendations(false); @@ -131,7 +144,7 @@ export const GroupPage = (props: GroupPageProps) => { setRecommendedChunks([]); setLoadingRecommendations(false); setSearchLoading(false); - setEditing(false); + setEditing(editSearch); } return curGroupId; @@ -415,6 +428,8 @@ export const GroupPage = (props: GroupPageProps) => { groupInfo()?.tracking_id == "" ? null : groupInfo()?.tracking_id, tag_set: groupInfo()?.tag_set, description: groupInfo()?.description, + metadata: groupInfo()?.metadata, + update_chunks: descendTagSet(), }; void fetch(`${apiHost}/chunk_group`, { method: "PUT", @@ -429,6 +444,9 @@ export const GroupPage = (props: GroupPageProps) => { setFetchingGroups(false); if (response.ok) { setEditing(false); + const searchParams = new URLSearchParams(window.location.search); + searchParams.set("edit", "false"); + navigate(`?${searchParams.toString()}`); } }); }; @@ -525,6 +543,58 @@ export const GroupPage = (props: GroupPageProps) => { }); }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const renderMetadataElements = (value: any) => { + if (Array.isArray(value)) { + // Determine if the array consists solely of objects + const allObjects = value.every( + (item) => typeof item === "object" && item !== null, + ); + + return ( +
+ + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {(item: any, itemIndex: () => number) => ( + + {typeof item === "object" + ? renderMetadataElements(item) + : item.toString()} + {itemIndex() < value.length - 1 && + (allObjects ? ( +
+ ) : ( + , + ))} +
+ )} +
+
+ ); + } else if (typeof value === "object" && value !== null) { + return ( +
+ + {(subKey: string) => ( +
+
+ + {subKey}: + + + {renderMetadataElements(value[subKey])} + +
+
+ )} +
+
+ ); + } else { + return value !== null && value !== undefined ? value.toString() : "null"; + } + }; + return ( <> @@ -567,14 +637,80 @@ export const GroupPage = (props: GroupPageProps) => {
-
-

- {groupInfo()?.name} -

-
- 0 && !editing())}> -
- {groupInfo()?.description} + +
+ + Name:{" "} + + {groupInfo()?.name} +
+
+ +
+ + Tag Set:{" "} + + + {groupInfo()?.tag_set?.join(",")} + +
+
+ +
+ + Description:{" "} + + + {groupInfo()?.description} + +
+
+ 0}> + + + +
+ + {(key) => ( + +
+
+ + {key}:{" "} + + + {groupInfo()?.metadata && + renderMetadataElements( + (groupInfo()?.metadata as any)[key], + )} + +
+
+
+ )} +
@@ -634,6 +770,17 @@ export const GroupPage = (props: GroupPageProps) => { } }} /> +

+ Descend Tag Set Update to Chunks: +

+ { + setDescendTagSet(e.target.checked); + }} + />

Description:

@@ -650,6 +797,21 @@ export const GroupPage = (props: GroupPageProps) => { } }} /> +

+ Metadata: +

+ { + const curGroupInfo = groupInfo(); + if (curGroupInfo) { + setGroupInfo({ + ...curGroupInfo, + metadata: j, + }); + } + }} + value={() => groupInfo()?.metadata ?? {}} + />
diff --git a/frontends/search/src/components/ResultsPage.tsx b/frontends/search/src/components/ResultsPage.tsx index 0ec1f80f8..51928f21c 100644 --- a/frontends/search/src/components/ResultsPage.tsx +++ b/frontends/search/src/components/ResultsPage.tsx @@ -45,10 +45,12 @@ import { } from "../hooks/useSearch"; import { downloadFile } from "../utils/downloadFile"; import ScoreChunk from "./ScoreChunk"; -import { FiEye } from "solid-icons/fi"; +import { FiEdit, FiEye } from "solid-icons/fi"; import { ServerTimings } from "./ServerTimings"; import { VsChevronRight } from "solid-icons/vs"; import { useCtrClickForChunk } from "../hooks/useCtrAnalytics"; +import { BiRegularChevronDown, BiRegularChevronUp } from "solid-icons/bi"; +import { Tooltip } from "shared/ui"; export interface ResultsPageProps { search: SearchStore; @@ -478,6 +480,58 @@ const ResultsPage = (props: ResultsPageProps) => { } }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const renderMetadataElements = (value: any) => { + if (Array.isArray(value)) { + // Determine if the array consists solely of objects + const allObjects = value.every( + (item) => typeof item === "object" && item !== null, + ); + + return ( +
+ + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {(item: any, itemIndex: () => number) => ( + + {typeof item === "object" + ? renderMetadataElements(item) + : item.toString()} + {itemIndex() < value.length - 1 && + (allObjects ? ( +
+ ) : ( + , + ))} +
+ )} +
+
+ ); + } else if (typeof value === "object" && value !== null) { + return ( +
+ + {(subKey: string) => ( +
+
+ + {subKey}: + + + {renderMetadataElements(value[subKey])} + +
+
+ )} +
+
+ ); + } else { + return value !== null && value !== undefined ? value.toString() : "null"; + } + }; + return ( <> @@ -673,6 +727,8 @@ const ResultsPage = (props: ResultsPageProps) => { {(groupResult) => { const [groupExpanded, setGroupExpanded] = createSignal(true); + const [expandGroupMetadata, setExpandGroupMetadata] = + createSignal(false); const toggle = () => { setGroupExpanded(!groupExpanded()); @@ -727,6 +783,81 @@ const ResultsPage = (props: ResultsPageProps) => {
+ +
+
+ + Tag Set:{" "} + + + {groupResult.group.tag_set?.join(",")} + +
+
+
+ 0 + } + > + + + +
+ + {(key) => ( + +
+
+ + {key}:{" "} + + + {groupResult.group.metadata && + renderMetadataElements( + groupResult.group.metadata[ + key as keyof typeof groupResult.group.metadata + ], + )} + +
+
+
+ )} +
+
+
@@ -740,6 +871,18 @@ const ResultsPage = (props: ResultsPageProps) => { )} + + + + } + tooltipText="Edit chunk" + /> i64 { match self { - MatchCondition::Text(text) => text.parse().unwrap(), + MatchCondition::Text(text) => text.parse().unwrap_or_default(), MatchCondition::Integer(int) => *int, MatchCondition::Float(float) => *float as i64, } @@ -4182,7 +4182,7 @@ impl MatchCondition { pub fn to_f64(&self) -> f64 { match self { - MatchCondition::Text(text) => text.parse().unwrap(), + MatchCondition::Text(text) => text.parse().unwrap_or_default(), MatchCondition::Integer(int) => *int as f64, MatchCondition::Float(float) => *float, } @@ -4249,6 +4249,8 @@ pub struct FieldCondition { pub match_all: Option>, /// Range is a JSON object which can be used to filter chunks by a range of values. This only works for numerical fields. You can specify this if you want values in a certain range. pub range: Option, + /// Boolean is a true false value for a field. This only works for boolean fields. You can specify this if you want values to be true or false. + pub boolean: Option, /// Date range is a JSON object which can be used to filter chunks by a range of dates. This only works for date fields. You can specify this if you want values in a certain range. You must provide ISO 8601 combined date and time without timezone. pub date_range: Option, /// Geo Bounding Box search is useful for when you want to find points inside a rectangular area. This is useful for when you want to filter chunks by location. The bounding box is defined by two points: the top-left and bottom-right corners of the box. @@ -4309,9 +4311,11 @@ impl FieldCondition { dataset_id: uuid::Uuid, pool: web::Data, ) -> Result, ServiceError> { - if (self.match_all.is_some() || self.match_any.is_some()) && self.range.is_some() { + if (self.match_all.is_some() || self.match_any.is_some()) + && (self.range.is_some() || self.boolean.is_some()) + { return Err(ServiceError::BadRequest( - "Cannot have both match and range conditions".to_string(), + "Cannot have both a match and range or boolean conditions".to_string(), )); } @@ -4344,6 +4348,13 @@ impl FieldCondition { return Ok(Some(qdrant::Condition::range(self.field.as_str(), range))); }; + if let Some(boolean) = self.boolean { + return Ok(Some(qdrant::Condition::matches( + self.field.as_str(), + boolean, + ))); + } + if let Some(geo_bounding_box) = self.geo_bounding_box.clone() { let top_left = geo_bounding_box.top_left; let bottom_right = geo_bounding_box.bottom_right; @@ -4473,7 +4484,7 @@ impl FieldCondition { } } else { Err(ServiceError::BadRequest( - "No filter condition provided. Field must not be null and date_range, range, geo_bounding_box, geo_radius, geo_polygon, match_any, or match_all must be populated.".to_string(), + "No filter condition provided. Field must not be null and date_range, range, boolean, geo_bounding_box, geo_radius, geo_polygon, match_any, or match_all must be populated.".to_string(), )) } } diff --git a/server/src/operators/search_operator.rs b/server/src/operators/search_operator.rs index ff6dbe868..2823d2cc5 100644 --- a/server/src/operators/search_operator.rs +++ b/server/src/operators/search_operator.rs @@ -122,6 +122,7 @@ async fn convert_group_tracking_ids_to_group_ids( match_all: None, date_range: None, range: None, + boolean: None, geo_bounding_box: None, geo_polygon: None, geo_radius: None, @@ -149,6 +150,7 @@ async fn convert_group_tracking_ids_to_group_ids( match_all: Some(correct_matches), date_range: None, range: None, + boolean: None, geo_bounding_box: None, geo_polygon: None, geo_radius: None, @@ -616,16 +618,16 @@ pub async fn get_metadata_filter_condition( key, string_val ))); } - MatchCondition::Integer(id_val) => { + MatchCondition::Integer(int_val) => { query = query.filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, int_val ))); } - MatchCondition::Float(id_val) => { + MatchCondition::Float(float_val) => { query = query.filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, float_val ))); } } @@ -639,16 +641,16 @@ pub async fn get_metadata_filter_condition( key, string_val ))); } - MatchCondition::Integer(id_val) => { + MatchCondition::Integer(int_val) => { query = query.or_filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, int_val ))); } - MatchCondition::Float(id_val) => { + MatchCondition::Float(float_val) => { query = query.or_filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, float_val ))); } } @@ -662,16 +664,16 @@ pub async fn get_metadata_filter_condition( key, string_val ))); } - MatchCondition::Integer(id_val) => { + MatchCondition::Integer(int_val) => { query = query.filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, int_val ))); } - MatchCondition::Float(id_val) => { + MatchCondition::Float(float_val) => { query = query.filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, float_val ))); } } @@ -685,16 +687,16 @@ pub async fn get_metadata_filter_condition( key, string_val ))); } - MatchCondition::Integer(id_val) => { + MatchCondition::Integer(int_val) => { query = query.filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, int_val ))); } - MatchCondition::Float(id_val) => { + MatchCondition::Float(float_val) => { query = query.filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, float_val ))); } } @@ -732,6 +734,12 @@ pub async fn get_metadata_filter_condition( }; } + if let Some(boolean) = &filter.boolean { + query = query.filter( + sql::(&format!("chunk_metadata.metadata->>'{}'", key)).eq(boolean.to_string()), + ); + } + if let Some(date_range) = &filter.date_range { if let Some(gt) = &date_range.gt { query = query.filter( @@ -834,16 +842,16 @@ pub async fn get_group_metadata_filter_condition( key, string_val ))); } - MatchCondition::Integer(id_val) => { + MatchCondition::Integer(int_val) => { query = query.filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, int_val ))); } - MatchCondition::Float(id_val) => { + MatchCondition::Float(float_val) => { query = query.filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, float_val ))); } } @@ -857,16 +865,16 @@ pub async fn get_group_metadata_filter_condition( key, string_val ))); } - MatchCondition::Integer(id_val) => { + MatchCondition::Integer(int_val) => { query = query.or_filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, int_val ))); } - MatchCondition::Float(id_val) => { + MatchCondition::Float(float_val) => { query = query.or_filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, float_val ))); } } @@ -880,16 +888,16 @@ pub async fn get_group_metadata_filter_condition( key, string_val ))); } - MatchCondition::Integer(id_val) => { + MatchCondition::Integer(int_val) => { query = query.filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, int_val ))); } - MatchCondition::Float(id_val) => { + MatchCondition::Float(float_val) => { query = query.filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, float_val ))); } } @@ -903,16 +911,16 @@ pub async fn get_group_metadata_filter_condition( key, string_val ))); } - MatchCondition::Integer(id_val) => { + MatchCondition::Integer(int_val) => { query = query.filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, int_val ))); } - MatchCondition::Float(id_val) => { + MatchCondition::Float(float_val) => { query = query.filter(sql::(&format!( "chunk_metadata.metadata @> '{{\"{}\":\"{}\"}}'", - key, id_val + key, float_val ))); } } @@ -946,10 +954,19 @@ pub async fn get_group_metadata_filter_condition( }; } - let qdrant_point_ids: Vec = query - .load::(&mut conn) - .await - .map_err(|_| ServiceError::BadRequest("Failed to load metadata".to_string()))?; + if let Some(boolean) = &filter.boolean { + query = query.filter( + sql::(&format!("chunk_group.metadata->>'{}'", key)).eq(boolean.to_string()), + ); + } + + let qdrant_point_ids: Vec = + query.load::(&mut conn).await.map_err(|_| { + ServiceError::BadRequest( + "Failed to load qdrant_point_ids from pg for get_group_metadata_filter_condition" + .to_string(), + ) + })?; let matching_point_ids: Vec = qdrant_point_ids .iter()