diff --git a/apps/web/components/ChatPanel/ChatBubble/ChatBubble.tsx b/apps/web/components/ChatPanel/ChatBubble/ChatBubble.tsx index 28bc4a6c60b6..6c0dcbec5553 100644 --- a/apps/web/components/ChatPanel/ChatBubble/ChatBubble.tsx +++ b/apps/web/components/ChatPanel/ChatBubble/ChatBubble.tsx @@ -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 { @@ -11,6 +15,7 @@ interface ChatBubbleProps { isVisible?: boolean onClick?: () => void loading?: boolean + embedDisclaimerProps?: Pick } export const ChatBubble = ({ @@ -19,34 +24,74 @@ export const ChatBubble = ({ onClick, pushUp = false, loading = false, + embedDisclaimerProps, }: ChatBubbleProps) => { + const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false) + const hasButtonBeenClicked = useRef(false) + return ( -
- - - - - - {text} - - - {loading && ( - - + <> + {isConfirmModalOpen && embedDisclaimerProps && ( + { + setIsConfirmModalOpen(false) + if (acceptsTerms) { + hasButtonBeenClicked.current = true + onClick?.() + } + }} + texts={embedDisclaimerProps.texts} + /> + )} +
+ { + 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) + }} + > + + + + + {text} + - )} + {loading && ( + + + + )} + - -
-
- -
+
+
+ +
+ ) } diff --git a/apps/web/components/ChatPanel/ChatBubble/EmbedDisclaimer/EmbedDisclaimer.css.ts b/apps/web/components/ChatPanel/ChatBubble/EmbedDisclaimer/EmbedDisclaimer.css.ts new file mode 100644 index 000000000000..246cccc3ddd8 --- /dev/null +++ b/apps/web/components/ChatPanel/ChatBubble/EmbedDisclaimer/EmbedDisclaimer.css.ts @@ -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)', +}) diff --git a/apps/web/components/ChatPanel/ChatBubble/EmbedDisclaimer/EmbedDisclaimer.tsx b/apps/web/components/ChatPanel/ChatBubble/EmbedDisclaimer/EmbedDisclaimer.tsx new file mode 100644 index 000000000000..b0ed2dc627c2 --- /dev/null +++ b/apps/web/components/ChatPanel/ChatBubble/EmbedDisclaimer/EmbedDisclaimer.tsx @@ -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 ( + + + + + { + if (ev.key === 'Enter' || ev.key === ' ') { + onAnswer(false) + ev.preventDefault() + } + }} + onClick={() => { + onAnswer(false) + }} + > + + + + + ( + + {children} + + ), + }} + > + + + {texts.message} + ( + { + onChange(e.target.checked) + try { + localStorage.setItem( + localStorageKey, + e.target.checked ? 'true' : 'false', + ) + } catch (error) { + console.warn('Failed to save preference:', error) + } + }} + /> + )} + /> + + + + + + + + + + + ) +} diff --git a/apps/web/components/ChatPanel/WatsonChatPanel/WatsonChatPanel.tsx b/apps/web/components/ChatPanel/WatsonChatPanel/WatsonChatPanel.tsx index 568cd54432f3..10d2c98ee4ff 100644 --- a/apps/web/components/ChatPanel/WatsonChatPanel/WatsonChatPanel.tsx +++ b/apps/web/components/ChatPanel/WatsonChatPanel/WatsonChatPanel.tsx @@ -283,6 +283,10 @@ export const WatsonChatPanel = (props: WatsonChatPanelProps) => { const watsonInstance = useRef(null) const [hasButtonBeenClicked, setHasButtonBeenClicked] = useState(false) + const replaceDirectorateOfImmigrationWebChatWithAI = Boolean( + namespace?.replaceDirectorateOfImmigrationWebChatWithAI, + ) + useEffect(() => { if (Object.keys(namespace).length === 0) { return () => { @@ -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 ( @@ -415,6 +415,8 @@ export const WatsonChatPanel = (props: WatsonChatPanelProps) => { if (showLauncher) return null + const chatTermsUrl = n('chatTermsUrl', '') + return ( {shouldDisplayFeedbackPanel && ( @@ -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' ? ( + + Þetta netspjall er hýst af þriðja aðila. Með því að + halda áfram samþykkir þú{' '} + {!chatTermsUrl ? ( + 'skilmála' + ) : ( + skilmála + )}{' '} + þeirra. + + ) : ( + + This AI chatbot is hosted by a third party and by + continuing, you are agreeing to their{' '} + {!chatTermsUrl ? ( + 'terms and conditions' + ) : ( + + terms and conditions + + )} + . The chatbots purpose is to assist you in your search + for information, it's still learning so suggestions + must be taken with caution. + + ), + 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 + } /> )}