@pega/cosmos-react-work 9.0.0-build.26.2 → 9.0.0-build.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"GenAICoach.d.ts","sourceRoot":"","sources":["../../../src/components/GenAICoach/GenAICoach.tsx"],"names":[],"mappings":"AAsGA,OAAO,EAAyC,KAAK,eAAe,EAAE,MAAM,GAAG,CAAC;AAmBhF,eAAO,MAAM,SAAS,+CAmDrB,CAAC;AAEF,eAAO,MAAM,gBAAgB,+CAQ5B,CAAC;iCAigDyB,eAAe;;;AAQ1C,wBAA6D"}
1
+ {"version":3,"file":"GenAICoach.d.ts","sourceRoot":"","sources":["../../../src/components/GenAICoach/GenAICoach.tsx"],"names":[],"mappings":"AAsGA,OAAO,EAAyC,KAAK,eAAe,EAAE,MAAM,GAAG,CAAC;AAmBhF,eAAO,MAAM,SAAS,+CAmDrB,CAAC;AAEF,eAAO,MAAM,gBAAgB,+CAQ5B,CAAC;iCAkjDyB,eAAe;;;AAQ1C,wBAA6D"}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import { Button, Flex, Icon, MenuButton, Progress, Text, createUID, menuHelpers, useI18n, useSpeechRecognition, useTestIds, useTheme, withTestIds, registerIcon, usePrevious, getFocusables, isMenuGroupProps, ErrorState, useArrows, focusableSelector, useLiveLog, useOuterEvent, ThemeOverride, hasProp, getActiveElement, useElement, Actions, useFullscreenContext, useBreakpoint, Grid, useRefMap, VisuallyHiddenText, FileList, Lightbox, FormField, useUID, SpeechToTextButton, Toaster, useToaster, throttle } from '@pega/cosmos-react-core';
3
+ import { Button, Flex, Icon, MenuButton, Progress, Text, createUID, menuHelpers, useI18n, useSpeechRecognition, useTestIds, useTheme, withTestIds, registerIcon, usePrevious, getFocusables, isMenuGroupProps, ErrorState, useArrows, useFocusTrap, useLiveLog, useOuterEvent, ThemeOverride, hasProp, getActiveElement, useElement, Actions, useFullscreenContext, useBreakpoint, Grid, useRefMap, VisuallyHiddenText, FileList, Lightbox, FormField, useUID, SpeechToTextButton, Toaster, useToaster, throttle } from '@pega/cosmos-react-core';
4
4
  import * as caretUpIcon from '@pega/cosmos-react-core/lib/components/Icon/icons/caret-up.icon';
5
5
  import * as timesIcon from '@pega/cosmos-react-core/lib/components/Icon/icons/times.icon';
6
6
  import * as caretDownIcon from '@pega/cosmos-react-core/lib/components/Icon/icons/caret-down.icon';
