feat(ui): ai summary UI (#1868)
* feat(ui): start ai summary UI * feat(ui): add api * feat(ui): rm console log * feat(ui): style fix * feat(ui): some ui changes * feat(ui): some ui changes * feat(ui): some text formatting * fix(ui): method fix * fix(ui): fix icon gen * fix(ui): fix global ts declaration for window env
This commit is contained in:
parent
b9e2cafe4b
commit
e0799f74e1
18 changed files with 531 additions and 69 deletions
|
|
@ -31,7 +31,8 @@ const siteIdRequiredPaths: string[] = [
|
||||||
'/feature-flags',
|
'/feature-flags',
|
||||||
'/check-recording-status',
|
'/check-recording-status',
|
||||||
'/usability-tests',
|
'/usability-tests',
|
||||||
'/tags'
|
'/tags',
|
||||||
|
'/intelligent'
|
||||||
];
|
];
|
||||||
|
|
||||||
export const clean = (obj: any, forbiddenValues: any[] = [undefined, '']): any => {
|
export const clean = (obj: any, forbiddenValues: any[] = [undefined, '']): any => {
|
||||||
|
|
@ -71,7 +72,7 @@ export default class APIClient {
|
||||||
this.siteId = siteId;
|
this.siteId = siteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getInit(method: string = 'GET', params?: any): RequestInit {
|
private getInit(method: string = 'GET', params?: any, reqHeaders?: Record<string, any>): RequestInit {
|
||||||
// Always fetch the latest JWT from the store
|
// Always fetch the latest JWT from the store
|
||||||
const jwt = store.getState().getIn(['user', 'jwt']);
|
const jwt = store.getState().getIn(['user', 'jwt']);
|
||||||
const headers = new Headers({
|
const headers = new Headers({
|
||||||
|
|
@ -79,6 +80,12 @@ export default class APIClient {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (reqHeaders) {
|
||||||
|
for (const [key, value] of Object.entries(reqHeaders)) {
|
||||||
|
headers.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
headers.set('Authorization', `Bearer ${jwt}`);
|
headers.set('Authorization', `Bearer ${jwt}`);
|
||||||
}
|
}
|
||||||
|
|
@ -122,15 +129,14 @@ export default class APIClient {
|
||||||
|
|
||||||
async fetch(path: string, params?: any, method: string = 'GET', options: {
|
async fetch(path: string, params?: any, method: string = 'GET', options: {
|
||||||
clean?: boolean
|
clean?: boolean
|
||||||
} = { clean: true }): Promise<Response> {
|
} = { clean: true }, headers?: Record<string, any>): Promise<Response> {
|
||||||
let jwt = store.getState().getIn(['user', 'jwt']);
|
let jwt = store.getState().getIn(['user', 'jwt']);
|
||||||
if (!path.includes('/refresh') && jwt && this.isTokenExpired(jwt)) {
|
if (!path.includes('/refresh') && jwt && this.isTokenExpired(jwt)) {
|
||||||
jwt = await this.handleTokenRefresh();
|
jwt = await this.handleTokenRefresh();
|
||||||
(this.init.headers as Headers).set('Authorization', `Bearer ${jwt}`);
|
(this.init.headers as Headers).set('Authorization', `Bearer ${jwt}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const init = this.getInit(method, options.clean && params ? clean(params) : params);
|
const init = this.getInit(method, options.clean && params ? clean(params) : params, headers);
|
||||||
|
|
||||||
|
|
||||||
if (params !== undefined) {
|
if (params !== undefined) {
|
||||||
const cleanedParams = options.clean ? clean(params) : params;
|
const cleanedParams = options.clean ? clean(params) : params;
|
||||||
|
|
@ -184,14 +190,14 @@ export default class APIClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get(path: string, params?: any, options?: any): Promise<Response> {
|
get(path: string, params?: any, options?: any, headers?: Record<string, any>): Promise<Response> {
|
||||||
this.init.method = 'GET';
|
this.init.method = 'GET';
|
||||||
return this.fetch(queried(path, params), 'GET', options);
|
return this.fetch(queried(path, params), options, 'GET', undefined, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
post(path: string, params?: any, options?: any): Promise<Response> {
|
post(path: string, params?: any, options?: any, headers?: Record<string, any>): Promise<Response> {
|
||||||
this.init.method = 'POST';
|
this.init.method = 'POST';
|
||||||
return this.fetch(path, params, 'POST');
|
return this.fetch(path, params, 'POST', options, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
put(path: string, params?: any, options?: any): Promise<Response> {
|
put(path: string, params?: any, options?: any): Promise<Response> {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
import { useStore } from 'App/mstore';
|
||||||
|
import SummaryBlock from "Components/Session/Player/ReplayPlayer/SummaryBlock";
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { Icon, Tooltip } from 'UI';
|
||||||
|
import QueueControls from 'App/components/Session_/QueueControls';
|
||||||
|
import Bookmark from 'Shared/Bookmark';
|
||||||
|
import SharePopup from 'Shared/SharePopup/SharePopup';
|
||||||
|
import Issues from 'App/components/Session_/Issues/Issues';
|
||||||
|
import NotePopup from 'App/components/Session_/components/NotePopup';
|
||||||
|
import ItemMenu from 'App/components/Session_/components/HeaderMenu';
|
||||||
|
import { useModal } from 'App/components/Modal';
|
||||||
|
import BugReportModal from 'App/components/Session_/BugReport/BugReportModal';
|
||||||
|
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import AutoplayToggle from 'Shared/AutoplayToggle';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import SessionTabs from 'Components/Session/Player/SharedComponents/SessionTabs';
|
||||||
|
import { IFRAME } from 'App/constants/storageKeys';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import { Switch } from 'antd';
|
||||||
|
|
||||||
|
const localhostWarn = (project: string): string => project + '_localhost_warn';
|
||||||
|
const disableDevtools = 'or_devtools_uxt_toggle';
|
||||||
|
|
||||||
|
function SubHeader(props: any) {
|
||||||
|
const localhostWarnKey = localhostWarn(props.siteId);
|
||||||
|
const defaultLocalhostWarn = localStorage.getItem(localhostWarnKey) !== '1';
|
||||||
|
const [showWarningModal, setWarning] = React.useState(defaultLocalhostWarn);
|
||||||
|
const { player, store } = React.useContext(PlayerContext);
|
||||||
|
const { width, height, endTime, location: currentLocation = 'loading...' } = store.get();
|
||||||
|
const hasIframe = localStorage.getItem(IFRAME) === 'true';
|
||||||
|
const { uxtestingStore } = useStore();
|
||||||
|
|
||||||
|
const enabledIntegration = useMemo(() => {
|
||||||
|
const { integrations } = props;
|
||||||
|
if (!integrations || !integrations.size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrations.some((i: Record<string, any>) => i.token);
|
||||||
|
}, [props.integrations]);
|
||||||
|
|
||||||
|
const { showModal, hideModal } = useModal();
|
||||||
|
|
||||||
|
const location =
|
||||||
|
currentLocation && currentLocation.length > 70
|
||||||
|
? `${currentLocation.slice(0, 25)}...${currentLocation.slice(-40)}`
|
||||||
|
: currentLocation;
|
||||||
|
|
||||||
|
const showReportModal = () => {
|
||||||
|
const { tabStates, currentTab } = store.get();
|
||||||
|
const resourceList = tabStates[currentTab]?.resourceList || [];
|
||||||
|
const exceptionsList = tabStates[currentTab]?.exceptionsList || [];
|
||||||
|
const eventsList = tabStates[currentTab]?.eventList || [];
|
||||||
|
const graphqlList = tabStates[currentTab]?.graphqlList || [];
|
||||||
|
const fetchList = tabStates[currentTab]?.fetchList || [];
|
||||||
|
|
||||||
|
const mappedResourceList = resourceList
|
||||||
|
.filter((r) => r.isRed || r.isYellow)
|
||||||
|
// @ts-ignore
|
||||||
|
.concat(fetchList.filter((i) => parseInt(i.status) >= 400))
|
||||||
|
// @ts-ignore
|
||||||
|
.concat(graphqlList.filter((i) => parseInt(i.status) >= 400));
|
||||||
|
|
||||||
|
player.pause();
|
||||||
|
const xrayProps = {
|
||||||
|
currentLocation: currentLocation,
|
||||||
|
resourceList: mappedResourceList,
|
||||||
|
exceptionsList: exceptionsList,
|
||||||
|
eventsList: eventsList,
|
||||||
|
endTime: endTime,
|
||||||
|
};
|
||||||
|
showModal(
|
||||||
|
// @ts-ignore
|
||||||
|
<BugReportModal width={width} height={height} xrayProps={xrayProps} hideModal={hideModal} />,
|
||||||
|
{ right: true, width: 620 }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showSummary = () => {
|
||||||
|
player.pause();
|
||||||
|
showModal(<SummaryBlock sessionId={props.sessionId} />, { right: true, width: 330 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const showWarning =
|
||||||
|
location && /(localhost)|(127.0.0.1)|(0.0.0.0)/.test(location) && showWarningModal;
|
||||||
|
const closeWarning = () => {
|
||||||
|
localStorage.setItem(localhostWarnKey, '1');
|
||||||
|
setWarning(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDevtools = (enabled: boolean): void => {
|
||||||
|
localStorage.setItem(disableDevtools, enabled ? '0' : '1');
|
||||||
|
uxtestingStore.setHideDevtools(!enabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
const additionalMenu = [
|
||||||
|
{
|
||||||
|
key: 1,
|
||||||
|
component: <AutoplayToggle />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 2,
|
||||||
|
component: <Bookmark noMargin sessionId={props.sessionId} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 3,
|
||||||
|
component: (
|
||||||
|
<div onClick={showReportModal} className={'flex items-center gap-2 p-3 text-black'}>
|
||||||
|
<Icon name={'file-pdf'} size={16} />
|
||||||
|
<div>Create Bug Report</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 4,
|
||||||
|
component: (
|
||||||
|
<SharePopup
|
||||||
|
entity="sessions"
|
||||||
|
id={props.sessionId}
|
||||||
|
showCopyLink={true}
|
||||||
|
trigger={
|
||||||
|
<div className={'flex items-center gap-2 p-3 text-black'}>
|
||||||
|
<Icon name={'share-alt'} size={16} />
|
||||||
|
<div>Share</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (enabledIntegration) {
|
||||||
|
additionalMenu.push({
|
||||||
|
key: 5,
|
||||||
|
component: <Issues isInline={true} sessionId={props.sessionId} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="w-full px-4 flex items-center border-b relative"
|
||||||
|
style={{
|
||||||
|
background: uxtestingStore.isUxt() ? (props.live ? '#F6FFED' : '#EBF4F5') : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showWarning ? (
|
||||||
|
<div
|
||||||
|
className="px-3 py-1 border border-gray-light drop-shadow-md rounded bg-active-blue flex items-center justify-between"
|
||||||
|
style={{
|
||||||
|
zIndex: 999,
|
||||||
|
position: 'absolute',
|
||||||
|
left: '50%',
|
||||||
|
bottom: '-24px',
|
||||||
|
transform: 'translate(-50%, 0)',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Some assets may load incorrectly on localhost.
|
||||||
|
<a
|
||||||
|
href="https://docs.openreplay.com/en/troubleshooting/session-recordings/#testing-in-localhost"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="link ml-1"
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</a>
|
||||||
|
<div className="py-1 ml-3 cursor-pointer" onClick={closeWarning}>
|
||||||
|
<Icon name="close" size={16} color="black" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<SessionTabs />
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'ml-auto text-sm flex items-center color-gray-medium gap-2',
|
||||||
|
hasIframe ? 'opacity-50 pointer-events-none' : ''
|
||||||
|
)}
|
||||||
|
style={{ width: 'max-content' }}
|
||||||
|
>
|
||||||
|
<SummaryButton onClick={showSummary} />
|
||||||
|
<NotePopup />
|
||||||
|
<ItemMenu items={additionalMenu} />
|
||||||
|
{uxtestingStore.isUxt() ? (
|
||||||
|
<Switch
|
||||||
|
checkedChildren={'DevTools'}
|
||||||
|
unCheckedChildren={'DevTools'}
|
||||||
|
onChange={toggleDevtools}
|
||||||
|
defaultChecked={!uxtestingStore.hideDevtools}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<QueueControls />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{location && (
|
||||||
|
<div className={'w-full bg-white border-b border-gray-light'}>
|
||||||
|
<div className="flex w-fit items-center cursor-pointer color-gray-medium text-sm p-1">
|
||||||
|
<Icon size="20" name="event/link" className="mr-1" />
|
||||||
|
<Tooltip title="Open in new tab" delay={0}>
|
||||||
|
<a href={currentLocation} target="_blank">
|
||||||
|
{location}
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SummaryButton({ onClick }: { onClick?: () => void }) {
|
||||||
|
const [isHovered, setHovered] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={gradientButton}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div style={isHovered ? onHoverFillStyle : fillStyle} onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}>
|
||||||
|
<Icon name={'sparkles'} size={16} />
|
||||||
|
<div className={'font-semibold text-main'}>AI Summary</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gradientButton = {
|
||||||
|
border: 'double 1px transparent',
|
||||||
|
borderRadius: '60px',
|
||||||
|
background:
|
||||||
|
'linear-gradient(#f6f6f6, #f6f6f6), linear-gradient(to right, #394EFF 0%, #3EAAAF 100%)',
|
||||||
|
backgroundOrigin: 'border-box',
|
||||||
|
backgroundClip: 'content-box, border-box',
|
||||||
|
cursor: 'pointer',
|
||||||
|
};
|
||||||
|
const onHoverFillStyle = {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
borderRadius: '60px',
|
||||||
|
gap: 2,
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '4px 8px',
|
||||||
|
background:
|
||||||
|
'linear-gradient(156deg, #E3E6FF 0%, #E4F3F4 69.48%)',
|
||||||
|
};
|
||||||
|
const fillStyle = {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
borderRadius: '60px',
|
||||||
|
gap: 2,
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '4px 8px',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect((state: Record<string, any>) => ({
|
||||||
|
siteId: state.getIn(['site', 'siteId']),
|
||||||
|
integrations: state.getIn(['issues', 'list']),
|
||||||
|
modules: state.getIn(['user', 'account', 'modules']) || [],
|
||||||
|
}))(observer(SubHeader));
|
||||||
|
|
@ -3,7 +3,7 @@ import cn from 'classnames';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Player from './PlayerInst';
|
import Player from './PlayerInst';
|
||||||
import SubHeader from 'Components/Session_/Subheader';
|
import SubHeader from 'Components/Session_/Subheader';
|
||||||
|
import AiSubheader from 'Components/Session/Player/ReplayPlayer/AiSubheader';
|
||||||
import styles from 'Components/Session_/playerBlock.module.css';
|
import styles from 'Components/Session_/playerBlock.module.css';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
|
@ -11,39 +11,33 @@ interface IProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
activeTab: string;
|
activeTab: string;
|
||||||
jiraConfig: Record<string, any>
|
jiraConfig: Record<string, any>;
|
||||||
fullView?: boolean
|
fullView?: boolean;
|
||||||
|
setActiveTab: (tab: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlayerBlock(props: IProps) {
|
function PlayerBlock(props: IProps) {
|
||||||
const {
|
const { fullscreen, sessionId, disabled, activeTab, jiraConfig, fullView = false, setActiveTab } = props;
|
||||||
fullscreen,
|
|
||||||
sessionId,
|
|
||||||
disabled,
|
|
||||||
activeTab,
|
|
||||||
jiraConfig,
|
|
||||||
fullView = false,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const shouldShowSubHeader = !fullscreen && !fullView
|
const originStr = window.env.ORIGIN || window.location.origin
|
||||||
|
const isSaas = /api\.openreplay\.com/.test(originStr)
|
||||||
|
|
||||||
|
const shouldShowSubHeader = !fullscreen && !fullView;
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn(styles.playerBlock, 'flex flex-col', 'overflow-x-hidden')}>
|
||||||
className={cn(styles.playerBlock, 'flex flex-col', 'overflow-x-hidden')}
|
{shouldShowSubHeader ?
|
||||||
>
|
isSaas
|
||||||
{shouldShowSubHeader ? (
|
? <AiSubheader sessionId={sessionId} disabled={disabled} jiraConfig={jiraConfig} activeTab={activeTab} setActiveTab={setActiveTab} />
|
||||||
<SubHeader sessionId={sessionId} disabled={disabled} jiraConfig={jiraConfig} />
|
: <SubHeader sessionId={sessionId} disabled={disabled} jiraConfig={jiraConfig} />
|
||||||
) : null}
|
: null}
|
||||||
<Player
|
<Player activeTab={activeTab} fullView={fullView} />
|
||||||
activeTab={activeTab}
|
|
||||||
fullView={fullView}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect((state: any) => ({
|
export default connect((state: Record<string, any>) => ({
|
||||||
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
||||||
sessionId: state.getIn(['sessions', 'current']).sessionId,
|
sessionId: state.getIn(['sessions', 'current']).sessionId,
|
||||||
disabled: state.getIn(['components', 'targetDefiner', 'inspectorMode']),
|
disabled: state.getIn(['components', 'targetDefiner', 'inspectorMode']),
|
||||||
jiraConfig: state.getIn(['issues', 'list'])[0],
|
jiraConfig: state.getIn(['issues', 'list'])[0],
|
||||||
}))(PlayerBlock)
|
}))(PlayerBlock);
|
||||||
|
|
|
||||||
|
|
@ -60,27 +60,16 @@ function PlayerContent({ session, fullscreen, activeTab, setActiveTab }: IProps)
|
||||||
style={activeTab && !fullscreen ? { maxWidth: 'calc(100% - 270px)' } : undefined}
|
style={activeTab && !fullscreen ? { maxWidth: 'calc(100% - 270px)' } : undefined}
|
||||||
>
|
>
|
||||||
<div className={cn(styles.session, 'relative')} data-fullscreen={fullscreen}>
|
<div className={cn(styles.session, 'relative')} data-fullscreen={fullscreen}>
|
||||||
<PlayerBlock activeTab={activeTab} />
|
<PlayerBlock setActiveTab={setActiveTab} activeTab={activeTab} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{activeTab !== '' && (
|
{!fullscreen && activeTab !== '' ? (
|
||||||
<RightMenu
|
<RightBlock setActiveTab={setActiveTab} activeTab={activeTab} />
|
||||||
activeTab={activeTab}
|
) : null}
|
||||||
setActiveTab={setActiveTab}
|
|
||||||
fullscreen={fullscreen}
|
|
||||||
tabs={TABS}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RightMenu({ tabs, activeTab, setActiveTab, fullscreen }: any) {
|
|
||||||
return (
|
|
||||||
!fullscreen ? <RightBlock tabs={tabs} setActiveTab={setActiveTab} activeTab={activeTab} /> : null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default observer(PlayerContent);
|
export default observer(PlayerContent);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'UI/Icon';
|
||||||
|
import { useStore } from 'App/mstore';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
const userBehaviorRegex = /User\s+(\w+\s+)?Behavior/i;
|
||||||
|
const issuesErrorsRegex = /Issues\s+(and\s+|,?\s+)?(\w+\s+)?Errors/i;
|
||||||
|
|
||||||
|
function testLine(line: string): boolean {
|
||||||
|
return userBehaviorRegex.test(line) || issuesErrorsRegex.test(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SummaryBlock({ sessionId }: { sessionId: string }) {
|
||||||
|
const { aiSummaryStore } = useStore();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
void aiSummaryStore.getSummary(sessionId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const formattedText = aiSummaryStore.text.split('\n').map((line) => {
|
||||||
|
if (testLine(line)) {
|
||||||
|
return <div className={'font-semibold mt-2'}>{line}</div>;
|
||||||
|
}
|
||||||
|
if (line.startsWith('*')) {
|
||||||
|
return <li className={'ml-1 marker:mr-1'}>{line.replace('* ', '')}</li>;
|
||||||
|
}
|
||||||
|
return <div>{line}</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={summaryBlockStyle}>
|
||||||
|
<div className={'flex items-center gap-2'}>
|
||||||
|
<Icon name={'sparkles'} size={18} />
|
||||||
|
<div className={'font-semibold text-xl'}>AI Summary</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{aiSummaryStore.text ? (
|
||||||
|
<div className={'rounded p-4 bg-white whitespace-pre-wrap flex flex-col'}>
|
||||||
|
<div>
|
||||||
|
Here’s the AI breakdown of the session, covering user behavior and technical insights.
|
||||||
|
</div>
|
||||||
|
<>{formattedText.map((v) => v)}</>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<TextPlaceholder />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextPlaceholder() {
|
||||||
|
return (
|
||||||
|
<div className={'animate-pulse rounded p-4 bg-white whitespace-pre-wrap flex flex-col gap-2'}>
|
||||||
|
<div className={'h-2 bg-gray-medium rounded'} />
|
||||||
|
<div className={'h-2 bg-gray-medium rounded'} />
|
||||||
|
<div className={'grid grid-cols-3 gap-2'}>
|
||||||
|
<div className={'h-2 bg-gray-medium rounded col-span-2'} />
|
||||||
|
<div className={'h-2 bg-gray-medium rounded col-span-1'} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'grid grid-cols-4 gap-2 mt-3'}>
|
||||||
|
<div className={'h-2 bg-gray-medium rounded col-span-1'} />
|
||||||
|
<div className={'h-2 bg-gray-medium rounded col-span-1'} />
|
||||||
|
<div className={'h-2 bg-gray-medium rounded col-span-2'} />
|
||||||
|
</div>
|
||||||
|
<div className={'grid grid-cols-4 gap-2'}>
|
||||||
|
<div className={'h-2 bg-gray-medium rounded col-span-2'} />
|
||||||
|
<div className={'h-2 bg-transparent rounded col-span-2'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const summaryBlockStyle: React.CSSProperties = {
|
||||||
|
background: 'linear-gradient(156deg, #E3E6FF 0%, #E4F3F4 69.48%)',
|
||||||
|
width: '100%',
|
||||||
|
height: '100vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.5rem',
|
||||||
|
padding: '1rem',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(SummaryBlock);
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
import SummaryBlock from 'Components/Session/Player/ReplayPlayer/SummaryBlock';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Session from 'Types/session/session';
|
||||||
import EventsBlock from '../Session_/EventsBlock';
|
import EventsBlock from '../Session_/EventsBlock';
|
||||||
import PageInsightsPanel from '../Session_/PageInsightsPanel/PageInsightsPanel';
|
import PageInsightsPanel from '../Session_/PageInsightsPanel/PageInsightsPanel';
|
||||||
import TagWatch from "Components/Session/Player/TagWatch";
|
import TagWatch from 'Components/Session/Player/TagWatch';
|
||||||
|
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import stl from './rightblock.module.css';
|
import stl from './rightblock.module.css';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Popover, Button } from 'UI';
|
import { Popover, Button, Icon } from 'UI';
|
||||||
import IssuesModal from './IssuesModal';
|
import IssuesModal from './IssuesModal';
|
||||||
import { fetchProjects, fetchMeta } from 'Duck/assignments';
|
import { fetchProjects, fetchMeta } from 'Duck/assignments';
|
||||||
|
|
||||||
|
|
@ -53,10 +53,7 @@ class Issues extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { sessionId, issuesIntegration, isInline } = this.props;
|
||||||
sessionId,
|
|
||||||
issuesIntegration,
|
|
||||||
} = this.props;
|
|
||||||
const provider = issuesIntegration.first()?.provider || '';
|
const provider = issuesIntegration.first()?.provider || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -68,11 +65,18 @@ class Issues extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
{isInline ? (
|
||||||
<Button icon={`integrations/${provider === 'jira' ? 'jira' : 'github'}`} variant="text">
|
<div className={'flex items-center gap-2 p-3 text-black'}>
|
||||||
Create Issue
|
<Icon name={`integrations/${provider === 'jira' ? 'jira' : 'github'}`} size={16} />
|
||||||
</Button>
|
<div>Create Issue</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="relative">
|
||||||
|
<Button icon={`integrations/${provider === 'jira' ? 'jira' : 'github'}`} variant="text">
|
||||||
|
Create Issue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ function Bookmark(props: Props) {
|
||||||
color={isFavorite ? 'teal' : undefined}
|
color={isFavorite ? 'teal' : undefined}
|
||||||
size="16"
|
size="16"
|
||||||
/>
|
/>
|
||||||
<span className="ml-2">{isEnterprise ? 'Vault' : 'Bookmark'}</span>
|
<span className="ml-2 text-black">{isEnterprise ? 'Vault' : 'Bookmark'}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button data-favourite={isFavorite}>
|
<Button data-favourite={isFavorite}>
|
||||||
|
|
@ -54,7 +54,7 @@ function Bookmark(props: Props) {
|
||||||
color={isFavorite ? 'teal' : undefined}
|
color={isFavorite ? 'teal' : undefined}
|
||||||
size="16"
|
size="16"
|
||||||
/>
|
/>
|
||||||
<span className="ml-2">{isEnterprise ? 'Vault' : 'Bookmark'}</span>
|
<span className="ml-2 text-black">{isEnterprise ? 'Vault' : 'Bookmark'}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -427,6 +427,7 @@ export { default as Sleep } from './sleep';
|
||||||
export { default as Sliders } from './sliders';
|
export { default as Sliders } from './sliders';
|
||||||
export { default as Social_slack } from './social_slack';
|
export { default as Social_slack } from './social_slack';
|
||||||
export { default as Social_trello } from './social_trello';
|
export { default as Social_trello } from './social_trello';
|
||||||
|
export { default as Sparkles } from './sparkles';
|
||||||
export { default as Speedometer2 } from './speedometer2';
|
export { default as Speedometer2 } from './speedometer2';
|
||||||
export { default as Spinner } from './spinner';
|
export { default as Spinner } from './spinner';
|
||||||
export { default as Star_solid } from './star_solid';
|
export { default as Star_solid } from './star_solid';
|
||||||
|
|
|
||||||
19
frontend/app/components/ui/Icons/sparkles.tsx
Normal file
19
frontend/app/components/ui/Icons/sparkles.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
/* Auto-generated, do not edit */
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
width?: number | string;
|
||||||
|
height?: number | string;
|
||||||
|
fill?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sparkles(props: Props) {
|
||||||
|
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 15 14" fill="none" width={ `${ width }px` } height={ `${ height }px` } ><path d="m9.653 3.106 1.485 3.515 3.515 1.485-3.515 1.485-1.485 3.515L8.168 9.59 4.653 8.106 8.168 6.62l1.485-3.515Z" fill="#3EAAAF"/><path d="m3.313.894.89 2.11 2.11.89-2.11.891-.89 2.11-.891-2.11-2.109-.89 2.109-.892.891-2.109Z" fill="#394EFF"/></svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sparkles;
|
||||||
File diff suppressed because one or more lines are too long
59
frontend/app/mstore/aiSummaryStore.ts
Normal file
59
frontend/app/mstore/aiSummaryStore.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { aiService } from 'App/services';
|
||||||
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
|
||||||
|
export default class AiSummaryStore {
|
||||||
|
text = '';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(text: string) {
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendText(text: string) {
|
||||||
|
this.text += text;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSummary = async (sessionId: string) => {
|
||||||
|
this.setText('');
|
||||||
|
const respBody = await aiService.getSummary(sessionId);
|
||||||
|
if (!respBody) return;
|
||||||
|
|
||||||
|
const reader = respBody.getReader();
|
||||||
|
|
||||||
|
let lastIncompleteWord = '';
|
||||||
|
|
||||||
|
const processTextChunk = (textChunk: string) => {
|
||||||
|
textChunk = lastIncompleteWord + textChunk;
|
||||||
|
const words = textChunk.split(' ');
|
||||||
|
|
||||||
|
lastIncompleteWord = words.pop() || '';
|
||||||
|
|
||||||
|
words.forEach((word) => {
|
||||||
|
if(word) this.appendText(word + ' ');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
// Processing any remaining incomplete word at the end of the stream
|
||||||
|
if (lastIncompleteWord) {
|
||||||
|
this.appendText(lastIncompleteWord + ' ');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let textChunk = new TextDecoder().decode(value, { stream: true });
|
||||||
|
if (this.text === '') {
|
||||||
|
textChunk = textChunk.trimStart()
|
||||||
|
}
|
||||||
|
processTextChunk(textChunk);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -19,8 +19,8 @@ import WeeklyReportStore from './weeklyReportConfigStore';
|
||||||
import AlertStore from './alertsStore';
|
import AlertStore from './alertsStore';
|
||||||
import FeatureFlagsStore from './featureFlagsStore';
|
import FeatureFlagsStore from './featureFlagsStore';
|
||||||
import UxtestingStore from './uxtestingStore';
|
import UxtestingStore from './uxtestingStore';
|
||||||
import TagWatchStore from './tagWatchStore';
|
import TagWatchStore from './tagWatchStore';
|
||||||
|
import AiSummaryStore from "./aiSummaryStore";
|
||||||
export class RootStore {
|
export class RootStore {
|
||||||
dashboardStore: DashboardStore;
|
dashboardStore: DashboardStore;
|
||||||
metricStore: MetricStore;
|
metricStore: MetricStore;
|
||||||
|
|
@ -41,6 +41,7 @@ export class RootStore {
|
||||||
featureFlagsStore: FeatureFlagsStore;
|
featureFlagsStore: FeatureFlagsStore;
|
||||||
uxtestingStore: UxtestingStore;
|
uxtestingStore: UxtestingStore;
|
||||||
tagWatchStore: TagWatchStore;
|
tagWatchStore: TagWatchStore;
|
||||||
|
aiSummaryStore: AiSummaryStore;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.dashboardStore = new DashboardStore();
|
this.dashboardStore = new DashboardStore();
|
||||||
|
|
@ -62,6 +63,7 @@ export class RootStore {
|
||||||
this.featureFlagsStore = new FeatureFlagsStore();
|
this.featureFlagsStore = new FeatureFlagsStore();
|
||||||
this.uxtestingStore = new UxtestingStore();
|
this.uxtestingStore = new UxtestingStore();
|
||||||
this.tagWatchStore = new TagWatchStore();
|
this.tagWatchStore = new TagWatchStore();
|
||||||
|
this.aiSummaryStore = new AiSummaryStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
initClient() {
|
initClient() {
|
||||||
|
|
|
||||||
13
frontend/app/services/AiService.ts
Normal file
13
frontend/app/services/AiService.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import BaseService from 'App/services/BaseService';
|
||||||
|
|
||||||
|
export default class AiService extends BaseService {
|
||||||
|
/**
|
||||||
|
* @returns stream of text symbols
|
||||||
|
* */
|
||||||
|
async getSummary(sessionId: string) {
|
||||||
|
const r = await this.client.post(
|
||||||
|
`/sessions/${sessionId}/intelligent/summary`,
|
||||||
|
);
|
||||||
|
return r.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ import FFlagsService from 'App/services/FFlagsService';
|
||||||
import AssistStatsService from './AssistStatsService';
|
import AssistStatsService from './AssistStatsService';
|
||||||
import UxtestingService from './UxtestingService';
|
import UxtestingService from './UxtestingService';
|
||||||
import TagWatchService from 'App/services/TagWatchService';
|
import TagWatchService from 'App/services/TagWatchService';
|
||||||
|
import AiService from "App/services/AiService";
|
||||||
|
|
||||||
export const dashboardService = new DashboardService();
|
export const dashboardService = new DashboardService();
|
||||||
export const metricService = new MetricService();
|
export const metricService = new MetricService();
|
||||||
|
|
@ -39,6 +40,8 @@ export const uxtestingService = new UxtestingService();
|
||||||
|
|
||||||
export const tagWatchService = new TagWatchService();
|
export const tagWatchService = new TagWatchService();
|
||||||
|
|
||||||
|
export const aiService = new AiService();
|
||||||
|
|
||||||
export const services = [
|
export const services = [
|
||||||
dashboardService,
|
dashboardService,
|
||||||
metricService,
|
metricService,
|
||||||
|
|
@ -57,4 +60,5 @@ export const services = [
|
||||||
assistStatsService,
|
assistStatsService,
|
||||||
uxtestingService,
|
uxtestingService,
|
||||||
tagWatchService,
|
tagWatchService,
|
||||||
|
aiService,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
7
frontend/app/svg/icons/sparkles.svg
Normal file
7
frontend/app/svg/icons/sparkles.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<svg viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="Frame 227">
|
||||||
|
<path id="Star 22" d="M9.65283 3.10565L11.1378 6.62073L14.6528 8.10565L11.1378 9.59058L9.65283 13.1057L8.16791 9.59058L4.65283 8.10565L8.16791 6.62073L9.65283 3.10565Z" fill="#3EAAAF"/>
|
||||||
|
<path id="Star 23" d="M3.31299 0.894348L4.20394 3.00339L6.31299 3.89435L4.20394 4.7853L3.31299 6.89435L2.42203 4.7853L0.312988 3.89435L2.42203 3.00339L3.31299 0.894348Z" fill="#394EFF"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 478 B |
11
frontend/globals.d.ts
vendored
Normal file
11
frontend/globals.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
interface Window {
|
||||||
|
env: {
|
||||||
|
ORIGIN: string;
|
||||||
|
API_EDP: string;
|
||||||
|
ASSETS_HOST: string;
|
||||||
|
VERSION: string;
|
||||||
|
TRACKER_VERSION: string;
|
||||||
|
TRACKER_HOST: string;
|
||||||
|
COMMIT_HASH: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -115,9 +115,7 @@ import {
|
||||||
${iconPaths.map(icon => ` ${titleCase(icon.fileName)}`).join(',\n')}
|
${iconPaths.map(icon => ` ${titleCase(icon.fileName)}`).join(',\n')}
|
||||||
} from './Icons'
|
} from './Icons'
|
||||||
|
|
||||||
|
export type IconNames = ${icons.map((icon, i) => `'${icon.slice(0, -4)}'`).join(' | ')};
|
||||||
// export type NewIconNames = ${icons.map((icon) => '\'' + icon.slice(0, -4).replaceAll('-', '_') + '\'').join(' | ')};
|
|
||||||
export type IconNames = ${icons.map((icon) => '\'' + icon.slice(0, -4) + '\'').join(' | ')};
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: IconNames;
|
name: IconNames;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue