diff --git a/frontend/app/components/shared/MainSearchBar/MainSearchBar.tsx b/frontend/app/components/shared/MainSearchBar/MainSearchBar.tsx index 9604b59e0d..a1535b3a15 100644 --- a/frontend/app/components/shared/MainSearchBar/MainSearchBar.tsx +++ b/frontend/app/components/shared/MainSearchBar/MainSearchBar.tsx @@ -1,5 +1,6 @@ import React from 'react'; import SessionSearchField from 'Shared/SessionSearchField'; +import AiSessionSearchField from 'Shared/SessionSearchField/AiSessionSearchField'; import SavedSearch from 'Shared/SavedSearch'; import { Button } from 'UI'; import { connect } from 'react-redux'; @@ -17,10 +18,14 @@ const MainSearchBar = (props: Props) => { const hasFilters = appliedFilter && appliedFilter.filters && appliedFilter.filters.size > 0; const hasSavedSearch = props.savedSearch && props.savedSearch.exists(); const hasSearch = hasFilters || hasSavedSearch; + + // @ts-ignore + const originStr = window.env.ORIGIN || window.location.origin; + const isSaas = /app\.openreplay\.com/.test(originStr); return (
- + {isSaas ? : }
@@ -37,6 +42,7 @@ const MainSearchBar = (props: Props) => {
); }; + 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