* ai query comp start

* ai cards...

* fix prop assign
This commit is contained in:
Delirium 2024-08-02 14:38:56 +02:00 committed by GitHub
parent 512230f224
commit 31d82f9d1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 367 additions and 124 deletions

View file

@ -1,60 +1,93 @@
import { Card, Col, Modal, Row, Typography } from 'antd';
import { GalleryVertical, Plus } from 'lucide-react';
import React from 'react'; import React from 'react';
import {Card, Col, Modal, Row, Typography} from "antd";
import {GalleryVertical, Plus} from "lucide-react"; import { useStore } from 'App/mstore';
import NewDashboardModal from "Components/Dashboard/components/DashboardList/NewDashModal"; import NewDashboardModal from 'Components/Dashboard/components/DashboardList/NewDashModal';
import {useStore} from "App/mstore";
import AiQuery from './DashboardView/AiQuery';
interface Props { interface Props {
open: boolean; open: boolean;
onClose?: () => void; onClose?: () => void;
} }
function AddCardSelectionModal(props: Props) { function AddCardSelectionModal(props: Props) {
const {metricStore} = useStore(); const { metricStore } = useStore();
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [isLibrary, setIsLibrary] = React.useState(false); const [isLibrary, setIsLibrary] = React.useState(false);
const onCloseModal = () => { const onCloseModal = () => {
setOpen(false); setOpen(false);
props.onClose && props.onClose(); props.onClose && props.onClose();
} };
const onClick = (isLibrary: boolean) => { const onClick = (isLibrary: boolean) => {
if (!isLibrary) { if (!isLibrary) {
metricStore.init(); metricStore.init();
}
setIsLibrary(isLibrary);
setOpen(true);
} }
return ( setIsLibrary(isLibrary);
<> setOpen(true);
<Modal };
title="Add a card to dashboard"
open={props.open} const originStr = window.env.ORIGIN || window.location.origin;
footer={null} const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true';
onCancel={props.onClose}
className='addCard' const isSaas = testingKey && /app\.openreplay\.com/.test(originStr);
return (
<>
<Modal
title="Add a card to dashboard"
open={props.open}
footer={null}
onCancel={props.onClose}
className="addCard"
width={isSaas ? 900 : undefined}
>
{isSaas ? (
<>
<Row gutter={16} justify="center" className="py-2">
<AiQuery />
</Row>
<div
className={
'flex items-center justify-center w-full text-disabled-text'
}
> >
<Row gutter={16} justify="center" className='py-5'> or
<Col span={12}> </div>
<div className="flex flex-col items-center justify-center hover:bg-indigo-50 border rounded-lg shadow-sm cursor-pointer gap-3" style={{height: '80px'}} onClick={() => onClick(true)}> </>
<GalleryVertical style={{fontSize: '24px', color: '#394EFF'}}/> ) : null}
<Typography.Text strong>Add from library</Typography.Text> <Row gutter={16} justify="center" className="py-5">
{/*<p>Select from 12 available</p>*/} <Col span={12}>
</div> <div
className="flex flex-col items-center justify-center hover:bg-indigo-50 border rounded-lg shadow-sm cursor-pointer gap-3"
</Col> style={{ height: '80px' }}
<Col span={12}> onClick={() => onClick(true)}
<div className="flex flex-col items-center justify-center hover:bg-indigo-50 border rounded-lg shadow-sm cursor-pointer gap-3" style={{height: '80px'}} onClick={() => onClick(false)}> >
<Plus style={{fontSize: '24px', color: '#394EFF'}}/> <GalleryVertical style={{ fontSize: '24px', color: '#394EFF' }} />
<Typography.Text strong>Create New</Typography.Text> <Typography.Text strong>Add from library</Typography.Text>
</div> </div>
</Col> </Col>
</Row> <Col span={12}>
</Modal> <div
<NewDashboardModal open={open} onClose={onCloseModal} isAddingFromLibrary={isLibrary}/> className="flex flex-col items-center justify-center hover:bg-indigo-50 border rounded-lg shadow-sm cursor-pointer gap-3"
</> style={{ height: '80px' }}
); onClick={() => onClick(false)}
>
<Plus style={{ fontSize: '24px', color: '#394EFF' }} />
<Typography.Text strong>Create New</Typography.Text>
</div>
</Col>
</Row>
</Modal>
<NewDashboardModal
open={open}
onClose={onCloseModal}
isAddingFromLibrary={isLibrary}
/>
</>
);
} }
export default AddCardSelectionModal; export default AddCardSelectionModal;

View file

