Skip to content

Commit

Permalink
feat(web): Hotfix - Add disclaimer for UTL web chat (#16577) (#16656)
Browse files Browse the repository at this point in the history
* Add disclaimer

* Move EmbedDisclaimer component

* Remove whitespace

* Address coderabbit suggestions

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
RunarVestmann and kodiakhq[bot] authored Oct 30, 2024
1 parent 57649a6 commit 9c960a5
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 29 deletions.
95 changes: 70 additions & 25 deletions apps/web/components/ChatPanel/ChatBubble/ChatBubble.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import React from 'react'
import React, { useRef, useState } from 'react'
import cn from 'classnames'

import { Box, FocusableBox, LoadingDots, Text } from '@island.is/island-ui/core'

import {
EmbedDisclaimer,
type EmbedDisclaimerProps,
} from './EmbedDisclaimer/EmbedDisclaimer'
import * as styles from './ChatBubble.css'

interface ChatBubbleProps {
Expand All @@ -11,6 +15,7 @@ interface ChatBubbleProps {
isVisible?: boolean
onClick?: () => void
loading?: boolean
embedDisclaimerProps?: Pick<EmbedDisclaimerProps, 'localStorageKey' | 'texts'>
}

export const ChatBubble = ({
Expand All @@ -19,34 +24,74 @@ export const ChatBubble = ({
onClick,
pushUp = false,
loading = false,
embedDisclaimerProps,
}: ChatBubbleProps) => {
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false)
const hasButtonBeenClicked = useRef(false)

return (
<div className={cn(styles.root, { [styles.hidden]: !isVisible })}>
<FocusableBox
component="button"
data-testid="chatbot"
tabIndex={0}
className={cn(styles.message, pushUp && styles.messagePushUp)}
onClick={onClick}
>
<Box position="relative">
<Box>
<Box style={{ visibility: loading ? 'hidden' : 'visible' }}>
<Text variant="h5" color="white">
{text}
</Text>
</Box>
{loading && (
<Box className={styles.loadingDots}>
<LoadingDots color="white" />
<>
{isConfirmModalOpen && embedDisclaimerProps && (
<EmbedDisclaimer
localStorageKey={embedDisclaimerProps.localStorageKey}
onAnswer={(acceptsTerms) => {
setIsConfirmModalOpen(false)
if (acceptsTerms) {
hasButtonBeenClicked.current = true
onClick?.()
}
}}
texts={embedDisclaimerProps.texts}
/>
)}
<div className={cn(styles.root, { [styles.hidden]: !isVisible })}>
<FocusableBox
component="button"
data-testid="chatbot"
tabIndex={0}
className={cn(styles.message, pushUp && styles.messagePushUp)}
onClick={() => {
if (!embedDisclaimerProps || hasButtonBeenClicked.current) {
onClick?.()
return
}

let itemValue: string | null = null
try {
itemValue = localStorage.getItem(
embedDisclaimerProps.localStorageKey,
)
} catch (error) {
console.warn('Failed to get preference:', error)
}

if (itemValue === 'true') {
onClick?.()
return
}

setIsConfirmModalOpen(true)
}}
>
<Box position="relative">
<Box>
<Box style={{ visibility: loading ? 'hidden' : 'visible' }}>
<Text variant="h5" color="white">
{text}
</Text>
</Box>
)}
{loading && (
<Box className={styles.loadingDots}>
<LoadingDots color="white" />
</Box>
)}
</Box>
</Box>
</Box>
<div className={styles.messageArrow} />
<div className={styles.messageArrowBorder} />
</FocusableBox>
</div>
<div className={styles.messageArrow} />
<div className={styles.messageArrowBorder} />
</FocusableBox>
</div>
</>
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { style } from '@vanilla-extract/css'

import { theme, themeUtils } from '@island.is/island-ui/theme'

export const link = style({
textDecoration: 'underline',
})

export const modal = style({
width: 357,
position: 'fixed',
zIndex: 10000,
bottom: theme.spacing[2],
right: theme.spacing[1],
...themeUtils.responsiveStyle({
md: {
bottom: theme.spacing[3],
right: theme.spacing[3],
},
}),
transition: 'opacity 0.3s ease',
boxShadow: '0 4px 30px rgba(0, 0, 0, 0.16)',
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React, { type ReactNode } from 'react'
import { Controller, useForm } from 'react-hook-form'

import {
Box,
Button,
Checkbox,
FocusableBox,
Icon,
LinkContext,
Stack,
} from '@island.is/island-ui/core'

import * as styles from './EmbedDisclaimer.css'

export interface EmbedDisclaimerProps {
texts: {
cancel: string
accept: string
message: ReactNode
remember: string
}
onAnswer: (acceptsTerms: boolean) => void
localStorageKey: string
}

export const EmbedDisclaimer = ({
texts,
onAnswer,
localStorageKey,
}: EmbedDisclaimerProps) => {
const methods = useForm()

const { control } = methods

return (
<Box
className={styles.modal}
background="blue100"
borderRadius="large"
padding="gutter"
>
<Stack space={1}>
<Box display="flex" justifyContent="spaceBetween">
<Box />
<FocusableBox
tabIndex={0}
aria-label={texts.cancel}
onKeyDown={(ev) => {
if (ev.key === 'Enter' || ev.key === ' ') {
onAnswer(false)
ev.preventDefault()
}
}}
onClick={() => {
onAnswer(false)
}}
>
<Icon icon="close" size="medium" />
</FocusableBox>
</Box>
<Box padding="p1">
<LinkContext.Provider
value={{
linkRenderer: (href, children) => (
<a
className={styles.link}
href={href}
rel="noopener noreferrer"
target="_blank"
>
{children}
</a>
),
}}
>
<Stack space={3}>
<Stack space={2} align="center">
<Box>{texts.message}</Box>
<Controller
name={localStorageKey}
defaultValue={false}
control={control}
rules={{ required: false }}
render={({ field: { onChange, value } }) => (
<Checkbox
label={texts.remember}
labelVariant="small"
checked={value}
onChange={(e) => {
onChange(e.target.checked)
try {
localStorage.setItem(
localStorageKey,
e.target.checked ? 'true' : 'false',
)
} catch (error) {
console.warn('Failed to save preference:', error)
}
}}
/>
)}
/>
</Stack>
<Stack space={2} align="center">
<Button
fluid={true}
colorScheme="default"
size="small"
onClick={() => {
onAnswer(true)
}}
>
{texts.accept}
</Button>
<Button
fluid={true}
variant="ghost"
colorScheme="default"
size="small"
onClick={() => {
onAnswer(false)
}}
>
{texts.cancel}
</Button>
</Stack>
</Stack>
</LinkContext.Provider>
</Box>
</Stack>
</Box>
)
}
62 changes: 58 additions & 4 deletions apps/web/components/ChatPanel/WatsonChatPanel/WatsonChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,10 @@ export const WatsonChatPanel = (props: WatsonChatPanelProps) => {
const watsonInstance = useRef<WatsonInstance | null>(null)
const [hasButtonBeenClicked, setHasButtonBeenClicked] = useState(false)

const replaceDirectorateOfImmigrationWebChatWithAI = Boolean(
namespace?.replaceDirectorateOfImmigrationWebChatWithAI,
)

useEffect(() => {
if (Object.keys(namespace).length === 0) {
return () => {
Expand All @@ -294,10 +298,6 @@ export const WatsonChatPanel = (props: WatsonChatPanelProps) => {
const namespaceValue = namespace?.[namespaceKey] ?? {}
const { cssVariables, ...languagePack } = namespaceValue

const replaceDirectorateOfImmigrationWebChatWithAI = Boolean(
namespace?.replaceDirectorateOfImmigrationWebChatWithAI,
)

const propsCopy = { ...props }

if (
Expand Down Expand Up @@ -415,6 +415,8 @@ export const WatsonChatPanel = (props: WatsonChatPanelProps) => {

if (showLauncher) return null

const chatTermsUrl = n('chatTermsUrl', '')

return (
<Hidden print>
{shouldDisplayFeedbackPanel && (
Expand Down Expand Up @@ -457,6 +459,58 @@ export const WatsonChatPanel = (props: WatsonChatPanelProps) => {
}}
pushUp={pushUp}
loading={loading}
embedDisclaimerProps={
replaceDirectorateOfImmigrationWebChatWithAI &&
props.integrationID === '9e320784-ad44-4da9-9eb3-f305057a196a'
? {
localStorageKey: 'watsonAssistantChatAllowed',
texts: {
message:
activeLocale === 'is' ? (
<Text variant="small">
Þetta netspjall er hýst af þriðja aðila. Með því að
halda áfram samþykkir þú{' '}
{!chatTermsUrl ? (
'skilmála'
) : (
<a href={chatTermsUrl ?? ''}>skilmála</a>
)}{' '}
þeirra.
</Text>
) : (
<Text variant="small">
This AI chatbot is hosted by a third party and by
continuing, you are agreeing to their{' '}
{!chatTermsUrl ? (
'terms and conditions'
) : (
<a href={chatTermsUrl ?? ''}>
terms and conditions
</a>
)}
. The chatbots purpose is to assist you in your search
for information, it's still learning so suggestions
must be taken with caution.
</Text>
),
remember: n(
'rememberThisSetting',
activeLocale === 'is'
? 'Muna þessa stillingu'
: 'Remember this setting',
),
accept: n(
'accept',
activeLocale === 'is' ? 'Halda áfram' : 'I agree',
),
cancel: n(
'cancel',
activeLocale === 'is' ? 'Hætta við' : 'I disagree',
),
},
}
: undefined
}
/>
)}
<ToastContainer />
Expand Down

0 comments on commit 9c960a5

Please sign in to comment.