Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create sample for teaser message feature #389

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
129 changes: 112 additions & 17 deletions src/components/ui/WidgetButton.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { ReactNode, useState } from 'react';
import styled, { css } from 'styled-components';

import { getColorBasedOnSaturation } from '../../colors';
import { elementIds } from '../../const';
import { elementIds, FLOATING_STYLES } from '../../const';
import { useConstantState } from '../../context/ConstantContext';
import BotOutlinedIcon from '../../icons/bot-outlined.svg';
import ChevronDownIcon from '../../icons/chevron-down.svg';
import { parseTextMessage, Token } from '../../utils';
import TokensBody from '../TokensBody';

const buttonEffect = css`
&:hover {
Expand Down Expand Up @@ -36,6 +40,7 @@ const ButtonContainer = styled.button<{
0px 6px 10px -5px rgba(33, 33, 33, 0.04);

span {
position: absolute;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요건 픽스가 필요한 버그.

width: 100%;
height: 100%;
border-radius: 50%;
Expand Down Expand Up @@ -85,7 +90,7 @@ type IconWrapperProps = {
};

const IconWrapper = styled.span`
position: absolute;
position: fixed;
`;

const OpenIconWrapper = styled(IconWrapper)<IconWrapperProps>`
Expand All @@ -110,6 +115,70 @@ const Icon = {
Close: () => <ChevronDownIcon />,
};

interface TeaserMessageProps {
isVisible: boolean;
}

const TeaserMessage = styled.div<TeaserMessageProps>`
cursor: pointer;
/* Slide-in from right */
@keyframes slideInFromRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}

/* Slide-out to the right */
@keyframes slideOutToRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}

${({ isVisible }) =>
isVisible
? {
animation: 'slideInFromRight 0.5s ease-out forwards',
}
: {
animation: 'slideOutToRight 0.5s ease-in forwards',
}};
`;

export const TeaserMessageComponent = ({ children }: { children: ReactNode }) => {
const [isVisible, setIsVisible] = useState(true);

const handleToggle = () => {
setIsVisible(false); // Trigger slide-out animation
};

return (
<TeaserMessage onClick={handleToggle} isVisible={isVisible}>
{children}
</TeaserMessage>
);
};

const TeaserMessagesContainer = styled.div`
position: fixed;
z-index: ${FLOATING_STYLES.TEASER_MESSAGES.zIndex};
bottom: ${FLOATING_STYLES.TEASER_MESSAGES.bottom};
right: ${FLOATING_STYLES.TEASER_MESSAGES.right};
display: flex;
flex-direction: column;
gap: 8px;
`;

