* 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,8 +1,11 @@
import { Card, Col, Modal, Row, Typography } from 'antd';
import { GalleryVertical, Plus } from 'lucide-react';
import React from 'react';
import {Card, Col, Modal, Row, Typography} from "antd";
import {GalleryVertical, Plus} from "lucide-react";
import NewDashboardModal from "Components/Dashboard/components/DashboardList/NewDashModal";
import {useStore} from "App/mstore";
import { useStore } from 'App/mstore';
import NewDashboardModal from 'Components/Dashboard/components/DashboardList/NewDashModal';
import AiQuery from './DashboardView/AiQuery';
interface Props {
open: boolean;
@ -10,14 +13,14 @@ interface Props {
}
function AddCardSelectionModal(props: Props) {
const {metricStore} = useStore();
const { metricStore } = useStore();
const [open, setOpen] = React.useState(false);
const [isLibrary, setIsLibrary] = React.useState(false);
const onCloseModal = () => {
setOpen(false);
props.onClose && props.onClose();
}
};
const onClick = (isLibrary: boolean) => {
if (!isLibrary) {
@ -25,7 +28,12 @@ function AddCardSelectionModal(props: Props) {
}
setIsLibrary(isLibrary);
setOpen(true);
}
};
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 (
<>
<Modal
@ -33,26 +41,51 @@ function AddCardSelectionModal(props: Props) {
open={props.open}
footer={null}
onCancel={props.onClose}
className='addCard'
className="addCard"
width={isSaas ? 900 : undefined}
>
<Row gutter={16} justify="center" className='py-5'>
<Col span={12}>
<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'}}/>
<Typography.Text strong>Add from library</Typography.Text>
{/*<p>Select from 12 available</p>*/}
{isSaas ? (
<>
<Row gutter={16} justify="center" className="py-2">
<AiQuery />
</Row>
<div
className={
'flex items-center justify-center w-full text-disabled-text'
}
>
or
</div>
</>
) : null}
<Row gutter={16} justify="center" className="py-5">
<Col span={12}>
<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' }} />
<Typography.Text strong>Add from library</Typography.Text>
</div>
</Col>
<Col span={12}>
<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'}}/>
<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' }} />
<Typography.Text strong>Create New</Typography.Text>
</div>
</Col>
</Row>
</Modal>
<NewDashboardModal open={open} onClose={onCloseModal} isAddingFromLibrary={isLibrary}/>
<NewDashboardModal
open={open}
onClose={onCloseModal}
isAddingFromLibrary={isLibrary}
/>
</>
);
}

View file

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

View file

@ -1,9 +1,11 @@
import React, { useEffect } from 'react';
import { Modal } from 'antd';
import SelectCard from './SelectCard';
import CreateCard from 'Components/Dashboard/components/DashboardList/NewDashModal/CreateCard';
import colors from 'tailwindcss/colors';
import React, { useEffect } from 'react';
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 {
onClose: () => void;
@ -18,10 +20,11 @@ const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
open,
isAddingFromLibrary = false,
isEnterprise = false,
isMobile = false
}) => {
isMobile = false,
}) => {
const [step, setStep] = React.useState<number>(0);
const [selectedCategory, setSelectedCategory] = React.useState<string>('product-analytics');
const [selectedCategory, setSelectedCategory] =
React.useState<string>('product-analytics');
useEffect(() => {
return () => {
@ -40,35 +43,42 @@ const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
closeIcon={false}
styles={{
content: {
backgroundColor: colors.gray[100]
}
backgroundColor: colors.gray[100],
},
}}
centered={true}
>
<div className="flex flex-col gap-4" style={{
<div
className="flex flex-col gap-4"
style={{
height: 'calc(100vh - 100px)',
overflowY: 'auto',
overflowX: 'hidden'
}}>
{step === 0 && <SelectCard onClose={onClose}
overflowX: 'hidden',
}}
>
{step === 0 && (
<SelectCard
onClose={onClose}
selected={selectedCategory}
setSelectedCategory={setSelectedCategory}
onCard={() => setStep(step + 1)}
isLibrary={isAddingFromLibrary}
isMobile={isMobile}
isEnterprise={isEnterprise} />}
isEnterprise={isEnterprise}
/>
)}
{step === 1 && <CreateCard onBack={() => setStep(0)} />}
</div>
</Modal>
</>
)
;
);
};
const mapStateToProps = (state: any) => ({
isMobile: state.getIn(['site', 'instance', 'platform']) === 'ios',
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee' ||
state.getIn(['user', 'account', 'edition']) === 'msaas'
isEnterprise:
state.getIn(['user', 'account', 'edition']) === 'ee' ||
state.getIn(['user', 'account', 'edition']) === 'msaas',
});
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 DashboardHeader from '../DashboardHeader';
import {useHistory} from "react-router";
import AiQuery from "./AiQuery";
interface IProps {
siteId: string;
@ -91,12 +92,16 @@ function DashboardView(props: Props) {
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 (
<Loader loading={loading}>
<div style={{maxWidth: '1360px', margin: 'auto'}}>
{/* @ts-ignore */}
<DashboardHeader renderReport={props.renderReport} siteId={siteId} dashboardId={dashboardId}/>
{isSaas ? <AiQuery /> : null}
<DashboardWidgetGrid
siteId={siteId}
dashboardId={dashboardId}

View file

@ -18,7 +18,6 @@ import {
TIMESERIES, TABLE, HEATMAP, FUNNEL, ERRORS, RESOURCE_MONITORING,
PERFORMANCE, WEB_VITALS, INSIGHTS, USER_PATH, RETENTION
} from 'App/constants/card';
import {useParams} from 'react-router-dom';
import {useHistory} from "react-router";
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',
borderRadius: '100px',
background:

View file

@ -1,5 +1,6 @@
import React from 'react';
import cn from 'classnames';
import React from 'react';
import { Icon } from 'UI';
interface Props {
@ -14,30 +15,56 @@ interface Props {
[x: string]: 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 {
height = 36,
width = 0,
className = '',
leadingButton = '',
wrapperClassName = '',
icon = '',
type = 'text',
rows = 4,
...rest
} = props;
return (
<div className={cn({ relative: icon || leadingButton }, wrapperClassName)}>
{icon && <Icon name={icon} className="absolute top-0 bottom-0 my-auto ml-4" size="14" />}
{icon && (
<Icon
name={icon}
className="absolute top-0 bottom-0 my-auto ml-4"
size="14"
/>
)}
{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 })}
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 })}
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 className="absolute top-0 bottom-0 right-0">{leadingButton}</div>
)}
</div>
);
});

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

View file

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

View file

@ -32,7 +32,7 @@ export default class AiService extends BaseService {
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', {
question: query,
chartType

View file

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

View file

@ -16655,6 +16655,25 @@ __metadata:
languageName: node
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":
version: 1.6.0
resolution: "loud-rejection@npm:1.6.0"
@ -18704,6 +18723,7 @@ __metadata:
jsbi: ^4.1.0
jshint: ^2.11.1
jspdf: ^2.5.1
lottie-react: ^2.4.0
lucide-react: ^0.396.0
luxon: ^1.24.1
microdiff: ^1.4.0