diff --git a/frontend/src/component/ai/AIChat.tsx b/frontend/src/component/ai/AIChat.tsx
index 77656797ff..193ee3f34f 100644
--- a/frontend/src/component/ai/AIChat.tsx
+++ b/frontend/src/component/ai/AIChat.tsx
@@ -17,6 +17,7 @@ import { Resizable } from 'component/common/Resizable/Resizable';
import { AIChatDisclaimer } from './AIChatDisclaimer';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import theme from 'themes/theme';
+import { Highlight } from 'component/common/Highlight/Highlight';
const AI_ERROR_MESSAGE = {
role: 'assistant',
@@ -199,19 +200,21 @@ export const AIChat = () => {
return (
- {
- trackEvent('unleash-ai-chat', {
- props: {
- eventType: 'open',
- },
- });
- setOpen(true);
- }}
- >
-
-
+
+ {
+ trackEvent('unleash-ai-chat', {
+ props: {
+ eventType: 'open',
+ },
+ });
+ setOpen(true);
+ }}
+ >
+
+
+
);
@@ -219,51 +222,53 @@ export const AIChat = () => {
return (
- scrollToEnd({ onlyIfAtEnd: true })}
- >
-
- {
- trackEvent('unleash-ai-chat', {
- props: {
- eventType: 'close',
- },
- });
- setOpen(false);
- }}
- />
-
-
-
- Hello, how can I assist you?
-
- {messages.map(({ role, content }, index) => (
-
- {content}
-
- ))}
- {loading && (
+
+ scrollToEnd({ onlyIfAtEnd: true })}
+ >
+
+ {
+ trackEvent('unleash-ai-chat', {
+ props: {
+ eventType: 'close',
+ },
+ });
+ setOpen(false);
+ }}
+ />
+
+
- _Unleash AI is typing..._
+ Hello, how can I assist you?
- )}
-
-
-
- scrollToEnd({ onlyIfAtEnd: true })
- }
- />
-
-
+ {messages.map(({ role, content }, index) => (
+
+ {content}
+
+ ))}
+ {loading && (
+
+ _Unleash AI is typing..._
+
+ )}
+
+
+
+ scrollToEnd({ onlyIfAtEnd: true })
+ }
+ />
+
+
+
);
};
diff --git a/frontend/src/component/changeRequest/ChangeRequest.test.tsx b/frontend/src/component/changeRequest/ChangeRequest.test.tsx
index 7c3a2230c7..5d63df8b3d 100644
--- a/frontend/src/component/changeRequest/ChangeRequest.test.tsx
+++ b/frontend/src/component/changeRequest/ChangeRequest.test.tsx
@@ -10,6 +10,7 @@ import { AnnouncerProvider } from '../common/Announcer/AnnouncerProvider/Announc
import { testServerRoute, testServerSetup } from '../../utils/testServer';
import { UIProviderContainer } from '../providers/UIProvider/UIProviderContainer';
import { StickyProvider } from 'component/common/Sticky/StickyProvider';
+import { HighlightProvider } from 'component/common/Highlight/HighlightProvider';
const server = testServerSetup();
@@ -233,14 +234,16 @@ const UnleashUiSetup: FC<{
-
- {children}
- }
- />
-
+
+
+ {children}
+ }
+ />
+
+
diff --git a/frontend/src/component/changeRequest/ChangeRequestPermissions.test.tsx b/frontend/src/component/changeRequest/ChangeRequestPermissions.test.tsx
index 5c48470a17..65af54ff10 100644
--- a/frontend/src/component/changeRequest/ChangeRequestPermissions.test.tsx
+++ b/frontend/src/component/changeRequest/ChangeRequestPermissions.test.tsx
@@ -12,6 +12,7 @@ import type { IPermission } from '../../interfaces/user';
import { SWRConfig } from 'swr';
import type { ProjectMode } from '../project/Project/hooks/useProjectEnterpriseSettingsForm';
import { StickyProvider } from 'component/common/Sticky/StickyProvider';
+import { HighlightProvider } from 'component/common/Highlight/HighlightProvider';
const server = testServerSetup();
@@ -196,12 +197,14 @@ const UnleashUiSetup: FC<{
-
-
-
+
+
+
+
+
diff --git a/frontend/src/component/common/Highlight/Highlight.tsx b/frontend/src/component/common/Highlight/Highlight.tsx
new file mode 100644
index 0000000000..b4e66197f9
--- /dev/null
+++ b/frontend/src/component/common/Highlight/Highlight.tsx
@@ -0,0 +1,43 @@
+import { alpha, styled } from '@mui/material';
+import type { ReactNode } from 'react';
+import { useHighlightContext } from './HighlightContext';
+import type { HighlightKey } from './HighlightProvider';
+
+const StyledHighlight = styled('div', {
+ shouldForwardProp: (prop) => prop !== 'highlighted',
+})<{ highlighted: boolean }>(({ theme, highlighted }) => ({
+ '&&& > *': {
+ animation: highlighted ? 'pulse 1.5s infinite linear' : 'none',
+ zIndex: highlighted ? theme.zIndex.tooltip : 'auto',
+ transition: 'box-shadow 0.3s ease',
+ '@keyframes pulse': {
+ '0%': {
+ boxShadow: `0 0 0 0px ${alpha(theme.palette.primary.main, 0.5)}`,
+ transform: 'scale(1)',
+ },
+ '50%': {
+ boxShadow: `0 0 0 15px ${alpha(theme.palette.primary.main, 0.2)}`,
+ transform: 'scale(1.05)',
+ },
+ '100%': {
+ boxShadow: `0 0 0 30px ${alpha(theme.palette.primary.main, 0)}`,
+ transform: 'scale(1)',
+ },
+ },
+ },
+}));
+
+interface IHighlightProps {
+ highlightKey: HighlightKey;
+ children: ReactNode;
+}
+
+export const Highlight = ({ highlightKey, children }: IHighlightProps) => {
+ const { isHighlighted } = useHighlightContext();
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/frontend/src/component/common/Highlight/HighlightContext.tsx b/frontend/src/component/common/Highlight/HighlightContext.tsx
new file mode 100644
index 0000000000..7bd757aece
--- /dev/null
+++ b/frontend/src/component/common/Highlight/HighlightContext.tsx
@@ -0,0 +1,18 @@
+import { createContext, useContext } from 'react';
+import type { IHighlightContext } from './HighlightProvider';
+
+export const HighlightContext = createContext(
+ undefined,
+);
+
+export const useHighlightContext = (): IHighlightContext => {
+ const context = useContext(HighlightContext);
+
+ if (!context) {
+ throw new Error(
+ 'useHighlightContext must be used within a HighlightProvider',
+ );
+ }
+
+ return context;
+};
diff --git a/frontend/src/component/common/Highlight/HighlightProvider.tsx b/frontend/src/component/common/Highlight/HighlightProvider.tsx
new file mode 100644
index 0000000000..e18bbd2458
--- /dev/null
+++ b/frontend/src/component/common/Highlight/HighlightProvider.tsx
@@ -0,0 +1,45 @@
+import { useState, type ReactNode } from 'react';
+import { HighlightContext } from './HighlightContext';
+
+const defaultState = {
+ eventTimeline: false,
+ unleashAI: false,
+};
+
+export type HighlightKey = keyof typeof defaultState;
+type HighlightState = typeof defaultState;
+
+export interface IHighlightContext {
+ isHighlighted: (key: HighlightKey) => boolean;
+ highlight: (key: HighlightKey, timeout?: number) => void;
+}
+
+interface IHighlightProviderProps {
+ children: ReactNode;
+}
+
+export const HighlightProvider = ({ children }: IHighlightProviderProps) => {
+ const [state, setState] = useState(defaultState);
+
+ const isHighlighted = (key: HighlightKey) => state[key];
+
+ const setHighlight = (key: HighlightKey, value: boolean) => {
+ setState((prevState) => ({ ...prevState, [key]: value }));
+ };
+
+ const highlight = (key: HighlightKey, timeout = 3000) => {
+ setHighlight(key, true);
+ setTimeout(() => setHighlight(key, false), timeout);
+ };
+
+ const contextValue: IHighlightContext = {
+ isHighlighted,
+ highlight,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/frontend/src/component/events/EventTimeline/EventTimelineProvider.tsx b/frontend/src/component/events/EventTimeline/EventTimelineProvider.tsx
index f7496859b4..2aa5740009 100644
--- a/frontend/src/component/events/EventTimeline/EventTimelineProvider.tsx
+++ b/frontend/src/component/events/EventTimeline/EventTimelineProvider.tsx
@@ -1,4 +1,4 @@
-import { useState, type ReactNode } from 'react';
+import type { ReactNode } from 'react';
import { EventTimelineContext } from './EventTimelineContext';
import { useLocalStorageState } from 'hooks/useLocalStorageState';
import type { IEnvironment } from 'interfaces/environments';
@@ -17,21 +17,15 @@ type EventTimelinePersistentState = {
signalsSuggestionSeen?: boolean;
};
-type EventTimelineTemporaryState = {
- highlighted: boolean;
-};
-
type EventTimelineStateSetters = {
setOpen: (open: boolean) => void;
setTimeSpan: (timeSpan: TimeSpanOption) => void;
setEnvironment: (environment: IEnvironment) => void;
setSignalsSuggestionSeen: (seen: boolean) => void;
- setHighlighted: (highlighted: boolean) => void;
};
export interface IEventTimelineContext
extends EventTimelinePersistentState,
- EventTimelineTemporaryState,
EventTimelineStateSetters {}
export const timeSpanOptions: TimeSpanOption[] = [
@@ -100,7 +94,6 @@ export const EventTimelineProvider = ({
'event-timeline:v1',
defaultState,
);
- const [highlighted, setHighlighted] = useState(false);
const setField = (
key: K,
@@ -109,16 +102,8 @@ export const EventTimelineProvider = ({
setState((prevState) => ({ ...prevState, [key]: value }));
};
- const onSetHighlighted = (highlighted: boolean) => {
- setHighlighted(highlighted);
- if (highlighted) {
- setTimeout(() => setHighlighted(false), 3000);
- }
- };
-
const contextValue: IEventTimelineContext = {
...state,
- highlighted,
setOpen: (open: boolean) => setField('open', open),
setTimeSpan: (timeSpan: TimeSpanOption) =>
setField('timeSpan', timeSpan),
@@ -126,7 +111,6 @@ export const EventTimelineProvider = ({
setField('environment', environment),
setSignalsSuggestionSeen: (seen: boolean) =>
setField('signalsSuggestionSeen', seen),
- setHighlighted: onSetHighlighted,
};
return (
diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleash.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleash.tsx
index a4341f4955..2872f662d3 100644
--- a/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleash.tsx
+++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleash.tsx
@@ -20,10 +20,10 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { ReactComponent as SignalsPreview } from 'assets/img/signals.svg';
import LinearScaleIcon from '@mui/icons-material/LinearScale';
import { useNavigate } from 'react-router-dom';
-import { useEventTimelineContext } from 'component/events/EventTimeline/EventTimelineContext';
import { ReactComponent as EventTimelinePreview } from 'assets/img/eventTimeline.svg';
import { ReactComponent as AIIcon } from 'assets/icons/AI.svg';
import { ReactComponent as AIPreview } from 'assets/img/aiPreview.svg';
+import { useHighlightContext } from 'component/common/Highlight/HighlightContext';
const StyledNewInUnleash = styled('div')(({ theme }) => ({
margin: theme.spacing(2, 0, 1, 0),
@@ -95,6 +95,7 @@ export const NewInUnleash = ({
onMiniModeClick,
}: INewInUnleashProps) => {
const navigate = useNavigate();
+ const { highlight } = useHighlightContext();
const { trackEvent } = usePlausibleTracker();
const [seenItems, setSeenItems] = useLocalStorageState(
'new-in-unleash-seen:v1',
@@ -108,8 +109,6 @@ export const NewInUnleash = ({
const signalsEnabled = useUiFlag('signals');
const unleashAIEnabled = useUiFlag('unleashAI');
- const { setHighlighted } = useEventTimelineContext();
-
const items: NewInUnleashItemDetails[] = [
{
label: 'Signals & Actions',
@@ -153,7 +152,7 @@ export const NewInUnleash = ({
icon: ,
preview: ,
onCheckItOut: () => {
- setHighlighted(true);
+ highlight('eventTimeline');
window.scrollTo({
top: 0,
behavior: 'smooth',
@@ -183,6 +182,7 @@ export const NewInUnleash = ({
'Enhance your Unleash experience with the help of the Unleash AI assistant',
icon: ,
preview: ,
+ onCheckItOut: () => highlight('unleashAI'),
show: Boolean(unleashAIAvailable) && unleashAIEnabled,
beta: true,
longDescription: (
diff --git a/frontend/src/component/menu/Header/HeaderEventTimelineButton.tsx b/frontend/src/component/menu/Header/HeaderEventTimelineButton.tsx
index 9340c166db..ab587c2b7c 100644
--- a/frontend/src/component/menu/Header/HeaderEventTimelineButton.tsx
+++ b/frontend/src/component/menu/Header/HeaderEventTimelineButton.tsx
@@ -1,43 +1,15 @@
-import { alpha, IconButton, styled, Tooltip } from '@mui/material';
+import { IconButton, Tooltip } from '@mui/material';
import LinearScaleIcon from '@mui/icons-material/LinearScale';
import { useEventTimelineContext } from 'component/events/EventTimeline/EventTimelineContext';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
-
-const StyledHeaderEventTimelineButton = styled(IconButton, {
- shouldForwardProp: (prop) => prop !== 'highlighted',
-})<{
- component?: 'a' | 'button';
- href?: string;
- target?: string;
- highlighted?: boolean;
-}>(({ theme, highlighted }) => ({
- animation: highlighted ? 'pulse 1.5s infinite linear' : 'none',
- zIndex: highlighted ? theme.zIndex.tooltip : 'auto',
- '@keyframes pulse': {
- '0%': {
- boxShadow: `0 0 0 0px ${alpha(theme.palette.primary.main, 0.5)}`,
- transform: 'scale(1)',
- },
- '50%': {
- boxShadow: `0 0 0 15px ${alpha(theme.palette.primary.main, 0.2)}`,
- transform: 'scale(1.1)',
- },
- '100%': {
- boxShadow: `0 0 0 30px ${alpha(theme.palette.primary.main, 0)}`,
- transform: 'scale(1)',
- },
- },
-}));
+import { Highlight } from 'component/common/Highlight/Highlight';
export const HeaderEventTimelineButton = () => {
const { trackEvent } = usePlausibleTracker();
const { isOss } = useUiConfig();
- const {
- open: showTimeline,
- setOpen: setShowTimeline,
- highlighted,
- } = useEventTimelineContext();
+ const { open: showTimeline, setOpen: setShowTimeline } =
+ useEventTimelineContext();
if (isOss()) return null;
@@ -46,20 +18,21 @@ export const HeaderEventTimelineButton = () => {
title={showTimeline ? 'Hide event timeline' : 'Show event timeline'}
arrow
>
- {
- trackEvent('event-timeline', {
- props: {
- eventType: showTimeline ? 'close' : 'open',
- },
- });
- setShowTimeline(!showTimeline);
- }}
- size='large'
- >
-
-
+
+ {
+ trackEvent('event-timeline', {
+ props: {
+ eventType: showTimeline ? 'close' : 'open',
+ },
+ });
+ setShowTimeline(!showTimeline);
+ }}
+ size='large'
+ >
+
+
+
);
};
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index ac9bcc7367..6165f97fe4 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -21,6 +21,7 @@ import { PlausibleProvider } from 'component/providers/PlausibleProvider/Plausib
import { Error as LayoutError } from './component/layout/Error/Error';
import { ErrorBoundary } from 'react-error-boundary';
import { useRecordUIErrorApi } from 'hooks/api/actions/useRecordUIErrorApi/useRecordUiErrorApi';
+import { HighlightProvider } from 'component/common/Highlight/HighlightProvider';
window.global ||= window;
@@ -56,10 +57,12 @@ const ApplicationRoot = () => {
-
-
-
-
+
+
+
+
+
+
diff --git a/frontend/src/utils/testRenderer.tsx b/frontend/src/utils/testRenderer.tsx
index 1b578f34ab..54e33cd7c3 100644
--- a/frontend/src/utils/testRenderer.tsx
+++ b/frontend/src/utils/testRenderer.tsx
@@ -14,6 +14,7 @@ import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
import { QueryParamProvider } from 'use-query-params';
import { FeedbackProvider } from 'component/feedbackNew/FeedbackProvider';
import { StickyProvider } from 'component/common/Sticky/StickyProvider';
+import { HighlightProvider } from 'component/common/Highlight/HighlightProvider';
export const render = (
ui: JSX.Element,
@@ -50,7 +51,9 @@ export const render = (
- {children}
+
+ {children}
+