);
};
+
export default connect(
(state: any) => ({
appliedFilter: state.getIn(['search', 'instance']),
diff --git a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx
index 3b2e933852..d73c041b68 100644
--- a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx
+++ b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx
@@ -1,11 +1,12 @@
import React, { useEffect } from 'react';
+import AnimatedSVG, { ICONS } from "Shared/AnimatedSVG/AnimatedSVG";
import FilterList from 'Shared/Filters/FilterList';
import FilterSelection from 'Shared/Filters/FilterSelection';
import SaveFilterButton from 'Shared/SaveFilterButton';
import { connect } from 'react-redux';
import { FilterKey } from 'Types/filter/filterType';
import { addOptionsToFilter } from 'Types/filter/newFilter';
-import { Button } from 'UI';
+import { Button, Loader } from 'UI';
import { edit, addFilter, fetchSessions, updateFilter } from 'Duck/search';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
@@ -27,7 +28,7 @@ interface Props {
}
function SessionSearch(props: Props) {
- const { tagWatchStore } = useStore();
+ const { tagWatchStore, aiFiltersStore } = useStore();
const { appliedFilter, saveRequestPayloads = false, metaLoading = false } = props;
const hasEvents = appliedFilter.filters.filter((i: any) => i.isEvent).size > 0;
const hasFilters = appliedFilter.filters.filter((i: any) => !i.isEvent).size > 0;
@@ -43,7 +44,7 @@ function SessionSearch(props: Props) {
FilterKey.TAGGED_ELEMENT,
tags.map((tag) => ({
label: tag.name,
- value: tag.tagId.toString()
+ value: tag.tagId.toString(),
}))
);
props.refreshFilterOptions();
@@ -96,32 +97,43 @@ function SessionSearch(props: Props) {
debounceFetch();
};
+ const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading;
return !metaLoading ? (
<>
- {hasEvents || hasFilters ? (
+ {showPanel ? (
-
+ {aiFiltersStore.isLoading ? (
+
+
+
Translating your query into search steps...
+
+ ) : null}
+ {hasEvents || hasFilters ? (
+
+ ) : null}
-
-
-
-
-
+ {hasEvents || hasFilters ? (
+
+
+
+
+
+
+
+
+
-
-
-
-
+ ) : null}
) : (
<>>
diff --git a/frontend/app/components/shared/SessionSearchField/AiSessionSearchField.tsx b/frontend/app/components/shared/SessionSearchField/AiSessionSearchField.tsx
new file mode 100644
index 0000000000..1590fa79bf
--- /dev/null
+++ b/frontend/app/components/shared/SessionSearchField/AiSessionSearchField.tsx
@@ -0,0 +1,190 @@
+import React, { useState } from 'react';
+import { connect } from 'react-redux';
+import { Input, Icon } from 'UI';
+import FilterModal from 'Shared/Filters/FilterModal';
+import { debounce } from 'App/utils';
+import { assist as assistRoute, isRoute } from 'App/routes';
+import { addFilterByKeyAndValue, fetchFilterSearch, edit } from 'Duck/search';
+import {
+ addFilterByKeyAndValue as liveAddFilterByKeyAndValue,
+ fetchFilterSearch as liveFetchFilterSearch,
+} from 'Duck/liveSearch';
+import { observer } from 'mobx-react-lite';
+import { useStore } from 'App/mstore';
+import { Segmented } from 'antd';
+import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
+
+const ASSIST_ROUTE = assistRoute();
+
+interface Props {
+ fetchFilterSearch: (query: any) => void;
+ addFilterByKeyAndValue: (key: string, value: string) => void;
+ liveAddFilterByKeyAndValue: (key: string, value: string) => void;
+ liveFetchFilterSearch: any;
+ edit: typeof edit;
+}
+
+function SessionSearchField(props: Props) {
+ const isLive =
+ isRoute(ASSIST_ROUTE, window.location.pathname) ||
+ window.location.pathname.includes('multiview');
+ const debounceFetchFilterSearch = React.useCallback(
+ debounce(isLive ? props.liveFetchFilterSearch : props.fetchFilterSearch, 1000),
+ []
+ );
+
+ const [showModal, setShowModal] = useState(false);
+ const [searchQuery, setSearchQuery] = useState('');
+
+ const onSearchChange = ({ target: { value } }: any) => {
+ setSearchQuery(value);
+ debounceFetchFilterSearch({ q: value });
+ };
+
+ const onAddFilter = (filter: any) => {
+ isLive
+ ? props.liveAddFilterByKeyAndValue(filter.key, filter.value)
+ : props.addFilterByKeyAndValue(filter.key, filter.value);
+ };
+
+ const onFocus = () => {
+ setShowModal(true);
+ };
+ const onBlur = () => {
+ setTimeout(() => {
+ setShowModal(false);
+ }, 200);
+ };
+ return (
+
+
+
+ {showModal && (
+
+
+
+ )}
+
+ );
+}
+
+const AiSearchField = observer(({ edit }: Props) => {
+ const { aiFiltersStore } = useStore();
+ const [searchQuery, setSearchQuery] = useState('');
+ const debounceAiFetch = React.useCallback(debounce(aiFiltersStore.getSearchFilters, 1000), []);
+
+ const onSearchChange = ({ target: { value } }: any) => {
+ if (value !== '' && value !== searchQuery) {
+ setSearchQuery(value);
+ debounceAiFetch(value);
+ }
+ };
+
+ React.useEffect(() => {
+ if (aiFiltersStore.filtersSetKey !== 0) {
+ console.log('updating filters', aiFiltersStore.filters, aiFiltersStore.filtersSetKey);
+ edit(aiFiltersStore.filters)
+ }
+ }, [aiFiltersStore.filters, aiFiltersStore.filtersSetKey])
+
+ return (
+
+
+
+ );
+})
+
+function AiSessionSearchField(props: Props) {
+ const [tab, setTab] = useState('search');
+ const [isFocused, setIsFocused] = useState(false);
+
+ const boxStyle = isFocused ? gradientBox : gradientBoxUnfocused;
+ return (
+
setIsFocused(false)}
+ className={'bg-white rounded-lg'}
+ >
+ setIsFocused(true)}>
+
setTab(value as string)}
+ options={[
+ {
+ label: (
+
+
+ Search
+
+ ),
+ value: 'search',
+ },
+ {
+ label: (
+
+
+ Ask AI
+
+ ),
+ value: 'ask',
+ },
+ ]}
+ />
+ {tab === 'ask' ? : }
+
+
+ );
+}
+
+const gradientBox = {
+ border: 'double 1px transparent',
+ borderRadius: '6px',
+ background:
+ 'linear-gradient(#f6f6f6, #f6f6f6), linear-gradient(to right, #394EFF 0%, #3EAAAF 100%)',
+ backgroundOrigin: 'border-box',
+ backgroundClip: 'content-box, border-box',
+ display: 'flex',
+ gap: '0.25rem',
+ alignItems: 'center',
+ width: '100%',
+};
+
+const gradientBoxUnfocused = {
+ borderRadius: '6px',
+ border: 'double 1px transparent',
+ background: '#f6f6f6',
+ display: 'flex',
+ gap: '0.25rem',
+ alignItems: 'center',
+ width: '100%',
+};
+
+export default connect(null, {
+ addFilterByKeyAndValue,
+ fetchFilterSearch,
+ liveFetchFilterSearch,
+ liveAddFilterByKeyAndValue,
+ edit,
+})(observer(AiSessionSearchField));
diff --git a/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx b/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx
index 64904e3587..eeac961bb5 100644
--- a/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx
+++ b/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx
@@ -9,7 +9,9 @@ import {
addFilterByKeyAndValue as liveAddFilterByKeyAndValue,
fetchFilterSearch as liveFetchFilterSearch,
} from 'Duck/liveSearch';
+
const ASSIST_ROUTE = assistRoute();
+import { observer } from 'mobx-react-lite';
interface Props {
fetchFilterSearch: (query: any) => void;
@@ -17,6 +19,7 @@ interface Props {
liveAddFilterByKeyAndValue: (key: string, value: string) => void;
liveFetchFilterSearch: any;
}
+
function SessionSearchField(props: Props) {
const isLive =
isRoute(ASSIST_ROUTE, window.location.pathname) ||
@@ -72,4 +75,4 @@ export default connect(null, {
fetchFilterSearch,
liveFetchFilterSearch,
liveAddFilterByKeyAndValue,
-})(SessionSearchField);
+})(observer(SessionSearchField));
diff --git a/frontend/app/mstore/aiFiltersStore.ts b/frontend/app/mstore/aiFiltersStore.ts
new file mode 100644
index 0000000000..59c8e851e4
--- /dev/null
+++ b/frontend/app/mstore/aiFiltersStore.ts
@@ -0,0 +1,117 @@
+import { makeAutoObservable } from 'mobx';
+import { aiService } from 'App/services';
+import Filter from 'Types/filter';
+import { FilterKey } from 'Types/filter/filterType';
+
+export default class AiFiltersStore {
+ filters: Record
= { filters: [] };
+ filtersSetKey = 0;
+ isLoading: boolean = false;
+
+ constructor() {
+ makeAutoObservable(this);
+ }
+
+ setFilters = (filters: Record): void => {
+ this.filters = filters;
+ this.filtersSetKey += 1;
+ };
+
+ getSearchFilters = async (query: string): Promise => {
+ this.isLoading = true;
+ try {
+ const r = await aiService.getSearchFilters(query);
+ const filterObj = Filter({
+ filters: r.filters.map((f: Record) => {
+ if (f.key === 'fetch') {
+ return mapFetch(f);
+ } else {
+ return { ...f, value: f.value ?? [] };
+ }
+ }),
+ eventsOrder: r.eventsOrder.toLowerCase(),
+ });
+
+ this.setFilters(filterObj);
+ return r;
+ } catch (e) {
+ console.trace(e);
+ } finally {
+ this.isLoading = false;
+ }
+ };
+}
+
+
+
+const defaultFetchFilter = {
+ value: [],
+ key: FilterKey.FETCH,
+ type: FilterKey.FETCH,
+ operator: 'is',
+ isEvent: true,
+ filters: [
+ {
+ value: [],
+ type: 'fetchUrl',
+ operator: 'is',
+ filters: [],
+ },
+ {
+ value: ['200'],
+ type: 'fetchStatusCode',
+ operator: '>',
+ filters: [],
+ },
+ {
+ value: [],
+ type: 'fetchMethod',
+ operator: 'is',
+ filters: [],
+ },
+ {
+ value: [],
+ type: 'fetchDuration',
+ operator: '=',
+ filters: [],
+ },
+ {
+ value: [],
+ type: 'fetchRequestBody',
+ operator: 'is',
+ filters: [],
+ },
+ {
+ value: [],
+ type: 'fetchResponseBody',
+ operator: 'is',
+ filters: [],
+ },
+ ],
+};
+
+export function isObject(item: any): boolean {
+ return item && typeof item === 'object' && !Array.isArray(item);
+}
+
+export function mergeDeep(target: Record, ...sources: any[]): Record {
+ if (!sources.length) return target;
+ const source = sources.shift();
+
+ if (isObject(target) && isObject(source)) {
+ for (const key in source) {
+ if (isObject(source[key])) {
+ if (!target[key]) Object.assign(target, { [key]: {} });
+ mergeDeep(target[key], source[key]);
+ } else {
+ Object.assign(target, { [key]: source[key] });
+ }
+ }
+ }
+
+ return mergeDeep(target, ...sources);
+}
+
+const mapFetch = (filter: Record): Record => {
+ return mergeDeep(filter, defaultFetchFilter);
+};
diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx
index 30ac1f930b..5147f1e89c 100644
--- a/frontend/app/mstore/index.tsx
+++ b/frontend/app/mstore/index.tsx
@@ -21,6 +21,8 @@ import FeatureFlagsStore from './featureFlagsStore';
import UxtestingStore from './uxtestingStore';
import TagWatchStore from './tagWatchStore';
import AiSummaryStore from "./aiSummaryStore";
+import AiFiltersStore from "./aiFiltersStore";
+
export class RootStore {
dashboardStore: DashboardStore;
metricStore: MetricStore;
@@ -42,6 +44,7 @@ export class RootStore {
uxtestingStore: UxtestingStore;
tagWatchStore: TagWatchStore;
aiSummaryStore: AiSummaryStore;
+ aiFiltersStore: AiFiltersStore;
constructor() {
this.dashboardStore = new DashboardStore();
@@ -64,6 +67,7 @@ export class RootStore {
this.uxtestingStore = new UxtestingStore();
this.tagWatchStore = new TagWatchStore();
this.aiSummaryStore = new AiSummaryStore();
+ this.aiFiltersStore = new AiFiltersStore();
}
initClient() {
diff --git a/frontend/app/services/AiService.ts b/frontend/app/services/AiService.ts
index ec3835cf43..37c9b2c667 100644
--- a/frontend/app/services/AiService.ts
+++ b/frontend/app/services/AiService.ts
@@ -4,11 +4,19 @@ export default class AiService extends BaseService {
/**
* @returns stream of text symbols
* */
- async getSummary(sessionId: string) {
+ async getSummary(sessionId: string): Promise {
const r = await this.client.post(
`/sessions/${sessionId}/intelligent/summary`,
);
return r.json()
}
+
+ async getSearchFilters(query: string): Promise> {
+ const r = await this.client.post('/intelligent/search', {
+ question: query
+ })
+ const { data } = await r.json();
+ return data
+ }
}
diff --git a/tracker/tracker/src/webworker/QueueSender.ts b/tracker/tracker/src/webworker/QueueSender.ts
index 3a41821bad..c1db1de9ab 100644
--- a/tracker/tracker/src/webworker/QueueSender.ts
+++ b/tracker/tracker/src/webworker/QueueSender.ts
@@ -16,7 +16,7 @@ export default class QueueSender {
private readonly onUnauthorised: () => any,
private readonly onFailure: (reason: string) => any,
private readonly MAX_ATTEMPTS_COUNT = 10,
- private readonly ATTEMPT_TIMEOUT = 1000,
+ private readonly ATTEMPT_TIMEOUT = 250,
private readonly onCompress?: (batch: Uint8Array) => any,
) {
this.ingestURL = ingestBaseURL + INGEST_PATH