export interface WidgetButtonProps {
isOpen: boolean;
accentColor: string;
Expand All @@ -119,6 +188,26 @@ export interface WidgetButtonProps {
animated?: boolean;
}

interface TeaserMessagesProps {
teaserMessages: string[];
}

export const TeaserMessages = ({ teaserMessages }: TeaserMessagesProps) => {
const { replacementTextList } = useConstantState();
return (
<TeaserMessagesContainer id={elementIds.widgetTeaserMessages}>
{teaserMessages.map((message, i) => {
const tokens: Token[] = parseTextMessage(message, replacementTextList);
return (
<TeaserMessageComponent key={i}>
{tokens && tokens.length > 0 ? <TokensBody tokens={tokens} /> : message}
</TeaserMessageComponent>
);
})}
</TeaserMessagesContainer>
);
};

export const WidgetButton = ({
isOpen,
imageUrl,
Expand All @@ -127,21 +216,27 @@ export const WidgetButton = ({
className,
animated = true,
}: WidgetButtonProps) => {
const { botStudioEditProps } = useConstantState();
const { teaserMessages } = botStudioEditProps ?? {};

return (
<ButtonContainer
id={elementIds.widgetToggleButton}
aria-label="Widget toggle button"
className={className}
onClick={onClick}
backgroundColor={accentColor}
animated={animated}
>
<OpenIconWrapper isOpen={isOpen} animated={animated}>
<Icon.Open url={imageUrl} />
</OpenIconWrapper>
<CloseIconWrapper isOpen={isOpen} animated={animated}>
<Icon.Close />
</CloseIconWrapper>
</ButtonContainer>
<>
{Array.isArray(teaserMessages) && !isOpen && <TeaserMessages teaserMessages={teaserMessages} />}
<ButtonContainer
id={elementIds.widgetToggleButton}
aria-label="Widget toggle button"
className={className}
onClick={onClick}
backgroundColor={accentColor}
animated={animated}
>
<OpenIconWrapper isOpen={isOpen} animated={animated}>
<Icon.Open url={imageUrl} />
</OpenIconWrapper>
<CloseIconWrapper isOpen={isOpen} animated={animated}>
<Icon.Close />
</CloseIconWrapper>
</ButtonContainer>
</>
);
};
8 changes: 4 additions & 4 deletions src/components/widget/WidgetToggleButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import styled from 'styled-components';

import { MAX_Z_INDEX } from '../../const';
import { FLOATING_STYLES } from '../../const';
import { useConstantState } from '../../context/ConstantContext';
import { useWidgetSetting } from '../../context/WidgetSettingContext';
import { useWidgetState } from '../../context/WidgetStateContext';
Expand All @@ -9,9 +9,9 @@ import { WidgetButton, WidgetButtonProps } from '../ui/WidgetButton';
const FloatingWidgetButton = styled(WidgetButton)`
&& {
position: fixed;
z-index: ${MAX_Z_INDEX};
bottom: 24px;
right: 24px;
z-index: ${FLOATING_STYLES.WIDGET_BUTTON.zIndex};
bottom: ${FLOATING_STYLES.WIDGET_BUTTON.bottom};
right: ${FLOATING_STYLES.WIDGET_BUTTON.right};
}
`;

Expand Down
14 changes: 14 additions & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ import { noop } from './utils';

// Most of browsers use a 32-bit signed integer as the maximum value for z-index
export const MAX_Z_INDEX = 2147483647;
export const FLOATING_STYLES = {
WIDGET_BUTTON: {
zIndex: MAX_Z_INDEX,
right: '24px',
bottom: '24px',
},
TEASER_MESSAGES: {
zIndex: MAX_Z_INDEX,
right: '24px',
bottom: '80px', // 24 + 48(button height) + 8(gap) = 80px
},
};
// .sendbird-modal-root will be on top of the widget window
export const WIDGET_WINDOW_Z_INDEX = MAX_Z_INDEX - 1;

Expand Down Expand Up @@ -113,6 +125,7 @@ export interface BotStudioEditProps {
welcomeMessages?: WelcomeUserMessage[];
styles?: WidgetStyles;
suggestedRepliesDirection?: 'horizontal' | 'vertical';
teaserMessages?: string[];
}

export interface MessageInputControls {
Expand Down Expand Up @@ -347,6 +360,7 @@ export interface CustomRefreshComponent extends CustomRefreshProps {
export const elementIds = {
widgetWindow: 'aichatbot-widget-window',
widgetToggleButton: 'aichatbot-widget-button',
widgetTeaserMessages: 'aichatbot-teaser-messages',
collapseIcon: 'aichatbot-widget-collapse-icon',
expandIcon: 'aichatbot-widget-expand-icon',
closeIcon: 'aichatbot-widget-close-icon',
Expand Down
16 changes: 15 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,21 @@ const WidgetApp = () => {
}

const host = getHost(region);
return <App applicationId={appId} botId={botId} locale={locale} apiHost={host.apiHost} wsHost={host.wsHost} />;
return (
<App
applicationId={appId}
botId={botId}
locale={locale}
apiHost={host.apiHost}
wsHost={host.wsHost}
botStudioEditProps={{
teaserMessages: [
'👋 Hi! I am a Chatbase AI, ask me anything about Chatbase!',
'By the way, you can create a chatbot like me for your website! 😊',
],
}}
/>
);
};

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
Expand Down