@@ -117,10 +117,13 @@ const GenAICoachContent = ({ testId, coachOptions: coachOptionsProps, onCoachCha
117
117
  const [message, setMessage] = useState('');
118
118
  const [interimMessage, setInterimMessage] = useState('');
119
119
  const [coachOptions, setCoachOptions] = useState(coachOptionsProps);
120
- const [arrowKey, setArrowKey] = useState(null);
120
+ const [focusedInMessage, setFocusedInMessage] = useState(false);
121
+ const scrollButtonRef = useRef(null);
122
+ const trapRef = useRef(null);
121
123
  const [animationInitialCursorPos, setAnimationInitialCursorPos] = useState(0);
122
124
  const [focused, setFocused] = useState(false);
123
125
  const [formAnswers, setFormAnswers] = useState();
126
+ const [questionnaireDismissed, setQuestionnaireDismissed] = useState(false);
124
127
  const [ctxFullscreen, toggleFullscreen] = useFullscreenContext() ?? [];
125
128
  const fullScreen = !!ctxFullscreen;
126
129
  const questionnaireKey = questionnaireData?.questions?.map(q => q.field).join(',') ?? '';
@@ -132,6 +135,11 @@ const GenAICoachContent = ({ testId, coachOptions: coachOptionsProps, onCoachCha
132
135
  const handleFormChange = useCallback((answers) => {
133
136
  setFormAnswers(answers);
134
137
  }, []);
138
+ const handleFormCancel = () => {
139
+ setFormAnswers(undefined);
140
+ setQuestionnaireDismissed(true);
141
+ textAreaRef.current?.focus();
142
+ };
135
143
  const latestMessage = messages.at(-1);
136
144
  const isLatestAgentMessageStreaming = !!latestMessage && isCoachMessage(latestMessage) && !!latestMessage.loading;
137
145
  const shouldKeepSplitLayoutDuringFullscreenToggle = !!conversationHistory &&
@@ -291,6 +299,12 @@ const GenAICoachContent = ({ testId, coachOptions: coachOptionsProps, onCoachCha
291
299
  scrollState.userAtBottom = true;
292
300
  }
293
301
  setShowScrollToBottom(false);
302
+ const lastMessage = conversationRef.current?.lastElementChild;
303
+ if (lastMessage instanceof HTMLElement) {
304
+ lastFocusedMessageRef.current = lastMessage;
305
+ focusInMessageListRef.current = true;
306
+ lastMessage.focus();
307
+ }
294
308
  }, [scrollToLatest]);
295
309
  const setInputContainerRef = useCallback((el) => {
296
310
  if (inputContainerRef.current === el)
@@ -407,7 +421,7 @@ const GenAICoachContent = ({ testId, coachOptions: coachOptionsProps, onCoachCha
407
421
  return isInUtilities(variant) && variant.state === 'minimized' ? (_jsxs(_Fragment, { children: [_jsx(Text, { variant: 'h2', children: selectedCoach }), _jsx(Flex, { container: { alignItems: 'center' }, children: _jsx(Button, { icon: true, label: t('maximize'), "aria-label": t('agent_noun', [t('maximize')]), variant: 'simple', onClick: () => {
408
422
  variant.onStateChange('maximized');
409
423
  isUserTriggered.current = true;
410
- }, children: _jsx(Icon, { name: 'caret-up' }) }) })] })) : (_jsxs(_Fragment, { children: [renderCoachOptions, _jsxs(Flex, { container: { gap: 0.5 }, as: StyledActionsContainer, children: [activeCases && _jsx(ActiveCases, { activeCases: activeCases }), onNewChat && (_jsx(Button, { icon: true, variant: 'simple', label: t('new_chat'), "aria-label": t('new_chat'), onClick: onNewChat, children: _jsx(Icon, { name: 'plus' }) })), _jsx(HeaderActions, { actions: actions, allowFullScreen: allowFullScreen, variant: variant, isUserTriggeredRef: isUserTriggered, onFullscreenToggle: toggleFullscreen, isFullscreen: fullScreen, selectedCoach: selectedCoach, actionsTestId: testIds.actions }), variant.placement === 'dialog' && (_jsx(Button, { icon: true, label: t('close'), "aria-label": t('close_chat_with_ai'), variant: 'simple', onClick: variant.onClose, children: _jsx(Icon, { name: 'times' }) }))] })] }));
424
+ }, children: _jsx(Icon, { name: 'caret-up' }) }) })] })) : (_jsxs(_Fragment, { children: [renderCoachOptions, _jsxs(Flex, { container: { gap: 0.5 }, as: StyledActionsContainer, children: [activeCases && _jsx(ActiveCases, { activeCases: activeCases }), onNewChat && (_jsx(Button, { icon: true, variant: 'simple', label: t('new_chat'), "aria-label": t('new_chat'), onClick: onNewChat, children: _jsx(Icon, { name: 'plus' }) })), _jsx(HeaderActions, { actions: actions, allowFullScreen: allowFullScreen, variant: variant, isUserTriggeredRef: isUserTriggered, onFullscreenToggle: toggleFullscreen, isFullscreen: fullScreen, selectedCoach: selectedCoach, actionsTestId: testIds.actions }), variant.placement === 'dialog' && !fullScreen && (_jsx(Button, { icon: true, label: t('close'), "aria-label": t('close_chat_with_ai'), variant: 'simple', onClick: variant.onClose, children: _jsx(Icon, { name: 'times' }) }))] })] }));
411
425
  }, [variant, coachOptions, activeCases, actions, allowFullScreen, onNewChat]);
