Skip to content

Commit 37423ef

Browse files
authored
feat: CP-11851 Core concierge (#432)
1 parent b03c34b commit 37423ef

File tree

27 files changed

+1854
-49
lines changed

27 files changed

+1854
-49
lines changed

apps/next/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@
4141
"date-fns": "4.1.0",
4242
"motion": "12.5.0",
4343
"react": "19.0.0",
44+
"react-custom-scrollbars-2": "4.5.0",
4445
"react-dom": "19.0.0",
4546
"react-i18next": "11.18.6",
4647
"react-icons": "5.4.0",
48+
"react-markdown": "10.1.0",
49+
"react-transition-group": "4.4.2",
4750
"react-virtualized": "9.22.3",
4851
"react-window": "1.8.11",
4952
"zod": "3.23.8"
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import {
2+
Box,
3+
getHexAlpha,
4+
Stack,
5+
Typography,
6+
useTheme,
7+
} from '@avalabs/k2-alpine';
8+
import { FC, useRef, useState } from 'react';
9+
import { useHistory } from 'react-router-dom';
10+
import { useTranslation } from 'react-i18next';
11+
import { CSSTransition, TransitionGroup } from 'react-transition-group';
12+
import {
13+
AnimatedButton,
14+
CSS_CLASSES,
15+
PromptButtonBackground,
16+
TextAnimation,
17+
} from './components/styledComponents';
18+
import { useFeatureFlagContext, useSettingsContext } from '@core/ui';
19+
import { FeatureGates } from '@core/types';
20+
import { ConciergePromptBackground } from './components/ConciergePromptBackground';
21+
import { ConciergeBackdrop } from './components/ConciergeBackdrop';
22+
23+
type ConciergePromptProps = {
24+
isAIBackdropOpen: boolean;
25+
setIsAIBackdropOpen: (isOpen: boolean) => void;
26+
};
27+
28+
export const ConciergePrompt: FC<ConciergePromptProps> = ({
29+
isAIBackdropOpen,
30+
setIsAIBackdropOpen,
31+
}) => {
32+
const theme = useTheme();
33+
const history = useHistory();
34+
const { t } = useTranslation();
35+
36+
const [isHoverAreaHidden, setIsHoverAreaHidden] = useState(false);
37+
const { coreAssistant } = useSettingsContext();
38+
const { featureFlags } = useFeatureFlagContext();
39+
const timer = useRef<NodeJS.Timeout | null>(null);
40+
const hasBackdropEntered = useRef(false);
41+
42+
const buttonLabels = [
43+
t('Ask Core Concierge to send crypto'),
44+
t('Ask Core Concierge to swap tokens'),
45+
t('Ask Core Concierge to bridge tokens'),
46+
t('Ask Core Concierge to transfer for you'),
47+
t('Ask Core Concierge to manage accounts'),
48+
];
49+
50+
const getRandomButtonLabel = () => {
51+
return buttonLabels[Math.floor(Math.random() * buttonLabels.length)];
52+
};
53+
54+
const conciergeButtonRef = useRef(null);
55+
const conciergeBackgroundRef = useRef(null);
56+
const conciergeBackdropRef = useRef(null);
57+
58+
if (!coreAssistant || !featureFlags[FeatureGates.CORE_ASSISTANT]) {
59+
return null;
60+
}
61+
62+
return (
63+
<>
64+
{/* THE BOX AREA WHERE WE WANT TO CATCH THE CURSOR */}
65+
<Stack
66+
sx={{
67+
width: '100%',
68+
height: '40px',
69+
position: 'absolute',
70+
top: '56px',
71+
zIndex: isHoverAreaHidden ? 0 : theme.zIndex.tooltip - 1,
72+
}}
73+
onMouseEnter={() => {
74+
timer.current = setTimeout(() => {
75+
setIsAIBackdropOpen(true);
76+
}, 400);
77+
}}
78+
onMouseLeave={() => {
79+
if (!isAIBackdropOpen && timer.current) {
80+
clearTimeout(timer.current);
81+
}
82+
}}
83+
/>
84+
<TransitionGroup component={null}>
85+
<Stack>
86+
{/* GRADIENT BACKGROUND */}
87+
<CSSTransition
88+
key={1}
89+
timeout={200}
90+
classNames={CSS_CLASSES.OVERLAY}
91+
appear
92+
enter
93+
exit
94+
in={isAIBackdropOpen}
95+
nodeRef={conciergeBackgroundRef}
96+
>
97+
<ConciergePromptBackground>
98+
<PromptButtonBackground
99+
ref={conciergeBackgroundRef}
100+
className={CSS_CLASSES.PROMPT_BACKGROUND}
101+
/>
102+
</ConciergePromptBackground>
103+
</CSSTransition>
104+
{/* BACKDROP */}
105+
<CSSTransition
106+
key={2}
107+
timeout={1000}
108+
classNames={CSS_CLASSES.BACKDROP}
109+
appear
110+
exit
111+
in={isAIBackdropOpen}
112+
nodeRef={conciergeBackdropRef}
113+
onExited={() => {
114+
hasBackdropEntered.current = false;
115+
}}
116+
>
117+
<ConciergeBackdrop
118+
conciergeBackdropRef={conciergeBackdropRef}
119+
hasBackdropEntered={hasBackdropEntered}
120+
setIsAIBackdropOpen={setIsAIBackdropOpen}
121+
setIsHoverAreaHidden={setIsHoverAreaHidden}
122+
/>
123+
</CSSTransition>
124+
{/* THE BUTTON */}
125+
<CSSTransition
126+
key={3}
127+
timeout={1000}
128+
classNames={CSS_CLASSES.BUTTON}
129+
appear
130+
exit
131+
in={isAIBackdropOpen}
132+
nodeRef={conciergeButtonRef}
133+
onEntered={() => {
134+
// it needs to be delayed (waiting for the button animation getting done) to avoid the glitch
135+
setTimeout(() => {
136+
setIsHoverAreaHidden(true);
137+
hasBackdropEntered.current = true;
138+
}, 500);
139+
}}
140+
>
141+
<Stack
142+
sx={{
143+
position: 'absolute',
144+
top: '56px',
145+
width: '100%',
146+
height: '40px',
147+
px: 1.5,
148+
}}
149+
>
150+
<AnimatedButton
151+
variant="contained"
152+
sx={{
153+
borderColor: 'common.white_10',
154+
maxWidth: '100%',
155+
justifyContent: 'flex-start',
156+
px: 1.5,
157+
backgroundColor: getHexAlpha(theme.palette.text.primary, 30),
158+
color: 'common.white',
159+
}}
160+
onClick={() => {
161+
history.push('/concierge');
162+
}}
163+
size="large"
164+
fullWidth
165+
ref={conciergeButtonRef}
166+
>
167+
<Box component="span" sx={{ mr: 1, fontSize: 24 }}>
168+
169+
</Box>
170+
<TextAnimation>
171+
<Typography variant="subtitle3">
172+
{isAIBackdropOpen && getRandomButtonLabel()}
173+
</Typography>
174+
</TextAnimation>
175+
</AnimatedButton>
176+
</Stack>
177+
</CSSTransition>
178+
</Stack>
179+
</TransitionGroup>
180+
</>
181+
);
182+
};

apps/next/src/components/Header/Header.tsx

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,23 @@
1-
import {
2-
getHexAlpha,
3-
Stack,
4-
styled,
5-
Typography,
6-
useTheme,
7-
} from '@avalabs/k2-alpine';
1+
import { getHexAlpha, Stack, Typography, useTheme } from '@avalabs/k2-alpine';
82
import { useAccountsContext } from '@core/ui';
93
import { useState } from 'react';
104
import { MdOutlineUnfoldMore } from 'react-icons/md';
115
import { useHistory } from 'react-router-dom';
126
import { PersonalAvatar } from '../PersonalAvatar';
137
import { AddressList } from './AddressList';
148
import { HeaderActions } from './components/HeaderActions';
15-
16-
const AccountInfo = styled(Stack)`
17-
cursor: pointer;
18-
border-radius: 10px;
19-
padding: ${({ theme }) => theme.spacing(0.5)};
20-
transition: ${({ theme }) =>
21-
theme.transitions.create(['background', 'opacity'])};
22-
flex-direction: row;
23-
align-items: center;
24-
& > svg {
25-
opacity: 0;
26-
}
27-
`;
28-
29-
const AccountSelectContainer = styled(Stack)`
30-
cursor: pointer;
31-
position: relative;
32-
&:hover > div:first-of-type {
33-
background: ${({ theme }) => getHexAlpha(theme.palette.primary.main, 10)};
34-
& > svg {
35-
opacity: 1;
36-
}
37-
}
38-
`;
9+
import {
10+
AccountInfo,
11+
AccountSelectContainer,
12+
} from './components/styledComponents';
13+
import { ConciergePrompt } from './ConciergePrompt';
3914

4015
export const Header = () => {
4116
const { accounts } = useAccountsContext();
4217
const activeAccount = accounts.active;
4318
const theme = useTheme();
4419
const [isAddressAppear, setIsAddressAppear] = useState(false);
20+
const [isAIBackdropOpen, setIsAIBackdropOpen] = useState(false);
4521
const history = useHistory();
4622

4723
// TODO: fix this after the transactions will be implemented
@@ -69,7 +45,10 @@ export const Header = () => {
6945
alignItems: 'center',
7046
justifyContent: 'space-between',
7147
px: 1,
72-
zIndex: 1,
48+
zIndex: theme.zIndex.tooltip + 1,
49+
}}
50+
onMouseEnter={() => {
51+
setIsAIBackdropOpen(false);
7352
}}
7453
>
7554
<AccountSelectContainer
@@ -95,6 +74,10 @@ export const Header = () => {
9574
pendingTransaction={isTransactionPending}
9675
/>
9776
</Stack>
77+
<ConciergePrompt
78+
isAIBackdropOpen={isAIBackdropOpen}
79+
setIsAIBackdropOpen={setIsAIBackdropOpen}
80+
/>
9881
</Stack>
9982
);
10083
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Stack, useTheme } from '@avalabs/k2-alpine';
2+
import { getClassSelector } from './styledComponents';
3+
4+
type ConciergeBackdropProps = {
5+
conciergeBackdropRef: React.RefObject<HTMLDivElement | null>;
6+
hasBackdropEntered: React.RefObject<boolean>;
7+
setIsAIBackdropOpen: (isOpen: boolean) => void;
8+
setIsHoverAreaHidden: (isHidden: boolean) => void;
9+
};
10+
11+
export const ConciergeBackdrop = ({
12+
conciergeBackdropRef,
13+
hasBackdropEntered,
14+
setIsAIBackdropOpen,
15+
setIsHoverAreaHidden,
16+
}: ConciergeBackdropProps) => {
17+
const theme = useTheme();
18+
return (
19+
<Stack
20+
sx={{
21+
position: 'fixed',
22+
top: 0,
23+
left: 0,
24+
right: 0,
25+
bottom: 0,
26+
background: '#28282ECC',
27+
backdropFilter: 'none',
28+
display: 'none',
29+
transition: 'opacity 400ms linear',
30+
opacity: 0,
31+
zIndex: theme.zIndex.appBar - 1,
32+
[`&.${getClassSelector('BACKDROP', 'enter')}`]: {
33+
display: 'flex',
34+
},
35+
[`&.${getClassSelector('BACKDROP', 'enter-done')}`]: {
36+
display: 'flex',
37+
opacity: 1,
38+
},
39+
}}
40+
ref={conciergeBackdropRef}
41+
onMouseMove={() => {
42+
if (hasBackdropEntered.current) {
43+
setIsAIBackdropOpen(false);
44+
setIsHoverAreaHidden(false);
45+
}
46+
}}
47+
/>
48+
);
49+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Stack } from '@avalabs/k2-alpine';
2+
import { PropsWithChildren } from 'react';
3+
import { CSS_CLASSES, getClassSelector } from './styledComponents';
4+
5+
export const ConciergePromptBackground = ({ children }: PropsWithChildren) => {
6+
return (
7+
<Stack
8+
sx={{
9+
[`.${CSS_CLASSES.PROMPT_BACKGROUND}`]: {
10+
display: 'none',
11+
opacity: 0,
12+
transition: `opacity 400ms linear`,
13+
},
14+
[`.${getClassSelector('OVERLAY', 'enter', 'PROMPT_BACKGROUND')}`]: {
15+
display: 'block',
16+
},
17+
[`.${getClassSelector('OVERLAY', 'enter-done', 'PROMPT_BACKGROUND')}`]:
18+
{
19+
display: 'block',
20+
opacity: 1,
21+
},
22+
[`.${getClassSelector('OVERLAY', 'exit', 'PROMPT_BACKGROUND')}`]: {
23+
display: 'block',
24+
opacity: 1,
25+
},
26+
[`.${getClassSelector('OVERLAY', 'exit-done', 'PROMPT_BACKGROUND')}`]: {
27+
display: 'block',
28+
opacity: 0,
29+
},
30+
}}
31+
>
32+
{children}
33+
</Stack>
34+
);
35+
};

0 commit comments

Comments
 (0)