diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx index cf827aa80..9e845c1e8 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx @@ -11,7 +11,7 @@ interface Props { function InsightItem(props: Props) { const { item, onClick = () => {} } = props; const className = - 'flex items-start flex-wrap py-4 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer'; + 'flex items-start py-3 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer'; switch (item.category) { case IssueCategory.RAGE: @@ -52,7 +52,7 @@ function ErrorItem({ item, className, onClick }: any) {
{item.isNew ? ( -
+
Users are encountering a new error called:
{item.name}
This error has occurred a total of
@@ -60,7 +60,7 @@ function ErrorItem({ item, className, onClick }: any) {
times
) : ( -
+
There has been an
{item.isIncreased ? 'increase' : 'decrease'}
in the error
@@ -82,7 +82,7 @@ function NetworkItem({ item, className, onClick }: any) { return (
-
+
Network request to path
{item.name}
has {item.change > 0 ? 'increased' : 'decreased'}
@@ -96,7 +96,7 @@ function ResourcesItem({ item, className, onClick }: any) { return (
-
+
There has been
{item.change > 0 ? 'Increase' : 'Decrease'}
in
@@ -113,14 +113,14 @@ function RageItem({ item, className, onClick }: any) {
{item.isNew ? ( -
+
New Click Rage detected
{item.value}
times on
{item.name}
) : ( -
+
Click rage has
{item.isIncreased ? 'increased' : 'decreased'} on
{item.name}
diff --git a/frontend/app/components/Session_/Player/Controls/Controls.tsx b/frontend/app/components/Session_/Player/Controls/Controls.tsx index d6d1d2c9b..bf1f010a7 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.tsx +++ b/frontend/app/components/Session_/Player/Controls/Controls.tsx @@ -386,7 +386,7 @@ export function SummaryButton({ ); } -const gradientButton = { +export const gradientButton = { border: 'double 1px transparent', borderRadius: '60px', background: diff --git a/frontend/app/components/shared/SessionSearchField/AiSessionSearchField.tsx b/frontend/app/components/shared/SessionSearchField/AiSessionSearchField.tsx index ff721d5c0..ac83aa3bb 100644 --- a/frontend/app/components/shared/SessionSearchField/AiSessionSearchField.tsx +++ b/frontend/app/components/shared/SessionSearchField/AiSessionSearchField.tsx @@ -1,19 +1,28 @@ +import { CloseOutlined, EnterOutlined } from '@ant-design/icons'; +import { Tour } from 'antd'; +import { observer } from 'mobx-react-lite'; 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 { useStore } from 'App/mstore'; import { assist as assistRoute, isRoute } from 'App/routes'; -import { addFilterByKeyAndValue, fetchFilterSearch, edit, clearSearch } from 'Duck/search'; +import { debounce } from 'App/utils'; 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'; -import { EnterOutlined, CloseOutlined } from '@ant-design/icons'; +import { + addFilterByKeyAndValue, + clearSearch, + edit, + fetchFilterSearch, +} from 'Duck/search'; +import { Icon, Input, Toggler as Switch } from 'UI'; + +import FilterModal from 'Shared/Filters/FilterModal'; + +import { SwitchToggle } from '../../ui/Toggler/Toggler'; +import OutsideClickDetectingDiv from '../OutsideClickDetectingDiv'; const ASSIST_ROUTE = assistRoute(); @@ -32,7 +41,10 @@ function SessionSearchField(props: Props) { isRoute(ASSIST_ROUTE, window.location.pathname) || window.location.pathname.includes('multiview'); const debounceFetchFilterSearch = React.useCallback( - debounce(isLive ? props.liveFetchFilterSearch : props.fetchFilterSearch, 1000), + debounce( + isLive ? props.liveFetchFilterSearch : props.fetchFilterSearch, + 1000 + ), [] ); @@ -64,12 +76,14 @@ function SessionSearchField(props: Props) { onFocus={onFocus} onBlur={onBlur} onChange={onSearchChange} - placeholder={'Search sessions using any captured event (click, input, page, error...)'} - style={{ minWidth: 360 }} + placeholder={ + 'Search sessions using any captured event (click, input, page, error...)' + } + style={{ minWidth: 360, height: 33 }} id="search" type="search" autoComplete="off" - className="text-lg placeholder-lg !border-0 w-full rounded-r-lg focus:!border-0 focus:ring-0" + className="px-2 py-1 text-lg placeholder-lg !border-0 rounded-r-lg focus:!border-0 focus:ring-0" /> {showModal && ( @@ -86,126 +100,217 @@ function SessionSearchField(props: Props) { ); } -const AiSearchField = observer(({ edit, appliedFilter, clearSearch }: Props) => { - const hasFilters = appliedFilter && appliedFilter.filters && appliedFilter.filters.size > 0; - const { aiFiltersStore } = useStore(); - const [searchQuery, setSearchQuery] = useState(''); - const debounceAiFetch = React.useCallback(debounce(aiFiltersStore.getSearchFilters, 1000), []); +const AiSearchField = observer( + ({ edit, appliedFilter, clearSearch }: Props) => { + const hasFilters = + appliedFilter && appliedFilter.filters && appliedFilter.filters.size > 0; + const { aiFiltersStore } = useStore(); + const [searchQuery, setSearchQuery] = useState(''); + const debounceAiFetch = React.useCallback( + debounce(aiFiltersStore.getSearchFilters, 1000), + [] + ); - const onSearchChange = ({ target: { value } }: any) => { - setSearchQuery(value); - }; + const onSearchChange = ({ target: { value } }: any) => { + setSearchQuery(value); + }; - const fetchResults = () => { - if (searchQuery) { - debounceAiFetch(searchQuery); - } - }; + const fetchResults = () => { + if (searchQuery) { + debounceAiFetch(searchQuery); + } + }; - const handleKeyDown = (event: any) => { - if (event.key === 'Enter') { - fetchResults(); - } - }; + const handleKeyDown = (event: any) => { + if (event.key === 'Enter') { + fetchResults(); + } + }; - const clearAll = () => { - clearSearch(); - setSearchQuery(''); - }; + const clearAll = () => { + clearSearch(); + setSearchQuery(''); + }; - React.useEffect(() => { - if (aiFiltersStore.filtersSetKey !== 0) { - console.log('updating filters', aiFiltersStore.filters, aiFiltersStore.filtersSetKey); - edit(aiFiltersStore.filters); - } - }, [aiFiltersStore.filters, aiFiltersStore.filtersSetKey]); + React.useEffect(() => { + if (aiFiltersStore.filtersSetKey !== 0) { + edit(aiFiltersStore.filters); + } + }, [aiFiltersStore.filters, aiFiltersStore.filtersSetKey]); - return ( -
- -
- {hasFilters ? : } + return ( +
+ +
+ {hasFilters ? : } +
-
- ) : null - } - /> -
- ); -}); + ) : null + } + /> +
+ ); + } +); function AiSessionSearchField(props: Props) { - const [tab, setTab] = useState('search'); - const [isFocused, setIsFocused] = useState(false); + 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 askAiRef = React.useRef(null); - const boxStyle = isFocused ? gradientBox : gradientBoxUnfocused; + 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); + }; return ( - setIsFocused(false)} - className={'bg-white rounded-lg'} - > -
setIsFocused(true)}> - setTab(value as string)} - options={[ +
+
+
+ +
+ {tab === 'ask' ? ( + + ) : ( + + )} + - - Search -
- ), - value: 'search', - }, - { - label: ( -
- + title: ( +
+ Introducing + Ask AI
), - value: 'ask', + 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: ( + + Ask AI + + + ), + onClick: () => { + changeValue('tab'); + closeTour(); + }, + }, }, ]} /> - {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%', +export const AskAiSwitchToggle = ({ + enabled, + setEnabled, + loading, +}: { + enabled: boolean; + loading: boolean; + setEnabled: () => void; +}) => { + return ( +
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', + }} + > +
+
+
Ask AI
+
+
+ ); }; const gradientBoxUnfocused = { borderRadius: '6px', border: 'double 1px transparent', - background: '#f6f6f6', + background: 'white', display: 'flex', gap: '0.25rem', alignItems: 'center', diff --git a/frontend/app/components/ui/Toggler/Toggler.js b/frontend/app/components/ui/Toggler/Toggler.js index 0810a4357..299db9080 100644 --- a/frontend/app/components/ui/Toggler/Toggler.js +++ b/frontend/app/components/ui/Toggler/Toggler.js @@ -1,14 +1,31 @@ import React from 'react'; + import styles from './toggler.module.css'; -export default ({ onChange, name, className = '', checked, label = '', plain = false }) => ( -
- -
+export default ({ + onChange, + name, + className = '', + checked, + label = '', + plain = false, +}) => ( +
+ +
); diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 99256c5f1..50f471bb9 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -22,10 +22,22 @@ module.exports = { opacity: '1' // transform: 'translateY(0)' } + }, + 'bg-spin': { + '0%': { + backgroundPosition: '0 50%', + }, + '50%': { + backgroundPosition: '100% 50%', + }, + '100%': { + backgroundPosition: '0 50%', + } } }, animation: { - 'fade-in': 'fade-in 0.2s ease-out' + 'fade-in': 'fade-in 0.2s ease-out', + 'bg-spin': 'bg-spin 1s ease infinite' }, colors: { 'disabled-text': 'rgba(0,0,0, 0.38)'