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

Kyber Assistant Interface #2540

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f728383
Add first UI
tienkane Sep 25, 2024
ef7ce94
Complete Assistant full flow
tienkane Sep 25, 2024
bd9772f
Add flow token check price
tienkane Sep 25, 2024
0550ab5
Change type to pass building process
tienkane Sep 25, 2024
0a85de2
Re-build
tienkane Sep 26, 2024
5602610
Disable chat input if the action is option type
tienkane Sep 26, 2024
b08abf2
Improve check token price
tienkane Sep 26, 2024
1060dde
Change color of action button, sort tokens by market cap, show symbol…
tienkane Sep 26, 2024
3d61c78
Change chat panel width
tienkane Sep 26, 2024
fb369b7
Add 'See market trends' action flow
tienkane Sep 26, 2024
16156b3
Add quote symbol by chain for price
tienkane Sep 26, 2024
cf983dc
Change panel max-height and fix coming soon action
tienkane Sep 26, 2024
42c0224
Change panel index
tienkane Sep 26, 2024
cf102f5
Change chain selector UI
tienkane Sep 26, 2024
ab8ffbf
Fix bug choose number of top market trends
tienkane Sep 26, 2024
485da4c
Fix to show symbol instead of address
tienkane Sep 26, 2024
a1604f3
Add action search another token
tienkane Sep 26, 2024
e8999a9
Add static flow for Swap token action
tienkane Sep 26, 2024
95153e8
Disabled previous actions
tienkane Sep 27, 2024
983825f
Change some Kai answer styles and re-sort MAIN MENU order
tienkane Sep 27, 2024
f34f6e9
Change logic check previous actions & remove uuid
tienkane Sep 27, 2024
33e27e6
Show address for token info
tienkane Sep 27, 2024
230620f
Fix Kai message when swap & add limit order action
tienkane Sep 27, 2024
a8084bd
Fix width
tienkane Sep 27, 2024
fa05a09
Fix width
tienkane Sep 27, 2024
dcf6cf0
Change prompt
tienkane Sep 27, 2024
5605a59
Fix top gainer filter params
tienkane Sep 27, 2024
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
4 changes: 4 additions & 0 deletions src/assets/svg/ic_send.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/assets/svg/kai_avatar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/assets/svg/kai_avatar2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
289 changes: 289 additions & 0 deletions src/components/Kai/KaiPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
import { ChainId } from '@kyberswap/ks-sdk-core'
import { ChangeEvent, KeyboardEvent, useEffect, useMemo, useRef, useState } from 'react'
import { Flex } from 'rebass'
import { useGetQuoteByChainQuery } from 'services/marketOverview'

import { ReactComponent as KaiAvatar } from 'assets/svg/kai_avatar.svg'
import NavGroup from 'components/Header/groups/NavGroup'
import { DropdownTextAnchor } from 'components/Header/styleds'
import { MAINNET_NETWORKS } from 'constants/networks'
import { useAllTokens } from 'hooks/Tokens'
import { NETWORKS_INFO } from 'hooks/useChainsConfig'

import { ActionType, KAI_ACTIONS, KaiAction, KaiOption } from './actions'
import {
ActionButton,
ActionPanel,
ActionText,
ChainAnchorBackground,
ChainAnchorWrapper,
ChainItem,
ChainSelectorWrapper,
ChatInput,
ChatPanel,
ChatWrapper,
Divider,
HeaderSubText,
HeaderTextName,
KaiHeaderLeft,
KaiHeaderWrapper,
Loader,
LoadingWrapper,
MainActionButton,
SelectedChainImg,
SendIcon,
UserMessage,
UserMessageWrapper,
} from './styled'

const DEFAULT_LOADING_TEXT = 'KAI is checking the data ...'
const DEFAULT_CHAT_PLACEHOLDER_TEXT = 'Write a message...'
const DEFAULT_CHAIN_ID = 1

