openreplay/frontend/app/components/shared/SessionSearchField/AiSessionSearchField.tsx
Delirium a71381da40
getting rid of redux for good (#2556)
* start moving ui to redux tlk

* remove unused reducer

* changes for gdpr and site types

* ui: migrating duck/roles to mobx

* ui: drop unreferenced types

* ui: drop unreferenced types

* ui: move player slice reducer to mobx family

* ui: move assignments to issueReportingStore.ts

* remove issues store

* some fixes after issues store

* remove errors reducer, drop old components

* finish removing errors reducer

* start moving integrations state to mobx

* change(ui): funnel duck cleanup

* change(ui): custom fields

* change(ui): customMetrics cleanup

* change(ui): customMetrics cleanup

* change(ui): duck/filters minor cleanup

* change(ui): duck/filters cleanup

* change(ui): duck/customMetrics cleanup and upgrades

* fix integrations service, fix babel config to >.25 + not ie

* refactoring integrations reducers etc WIP

* finish removing integrations state

* some fixes for integrated check

* start of projects refactoring

* move api and "few" files to new project store

* new batch for site -> projects

* fix setid context

* move all critical components, drop site duck

* remove all duck/site refs, remove old components

* fixup for SessionTags.tsx, remove duck/sources (?)

* move session store

* init sessionstore outside of context

* fix userfilter

* replace simple actions for session store

* sessions sotre

* Rtm temp (#2597)

* change(ui): duck/search wip

* change(ui): duck/search wip

* change(ui): duck/search wip

* change(ui): duck/searchLive wip

* change(ui): duck/searchLive wip

* change(ui): duck/searchLive wip

* change(ui): duck/searchLive wip

* change(ui): search states

* change(ui): search states

* change(ui): search states

* change(ui): fix savedSearch store

* change(ui): fix savedSearch store

* some fixes for session connector

* change(ui): fix savedSearch store

* change(ui): fix searchLive

* change(ui): fix searchLive

* fixes for session replay

* change(ui): bookmark fetch

* last components for sessions

* add fetchautoplaylist

* finish session reducer, remove deleted reducers

* change(ui): fix the search fetch

* change(ui): fix the search fetch

* fix integrations call ctx

* ensure ctx for sessionstore

* fix(ui): checking for latest sessions path

* start removing user reducer

* removing user reducer pt2...

* finish user store

* remove rand log

* fix crashes

* tinkering workflow file for tracker test

* making sure prefetched sessions work properly

* fix conflict

* fix router redirects during loading

---------

Co-authored-by: Shekar Siri <sshekarsiri@gmail.com>
2024-10-03 11:38:36 +02:00

334 lines
9.4 KiB
TypeScript

import { CloseOutlined, EnterOutlined } from '@ant-design/icons';
import { Tour } from 'antd';
import { observer } from 'mobx-react-lite';
import React, { useState } from 'react';
import { useStore } from 'App/mstore';
import { assist as assistRoute, isRoute } from 'App/routes';
import { debounce } from 'App/utils';
import { Icon, Input } from 'UI';
import FilterModal from 'Shared/Filters/FilterModal';
import OutsideClickDetectingDiv from '../OutsideClickDetectingDiv';
const ASSIST_ROUTE = assistRoute();
interface Props {
setFocused?: (focused: boolean) => void;
}
function SessionSearchField(props: Props) {
const { searchStore, searchStoreLive } = useStore();
const isLive =
isRoute(ASSIST_ROUTE, window.location.pathname) ||
window.location.pathname.includes('multiview');
const debounceFetchFilterSearch = React.useCallback(
debounce(
isLive ? searchStoreLive.fetchFilterSearch : searchStore.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
? searchStoreLive.addFilterByKeyAndValue(filter.key, filter.value)
: searchStore.addFilterByKeyAndValue(filter.key, filter.value);
};
const onFocus = () => {
setShowModal(true);
props.setFocused?.(true);
};
const onBlur = () => {
setTimeout(() => {
setShowModal(false);
props.setFocused?.(false);
}, 200);
};
return (
<div className="relative w-full">
<Input
onFocus={onFocus}
onBlur={onBlur}
onChange={onSearchChange}
placeholder={
'Search sessions using any captured event (click, input, page, error...)'
}
style={{ minWidth: 360, height: 30 }}
id="search"
type="search"
autoComplete="off"
className="px-2 py-1 text-lg placeholder-lg !border-0 rounded-r-full nofocus"
/>
{showModal && (
<div className="absolute left-0 shadow-sm rounded-lg bg-white z-50">
<FilterModal
searchQuery={searchQuery}
isMainSearch={true}
onFilterClick={onAddFilter}
isLive={isLive}
/>
</div>
)}
</div>
);
}
const AiSearchField = observer(() => {
const { searchStore } = useStore();
const appliedFilter = searchStore.instance;
const hasFilters =
appliedFilter && appliedFilter.filters && appliedFilter.filters.length > 0;
const { aiFiltersStore } = useStore();
const [searchQuery, setSearchQuery] = useState('');
const onSearchChange = ({ target: { value } }: any) => {
setSearchQuery(value);
};
const fetchResults = () => {
if (searchQuery) {
void aiFiltersStore.getSearchFilters(searchQuery);
}
};
const handleKeyDown = (event: any) => {
if (event.key === 'Enter') {
fetchResults();
}
};
const clearAll = () => {
searchStore.clearSearch();
setSearchQuery('');
};
React.useEffect(() => {
if (aiFiltersStore.filtersSetKey !== 0) {
searchStore.edit(aiFiltersStore.filters);
}
}, [aiFiltersStore.filters, aiFiltersStore.filtersSetKey]);
return (
<div className={'w-full'}>
<Input
onChange={onSearchChange}
placeholder={'E.g., "Sessions with login issues this week"'}
id="search"
onKeyDown={handleKeyDown}
value={searchQuery}
style={{ minWidth: 360, height: 30 }}
autoComplete="off"
className="px-2 py-1 pe-9 text-lg placeholder-lg !border-0 rounded-e-full nofocus"
leadingButton={
searchQuery !== '' ? (
<div
className={'h-full flex items-center cursor-pointer'}
onClick={hasFilters ? clearAll : fetchResults}
>
<div className={'px-2 py-1 hover:bg-active-blue rounded mr-2'}>
{hasFilters ? <CloseOutlined /> : <EnterOutlined />}
</div>
</div>
) : null
}
/>
</div>
);
}
);
function AiSessionSearchField(props: Props) {
const askTourKey = '__or__ask-tour';
const tabKey = '__or__tab';
const { aiFiltersStore } = useStore();
const isTourShown = localStorage.getItem(askTourKey) !== null;
const [tab, setTab] = useState(localStorage.getItem(tabKey) || 'search');
const [touring, setTouring] = useState(!isTourShown);
const [isFocused, setFocused] = React.useState(false);
const askAiRef = React.useRef(null);
const closeTour = () => {
setTouring(false);
localStorage.setItem(askTourKey, 'true');
};
const changeValue = (v?: string) => {
const newTab = v ? v : tab !== 'ask' ? 'ask' : 'search';
setTab(newTab);
localStorage.setItem(tabKey, newTab);
};
const boxStyle = tab === 'ask'
? gradientBox
: isFocused ? regularBoxFocused : regularBoxUnfocused;
return (
<div className={'bg-white rounded-full shadow-sm'}>
<div
className={aiFiltersStore.isLoading ? 'animate-bg-spin' : ''}
style={boxStyle}
>
<div ref={askAiRef} className={'px-2'}>
<AskAiSwitchToggle
enabled={tab === 'ask'}
setEnabled={changeValue}
loading={aiFiltersStore.isLoading}
/>
</div>
{tab === 'ask' ? (
<AiSearchField {...props} />
) : (
<SessionSearchField {...props} setFocused={setFocused} />
)}
<Tour
open={touring}
onClose={closeTour}
steps={[
{
title: (
<div
className={'text-xl font-semibold flex items-center gap-2'}
>
<span>Introducing</span>
<Icon name={'sparkles'} size={18} />
<span>Ask AI</span>
</div>
),
target: () => askAiRef.current,
description:
'Easily find sessions with our AI search. Just enable Ask AI, type in your query naturally, and the AI will swiftly and precisely display relevant sessions.',
nextButtonProps: {
children: (
<OutsideClickDetectingDiv
onClickOutside={closeTour}
className={
'w-full h-full text-white flex items-center gap-2'
}
>
<span>Ask AI</span>
<Icon
name={'arrow-right-short'}
size={16}
color={'white'}
/>
</OutsideClickDetectingDiv>
),
onClick: () => {
changeValue('ask');
closeTour();
}
}
}
]}
/>
</div>
</div>
);
}
export const AskAiSwitchToggle = ({
enabled,
setEnabled,
loading
}: {
enabled: boolean;
loading: boolean;
setEnabled: () => void;
}) => {
return (
<div
role="switch"
aria-checked={enabled}
onClick={() => setEnabled()}
className={loading ? 'animate-bg-spin' : ''}
style={{
position: 'relative',
display: 'inline-block',
height: 24,
background: enabled
? 'linear-gradient(-25deg, #394eff, #3EAAAf, #3ccf65)'
: 'rgb(170 170 170)',
backgroundSize: loading ? '200% 200%' : 'unset',
borderRadius: 100,
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
border: 0,
verticalAlign: 'middle'
}}
>
<div
style={{
display: 'inline-block',
insetInlineStart: enabled ? 'calc(100% - 21px)' : '3px',
position: 'absolute',
top: 3,
width: 18,
height: 18,
transition: 'all 0.2s ease-in-out',
background: '#fff',
borderRadius: 100,
verticalAlign: 'middle'
}}
/>
<div
style={{
display: 'inline-block',
overflow: 'hidden',
borderRadius: 100,
height: '100%',
transition: 'all 0.2s ease-in-out',
paddingInline: !enabled ? '30px 0px' : '10px 24px',
width: 88
}}
>
<div style={{ color: 'white', fontSize: 16 }}>Ask AI</div>
</div>
</div>
);
};
export const gradientBox = {
border: 'double 1.5px transparent',
borderRadius: '100px',
background:
'linear-gradient(#ffffff, #ffffff), linear-gradient(-45deg, #394eff, #3eaaaf, #3ccf65)',
backgroundOrigin: 'border-box',
backgroundSize: '200% 200%',
backgroundClip: 'content-box, border-box',
display: 'flex',
gap: '0.25rem',
alignItems: 'center',
width: '100%'
};
const regularBoxUnfocused = {
borderRadius: '100px',
border: 'solid 1.5px #BFBFBF',
background: '#fffff',
display: 'flex',
gap: '0.25rem',
alignItems: 'center',
width: '100%'
};
const regularBoxFocused = {
borderRadius: '100px',
border: 'solid 1.5px #394EFF',
background: '#fffff',
display: 'flex',
gap: '0.25rem',
alignItems: 'center',
width: '100%'
};
export default observer(AiSessionSearchField);