diff --git a/.changeset/kind-balloons-behave.md b/.changeset/kind-balloons-behave.md new file mode 100644 index 0000000000..7c218a4526 --- /dev/null +++ b/.changeset/kind-balloons-behave.md @@ -0,0 +1,5 @@ +--- +'@commercetools-docs/gatsby-theme-docs': minor +--- + +Add feedback functionality to self-learning pages diff --git a/packages/gatsby-theme-docs/src/components/page-feedback-buttons.tsx b/packages/gatsby-theme-docs/src/components/page-feedback-buttons.tsx new file mode 100644 index 0000000000..6d55af403c --- /dev/null +++ b/packages/gatsby-theme-docs/src/components/page-feedback-buttons.tsx @@ -0,0 +1,98 @@ +import styled from '@emotion/styled'; +import thumbsUpIcon from '../icons/assistant-thumbs-up.png'; +import thumbsDownIcon from '../icons/assistant-thumbs-down.png'; +import thumbsUpIconFilled from '../icons/assistant-thumbs-up-filled.png'; +import thumbsDownIconFilled from '../icons/assistant-thumbs-down-filled.png'; +import { designSystem } from '@commercetools-docs/ui-kit'; + +type ThumbsButtonProps = { + isClickable: boolean; + iconSize: number; +}; + +export const FEEDBACK_UP = 1; +export const FEEDBACK_DOWN = -1; + +const ThumbsButton = styled.button` + display: flex; + align-items: center; + cursor: ${(props) => (props.isClickable ? 'pointer' : 'default')}; + background: transparent; + height: ${(props) => props.iconSize}px; + width: ${(props) => props.iconSize}px; + border: 0; + padding: 3px; + color: ${designSystem.colors.light.linkHover}; + :focus { + outline: none; + } + img { + margin-right: 5px; + } +`; + +type ButtonsWrapperProps = { + hasText: boolean; +}; + +const ButtonsWrapper = styled.div` + display: flex; + gap: ${(props) => (props.hasText ? '40px' : '0')}; +`; + +type TPageFeedbackButtonsProps = { + isPositiveClickable: boolean; + isNegativeClickable: boolean; + currentFeedback: number; + onPositiveClick: () => void; + onNegativeClick: () => void; + iconSize?: number; + positiveText?: string; + negativeText?: string; +}; + +const PageFeedbackButtons = (props: TPageFeedbackButtonsProps) => { + const iconSize = props.iconSize || 28; + return ( + + + positive feedback + {props.positiveText && {props.positiveText}} + + + negative feedback + {props.negativeText && {props.negativeText}} + + + ); +}; + +export default PageFeedbackButtons; diff --git a/packages/gatsby-theme-docs/src/components/page-feedback.tsx b/packages/gatsby-theme-docs/src/components/page-feedback.tsx new file mode 100644 index 0000000000..699f1b7732 --- /dev/null +++ b/packages/gatsby-theme-docs/src/components/page-feedback.tsx @@ -0,0 +1,114 @@ +import { useState } from 'react'; +import styled from '@emotion/styled'; + +import PageFeedbackButtons, { + FEEDBACK_DOWN, + FEEDBACK_UP, +} from './page-feedback-buttons'; +import { designSystem } from '@commercetools-docs/ui-kit'; +import { gtagEvent } from '../modules/sso/utils/analytics.utils'; + +const POSITIVE_SURVEY_ID = 3628; // id for the userguiding survey triggered by thumbs up click +const NEGATIVE_SURVEY_ID = 3627; // id for the userguiding survey triggered by thumbs down click +const USERGUIDING_SESSION_KEY = '__UGS__uid'; // local storage key for userguiding session +const USER_GUIDING_ID = 'U4I78799B6RID'; // userguiding user id for the embedded script +const MAX_SCRIPT_LOAD_TIME = 30 * 1000; // 30 seconds + +const FeedbackQuestion = styled.div` + padding-bottom: ${designSystem.dimensions.spacings.s}; +`; + +const PageFeedbackWrapper = styled.div` + border-top: 1px solid ${designSystem.colors.light.borderPrimary}; + padding-top: ${designSystem.dimensions.spacings.l}; + font-size: ${designSystem.typography.fontSizes.small}; +`; + +const PageFeedback = () => { + const [currentFeedback, setCurrentFeedback] = useState(0); + + const isScriptLoaded = (): boolean => { + const isUserGuidingSessionReady = + localStorage.getItem(USERGUIDING_SESSION_KEY) !== null; + const isUserGuidingScriptLoaded = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + typeof (window as any).userGuiding?.launchSurvey === 'function'; + return isUserGuidingScriptLoaded && isUserGuidingSessionReady; + }; + + const injectUserGuidingScript = (): Promise => { + return new Promise((resolve, reject) => { + if (isScriptLoaded()) { + resolve(); // Script is already loaded, resolve immediately + return; + } + + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = `https://eu-static.userguiding.com/media/user-guiding-${USER_GUIDING_ID}-embedded.js`; + + script.onload = () => { + // Poll for userGuiding object in the global scope + let loadTime = 0; + const interval = setInterval(() => { + if (isScriptLoaded()) { + clearInterval(interval); // Stop polling + resolve(); + return; + } + if (loadTime >= MAX_SCRIPT_LOAD_TIME) { + clearInterval(interval); // Stop polling + reject(new Error('Userguiding script loading timeout.')); + } + loadTime += 100; + }, 100); + }; + + script.onerror = () => { + reject(new Error('Userguiding script loading failed.')); + }; + + document.head.appendChild(script); + }); + }; + + const handleClick = async (feedback: number) => { + setCurrentFeedback(feedback); + + // track the event on google analytics + gtagEvent('page_feedback', { + feedback_page: window.location.pathname, + feedback_value: feedback.toString(), + }); + + try { + await injectUserGuidingScript(); + const surveyId = + feedback === FEEDBACK_UP ? POSITIVE_SURVEY_ID : NEGATIVE_SURVEY_ID; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).userGuiding?.launchSurvey(surveyId); + } catch (error) { + console.error(error); + } + }; + + const isClickable = currentFeedback === 0; + + return ( + + Was this page helpful? + handleClick(FEEDBACK_UP)} + onNegativeClick={() => handleClick(FEEDBACK_DOWN)} + currentFeedback={currentFeedback} + isPositiveClickable={isClickable} + isNegativeClickable={isClickable} + iconSize={30} + positiveText="Yes" + negativeText="No" + /> + + ); +}; + +export default PageFeedback; diff --git a/packages/gatsby-theme-docs/src/modules/ai-assistant/icons/assistant-thumbs-down-filled.png b/packages/gatsby-theme-docs/src/icons/assistant-thumbs-down-filled.png similarity index 100% rename from packages/gatsby-theme-docs/src/modules/ai-assistant/icons/assistant-thumbs-down-filled.png rename to packages/gatsby-theme-docs/src/icons/assistant-thumbs-down-filled.png diff --git a/packages/gatsby-theme-docs/src/modules/ai-assistant/icons/assistant-thumbs-down.png b/packages/gatsby-theme-docs/src/icons/assistant-thumbs-down.png similarity index 100% rename from packages/gatsby-theme-docs/src/modules/ai-assistant/icons/assistant-thumbs-down.png rename to packages/gatsby-theme-docs/src/icons/assistant-thumbs-down.png diff --git a/packages/gatsby-theme-docs/src/modules/ai-assistant/icons/assistant-thumbs-up-filled.png b/packages/gatsby-theme-docs/src/icons/assistant-thumbs-up-filled.png similarity index 100% rename from packages/gatsby-theme-docs/src/modules/ai-assistant/icons/assistant-thumbs-up-filled.png rename to packages/gatsby-theme-docs/src/icons/assistant-thumbs-up-filled.png diff --git a/packages/gatsby-theme-docs/src/modules/ai-assistant/icons/assistant-thumbs-up.png b/packages/gatsby-theme-docs/src/icons/assistant-thumbs-up.png similarity index 100% rename from packages/gatsby-theme-docs/src/modules/ai-assistant/icons/assistant-thumbs-up.png rename to packages/gatsby-theme-docs/src/icons/assistant-thumbs-up.png diff --git a/packages/gatsby-theme-docs/src/layouts/content.js b/packages/gatsby-theme-docs/src/layouts/content.js index 91e8e25f15..16fec8d1db 100644 --- a/packages/gatsby-theme-docs/src/layouts/content.js +++ b/packages/gatsby-theme-docs/src/layouts/content.js @@ -141,6 +141,7 @@ const LayoutContent = (props) => { navLevels={props.pageData.navLevels} beta={isBeta} planTags={planTags} + isSelfLearning={siteData.siteMetadata?.isSelfLearning} /> diff --git a/packages/gatsby-theme-docs/src/layouts/internals/layout-page-navigation.js b/packages/gatsby-theme-docs/src/layouts/internals/layout-page-navigation.js index a245b6efed..90f9f926fd 100644 --- a/packages/gatsby-theme-docs/src/layouts/internals/layout-page-navigation.js +++ b/packages/gatsby-theme-docs/src/layouts/internals/layout-page-navigation.js @@ -17,6 +17,7 @@ import PlaceholderPageHeaderSide from '../../overrides/page-header-side'; import PlaceholderPageHeaderSideBannerArea from '../../overrides/page-header-banner-area'; import { Overlay, BetaTag, SearchInput, PlanTag } from '../../components'; import PageNavigation from './page-navigation'; +import PageFeedback from '../../components/page-feedback'; import { GRID_ID_PAGE_NAVIGATION } from './layout-design-config'; const StackedLinesIndentedIcon = createStyledIcon( @@ -167,6 +168,14 @@ const OverlayBackground = styled.div` ); `; +const PageFeedbackContainer = styled.div` + @media screen and (${designSystem.dimensions.viewports.mobile}) { + display: none; + } + padding: ${designSystem.dimensions.spacings.s} + ${designSystem.dimensions.spacings.m} 0; +`; + const LayoutPageNavigation = (props) => { const [isMenuOpen, setMenuOpen] = React.useState(false); const [modalPortalNode, setModalPortalNode] = React.useState(); @@ -246,6 +255,11 @@ const LayoutPageNavigation = (props) => { tableOfContents={props.tableOfContents} navLevels={props.navLevels} /> + {props.isSelfLearning && ( + + + + )} ); @@ -323,6 +337,7 @@ LayoutPageNavigation.propTypes = { navLevels: PropTypes.number.isRequired, beta: PropTypes.bool.isRequired, planTags: PropTypes.arrayOf(PropTypes.string).isRequired, + isSelfLearning: PropTypes.bool, }; export default LayoutPageNavigation; diff --git a/packages/gatsby-theme-docs/src/modules/ai-assistant/components/chat-messages.jsx b/packages/gatsby-theme-docs/src/modules/ai-assistant/components/chat-messages.jsx index efeeb2a320..748d3344de 100644 --- a/packages/gatsby-theme-docs/src/modules/ai-assistant/components/chat-messages.jsx +++ b/packages/gatsby-theme-docs/src/modules/ai-assistant/components/chat-messages.jsx @@ -12,12 +12,9 @@ import { import { FirstName } from '@commercetools-docs/gatsby-theme-docs'; import { CHAT_ROLE_ASSISTANT, CHAT_ROLE_USER } from './chat.const'; import { getAssistantAvatarIcon } from './chat.utils'; -import thumbsUpIcon from '../icons/assistant-thumbs-up.png'; -import thumbsDownIcon from '../icons/assistant-thumbs-down.png'; -import thumbsUpIconFilled from '../icons/assistant-thumbs-up-filled.png'; -import thumbsDownIconFilled from '../icons/assistant-thumbs-down-filled.png'; import codeIcon from '../icons/assistant-code.png'; import { DEV_TOOLING_MODE } from './chat-modal'; +import PageFeedbackButtons from '../../../components/page-feedback-buttons'; export const FEEDBACK_UP = 1; export const FEEDBACK_DOWN = -1; @@ -82,34 +79,6 @@ const FeedbackWrapper = styled.div` justify-content: flex-end; `; -const ThumbsDownButton = styled.button` - cursor: ${(props) => (props.isClickable ? 'pointer' : 'default')}; - background: transparent; - height: 28px; - width: 28px; - border: 0; - padding: 3px; - margin-right: 8px; - img { - transform: scaleX(-1); - } - :focus { - outline: none; - } -`; - -const ThumbsUpButton = styled.button` - cursor: ${(props) => (props.isClickable ? 'pointer' : 'default')}; - background: transparent; - height: 28px; - width: 28px; - border: 0; - padding: 3px; - :focus { - outline: none; - } -`; - const ChatMessages = (props) => { const [feedbackResults, setFeedbackResults] = useState({}); const AssistantAvatarIcon = @@ -145,7 +114,9 @@ const ChatMessages = (props) => { Hello , {markdownFragmentToReact(props.chatMode.intro)} - {props.messages.map((message, index) => ( + {props.messages.map((message, index) => { + const messageFeedback = feedbackResults[message.id]; + return (
{message.role === 'assistant' ? ( @@ -185,46 +156,26 @@ const ChatMessages = (props) => { {message.role === 'assistant' ? ( - handleThumbsClick(e, message.id, FEEDBACK_UP) : null } - > - positive feedback - - handleThumbsClick(e, message.id, FEEDBACK_DOWN) : null } - > - negative feedback - + currentFeedback={messageFeedback} + isPositiveClickable={!messageFeedback} + isNegativeClickable={!messageFeedback} + iconSize={24} + /> ) : null}
- ))} + )})} {props.chatLocked && ( diff --git a/packages/gatsby-theme-docs/src/modules/sso/utils/analytics.utils.ts b/packages/gatsby-theme-docs/src/modules/sso/utils/analytics.utils.ts index d9287b78d5..9da7ea7985 100644 --- a/packages/gatsby-theme-docs/src/modules/sso/utils/analytics.utils.ts +++ b/packages/gatsby-theme-docs/src/modules/sso/utils/analytics.utils.ts @@ -31,7 +31,8 @@ type CustomEventNames = | 'update_userinfo' | 'logout' | 'ai_assistant_launch' - | 'search_tag_click'; + | 'search_tag_click' + | 'page_feedback'; type EventNames = // | 'add_payment_info' @@ -93,4 +94,6 @@ interface EventParams { value?: number | undefined; event_label?: string | undefined; event_category?: string | undefined; + feedback_page?: string | undefined; + feedback_value?: string | undefined; // 1 = thumbs up, -1 = thumbs down } diff --git a/type-definition/png.d.ts b/type-definition/png.d.ts new file mode 100644 index 0000000000..1c5923252c --- /dev/null +++ b/type-definition/png.d.ts @@ -0,0 +1,4 @@ +declare module '*.png' { + const value: string; + export default value; +}