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',
|
||||
'/check-recording-status',
|
||||
'/usability-tests',
|
||||
'/tags'
|
||||
'/tags',
|
||||
'/intelligent'
|
||||
];
|
||||
|
||||
export const clean = (obj: any, forbiddenValues: any[] = [undefined, '']): any => {
|
||||
|
|
@ -71,7 +72,7 @@ export default class APIClient {
|
|||
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
|
||||
const jwt = store.getState().getIn(['user', 'jwt']);
|
||||
const headers = new Headers({
|
||||
|
|
@ -79,6 +80,12 @@ export default class APIClient {
|
|||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
if (reqHeaders) {
|
||||
for (const [key, value] of Object.entries(reqHeaders)) {
|
||||
headers.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (jwt) {
|
||||
headers.set('Authorization', `Bearer ${jwt}`);
|
||||
}
|
||||
|
|
@ -122,15 +129,14 @@ export default class APIClient {
|
|||
|
||||
async fetch(path: string, params?: any, method: string = 'GET', options: {
|
||||
clean?: boolean
|
||||
} = { clean: true }): Promise<Response> {
|
||||
} = { clean: true }, headers?: Record<string, any>): Promise<Response> {
|
||||
let jwt = store.getState().getIn(['user', 'jwt']);
|
||||
if (!path.includes('/refresh') && jwt && this.isTokenExpired(jwt)) {
|
||||
jwt = await this.handleTokenRefresh();
|
||||
(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) {
|
||||
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';
|
||||
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';
|
||||
return this.fetch(path, params, 'POST');
|
||||
return this.fetch(path, params, 'POST', options, headers);
|
||||
}
|
||||
|
||||
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 Player from './PlayerInst';
|
||||
import SubHeader from 'Components/Session_/Subheader';
|
||||
|
||||
import AiSubheader from 'Components/Session/Player/ReplayPlayer/AiSubheader';
|
||||
import styles from 'Components/Session_/playerBlock.module.css';
|
||||
|
||||
interface IProps {
|
||||
|
|
@ -11,39 +11,33 @@ interface IProps {
|
|||
sessionId: string;
|
||||
disabled: boolean;
|
||||
activeTab: string;
|
||||
jiraConfig: Record<string, any>
|
||||
fullView?: boolean
|
||||
jiraConfig: Record<string, any>;
|
||||
fullView?: boolean;
|
||||
setActiveTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
function PlayerBlock(props: IProps) {
|
||||
const {
|
||||
fullscreen,
|
||||
sessionId,
|
||||
disabled,
|
||||
activeTab,
|
||||
jiraConfig,
|
||||
fullView = false,
|
||||
} = props;
|
||||
const { fullscreen, sessionId, disabled, activeTab, jiraConfig, fullView = false, setActiveTab } = 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 (
|
||||
<div
|
||||
className={cn(styles.playerBlock, 'flex flex-col', 'overflow-x-hidden')}
|
||||
>
|
||||
{shouldShowSubHeader ? (
|
||||
<SubHeader sessionId={sessionId} disabled={disabled} jiraConfig={jiraConfig} />
|
||||
) : null}
|
||||
<Player
|
||||
activeTab={activeTab}
|
||||
fullView={fullView}
|
||||
/>
|
||||
<div className={cn(styles.playerBlock, 'flex flex-col', 'overflow-x-hidden')}>
|
||||
{shouldShowSubHeader ?
|
||||
isSaas
|
||||
? <AiSubheader sessionId={sessionId} disabled={disabled} jiraConfig={jiraConfig} activeTab={activeTab} setActiveTab={setActiveTab} />
|
||||
: <SubHeader sessionId={sessionId} disabled={disabled} jiraConfig={jiraConfig} />
|
||||
: null}
|
||||
<Player activeTab={activeTab} fullView={fullView} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state: any) => ({
|
||||
export default connect((state: Record<string, any>) => ({
|
||||
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
||||
sessionId: state.getIn(['sessions', 'current']).sessionId,
|
||||
disabled: state.getIn(['components', 'targetDefiner', 'inspectorMode']),
|
||||
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}
|
||||
>
|
||||
<div className={cn(styles.session, 'relative')} data-fullscreen={fullscreen}>
|
||||
<PlayerBlock activeTab={activeTab} />
|
||||
<PlayerBlock setActiveTab={setActiveTab} activeTab={activeTab} />
|
||||
</div>
|
||||
</div>
|
||||
{activeTab !== '' && (
|
||||
<RightMenu
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
fullscreen={fullscreen}
|
||||
tabs={TABS}
|
||||
/>
|
||||
)}
|
||||
{!fullscreen && activeTab !== '' ? (
|
||||
<RightBlock setActiveTab={setActiveTab} activeTab={activeTab} />
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RightMenu({ tabs, activeTab, setActiveTab, fullscreen }: any) {
|
||||
return (
|
||||
!fullscreen ? <RightBlock tabs={tabs} setActiveTab={setActiveTab} activeTab={activeTab} /> : null
|
||||
);
|
||||
}
|
||||
|
||||
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 Session from 'Types/session/session';
|
||||
import EventsBlock from '../Session_/EventsBlock';
|
||||
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 stl from './rightblock.module.css';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Popover, Button } from 'UI';
|
||||
import { Popover, Button, Icon } from 'UI';
|
||||
import IssuesModal from './IssuesModal';
|
||||
import { fetchProjects, fetchMeta } from 'Duck/assignments';
|
||||
|
||||
|
|
@ -53,10 +53,7 @@ class Issues extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
sessionId,
|
||||
issuesIntegration,
|
||||
} = this.props;
|
||||
const { sessionId, issuesIntegration, isInline } = this.props;
|
||||
const provider = issuesIntegration.first()?.provider || '';
|
||||
|
||||
return (
|
||||
|
|
@ -68,11 +65,18 @@ class Issues extends React.Component {
|
|||
</div>
|
||||
)}
|
||||
>
|
||||
{isInline ? (
|
||||
<div className={'flex items-center gap-2 p-3 text-black'}>
|
||||
<Icon name={`integrations/${provider === 'jira' ? 'jira' : 'github'}`} size={16} />
|
||||
<div>Create Issue</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Button icon={`integrations/${provider === 'jira' ? 'jira' : 'github'}`} variant="text">
|
||||
Create Issue
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function Bookmark(props: Props) {
|
|||
color={isFavorite ? 'teal' : undefined}
|
||||
size="16"
|
||||
/>
|
||||
<span className="ml-2">{isEnterprise ? 'Vault' : 'Bookmark'}</span>
|
||||
<span className="ml-2 text-black">{isEnterprise ? 'Vault' : 'Bookmark'}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Button data-favourite={isFavorite}>
|
||||
|
|
@ -54,7 +54,7 @@ function Bookmark(props: Props) {
|
|||
color={isFavorite ? 'teal' : undefined}
|
||||
size="16"
|
||||
/>
|
||||
<span className="ml-2">{isEnterprise ? 'Vault' : 'Bookmark'}</span>
|
||||
<span className="ml-2 text-black">{isEnterprise ? 'Vault' : 'Bookmark'}</span>
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -427,6 +427,7 @@ export { default as Sleep } from './sleep';
|
|||
export { default as Sliders } from './sliders';
|
||||
export { default as Social_slack } from './social_slack';
|
||||
export { default as Social_trello } from './social_trello';
|
||||
export { default as Sparkles } from './sparkles';
|
||||
export { default as Speedometer2 } from './speedometer2';
|
||||
export { default as Spinner } from './spinner';
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ import AlertStore from './alertsStore';
|
|||
import FeatureFlagsStore from './featureFlagsStore';
|
||||
import UxtestingStore from './uxtestingStore';
|
||||
import TagWatchStore from './tagWatchStore';
|
||||
|
||||
import AiSummaryStore from "./aiSummaryStore";
|
||||
export class RootStore {
|
||||
dashboardStore: DashboardStore;
|
||||
metricStore: MetricStore;
|
||||
|
|
@ -41,6 +41,7 @@ export class RootStore {
|
|||
featureFlagsStore: FeatureFlagsStore;
|
||||
uxtestingStore: UxtestingStore;
|
||||
tagWatchStore: TagWatchStore;
|
||||
aiSummaryStore: AiSummaryStore;
|
||||
|
||||
constructor() {
|
||||
this.dashboardStore = new DashboardStore();
|
||||
|
|
@ -62,6 +63,7 @@ export class RootStore {
|
|||
this.featureFlagsStore = new FeatureFlagsStore();
|
||||
this.uxtestingStore = new UxtestingStore();
|
||||
this.tagWatchStore = new TagWatchStore();
|
||||
this.aiSummaryStore = new AiSummaryStore();
|
||||
}
|
||||
|
||||
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 UxtestingService from './UxtestingService';
|
||||
import TagWatchService from 'App/services/TagWatchService';
|
||||
import AiService from "App/services/AiService";
|
||||
|
||||
export const dashboardService = new DashboardService();
|
||||
export const metricService = new MetricService();
|
||||
|
|
@ -39,6 +40,8 @@ export const uxtestingService = new UxtestingService();
|
|||
|
||||
export const tagWatchService = new TagWatchService();
|
||||
|
||||
export const aiService = new AiService();
|
||||
|
||||
export const services = [
|
||||
dashboardService,
|
||||
metricService,
|
||||
|
|
@ -57,4 +60,5 @@ export const services = [
|
|||
assistStatsService,
|
||||
uxtestingService,
|
||||
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')}
|
||||
} from './Icons'
|
||||
|
||||
|
||||
// export type NewIconNames = ${icons.map((icon) => '\'' + icon.slice(0, -4).replaceAll('-', '_') + '\'').join(' | ')};
|
||||
export type IconNames = ${icons.map((icon) => '\'' + icon.slice(0, -4) + '\'').join(' | ')};
|
||||
export type IconNames = ${icons.map((icon, i) => `'${icon.slice(0, -4)}'`).join(' | ')};
|
||||
|
||||
interface Props {
|
||||
name: IconNames;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue