ui: embed player, some pdf fixes for kai

This commit is contained in:
nick-delirium 2025-05-28 17:12:52 +02:00
parent 3c249b2b5a
commit 256b049e7d
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
23 changed files with 377 additions and 51 deletions

View file

@ -5,6 +5,7 @@ import Ideas from './Ideas';
import { Loader } from 'UI';
import { kaiStore } from '../KaiStore';
import { observer } from 'mobx-react-lite';
import EmbedPlayer from './EmbedPlayer';
function ChatLog({
projectId,
@ -21,6 +22,7 @@ function ChatLog({
chatTitle: string | null;
onCancel: () => void;
}) {
const [embedSession, setEmbedSession] = React.useState<any>(null);
const messages = kaiStore.messages;
const loading = kaiStore.loadingChat;
const chatRef = React.useRef<HTMLDivElement>(null);
@ -64,6 +66,12 @@ function ChatLog({
'overflow-y-auto relative flex flex-col items-center justify-between w-full h-full pt-4'
}
>
{embedSession ? (
<EmbedPlayer
session={embedSession}
onClose={() => setEmbedSession(null)}
/>
) : null}
<div className={'flex flex-col gap-2 w-2/3 min-h-max'}>
{messages.map((msg, index) => (
<React.Fragment key={msg.messageId ?? index}>
@ -71,6 +79,7 @@ function ChatLog({
siteId={projectId}
message={msg}
chatTitle={chatTitle}
onReplay={(session) => setEmbedSession(session)}
canEdit={
processingStage === null &&
msg.isUser &&

View file

@ -27,12 +27,14 @@ function ChatMsg({
canEdit,
message,
chatTitle,
onReplay,
}: {
message: Message;
userName?: string;
canEdit?: boolean;
siteId: string;
chatTitle: string | null;
onReplay: (session: any) => void;
}) {
const { t } = useTranslation();
const [metric, setMetric] = React.useState<Widget | null>(null);
@ -73,7 +75,6 @@ function ChatMsg({
.then(async ({ jsPDF }) => {
const doc = new jsPDF();
const blockWidth = 170; // mm
doc.addImage('/assets/img/logo-img.png', 20, 15, 30, 5);
const content = bodyRef.current!.cloneNode(true) as HTMLElement;
if (userPrompt) {
const titleHeader = document.createElement('h2');
@ -81,6 +82,18 @@ function ChatMsg({
titleHeader.style.marginBottom = '10px';
content.prepend(titleHeader);
}
// insert logo /assets/img/logo-img.png
const logo = new Image();
logo.src = '/assets/img/logo-img.png';
logo.style.width = '130px';
const container = document.createElement('div');
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.justifyContent = 'center';
container.style.marginBottom = '10mm';
container.style.width = `${blockWidth}mm`;
container.appendChild(logo);
content.prepend(container);
content.querySelectorAll('ul').forEach((ul) => {
const frag = document.createDocumentFragment();
ul.querySelectorAll('li').forEach((li) => {
@ -123,9 +136,9 @@ function ChatMsg({
doc.save((chatTitle ?? 'document') + '.pdf');
},
// top, bottom, ?, left
margin: [5, 10, 20, 20],
margin: [10, 10, 20, 20],
x: 0,
y: 15,
y: 0,
// Target width
width: blockWidth,
// Window width for rendering
@ -172,7 +185,7 @@ function ChatMsg({
}, [metricData, chart_data]);
return (
<div className={cn('flex gap-2', isUser ? 'flex-row-reverse' : 'flex-row')}>
<div className={'mt-1 flex flex-col group/actions max-w-[880px]'}>
<div className={'mt-1 flex flex-col group/actions max-w-[60svw]'}>
<div
className={cn(
'markdown-body',
@ -195,8 +208,18 @@ function ChatMsg({
{message.sessions ? (
<div className="flex flex-col">
{message.sessions.map((session) => (
<div className="shadow border rounded-xl overflow-hidden mb-2">
<SessionItem key={session.sessionId} session={session} slim />
<div className="shadow border rounded-2xl overflow-hidden mb-2">
<SessionItem
disableUser
key={session.sessionId}
session={session}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onReplay(session);
}}
slim
/>
</div>
))}
</div>

View file

@ -0,0 +1,75 @@
import React from 'react';
import cn from 'classnames';
import { useStore } from 'App/mstore';
import { Loader } from 'UI';
import { observer } from 'mobx-react-lite';
import MobileClipsPlayer from 'App/components/Session/MobileClipsPlayer';
import ClipsPlayer from 'App/components/Session/ClipsPlayer';
import Session from '@/types/session/session';
interface Clip {
sessionId: string | undefined;
range: [number, number];
message: string;
}
function EmbedPlayer({
session,
onClose,
}: {
session: Session;
onClose: () => void;
}) {
const { projectsStore } = useStore();
const clip = {
sessionId: session.sessionId,
range: [0, session.durationMs],
message: '',
};
const { isMobile } = projectsStore;
const onBgClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
}
};
return (
<div
className="w-screen h-screen fixed top-0 left-0 flex items-center justify-center"
style={{ zIndex: 100, background: 'rgba(0,0,0, 0.15)' }}
onClick={onBgClick}
>
<div
className={cn(
'rounded-lg overflow-hidden',
'rounded shadow boarder bg-white',
)}
style={{ width: 960 }}
>
{isMobile ? (
<MobileClipsPlayer
isHighlight
onClose={onClose}
clip={clip}
currentIndex={0}
isCurrent
autoplay={false}
isFull
/>
) : (
<ClipsPlayer
isHighlight
onClose={onClose}
clip={clip}
currentIndex={0}
isCurrent
autoplay={false}
isFull
/>
)}
</div>
</div>
);
}
export default observer(EmbedPlayer);

View file

@ -26,10 +26,11 @@ interface Props {
autoplay: boolean;
onClose?: () => void;
isHighlight?: boolean;
isFull?: boolean;
}
function ClipsPlayer(props: Props) {
const { clip, currentIndex, isCurrent, onClose, isHighlight } = props;
const { clip, currentIndex, isCurrent, onClose, isHighlight, isFull } = props;
const { sessionStore } = useStore();
const { prefetched } = sessionStore;
const [windowActive, setWindowActive] = useState(!document.hidden);
@ -146,6 +147,7 @@ function ClipsPlayer(props: Props) {
onClose={onClose}
range={clip.range}
session={session!}
isFull={isFull}
/>
<ClipPlayerContent
message={clip.message}
@ -153,6 +155,7 @@ function ClipsPlayer(props: Props) {
autoplay={props.autoplay}
range={clip.range}
session={session!}
isFull={isFull}
/>
</>
) : (

View file

@ -19,13 +19,14 @@ interface Props {
isHighlight?: boolean;
message?: string;
isMobile?: boolean;
isFull?: boolean;
}
function ClipPlayerContent(props: Props) {
const playerContext = React.useContext<IPlayerContext>(PlayerContext);
const screenWrapper = React.useRef<HTMLDivElement>(null);
const { time } = playerContext.store.get();
const { range } = props;
const { range, isFull } = props;
React.useEffect(() => {
if (!playerContext.player) return;
@ -90,7 +91,7 @@ function ClipPlayerContent(props: Props) {
<div className="leading-none font-medium">{props.message}</div>
</div>
) : null}
<ClipPlayerControls session={props.session} range={props.range} />
<ClipPlayerControls isFull={isFull} session={props.session} range={props.range} />
</div>
</div>
);

View file

@ -15,9 +15,11 @@ import { useTranslation } from 'react-i18next';
function ClipPlayerControls({
session,
range,
isFull,
}: {
session: Session;
range: [number, number];
isFull?: boolean;
}) {
const { t } = useTranslation();
const { projectsStore } = useStore();
@ -47,7 +49,7 @@ function ClipPlayerControls({
<PlayButton state={state} togglePlay={togglePlay} iconSize={30} />
<Timeline range={range} />
<Button size="small" type="primary" onClick={showFullSession}>
{t('Play Full Session')}
{isFull ? t('Open Session') : t('Play Full Session')}
<CirclePlay size={16} style={{ marginLeft: '0px'}} />
</Button>
</div>

View file

@ -16,12 +16,13 @@ interface Props {
range: [number, number];
onClose?: () => void;
isHighlight?: boolean;
isFull?: boolean;
}
function ClipPlayerHeader(props: Props) {
const { t } = useTranslation();
const { projectsStore } = useStore();
const { session, range, onClose, isHighlight } = props;
const { session, range, onClose, isHighlight, isFull } = props;
const { siteId } = projectsStore;
const { message } = App.useApp();
@ -33,7 +34,7 @@ function ClipPlayerHeader(props: Props) {
};
return (
<div className="bg-white p-3 flex justify-between items-center border-b relative">
{isHighlight ? <PartialSessionBadge /> : null}
{isHighlight && !isFull ? <PartialSessionBadge /> : null}
<UserCard session={props.session} />
<Space>

View file

@ -10,7 +10,7 @@ import { Icon, Link } from 'UI';
import { useStore } from 'App/mstore';
const PLAY_ICON_NAMES = {
notPlayed: 'play-fill',
notPlayed: 'play-v2',
played: 'play-circle-light',
} as const;
@ -76,10 +76,14 @@ function PlayLink(props: Props) {
rel={props.newTab ? 'noopener noreferrer' : undefined}
>
<div className="group-hover:block hidden">
<Icon name="play-hover" size={38} color={isAssist ? 'tealx' : 'teal'} />
<Icon name={`play-fill-v2${isAssist ? '-assist' : ''}`} size={38} />
</div>
<div className="group-hover:hidden block">
<Icon name={iconName} size={38} color={isAssist ? 'tealx' : 'teal'} />
<Icon
name={`${iconName}${isAssist ? '-assist' : ''}`}
size={38}
color="teal"
/>
</div>
</Link>
);

View file

@ -82,7 +82,8 @@ const PREFETCH_STATE = {
function SessionItem(props: RouteComponentProps & Props) {
const { location } = useHistory();
const { settingsStore, sessionStore, searchStore, searchStoreLive } = useStore();
const { settingsStore, sessionStore, searchStore, searchStoreLive } =
useStore();
const { timezone, shownTimezone } = settingsStore.sessionSettings;
const { t } = useTranslation();
const [prefetchState, setPrefetched] = useState(PREFETCH_STATE.none);
@ -178,7 +179,7 @@ function SessionItem(props: RouteComponentProps & Props) {
await sessionStore.getFirstMob(sessionId);
setPrefetched(PREFETCH_STATE.fetched);
} catch (e) {
setPrefetched(PREFETCH_STATE.none)
setPrefetched(PREFETCH_STATE.none);
console.error('Error while prefetching first mob', e);
}
}, [prefetchState, live, isAssist, isMobile, sessionStore, sessionId]);
@ -247,13 +248,13 @@ function SessionItem(props: RouteComponentProps & Props) {
);
}, [startedAt, timezone, userTimezone]);
const onMetaClick = (meta: { name: string, value: string }) => {
const onMetaClick = (meta: { name: string; value: string }) => {
if (isAssist) {
searchStoreLive.addFilterByKeyAndValue(meta.name, meta.value)
searchStoreLive.addFilterByKeyAndValue(meta.name, meta.value);
} else {
searchStore.addFilterByKeyAndValue(meta.name, meta.value);
}
}
};
return (
<Tooltip
title={
@ -263,7 +264,11 @@ function SessionItem(props: RouteComponentProps & Props) {
}
>
<div
className={cn(stl.sessionItem, 'flex flex-col', slim ? 'px-4 py-2' : 'p-4')}
className={cn(
stl.sessionItem,
'flex flex-col',
slim ? 'px-4 py-2 text-sm' : 'p-4',
)}
id="session-item"
onClick={(e) => e.stopPropagation()}
onMouseEnter={handleHover}
@ -293,13 +298,16 @@ function SessionItem(props: RouteComponentProps & Props) {
</div>
<div className="overflow-hidden color-gray-medium ml-3 justify-between items-center shrink-0">
<div
className={cn('text-lg', {
className={cn(
{
'color-teal cursor-pointer':
!disableUser && hasUserId && !isDisabled,
[stl.userName]:
!disableUser && hasUserId && !isDisabled,
'color-gray-medium': disableUser || !hasUserId,
})}
},
slim ? 'text-base' : 'text-lg',
)}
onClick={handleUserClick}
>
<TextEllipsis
@ -310,15 +318,20 @@ function SessionItem(props: RouteComponentProps & Props) {
</div>
</div>
</div>
{_metaList.length > 0 && (
<SessionMetaList onMetaClick={onMetaClick} maxLength={3} metaList={_metaList} />
{!slim && _metaList.length > 0 && (
<SessionMetaList
onMetaClick={onMetaClick}
maxLength={3}
metaList={_metaList}
/>
)}
</div>
)}
<div
className={cn(
'px-2 flex flex-col justify-between gap-2 mt-3 lg:mt-0',
'px-2 flex flex-col justify-between lg:mt-0',
compact ? 'w-[40%]' : 'lg:w-1/5',
slim ? 'gap-1 mt-1' : 'gap-2 mt-3',
)}
>
<div>
@ -355,9 +368,12 @@ function SessionItem(props: RouteComponentProps & Props) {
</div>
<div
style={{ width: '30%' }}
className="px-2 flex flex-col justify-between gap-2"
className={cn(
'px-2 flex flex-col justify-between',
slim ? 'gap-1' : 'gap-2',
)}
>
<div style={{ height: '21px' }}>
<div style={{ height: slim ? undefined : '21px' }}>
<CountryFlag
userCity={userCity}
userState={userState}
@ -454,7 +470,9 @@ function SessionItem(props: RouteComponentProps & Props) {
onClick={onClick}
queryParams={queryParams}
query={query}
beforeOpen={live || isAssist ? undefined : populateData}
beforeOpen={
slim || live || isAssist ? undefined : populateData
}
/>
)}
</div>

View file

@ -416,9 +416,14 @@ export { default as Play_circle_bold } from './play_circle_bold';
export { default as Play_circle_light } from './play_circle_light';
export { default as Play_circle } from './play_circle';
export { default as Play_fill_new } from './play_fill_new';
export { default as Play_fill_v2_assist } from './play_fill_v2_assist';
export { default as Play_fill_v2 } from './play_fill_v2';
export { default as Play_fill } from './play_fill';
export { default as Play_hover } from './play_hover';
export { default as Play_v2_assist } from './play_v2_assist';
export { default as Play_v2 } from './play_v2';
export { default as Play } from './play';
export { default as Played_v2 } from './played_v2';
export { default as Plug } from './plug';
export { default as Plus_circle } from './plus_circle';
export { default as Plus } from './plus';

View file

@ -0,0 +1,18 @@
/* Auto-generated, do not edit */
import React from 'react';
interface Props {
size?: number | string;
width?: number | string;
height?: number | string;
fill?: string;
}
function Play_fill_v2(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 37 36" fill="none" width={ `${ width }px` } height={ `${ height }px` } ><rect x=".663" width="36" height="36" rx="18" fill="#394DFE"/><path d="M24.851 16.358a2 2 0 0 1 0 3.284l-7.323 5.09c-1.326.922-3.141-.027-3.141-1.642V12.91c0-1.615 1.815-2.564 3.141-1.643l7.323 5.09Z" fill="#E2E4FD"/></svg>
);
}
export default Play_fill_v2;

View file

@ -0,0 +1,18 @@
/* Auto-generated, do not edit */
import React from 'react';
interface Props {
size?: number | string;
width?: number | string;
height?: number | string;
fill?: string;
}
function Play_fill_v2_assist(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 37 36" fill="none" width={ `${ width }px` } height={ `${ height }px` } ><rect x=".663" width="36" height="36" rx="18" fill="#3EAAAF"/><path d="M24.851 16.358a2 2 0 0 1 0 3.284l-7.323 5.09c-1.326.922-3.141-.027-3.141-1.642V12.91c0-1.615 1.815-2.564 3.141-1.643l7.323 5.09Z" fill="#E2E4FD"/></svg>
);
}
export default Play_fill_v2_assist;

View file

@ -0,0 +1,18 @@
/* Auto-generated, do not edit */
import React from 'react';
interface Props {
size?: number | string;
width?: number | string;
height?: number | string;
fill?: string;
}
function Play_v2(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 37 36" fill="none" width={ `${ width }px` } height={ `${ height }px` } ><rect x=".663" width="36" height="36" rx="18" fill="#E2E4FD"/><path d="M24.851 16.358a2 2 0 0 1 0 3.284l-7.323 5.09c-1.326.922-3.141-.027-3.141-1.642V12.91c0-1.615 1.815-2.564 3.141-1.643l7.323 5.09Z" fill="#394DFE"/></svg>
);
}
export default Play_v2;

View file

@ -0,0 +1,18 @@
/* Auto-generated, do not edit */
import React from 'react';
interface Props {
size?: number | string;
width?: number | string;
height?: number | string;
fill?: string;
}
function Play_v2_assist(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 37 36" fill="none" width={ `${ width }px` } height={ `${ height }px` } ><rect x=".663" width="36" height="36" rx="18" fill="#E2E4FD"/><path d="M24.851 16.358a2 2 0 0 1 0 3.284l-7.323 5.09c-1.326.922-3.141-.027-3.141-1.642V12.91c0-1.615 1.815-2.564 3.141-1.643l7.323 5.09Z" fill="#3EAAAF"/></svg>
);
}
export default Play_v2_assist;

View file

@ -0,0 +1,18 @@
/* Auto-generated, do not edit */
import React from 'react';
interface Props {
size?: number | string;
width?: number | string;
height?: number | string;
fill?: string;
}
function Played_v2(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 37 36" fill="none" width={ `${ width }px` } height={ `${ height }px` } ><rect x=".663" width="36" height="36" rx="18" fill="#E2E4FD"/><path d="M24.851 16.358a2 2 0 0 1 0 3.284l-7.323 5.09c-1.326.922-3.141-.027-3.141-1.642V12.91c0-1.615 1.815-2.564 3.141-1.643l7.323 5.09Z" fill="#fff"/></svg>
);
}
export default Played_v2;

File diff suppressed because one or more lines are too long

View file

@ -29,6 +29,7 @@ import {
spotOnlyCats,
} from './data';
import { useTranslation } from 'react-i18next';
import { tag } from '@/components/Session_/Inspector/inspector.css';
const { Text } = Typography;
@ -104,7 +105,7 @@ function SideMenu(props: Props) {
modules.includes(MODULES.USABILITY_TESTS),
item.isAdmin && !isAdmin,
item.isEnterprise && !isEnterprise,
item.key === MENU.KAI && !hasAi
item.key === MENU.KAI && !hasAi,
].some((cond) => cond);
return { ...item, hidden: isHidden };
@ -118,7 +119,15 @@ function SideMenu(props: Props) {
hidden: allItemsHidden,
};
});
}, [isAdmin, isEnterprise, isPreferencesActive, modules, spotOnly, siteId, i18n.language]);
}, [
isAdmin,
isEnterprise,
isPreferencesActive,
modules,
spotOnly,
siteId,
i18n.language,
]);
const menuRoutes: any = {
[MENU.EXIT]: () =>
@ -216,7 +225,10 @@ function SideMenu(props: Props) {
color={isActive ? 'teal' : 'black'}
/>
}
className={cn('!rounded-lg hover-fill-teal', isActive ? 'color-main' : 'color-black')}
className={cn(
'!rounded-lg hover-fill-teal',
isActive ? 'color-main' : 'color-black',
)}
>
{item.label}
</Menu.Item>
@ -235,7 +247,10 @@ function SideMenu(props: Props) {
/>
}
style={{ paddingLeft: '20px' }}
className={cn('!rounded-lg !pe-0', isActive ? 'color-main' : 'color-black')}
className={cn(
'!rounded-lg !pe-0',
isActive ? 'color-main' : 'color-black',
)}
itemIcon={
item.leading ? (
<Icon
@ -255,13 +270,6 @@ function SideMenu(props: Props) {
}}
>
{item.label}
<Tag
color="cyan"
bordered={false}
className="text-xs ml-2"
>
{t('Beta')}
</Tag>
</div>
</Menu.Item>
);
@ -285,7 +293,20 @@ function SideMenu(props: Props) {
})}
key={child.key}
>
{child.label}
<div className="flex items-center justify-between">
<span>{child.label}</span>
{child.tag ? (
<div className="ml-auto">
<Tag
color={child.tag.color}
bordered={child.tag.border}
className="text-xs ml-2"
>
{child.tag.label}
</Tag>
</div>
) : null}
</div>
</Menu.Item>
))}
</Menu.SubMenu>
@ -301,7 +322,10 @@ function SideMenu(props: Props) {
/>
}
style={{ paddingLeft: '20px' }}
className={cn('!rounded-lg hover-fill-teal', isActive ? 'color-main' : 'color-black')}
className={cn(
'!rounded-lg hover-fill-teal',
isActive ? 'color-main' : 'color-black',
)}
itemIcon={
item.leading ? (
<Icon
@ -312,7 +336,20 @@ function SideMenu(props: Props) {
) : null
}
>
{item.label}
<div className="flex items-center justify-between">
<span>{item.label}</span>
{item.tag ? (
<div className="ml-auto">
<Tag
color={item.tag.color}
bordered={item.tag.border}
className="text-xs ml-2"
>
{item.tag.label}
</Tag>
</div>
) : null}
</div>
</Menu.Item>
);
})}

View file

@ -1,5 +1,5 @@
import { TFunction } from 'i18next';
import { IconNames } from "../components/ui/SVG";
import { IconNames } from '../components/ui/SVG';
import React from 'react';
export interface MenuItem {
@ -99,7 +99,15 @@ export const categories: (t: TFunction) => Category[] = (t) => [
title: '',
key: 'kai',
items: [
{ label: t('Kai'), key: MENU.KAI, icon: 'kai' },
{
label: t('Kai'),
key: MENU.KAI,
icon: 'kai-mono',
tag: {
label: t('New'),
color: '#394DFE',
},
},
],
},
{

View file

@ -0,0 +1,6 @@
<svg viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.662598" width="36" height="36" rx="18" fill="#3EAAAF" />
<path
d="M24.8513 16.3578C25.9959 17.1534 25.9959 18.8466 24.8513 19.6422L17.5283 24.7325C16.2022 25.6543 14.3868 24.7053 14.3868 23.0902L14.3868 12.9098C14.3868 11.2947 16.2022 10.3457 17.5283 11.2675L24.8513 16.3578Z"
fill="#E2E4FD" />
</svg>

After

Width:  |  Height:  |  Size: 398 B

View file

@ -0,0 +1,6 @@
<svg viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.662598" width="36" height="36" rx="18" fill="#394DFE" />
<path
d="M24.8513 16.3578C25.9959 17.1534 25.9959 18.8466 24.8513 19.6422L17.5283 24.7325C16.2022 25.6543 14.3868 24.7053 14.3868 23.0902L14.3868 12.9098C14.3868 11.2947 16.2022 10.3457 17.5283 11.2675L24.8513 16.3578Z"
fill="#E2E4FD" />
</svg>

After

Width:  |  Height:  |  Size: 398 B

View file

@ -0,0 +1,6 @@
<svg viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.662598" width="36" height="36" rx="18" fill="#E2E4FD" />
<path
d="M24.8513 16.3578C25.9959 17.1534 25.9959 18.8466 24.8513 19.6422L17.5283 24.7325C16.2022 25.6543 14.3868 24.7053 14.3868 23.0902L14.3868 12.9098C14.3868 11.2947 16.2022 10.3457 17.5283 11.2675L24.8513 16.3578Z"
fill="#3EAAAF" />
</svg>

After

Width:  |  Height:  |  Size: 398 B

View file

@ -0,0 +1,6 @@
<svg viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.662598" width="36" height="36" rx="18" fill="#E2E4FD" />
<path
d="M24.8513 16.3578C25.9959 17.1534 25.9959 18.8466 24.8513 19.6422L17.5283 24.7325C16.2022 25.6543 14.3868 24.7053 14.3868 23.0902L14.3868 12.9098C14.3868 11.2947 16.2022 10.3457 17.5283 11.2675L24.8513 16.3578Z"
fill="#394DFE" />
</svg>

After

Width:  |  Height:  |  Size: 398 B

View file

@ -0,0 +1,6 @@
<svg viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.662598" width="36" height="36" rx="18" fill="#E2E4FD" />
<path
d="M24.8513 16.3578C25.9959 17.1534 25.9959 18.8466 24.8513 19.6422L17.5283 24.7325C16.2022 25.6543 14.3868 24.7053 14.3868 23.0902L14.3868 12.9098C14.3868 11.2947 16.2022 10.3457 17.5283 11.2675L24.8513 16.3578Z"
fill="white" />
</svg>

After

Width:  |  Height:  |  Size: 396 B