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:
Delirium 2024-02-12 15:34:43 +01:00 committed by GitHub
parent b9e2cafe4b
commit e0799f74e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 531 additions and 69 deletions

View file

@ -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> {

View file

@ -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));

View file

@ -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);

View file

@ -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);

View file

@ -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>
Heres 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);

View file

@ -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';

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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';

View 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

View 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);
}
};
}

View file

@ -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() {

View 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;
}
}

View file

@ -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,
];

View 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
View 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;
}
}

View file

@ -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;