const KaiPanel = () => {
const chatPanelRef = useRef<HTMLDivElement>(null)

const [chatPlaceHolderText, setChatPlaceHolderText] = useState(DEFAULT_CHAT_PLACEHOLDER_TEXT)
const [loading, setLoading] = useState(false)
const [loadingText, setLoadingText] = useState(DEFAULT_LOADING_TEXT)
const [listActions, setListActions] = useState<KaiAction[]>([KAI_ACTIONS.MAIN_MENU])
const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID)

const whitelistTokens = useAllTokens(true, chainId)
const whitelistTokenAddress = useMemo(() => Object.keys(whitelistTokens), [whitelistTokens])

const { data: quoteData } = useGetQuoteByChainQuery()
const quoteSymbol = useMemo(
() => quoteData?.data?.onchainPrice?.usdQuoteTokenByChainId?.[chainId || 1]?.symbol,
[chainId, quoteData],
)

const lastAction = useMemo(() => {
const cloneListActions = [...listActions]
cloneListActions.reverse()

return cloneListActions.find(
(action: KaiAction) =>
action.type !== ActionType.INVALID &&
action.type !== ActionType.INVALID_AND_BACK &&
action.type !== ActionType.USER_MESSAGE,
)
}, [listActions])

const lastActiveActionIndex = useMemo(() => {
const clonelistActions = [...listActions]
let index = clonelistActions.length - 1
for (let i = clonelistActions.length - 1; i >= 0; i--) {
if (clonelistActions[i].type !== ActionType.INVALID && clonelistActions[i].type !== ActionType.USER_MESSAGE) {
index = i
break
}
}

return index
}, [listActions])

const onSubmitChat = (text: string) => {
if (loading || !lastAction) return
if (lastAction.loadingText) setLoadingText(lastAction.loadingText)
setLoading(true)
onChangeListActions([
{
title: text,
type: ActionType.USER_MESSAGE,
},
])
}

const onChangeListActions = (newActions: KaiAction[]) => {
const cloneListActions = [...listActions]
setListActions(cloneListActions.concat(newActions))
}

const getActionResponse = async () => {
const lastUserAction = listActions[listActions.length - 1]
if (lastUserAction?.type === ActionType.USER_MESSAGE) {
const newActions: KaiAction[] =
(await lastAction?.response?.({
answer: lastUserAction?.title?.toLowerCase() || '',
chainId,
whitelistTokenAddress,
arg: lastAction.arg,
quoteSymbol,
})) || []
if (newActions.length) onChangeListActions(newActions)

setLoading(false)
setLoadingText(DEFAULT_LOADING_TEXT)
}
}

useEffect(() => {
if (lastAction?.placeholder) setChatPlaceHolderText(lastAction.placeholder)
else setChatPlaceHolderText(DEFAULT_CHAT_PLACEHOLDER_TEXT)
}, [lastAction])

useEffect(() => {
if (chatPanelRef.current)
chatPanelRef.current.scrollTo({ top: chatPanelRef.current.scrollHeight, behavior: 'smooth' })
}, [listActions])

useEffect(() => {
getActionResponse()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listActions])

