Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion static/app/components/core/chat/thinkingBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function ThinkingBlock({title, startTime, endTime, children}: ThinkingBlo
>
<Global styles={streamingAnimationStyles} />
<Disclosure.Title
leadingItems={<IconSeer size="xs" animation={isActive ? 'waiting' : undefined} />}
leadingItems={<IconSeer size="xs" animation={isActive ? 'loading' : undefined} />}
trailingItems={
<Text variant="secondary" size="sm" align="right" monospace>
{getDuration(elapsed / 1000, 1, true, false, false, SECOND)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function AskSeerSearchHeader({
{isError ? (
<IconExclamation aria-label={t('Error')} variant="danger" />
) : (
<StyledIconSeer animation={loading ? 'loading' : undefined} />
<StyledIconSeer animation={loading ? 'idle' : undefined} />
)}
<Text monospace>{title}</Text>
</Flex>
Expand Down
200 changes: 159 additions & 41 deletions static/app/icons/iconSeer.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,127 @@
import {Fragment} from 'react';
import {Fragment, useId} from 'react';
import {useTheme} from '@emotion/react';
import {useReducedMotion} from 'framer-motion';

import type {SVGIconProps} from './svgIcon';
import {SvgIcon} from './svgIcon';
import {useIconDefaults} from './useIconDefaults';

// Safari claims CSS `d` property support but can't interpolate it in @keyframes
let supportsCssDAnimation = false;
try {
supportsCssDAnimation =
CSS.supports('d: path("M0 0")') &&
!(/Safari\//.test(navigator.userAgent) && !/Chrom/.test(navigator.userAgent));
} catch {
// jsdom/SSR — CSS.supports not available
}

interface IconSeerProps extends SVGIconProps {
animation?: 'loading' | 'waiting';
animation?: 'idle' | 'loading';
}

const commonPath =
'M8.01759 0.25C8.23262 0.249787 8.43757 0.341936 8.58009 0.50293C9.70729 1.77804 11.2269 3.70119 12.626 5.82324C13.9841 7.8832 15.2561 10.1746 15.9317 12.2656C15.9736 12.3357 16.005 12.4135 16.0225 12.4971C16.0949 12.8447 15.9134 13.1959 15.5879 13.3379C13.4024 14.2912 11.151 15 8.01857 15C4.88669 15 2.63318 14.2943 0.451185 13.3457C0.17589 13.226 -0.00176762 12.9535 1.33514e-05 12.6533C0.00080069 12.526 0.0359022 12.4049 0.0947399 12.2979C0.767604 10.203 2.04619 7.9014 3.41115 5.83105C4.81012 3.70913 6.32944 1.78347 7.45607 0.503906L7.51173 0.447266C7.64909 0.321318 7.82933 0.250248 8.01759 0.25ZM13.666 10.6562C12.0686 11.1903 10.117 11.5 8.01857 11.5C5.92124 11.5 3.96583 11.1911 2.37111 10.6572C2.11538 11.1963 1.88727 11.7258 1.69923 12.2402C3.54489 12.9877 5.45222 13.5 8.01857 13.5C10.5835 13.5 12.4883 12.9863 14.336 12.2354C14.1485 11.7218 13.9206 11.1939 13.666 10.6562ZM8.01857 5.5C5.93882 5.50013 4.03972 6.99814 3.14259 9.3291C4.50943 9.74712 6.18916 9.99997 8.01857 10C9.84849 9.99998 11.5258 9.74671 12.8955 9.32812C11.9966 6.998 10.098 5.50012 8.01857 5.5ZM8.01954 2.15234C7.51245 2.75664 6.94957 3.46183 6.37013 4.23438C6.89523 4.08216 7.44681 4.00003 8.01857 4C8.59213 4.00002 9.14526 4.08221 9.67189 4.23535C9.09134 3.46176 8.52751 2.756 8.01954 2.15234Z';

// 32x32 stroked paths for the loading pyramid-spin animation.
const SPIN_BODY =
'M16 2C11.58 7.02 4.02 17.18 1.5 25.3L1.463 25.316C5.701 27.158 10 28.5 16 28.5C22 28.5 26.295 27.152 30.54 25.3L30.5 25.3C27.98 17.16 20.42 7 16 2Z';
const SPIN_EYE =
'M27.649 19.599C24.5 20.785 20.443 21.5 16 21.5C11.559 21.5 7.494 20.785 4.355 19.6C6.02 13.72 10.6 9.5 16 9.5C21.414 9.5 25.985 13.766 27.649 19.599Z';
const SPIN_SWEEP_L = 'M16 2C11.568 7.025 3.995 17.191 1.463 25.316';
const SPIN_SWEEP_R = 'M16 2C20.432 6.98 28.005 17.16 30.54 25.3';

const loadingStyles = `
@keyframes seerR {
0% { visibility: visible; transform: translate(16px,18px) rotate(0deg) scale(1,1) translate(-16px,-18px); }
8% { visibility: visible; transform: translate(16.06px,18px) rotate(-0.1deg) scale(0.995,1) translate(-16px,-18px); }
16% { visibility: visible; transform: translate(17.1px,18px) rotate(-1.9deg) scale(0.899,1) translate(-16px,-18px); }
25% { visibility: visible; transform: translate(18.9px,18px) rotate(-4.93deg) scale(0.734,1) translate(-16px,-18px); }
33% { visibility: visible; transform: translate(20.93px,18px) rotate(-8.22deg) scale(0.548,1) translate(-16px,-18px); }
41% { visibility: visible; transform: translate(23.29px,18px) rotate(-11.75deg) scale(0.332,1) translate(-16px,-18px); }
47% { visibility: visible; transform: translate(26.41px,18px) rotate(-15.87deg) scale(0.045,1) translate(-16px,-18px); }
47.99% { visibility: visible; transform: translate(26.41px,18px) rotate(-16deg) scale(0.001,1) translate(-16px,-18px); }
48% { visibility: hidden; transform: translate(26.41px,18px) rotate(-16deg) scale(0.001,1) translate(-16px,-18px); }
93.99% { visibility: hidden; transform: translate(16px,18px) rotate(0deg) scale(0.001,1) translate(-16px,-18px); }
94% { visibility: visible; transform: translate(16px,18px) rotate(0deg) scale(1,1) translate(-16px,-18px); }
100% { visibility: visible; transform: translate(16px,18px) rotate(0deg) scale(1,1) translate(-16px,-18px); }
}
@keyframes seerL {
0% { visibility: hidden; transform: translate(5.1px,18px) rotate(0deg) scale(0.001,1) translate(-16px,-18px); }
53.99% { visibility: hidden; transform: translate(5.1px,18px) rotate(0deg) scale(0.001,1) translate(-16px,-18px); }
54% { visibility: visible; transform: translate(6.83px,18px) rotate(14.31deg) scale(0.159,1) translate(-16px,-18px); }
60% { visibility: visible; transform: translate(8.44px,18px) rotate(12.14deg) scale(0.306,1) translate(-16px,-18px); }
68% { visibility: visible; transform: translate(10.86px,18px) rotate(8.56deg) scale(0.528,1) translate(-16px,-18px); }
76% { visibility: visible; transform: translate(12.91px,18px) rotate(5.25deg) scale(0.716,1) translate(-16px,-18px); }
82% { visibility: visible; transform: translate(14.74px,18px) rotate(2.17deg) scale(0.884,1) translate(-16px,-18px); }
90% { visibility: visible; transform: translate(15.89px,18px) rotate(0.19deg) scale(0.990,1) translate(-16px,-18px); }
93.99% { visibility: visible; transform: translate(15.89px,18px) rotate(0.19deg) scale(0.990,1) translate(-16px,-18px); }
94% { visibility: hidden; transform: translate(5.1px,18px) rotate(0deg) scale(0.001,1) translate(-16px,-18px); }
100% { visibility: hidden; transform: translate(5.1px,18px) rotate(0deg) scale(0.001,1) translate(-16px,-18px); }
}
@keyframes seerSwA {
0% { visibility: hidden; d: path("${SPIN_SWEEP_L}"); }
7.99% { visibility: hidden; d: path("${SPIN_SWEEP_L}"); }
8% { visibility: visible; d: path("M16 2C11.614 7.046 4.088 17.231 1.601 25.376"); }
16% { visibility: visible; d: path("M16 2C12.414 7.515 5.847 17.911 4.066 26.369"); }
25% { visibility: visible; d: path("M16 2C13.786 8.726 9.466 18.899 8.526 27.705"); }
33% { visibility: visible; d: path("M16 2C15.505 10.412 14.541 19.59 14.329 28.464"); }
41% { visibility: visible; d: path("M16 2C17.64 9.294 20.866 19.194 21.547 28.073"); }
47% { visibility: visible; d: path("M16 2C20.057 7.188 27.224 17.486 29.4 25.784"); }
47.99% { visibility: visible; d: path("${SPIN_SWEEP_R}"); }
48% { visibility: hidden; d: path("${SPIN_SWEEP_R}"); }
100% { visibility: hidden; d: path("${SPIN_SWEEP_R}"); }
}
@keyframes seerSwB {
0% { visibility: hidden; d: path("${SPIN_SWEEP_L}"); }
53.99% { visibility: hidden; d: path("${SPIN_SWEEP_L}"); }
54% { visibility: visible; d: path("${SPIN_SWEEP_L}"); }
60% { visibility: visible; d: path("M16 2C14.134 9.077 10.472 19.094 9.693 27.944"); }
68% { visibility: visible; d: path("M16 2C16.295 10.586 16.865 19.625 16.993 28.487"); }
76% { visibility: visible; d: path("M16 2C18.059 8.868 22.089 18.974 22.956 27.813"); }
82% { visibility: visible; d: path("M16 2C19.466 7.581 25.865 17.983 27.556 26.497"); }
90% { visibility: visible; d: path("M16 2C20.349 7.038 27.837 17.233 30.287 25.41"); }
93% { visibility: visible; d: path("M16 2C20.349 7.038 27.837 17.233 30.287 25.41"); }
93.99% { visibility: visible; d: path("${SPIN_SWEEP_R}"); }
94% { visibility: hidden; d: path("${SPIN_SWEEP_R}"); }
100% { visibility: hidden; d: path("${SPIN_SWEEP_R}"); }
}`;
Comment thread
sentry[bot] marked this conversation as resolved.
Resolved

export function IconSeer({animation, ...props}: IconSeerProps) {
const commonPath =
'M8.01759 0.25C8.23262 0.249787 8.43757 0.341936 8.58009 0.50293C9.70729 1.77804 11.2269 3.70119 12.626 5.82324C13.9841 7.8832 15.2561 10.1746 15.9317 12.2656C15.9736 12.3357 16.005 12.4135 16.0225 12.4971C16.0949 12.8447 15.9134 13.1959 15.5879 13.3379C13.4024 14.2912 11.151 15 8.01857 15C4.88669 15 2.63318 14.2943 0.451185 13.3457C0.17589 13.226 -0.00176762 12.9535 1.33514e-05 12.6533C0.00080069 12.526 0.0359022 12.4049 0.0947399 12.2979C0.767604 10.203 2.04619 7.9014 3.41115 5.83105C4.81012 3.70913 6.32944 1.78347 7.45607 0.503906L7.51173 0.447266C7.64909 0.321318 7.82933 0.250248 8.01759 0.25ZM13.666 10.6562C12.0686 11.1903 10.117 11.5 8.01857 11.5C5.92124 11.5 3.96583 11.1911 2.37111 10.6572C2.11538 11.1963 1.88727 11.7258 1.69923 12.2402C3.54489 12.9877 5.45222 13.5 8.01857 13.5C10.5835 13.5 12.4883 12.9863 14.336 12.2354C14.1485 11.7218 13.9206 11.1939 13.666 10.6562ZM8.01857 5.5C5.93882 5.50013 4.03972 6.99814 3.14259 9.3291C4.50943 9.74712 6.18916 9.99997 8.01857 10C9.84849 9.99998 11.5258 9.74671 12.8955 9.32812C11.9966 6.998 10.098 5.50012 8.01857 5.5ZM8.01954 2.15234C7.51245 2.75664 6.94957 3.46183 6.37013 4.23438C6.89523 4.08216 7.44681 4.00003 8.01857 4C8.59213 4.00002 9.14526 4.08221 9.67189 4.23535C9.09134 3.46176 8.52751 2.756 8.01954 2.15234Z';
const prefersReducedMotion = useReducedMotion();

if (!prefersReducedMotion) {
if (animation === 'waiting') {
return (
<SvgIcon {...props}>
<Fragment>
<style>{`
@keyframes moveHorizontal {
0% { transform: translateX(0); }
5%, 40% { transform: translateX(-1.6px); }
50%, 95% { transform: translateX(1.6px); }
100% { transform: translateX(0); }
if (!prefersReducedMotion && animation === 'idle') {
return (
<SvgIcon {...props}>
<Fragment>
<style>{`
@keyframes seerRoll {
0% { transform: translateX(-1.8px) scaleX(1); }
46% { transform: translateX(1.8px) scaleX(1); }
50% { transform: translateX(2px) scaleX(0.001); }
54% { transform: translateX(-2px) scaleX(0.001); }
58% { transform: translateX(-1.8px) scaleX(1); }
100% { transform: translateX(-1.8px) scaleX(1); }
}

.pupil-waiting {
animation: moveHorizontal 4s ease-out infinite;
.eye-idle {
transform-box: fill-box;
transform-origin: center;
animation: seerRoll 4s linear infinite;
}
`}</style>
<path d={commonPath} />
<circle className="pupil-waiting" cx="8" cy="9" r="2" />
</Fragment>
</SvgIcon>
);
}

if (animation === 'loading') {
return (
<SvgIcon {...props}>
<Fragment>
<path d={commonPath} />
<circle className="pupil-loading" r="2">
<animateMotion
path="M 8 7 A 1 1 0 0 1 8 9 A 1 1 0 0 1 8 7"
dur="1s"
repeatCount="indefinite"
/>
</circle>
</Fragment>
</SvgIcon>
);
}
<path d={commonPath} />
<circle className="eye-idle" cx="8" cy="9" r="2" />
</Fragment>
</SvgIcon>
);
}

if (!prefersReducedMotion && animation === 'loading') {
return supportsCssDAnimation ? (
<SeerLoadingSpinner {...props} />
) : (
<IconSeer animation="idle" {...props} />
);
}

return (
Expand All @@ -61,3 +130,52 @@ export function IconSeer({animation, ...props}: IconSeerProps) {
</SvgIcon>
);
}

function SeerLoadingSpinner(props: SVGIconProps) {
const clipId = useId();
const theme = useTheme();
const iconProps = useIconDefaults(props);
const size = iconProps.legacySize ?? SvgIcon.ICON_SIZES[iconProps.size ?? 'md'];
const fill =
iconProps.variant === 'warning'
? theme.tokens.graphics.warning.vibrant
: iconProps.variant
? theme.tokens.content[
iconProps.variant === 'muted' ? 'secondary' : iconProps.variant
]
: 'currentColor';

return (
<svg
role="img"
viewBox="0 0 32 32"
height={size}
width={size}
fill="none"
stroke={fill}
strokeWidth="3"
strokeLinejoin="round"
strokeLinecap="round"
>
<style>{loadingStyles}</style>
<defs>
<clipPath id={clipId}>
<path d={SPIN_BODY} />
</clipPath>
</defs>
<path d={SPIN_BODY} />
<g clipPath={`url(#${clipId})`}>
<g style={{transformBox: 'view-box', animation: 'seerR 2s linear infinite'}}>
<path d={SPIN_EYE} />
<circle cx="16" cy="18" r="4" fill={fill} stroke="none" />
</g>
<g style={{transformBox: 'view-box', animation: 'seerL 2s linear infinite'}}>
<path d={SPIN_EYE} />
<circle cx="16" cy="18" r="4" fill={fill} stroke="none" />
</g>
</g>
<path d={SPIN_SWEEP_L} style={{animation: 'seerSwA 2s linear infinite'}} />
<path d={SPIN_SWEEP_L} style={{animation: 'seerSwB 2s linear infinite'}} />
</svg>
);
}
4 changes: 2 additions & 2 deletions static/app/icons/icons.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,11 @@ const SECTIONS: TSection[] = [
defaultProps: {},
},
{
id: 'seer-waiting',
id: 'seer-idle',
groups: ['product', 'seer'],
keywords: ['seer', 'ai', 'eye', 'pyramid'],
name: 'Seer',
defaultProps: {animation: 'waiting'},
defaultProps: {animation: 'idle'},
},
{
id: 'seer-loading',
Expand Down
4 changes: 2 additions & 2 deletions static/app/views/investigations/detail/cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ function CellProgress({state}: {state: CellProgressState}) {
<Flex align="center" gap="xs" data-test-id={`cell-progress-${state}`}>
<IconSeer
size="xs"
animation={['running', 'waiting'].includes(state) ? 'waiting' : undefined}
animation={['running', 'waiting'].includes(state) ? 'idle' : undefined}
/>
<Text variant="muted">{message}</Text>
</Flex>
Expand Down Expand Up @@ -716,7 +716,7 @@ function RefinementPanel({
return (
<RefinementDisclosure defaultExpanded size="sm">
<AgentActivityDisclosureTitle
leadingItems={<IconSeer size="xs" animation={active ? 'waiting' : undefined} />}
leadingItems={<IconSeer size="xs" animation={active ? 'idle' : undefined} />}
trailingItems={
<Flex align="center" gap="sm">
{elapsed === null ? null : <ElapsedDuration milliseconds={elapsed} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,7 @@ export function AskSeerButton() {
<IconSeer
variant={state === 'thinking' && prefersReducedMotion ? 'accent' : undefined}
animation={
showMessageIndicator
? 'waiting'
: state === 'thinking'
? 'loading'
: undefined
state === 'thinking' ? 'loading' : showMessageIndicator ? 'idle' : undefined
}
/>
{showMessageIndicator && isIconOnly ? <MessageIndicator /> : null}
Expand Down
2 changes: 1 addition & 1 deletion static/app/views/seerExplorer/components/emptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function EmptyState({
</Fragment>
) : (
<Fragment>
<IconSeer size="xl" animation="waiting" />
<IconSeer size="xl" animation="idle" />
<Text>{t('Ask Seer anything about your application.')}</Text>
{onSuggestionClick && (
<Stack align="center" gap="md" paddingTop="2xl">
Expand Down
2 changes: 1 addition & 1 deletion static/gsApp/components/ai/AiSetupDataConsent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function AiSetupDataConsent({groupId}: AiSetupDataConsentProps) {
<SingleCard>
<Flex align="center" gap="md">
<MeetSeerHeader>MEET SEER</MeetSeerHeader>
<IconSeer animation="waiting" variant="muted" size="lg" />
<IconSeer animation="idle" variant="muted" size="lg" />
</Flex>
<Paragraph>
{t(
Expand Down
2 changes: 1 addition & 1 deletion static/gsApp/views/amCheckout/steps/productSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function ProductSelect({
ariaRole="checkbox"
topDecoration={
<Flex align="center" gap="lg">
<IconSeer animation="waiting" size="lg" />
<IconSeer animation="idle" size="lg" />
<Heading as="h2">
{t('Detect and fix issues faster with our AI agent')}
</Heading>
Expand Down
Loading