412
426
  const getMessageContainingElement = (element) => {
413
427
  if (element)
@@ -423,11 +437,27 @@ const GenAICoachContent = ({ testId, coachOptions: coachOptionsProps, onCoachCha
423
437
  getMessageContainingElement(lastFocusedMessageRef.current) ?? null;
424
438
  }
425
439
  };
440
+ const focusLastMessageLi = useCallback((e) => {
441
+ if (e.key !== 'Tab' || !e.shiftKey)
442
+ return;
443
+ const target = lastFocusedMessageRef.current ?? conversationRef.current?.lastElementChild;
444
+ if (!(target instanceof HTMLElement))
445
+ return;
446
+ e.preventDefault();
447
+ focusInMessageListRef.current = true;
448
+ target.focus();
449
+ }, []);
426
450
  const variantState = useMemo(() => {
427
451
  if (variant.placement === 'utilities') {
428
452
  return variant.state;
429
453
  }
430
454
  }, [variant]);
455
+ useEffect(() => {
456
+ if (questionnaireData) {
457
+ setQuestionnaireDismissed(false);
458
+ setFormAnswers(undefined);
459
+ }
460
+ }, [questionnaireData]);
431
461
  useEffect(() => {
432
462
  if (suggestionCardsView && draftMessage !== undefined) {
433
463
  setMessage(draftMessage);
@@ -472,7 +502,6 @@ const GenAICoachContent = ({ testId, coachOptions: coachOptionsProps, onCoachCha
472
502
  textAreaRef.current?.focus();
473
503
  setLastFocusableElement();
474
504
  elementRef.current = null;
475
- setArrowKey(null);
476
505
  }
477
506
  else if (latestMessage?.loading) {
478
507
  isGeneratingResponse.current = true;
@@ -562,16 +591,29 @@ const GenAICoachContent = ({ testId, coachOptions: coachOptionsProps, onCoachCha
562
591
  setShowScrollToBottom(false);
563
592
  }
564
593
  }, [messages.length, loading, error]);
565
- /** Supports arrow key behaviors */
594
+ /** Supports Enter key to enter a message and focus first focusable element inside it */
566
595
  useEffect(() => {
596
+ if (!focusedInMessage)
597
+ return;
567
598
  const focusables = getFocusables(elementRef);
568
- if (arrowKey === 'ArrowRight') {
569
- focusables[0]?.focus();
570
- }
571
- else if (arrowKey === 'ArrowLeft') {
572
- focusables[focusables.length - 1]?.focus();
573
- }
574
- }, [arrowKey, elementRef.current]);
599
+ focusables[0]?.focus();
600
+ }, [focusedInMessage, elementRef.current]);
601
+ /**
602
+ * While inside a message, stop ArrowUp/Down from bubbling to conversationRef
603
+ * so useArrows does not navigate between message rows.
604
+ */
605
+ useEffect(() => {
606
+ const el = elementRef.current;
607
+ if (!focusedInMessage || !el)
608
+ return;
609
+ const blockArrows = (e) => {
610
+ if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
611
+ e.stopPropagation();
612
+ }
613
+ };
614
+ el.addEventListener('keydown', blockArrows);
615
+ return () => el.removeEventListener('keydown', blockArrows);
616
+ }, [focusedInMessage]);
575
617
  useArrows(conversationRef, {
576
618
  cycle: true,
577
619
  selector: ':scope > li',
@@ -579,15 +621,10 @@ const GenAICoachContent = ({ testId, coachOptions: coachOptionsProps, onCoachCha
579
621
  allowTabFocus: true,
580
622
  initialFocusElement: initialFocusedElementRef.current
581
623
  }, [messages, initialFocusedElementRef.current]);
582
- useArrows(elementRef, {
583
- cycle: true,
584
- selector: `li ${focusableSelector}`,
585
- dir: 'left-right',
586
- allowTabFocus: true,
587
- updateTabIndex: false
588
- }, [messages, elementRef.current, arrowKey]);
589
- useOuterEvent('mousedown', [conversationRef.current], () => {
590
- setArrowKey(null);
624
+ useFocusTrap(trapRef, true, [focusedInMessage]);
625
+ useOuterEvent('mousedown', [focusedInMessage ? elementRef.current : conversationRef.current], () => {
626
+ setFocusedInMessage(false);
627
+ trapRef.current = null;
591
628
  focusInMessageListRef.current = false;
592
629
  });
593
630
  const { push } = useToaster();
@@ -615,11 +652,11 @@ const GenAICoachContent = ({ testId, coachOptions: coachOptionsProps, onCoachCha
615
652
  push({ content: voiceToTextError });
616
653
  }
617
654
  }, [voiceToTextError]);
618
- const genAIFooter = (_jsxs(Flex, { container: { direction: 'column' }, as: StyledGenAIFooter, children: [_jsxs(StyledComposerWrapper, { focused: focused, hasQuestionnaire: !!questionnaireData, onFocus: () => setFocused(true), onBlur: e => {
655
+ const genAIFooter = (_jsxs(Flex, { container: { direction: 'column' }, as: StyledGenAIFooter, children: [_jsxs(StyledComposerWrapper, { focused: focused, hasQuestionnaire: !!questionnaireData && !questionnaireDismissed, onFocus: () => setFocused(true), onBlur: e => {
619
656
  if (!(e.relatedTarget instanceof Node) || !e.currentTarget.contains(e.relatedTarget)) {
620
657
  setFocused(false);
621
658
  }
622
- }, children: [questionnaireData && (_jsx(Questionnaire, { data: questionnaireData, onChange: handleFormChange }, questionnaireKey)), _jsx(FormField, { label: t('message_pega_gen_ai_coach'), labelHidden: true, labelFor: composerId, children: _jsx(StyledGenAIFormControl, { focused: !questionnaireData && focused, children: _jsxs(Flex, { container: { direction: 'column', gap: 0.25 }, children: [_jsx(StyledGenAITextArea, { id: composerId, ref: textAreaRef, value: interimMessage ? `${message}${interimMessage}` : message, placeholder: t('write_message'), onKeyDown: handleEnterKeyDown, onChange: handleTextAreaChange, onFocus: () => {
659
+ }, children: [questionnaireData && !questionnaireDismissed && (_jsx(Questionnaire, { data: questionnaireData, onChange: handleFormChange, onCancel: handleFormCancel }, questionnaireKey)), _jsx(FormField, { label: t('message_pega_gen_ai_coach'), labelHidden: true, labelFor: composerId, children: _jsx(StyledGenAIFormControl, { focused: (!questionnaireData || questionnaireDismissed) && focused, children: _jsxs(Flex, { container: { direction: 'column', gap: 0.25 }, children: [_jsx(StyledGenAITextArea, { id: composerId, ref: textAreaRef, value: interimMessage ? `${message}${interimMessage}` : message, placeholder: t('write_message'), onKeyDown: handleEnterKeyDown, onChange: handleTextAreaChange, onFocus: () => {
623
660
  if (active)
624
661
  stop();
625
662
  }, autoResize: true }), _jsxs(Flex, { container: {
@@ -722,37 +759,43 @@ const GenAICoachContent = ({ testId, coachOptions: coachOptionsProps, onCoachCha
722
759
  textAreaRef.current?.focus();
723
760
  }, testId: initialSuggestedMessage.id }))) })), fullPageAndInitialScreen && !guidedMode && !suggestionCardsView && genAIFooter] })) : (_jsx(Flex, { as: StyledMessagesList, ref: conversationRef, "aria-label": `${t('conversation')} ${t('view')}`, container: { direction: 'column' }, onFocus: () => {
724
761
  if (!focusInMessageListRef.current) {
725
- if (lastFocusedMessageRef.current) {
726
- lastFocusedMessageRef.current.focus();
727
- }
728
- else {
729
- /** Focus on the latest message if the chat message list was never focused */
730
- const lastChild = conversationRef.current && conversationRef.current.lastElementChild;
731
- if (lastChild instanceof HTMLElement) {
732
- lastChild.focus();
733
- }
734
- }
762
+ focusInMessageListRef.current = true;
763
+ const target = lastFocusedMessageRef.current ??
764
+ (conversationRef.current?.lastElementChild instanceof HTMLElement
765
+ ? conversationRef.current.lastElementChild
766
+ : null);
767
+ target?.focus();
735
768
  }
736
769
  focusInMessageListRef.current = true;
737
- setArrowKey(null);
738
770
  }, onKeyDown: (e) => {
739
771
  if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
740
- setArrowKey(e.key);
772
+ if (focusedInMessage)
773
+ return;
741
774
  elementRef.current =
742
775
  getMessageContainingElement(document.activeElement) ?? null;
743
776
  lastFocusedMessageRef.current = elementRef.current;
744
777
  }
745
- else if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
778
+ else if (e.key === 'Enter' && !focusedInMessage) {
746
779
  const activeMessageElement = Array.from(messageEls.values()).find(el => el === document.activeElement);
747
780
  elementRef.current =
748
781
  getMessageContainingElement(document.activeElement) ?? null;
749
782
  initialFocusedElementRef.current = elementRef.current;
750
783
  if (activeMessageElement &&
751
784
  getFocusables(activeMessageElement).length > 0) {
752
- setArrowKey(e.key);
785
+ e.preventDefault();
786
+ trapRef.current = elementRef.current;
787
+ setFocusedInMessage(true);
753
788
  }
754
789
  }
790
+ else if (e.key === 'Escape' && focusedInMessage) {
791
+ e.preventDefault();
792
+ trapRef.current = null;
793
+ setFocusedInMessage(false);
794
+ elementRef.current?.focus();
795
+ }
755
796
  else if (e.key === 'Tab') {
797
+ if (focusedInMessage)
798
+ return;
756
799
  e.preventDefault();
757
800
  setLastFocusableElement();
758
801
  if (e.shiftKey) {
@@ -778,15 +821,17 @@ const GenAICoachContent = ({ testId, coachOptions: coachOptionsProps, onCoachCha
778
821
  }
779
822
  }
780
823
  else {
824
+ if (scrollButtonRef.current) {
825
+ scrollButtonRef.current.focus();
826
+ return;
827
+ }
781
828
  const nextElement = conversationRef.current?.parentElement?.nextElementSibling;
782
829
  if (nextElement) {
783
830
  const focusables = getFocusables(nextElement);
784
831
  if (focusables.length) {
785
- e.preventDefault();
786
832
  focusables[0].focus();
787
833
  }
788
834
  }
789
- setArrowKey(null);
790
835
  }
791
836
  }
792
837
  }, children: messages.map(item => {
@@ -814,11 +859,11 @@ const GenAICoachContent = ({ testId, coachOptions: coachOptionsProps, onCoachCha
814
859
  setAnimationInitialCursorPos(animatedTillCursor);
815
860
  }
816
861
  }, ...(prevFullScreen !== fullScreen ? { animationInitialCursorPos } : {}) }));
817
- }) })) })) }), !loading && messages.length === 0 && suggestionCardsView ? (_jsxs(_Fragment, { children: [((isInUtilities(variant) && variant.state !== 'minimized') ||
862
+ }) })) })) }), showScrollToBottom && (_jsx(StyledScrollButton, { ref: scrollButtonRef, icon: true, onClick: handleScrollToBottomClick, ariaLabel: t('scroll_to_latest_message'), onKeyDown: focusLastMessageLi, children: _jsx(Icon, { name: 'caret-down' }) })), !loading && messages.length === 0 && suggestionCardsView ? (_jsxs(_Fragment, { children: [((isInUtilities(variant) && variant.state !== 'minimized') ||
818
863
  !isInUtilities(variant)) &&
819
864
  !guidedMode && (_jsx(StyledStickyComposer, { children: _jsx(Flex, { container: { direction: 'column' }, as: StyledInputContainer, ref: setInputContainerRef, children: _jsx(StyledComposerSection, { children: genAIFooter }) }) })), _jsx(Flex, { container: { direction: 'column' }, as: StyledSuggestionCardsBottomHalf, children: _jsx(StyledSuggestionCardsView, { children: suggestionCardsView }) })] })) : (((isInUtilities(variant) && variant.state !== 'minimized') ||
820
865
  !isInUtilities(variant)) &&
821
- !loading && (_jsx(Flex, { container: { direction: 'column' }, as: StyledInputContainer, ref: setInputContainerRef, children: _jsxs(StyledComposerSection, { children: [guidedMode && questionnaireData && (_jsx(Questionnaire, { data: questionnaireData, onChange: handleFormChange }, questionnaireKey)), guidedMode ? (_jsx(_Fragment, { children: suggestions && messages.length > 0 && (_jsxs(_Fragment, { children: [latestMessage &&
866
+ !loading && (_jsx(Flex, { container: { direction: 'column' }, as: StyledInputContainer, ref: setInputContainerRef, children: _jsxs(StyledComposerSection, { children: [guidedMode && questionnaireData && !questionnaireDismissed && (_jsx(Questionnaire, { data: questionnaireData, onChange: handleFormChange, onCancel: handleFormCancel }, questionnaireKey)), guidedMode ? (_jsx(_Fragment, { children: suggestions && messages.length > 0 && (_jsxs(_Fragment, { children: [latestMessage &&
822
867
  isCoachMessage(latestMessage) &&
823
868
  latestMessage.loading ? (_jsxs(Flex, { container: {
824
869
  gap: 1,
@@ -844,7 +889,7 @@ const GenAICoachContent = ({ testId, coachOptions: coachOptionsProps, onCoachCha
844
889
  });
845
890
  }
846
891
  }
847
- } }) })), _jsx(Flex, { container: { justify: 'center' }, children: _jsx(StyledDisclaimerText, { inFullPage: !condition, children: t('ai_disclaimer') }) })] })) })) : (renderTextArea)] }) }))), showScrollToBottom && (_jsx(StyledScrollButton, { icon: true, onClick: handleScrollToBottomClick, ariaLabel: t('scroll_to_latest_message'), children: _jsx(Icon, { name: 'caret-down' }) }))] })) }));
892
+ } }) })), _jsx(Flex, { container: { justify: 'center' }, children: _jsx(StyledDisclaimerText, { inFullPage: !condition, children: t('ai_disclaimer') }) })] })) })) : (renderTextArea)] }) })))] })) }));
848
893
  useEffect(() => {
849
894
  if (focusTextArea.current && !conversationHistory && textAreaRef.current) {
850
895
  textAreaRef.current.focus();