return (
<>
<KaiHeader chainId={chainId} setChainId={setChainId} />

<ChatPanel ref={chatPanelRef}>
<div>GM! What can I do for you today? 👋</div>
{listActions.map((action: KaiAction, index: number) => {
const disabled = index !== lastActiveActionIndex

return action.type === ActionType.MAIN_OPTION ? (
<ActionPanel key={index}>
{action.data?.map((option: KaiOption, optionIndex: number) => (
<MainActionButton
disabled={disabled}
key={optionIndex}
width={option.space}
onClick={() => !disabled && onSubmitChat(option.title)}
>
{option.title}
</MainActionButton>
))}
</ActionPanel>
) : action.type === ActionType.OPTION || action.type === ActionType.INVALID_AND_BACK ? (
<ActionPanel key={index}>
{action.data?.map((option: KaiOption, optionIndex: number) => (
<ActionButton
disabled={disabled}
key={optionIndex}
width={option.space}
onClick={() => !disabled && onSubmitChat(option.title)}
>
{option.title}
</ActionButton>
))}
</ActionPanel>
) : action.type === ActionType.TEXT || action.type === ActionType.INVALID ? (
<ActionText key={index}>{action.title}</ActionText>
) : action.type === ActionType.HTML && action.title ? (
<ActionText key={index} dangerouslySetInnerHTML={{ __html: action.title }} />
Fixed Show fixed Hide fixed
) : action.type === ActionType.USER_MESSAGE ? (
<UserMessageWrapper key={index} havePrevious={listActions[index - 1].type === ActionType.USER_MESSAGE}>
<UserMessage
havePrevious={listActions[index - 1].type === ActionType.USER_MESSAGE}
haveFollowing={
index < listActions.length - 1 && listActions[index + 1].type === ActionType.USER_MESSAGE
}
>
{action.title}
</UserMessage>
</UserMessageWrapper>
) : null
})}
</ChatPanel>

{loading && <KaiLoading loadingText={loadingText} />}
<KaiChat
disabled={loading || lastAction?.type === ActionType.OPTION || lastAction?.type === ActionType.MAIN_OPTION}
chatPlaceHolderText={chatPlaceHolderText}
onSubmitChat={onSubmitChat}
/>
</>
)
}

const KaiHeader = ({ chainId, setChainId }: { chainId: ChainId; setChainId: (value: number) => void }) => {
return (
<>
<KaiHeaderWrapper>
<KaiHeaderLeft>
<KaiAvatar />
<Flex sx={{ gap: '2px' }} flexDirection={'column'}>
<HeaderTextName>I&apos;m KAI</HeaderTextName>
<HeaderSubText>Kyber Assistant Interface</HeaderSubText>
</Flex>
</KaiHeaderLeft>
<NavGroup
dropdownAlign={'right'}
anchor={
<DropdownTextAnchor>
<ChainAnchorWrapper>
<SelectedChainImg src={NETWORKS_INFO[chainId].icon} alt="" />
<ChainAnchorBackground />
</ChainAnchorWrapper>
</DropdownTextAnchor>
}
dropdownContent={
<ChainSelectorWrapper>
{MAINNET_NETWORKS.map(item => (
<ChainItem key={item} onClick={() => setChainId(item)} active={item === chainId}>
<img src={NETWORKS_INFO[item].icon} width="18px" height="18px" alt="" />
<span>{NETWORKS_INFO[item].displayName}</span>
</ChainItem>
))}
</ChainSelectorWrapper>
}
/>
</KaiHeaderWrapper>
<Divider />
</>
)
}

const KaiLoading = ({ loadingText }: { loadingText: string }) => {
return (
<LoadingWrapper>
<Loader />
<span>{loadingText}</span>
</LoadingWrapper>
)
}

const KaiChat = ({
chatPlaceHolderText,
onSubmitChat,
disabled = false,
}: {
chatPlaceHolderText: string
onSubmitChat: (text: string) => void
disabled?: boolean
}) => {
const [chatInput, setChatInput] = useState('')

const onChangeChatInput = (e: ChangeEvent<HTMLInputElement>) => setChatInput(e.target.value)

const handleSubmitChatInput = () => {
if (disabled || !chatInput) return
onSubmitChat(chatInput.trim())
setChatInput('')
}

const handleEnter = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'Enter') return
handleSubmitChatInput()
}

return (
<ChatWrapper disabled={disabled}>
<ChatInput
type="text"
id="token-search-input"
data-testid="token-search-input"
placeholder={chatPlaceHolderText}
value={chatInput}
onChange={onChangeChatInput}
onKeyDown={handleEnter}
autoComplete="off"
disabled={disabled}
/>
<SendIcon disabled={disabled} onClick={handleSubmitChatInput} />
</ChatWrapper>
)
}

export default KaiPanel
Loading
Loading