@ -20,7 +20,8 @@ const getTitleByType = (type: string) => {
interface Props { interface Props {
// cardType: string, // cardType: string,
onBack: () => void onBack?: () => void
onAdded?: () => void
} }
function CreateCard(props: Props) { function CreateCard(props: Props) {
@ -67,7 +68,8 @@ function CreateCard(props: Props) {
if (dashboardId) { if (dashboardId) {
await addCardToDashboard(dashboardId, cardId); await addCardToDashboard(dashboardId, cardId);
dashboardStore.fetch(dashboardId); void dashboardStore.fetch(dashboardId);
props.onAdded?.();
} else if (isItDashboard) { } else if (isItDashboard) {
const dashboardId = await createNewDashboard(); const dashboardId = await createNewDashboard();
await addCardToDashboard(dashboardId, cardId); await addCardToDashboard(dashboardId, cardId);
@ -81,9 +83,9 @@ function CreateCard(props: Props) {
<div className="flex gap-4 flex-col"> <div className="flex gap-4 flex-col">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Space> <Space>
<Button type="text" onClick={props.onBack}> {props.onBack ? <Button type="text" onClick={props.onBack}>
<ArrowLeft size={16}/> <ArrowLeft size={16} />
</Button> </Button> : null}
<div className="text-xl leading-4 font-medium"> <div className="text-xl leading-4 font-medium">
{metric.name} {metric.name}
</div> </div>

View file

@ -1,9 +1,11 @@
import React, { useEffect } from 'react';
import { Modal } from 'antd'; import { Modal } from 'antd';
import SelectCard from './SelectCard'; import React, { useEffect } from 'react';
import CreateCard from 'Components/Dashboard/components/DashboardList/NewDashModal/CreateCard';
import colors from 'tailwindcss/colors';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import colors from 'tailwindcss/colors';
import CreateCard from 'Components/Dashboard/components/DashboardList/NewDashModal/CreateCard';
import SelectCard from './SelectCard';
interface NewDashboardModalProps { interface NewDashboardModalProps {
onClose: () => void; onClose: () => void;
@ -14,14 +16,15 @@ interface NewDashboardModalProps {
} }
const NewDashboardModal: React.FC<NewDashboardModalProps> = ({ const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
onClose, onClose,
open, open,
isAddingFromLibrary = false, isAddingFromLibrary = false,
isEnterprise = false, isEnterprise = false,
isMobile = false isMobile = false,
}) => { }) => {
const [step, setStep] = React.useState<number>(0); const [step, setStep] = React.useState<number>(0);
const [selectedCategory, setSelectedCategory] = React.useState<string>('product-analytics'); const [selectedCategory, setSelectedCategory] =
React.useState<string>('product-analytics');
useEffect(() => { useEffect(() => {
return () => { return () => {
@ -40,35 +43,42 @@ const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
closeIcon={false} closeIcon={false}
styles={{ styles={{
content: { content: {
backgroundColor: colors.gray[100] backgroundColor: colors.gray[100],
} },
}} }}
centered={true} centered={true}
> >
<div className="flex flex-col gap-4" style={{ <div
height: 'calc(100vh - 100px)', className="flex flex-col gap-4"
overflowY: 'auto', style={{
overflowX: 'hidden' height: 'calc(100vh - 100px)',
}}> overflowY: 'auto',
{step === 0 && <SelectCard onClose={onClose} overflowX: 'hidden',
selected={selectedCategory} }}
setSelectedCategory={setSelectedCategory} >
onCard={() => setStep(step + 1)} {step === 0 && (
isLibrary={isAddingFromLibrary} <SelectCard
isMobile={isMobile} onClose={onClose}
isEnterprise={isEnterprise} />} selected={selectedCategory}
setSelectedCategory={setSelectedCategory}
onCard={() => setStep(step + 1)}
isLibrary={isAddingFromLibrary}
isMobile={isMobile}
isEnterprise={isEnterprise}
/>
)}
{step === 1 && <CreateCard onBack={() => setStep(0)} />} {step === 1 && <CreateCard onBack={() => setStep(0)} />}
</div> </div>
</Modal> </Modal>
</> </>
) );
;
}; };
const mapStateToProps = (state: any) => ({ const mapStateToProps = (state: any) => ({
isMobile: state.getIn(['site', 'instance', 'platform']) === 'ios', isMobile: state.getIn(['site', 'instance', 'platform']) === 'ios',
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee' || isEnterprise:
state.getIn(['user', 'account', 'edition']) === 'msaas' state.getIn(['user', 'account', 'edition']) === 'ee' ||
state.getIn(['user', 'account', 'edition']) === 'msaas',
}); });
export default connect(mapStateToProps)(NewDashboardModal); export default connect(mapStateToProps)(NewDashboardModal);

View file

@ -0,0 +1,129 @@
import { SendOutlined } from '@ant-design/icons';
import { Modal } from 'antd';
import Lottie from 'lottie-react';
import { observer } from 'mobx-react-lite';
import React from 'react';
import colors from 'tailwindcss/colors';
import { gradientBox } from 'App/components/shared/SessionSearchField/AiSessionSearchField';
import aiSpinner from 'App/lottie/aiSpinner.json';
import { useStore } from 'App/mstore';
import { Icon, Input } from 'UI';
import CreateCard from '../DashboardList/NewDashModal/CreateCard';
function AiQuery() {
const grad = {
background: 'linear-gradient(90deg, #F3F4FF 0%, #F2FEFF 100%)',
};
return (
<>
<QueryModal />
<div className={'rounded p-4 mb-4'} style={grad}>
<InputBox />
</div>
</>
);
}
const InputBox = observer(({ inModal }: { inModal?: boolean }) => {
const { aiFiltersStore, metricStore } = useStore();
const metric = metricStore.instance;
const fetchResults = () => {
aiFiltersStore
.getCardFilters(aiFiltersStore.query, undefined)
.then((f) => metric.createSeries(f.filters));
if (!inModal) {
aiFiltersStore.setModalOpen(true);
}
};
return (
<>
<div className={'flex items-center mb-2 gap-2'}>
<Icon name={'sparkles'} size={16} />
<div className={'font-semibold'}>What would you like to visualize?</div>
</div>
<div style={gradientBox}>
<Input
wrapperClassName={'w-full pr-2'}
value={aiFiltersStore.query}
style={{
minWidth: inModal ? '600px' : '840px',
height: 34,
borderRadius: 32,
}}
onChange={({ target }: any) => aiFiltersStore.setQuery(target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && aiFiltersStore.query.trim().length > 2) {
fetchResults();
}
}}
placeholder={'E.g., Track all the errors in checkout flow.'}
className="ml-2 px-2 pe-9 text-lg placeholder-lg !border-0 rounded-e-full nofocus"
leadingButton={
aiFiltersStore.query !== '' ? (
<div
className={'h-full flex items-center cursor-pointer'}
onClick={fetchResults}
>
<div className={'px-2 py-1 hover:bg-active-blue rounded mr-2'}>
<SendOutlined />
</div>
</div>
) : null
}
/>
</div>
</>
);
});
const QueryModal = observer(() => {
const { aiFiltersStore } = useStore();
const onClose = () => {
aiFiltersStore.setModalOpen(false);
};
return (
<Modal
open={aiFiltersStore.modalOpen}
onCancel={onClose}
width={900}
destroyOnClose={true}
footer={null}
closeIcon={false}
styles={{
content: {
backgroundColor: colors.gray[100],
},
}}
centered={true}
>
<div className={'flex flex-col gap-2'}>
<InputBox inModal />
{aiFiltersStore.isLoading ? (
<Loader />
) : (
<CreateCard onAdded={onClose} />
)}
</div>
</Modal>
);
});
function Loader() {
return (
<div
className={
'flex items-center justify-center flex-col font-semibold text-xl min-h-80'
}
>
<div style={{ width: 150, height: 150 }}>
<Lottie animationData={aiSpinner} loop={true} />
</div>
<div>AI is brewing your card, wait a few seconds...</div>
</div>
);
}
export default observer(AiQuery);

View file

@ -13,6 +13,7 @@ import withPageTitle from 'HOCs/withPageTitle';
import withReport from 'App/components/hocs/withReport'; import withReport from 'App/components/hocs/withReport';
import DashboardHeader from '../DashboardHeader'; import DashboardHeader from '../DashboardHeader';
import {useHistory} from "react-router"; import {useHistory} from "react-router";
import AiQuery from "./AiQuery";
interface IProps { interface IProps {
siteId: string; siteId: string;
@ -91,12 +92,16 @@ function DashboardView(props: Props) {
if (!dashboard) return null; if (!dashboard) return null;
const originStr = window.env.ORIGIN || window.location.origin;
const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true';
const isSaas = testingKey && /app\.openreplay\.com/.test(originStr);
return ( return (
<Loader loading={loading}> <Loader loading={loading}>
<div style={{maxWidth: '1360px', margin: 'auto'}}> <div style={{maxWidth: '1360px', margin: 'auto'}}>
{/* @ts-ignore */} {/* @ts-ignore */}
<DashboardHeader renderReport={props.renderReport} siteId={siteId} dashboardId={dashboardId}/> <DashboardHeader renderReport={props.renderReport} siteId={siteId} dashboardId={dashboardId}/>
{isSaas ? <AiQuery /> : null}
<DashboardWidgetGrid <DashboardWidgetGrid
siteId={siteId} siteId={siteId}
dashboardId={dashboardId} dashboardId={dashboardId}

View file

@ -18,7 +18,6 @@ import {
TIMESERIES, TABLE, HEATMAP, FUNNEL, ERRORS, RESOURCE_MONITORING, TIMESERIES, TABLE, HEATMAP, FUNNEL, ERRORS, RESOURCE_MONITORING,
PERFORMANCE, WEB_VITALS, INSIGHTS, USER_PATH, RETENTION PERFORMANCE, WEB_VITALS, INSIGHTS, USER_PATH, RETENTION
} from 'App/constants/card'; } from 'App/constants/card';
import {useParams} from 'react-router-dom';
import {useHistory} from "react-router"; import {useHistory} from "react-router";
const tableOptions = metricOf.filter((i) => i.type === 'table'); const tableOptions = metricOf.filter((i) => i.type === 'table');

View file

@ -313,7 +313,7 @@ export const AskAiSwitchToggle = ({
); );
}; };
const gradientBox = { export const gradientBox = {
border: 'double 1.5px transparent', border: 'double 1.5px transparent',
borderRadius: '100px', borderRadius: '100px',
background: background:

View file

@ -1,45 +1,72 @@
import React from 'react';
import cn from 'classnames'; import cn from 'classnames';
import React from 'react';
import { Icon } from 'UI'; import { Icon } from 'UI';
interface Props { interface Props {
wrapperClassName?: string; wrapperClassName?: string;
className?: string; className?: string;
icon?: string; icon?: string;
leadingButton?: React.ReactNode; leadingButton?: React.ReactNode;
type?: string; type?: string;
rows?: number; rows?: number;
height?: number; height?: number;
width?: number; width?: number;
[x: string]: any; [x: string]: any;
} }
const Input = React.forwardRef((props: Props, ref: any) => { const Input = React.forwardRef((props: Props, ref: any) => {
const { height = 36, width = 0, className = '', leadingButton = '', wrapperClassName = '', icon = '', type = 'text', rows = 4, ...rest } = props; const {
return ( height = 36,
<div className={cn({ relative: icon || leadingButton }, wrapperClassName)}> width = 0,
{icon && <Icon name={icon} className="absolute top-0 bottom-0 my-auto ml-4" size="14" />} className = '',
{type === 'textarea' ? ( leadingButton = '',
<textarea wrapperClassName = '',
ref={ref} icon = '',
rows={rows} type = 'text',
style={{ resize: 'none' }} rows = 4,
maxLength={500} ...rest
className={cn('p-2 border border-gray-light bg-white w-full rounded-lg', className, { 'pl-10': icon })} } = props;
{...rest} return (
/> <div className={cn({ relative: icon || leadingButton }, wrapperClassName)}>
) : ( {icon && (
<input <Icon
ref={ref} name={icon}
type={type} className="absolute top-0 bottom-0 my-auto ml-4"
style={{ height: `${height}px`, width: width? `${width}px` : '' }} size="14"
className={cn('p-2 border border-gray-light bg-white w-full rounded-lg', className, { 'pl-10': icon })} />
{...rest} )}
/> {type === 'textarea' ? (
)} <textarea
ref={ref}
rows={rows}
style={{ resize: 'none' }}
maxLength={500}
className={cn(
'p-2 border border-gray-light bg-white w-full rounded-lg',
className,
{ 'pl-10': icon }
)}
{...rest}
/>
) : (
<input
ref={ref}
type={type}
style={{ height: `${height}px`, width: width ? `${width}px` : '' }}
className={cn(
'p-2 border border-gray-light bg-white w-full rounded-lg',
className,
{ 'pl-10': icon }
)}
{...rest}
/>
)}
{leadingButton && <div className="absolute top-0 bottom-0 right-0">{leadingButton}</div>} {leadingButton && (
</div> <div className="absolute top-0 bottom-0 right-0">{leadingButton}</div>
); )}
</div>
);
}); });
export default Input; export default Input;

File diff suppressed because one or more lines are too long

View file

@ -10,11 +10,21 @@ export default class AiFiltersStore {
cardFilters: Record<string, any> = { filters: [] }; cardFilters: Record<string, any> = { filters: [] };
filtersSetKey = 0; filtersSetKey = 0;
isLoading: boolean = false; isLoading: boolean = false;
query: string = '';
modalOpen: boolean = false;
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);
} }
setQuery = (query: string): void => {
this.query = query;
}
setModalOpen = (isOpen: boolean): void => {
this.modalOpen = isOpen;
}
setFilters = (filters: Record<string, any>): void => { setFilters = (filters: Record<string, any>): void => {
this.filters = filters; this.filters = filters;
this.filtersSetKey += 1; this.filtersSetKey += 1;
@ -35,8 +45,12 @@ export default class AiFiltersStore {
console.log(r) console.log(r)
} }
getCardFilters = async (query: string, chartType: string): Promise<any> => { setLoading = (loading: boolean): void => {
this.isLoading = true; this.isLoading = loading;
}
getCardFilters = async (query: string, chartType?: string): Promise<any> => {
this.setLoading(true)
try { try {
const r = await aiService.getCardFilters(query, chartType); const r = await aiService.getCardFilters(query, chartType);
const filterObj = Filter({ const filterObj = Filter({
@ -57,6 +71,7 @@ export default class AiFiltersStore {
? { ...filtersMap[matchingFilter], ...f } ? { ...filtersMap[matchingFilter], ...f }
: { ...f, value: f.value ?? [] }; : { ...f, value: f.value ?? [] };
return { return {
type: filter.key,
...filter, ...filter,
value: filter.value value: filter.value
? filter.value.map((i: string) => parseInt(i, 10) * 60 * 1000) ? filter.value.map((i: string) => parseInt(i, 10) * 60 * 1000)
@ -76,7 +91,7 @@ export default class AiFiltersStore {
} catch (e) { } catch (e) {
console.trace(e); console.trace(e);
} finally { } finally {
this.isLoading = false; this.setLoading(false)
} }
}; };

View file

@ -62,6 +62,7 @@ export default class FilterItem {
} }
fromData(data: any) { fromData(data: any) {
Object.assign(this, data)
this.type = data.type this.type = data.type
this.key = data.key this.key = data.key
this.label = data.label this.label = data.label
@ -78,7 +79,7 @@ export default class FilterItem {
this.isActive = Boolean(data.isActive) this.isActive = Boolean(data.isActive)
this.completed = data.completed this.completed = data.completed
this.dropped = data.dropped this.dropped = data.dropped
this.options = data.options
return this return this
} }

View file

@ -32,7 +32,7 @@ export default class AiService extends BaseService {
return data; return data;
} }
async getCardFilters(query: string, chartType: string): Promise<Record<string, any>> { async getCardFilters(query: string, chartType?: string): Promise<Record<string, any>> {
const r = await this.client.post('/intelligent/search-plus', { const r = await this.client.post('/intelligent/search-plus', {
question: query, question: query,
chartType chartType

View file

@ -49,6 +49,7 @@
"jsbi": "^4.1.0", "jsbi": "^4.1.0",
"jshint": "^2.11.1", "jshint": "^2.11.1",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"lottie-react": "^2.4.0",
"lucide-react": "^0.396.0", "lucide-react": "^0.396.0",
"luxon": "^1.24.1", "luxon": "^1.24.1",
"microdiff": "^1.4.0", "microdiff": "^1.4.0",

View file

@ -16655,6 +16655,25 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lottie-react@npm:^2.4.0":
version: 2.4.0
resolution: "lottie-react@npm:2.4.0"
dependencies:
lottie-web: ^5.10.2
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 5c0ef3f1832b21232fe6826cc021cd90bb0e3c9d63f1047031ce77a0992092f8712b6f3a6aeeaa0f410d918ca557df160b1c776399f69b498c560273767befe0
languageName: node
linkType: hard
"lottie-web@npm:^5.10.2":
version: 5.12.2
resolution: "lottie-web@npm:5.12.2"
checksum: 0aeaf631b10a76afd025df70c2a1486543530708e07a316946c08e55891dac483ffbaf2bf3648ae0b9c54c733118a0a086fd150aa76f7848606214c67ad72c30
languageName: node
linkType: hard
"loud-rejection@npm:^1.0.0": "loud-rejection@npm:^1.0.0":
version: 1.6.0 version: 1.6.0
resolution: "loud-rejection@npm:1.6.0" resolution: "loud-rejection@npm:1.6.0"
@ -18704,6 +18723,7 @@ __metadata:
jsbi: ^4.1.0 jsbi: ^4.1.0
jshint: ^2.11.1 jshint: ^2.11.1
jspdf: ^2.5.1 jspdf: ^2.5.1
lottie-react: ^2.4.0
lucide-react: ^0.396.0 lucide-react: ^0.396.0
luxon: ^1.24.1 luxon: ^1.24.1
microdiff: ^1.4.0 microdiff: ^1.4.0