diff --git a/src/components/CompletionProviderStatus/CompletionProviderStatus.css b/src/components/CompletionProviderStatus/CompletionProviderStatus.css new file mode 100644 index 00000000..f751f6d4 --- /dev/null +++ b/src/components/CompletionProviderStatus/CompletionProviderStatus.css @@ -0,0 +1,18 @@ +.memori--completion-provider-status--icon { + width: 1em; + height: 1em; + fill: var(--memori-error-color, red); +} + +.memori--completion-provider-status--tooltip.memori-tooltip.memori-tooltip--align-topLeft:not(.memori-tooltip--disabled).memori-tooltip--visible .memori-tooltip--content, +.memori--completion-provider-status--tooltip.memori-tooltip.memori-tooltip--align-topLeft:not(.memori-tooltip--disabled):not(.memori-tooltip--visible):hover .memori-tooltip--content { + transform: translateY(calc(-100% - 2em)) translateX(3em); +} + +.memori--completion-provider-status--tooltip.memori-tooltip .memori-tooltip--content p { + margin: 0.5em auto; +} + +.memori--completion-provider-status--tooltip.memori-tooltip .memori-tooltip--content p+p { + margin-top: 1em; +} \ No newline at end of file diff --git a/src/components/CompletionProviderStatus/CompletionProviderStatus.stories.tsx b/src/components/CompletionProviderStatus/CompletionProviderStatus.stories.tsx new file mode 100644 index 00000000..489f2493 --- /dev/null +++ b/src/components/CompletionProviderStatus/CompletionProviderStatus.stories.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Meta, Story } from '@storybook/react'; +import CompletionProviderStatus, { Props } from './CompletionProviderStatus'; + +import './CompletionProviderStatus.css'; + +const meta: Meta = { + title: 'Completion Provider Status', + component: CompletionProviderStatus, + argTypes: {}, + parameters: { + layout: 'centered', + controls: { expanded: true }, + }, +}; + +export default meta; + +const Template: Story = args => ; + +export const Default = Template.bind({}); +Default.args = {}; + +export const Errored = Template.bind({}); +Errored.args = { + forceStatus: 'major', +}; + +export const WithSpecifiedProvider = Template.bind({}); +WithSpecifiedProvider.args = { + provider: 'OpenAI', +}; + +export const ErroredWithSpecifiedProvider = Template.bind({}); +ErroredWithSpecifiedProvider.args = { + forceStatus: 'major', + provider: 'OpenAI', +}; diff --git a/src/components/CompletionProviderStatus/CompletionProviderStatus.test.tsx b/src/components/CompletionProviderStatus/CompletionProviderStatus.test.tsx new file mode 100644 index 00000000..856547aa --- /dev/null +++ b/src/components/CompletionProviderStatus/CompletionProviderStatus.test.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import CompletionProviderStatus from './CompletionProviderStatus'; + +it('renders CompletionProviderStatus unchanged', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); +}); + +it('renders CompletionProviderStatus errored unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); +}); + +it('renders CompletionProviderStatus with provider specified unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); +}); + +it('renders CompletionProviderStatus errored with provider specified unchanged', () => { + const { container } = render( + + ); + expect(container).toMatchSnapshot(); +}); diff --git a/src/components/CompletionProviderStatus/CompletionProviderStatus.tsx b/src/components/CompletionProviderStatus/CompletionProviderStatus.tsx new file mode 100644 index 00000000..7310ab27 --- /dev/null +++ b/src/components/CompletionProviderStatus/CompletionProviderStatus.tsx @@ -0,0 +1,75 @@ +import { useEffect, useState } from 'react'; +import Tooltip from '../ui/Tooltip'; +import Warning from '../icons/Warning'; +import { useTranslation } from 'react-i18next'; + +export interface Props { + forceStatus?: string; + provider?: 'OpenAI' | string | null; +} + +const initProviderStatus = (provider?: Props['provider']) => { + switch (provider) { + case 'DEFAULT': + case 'OpenAI': + return { + getStatus: async () => { + const res = await fetch( + 'https://status.openai.com/api/v2/summary.json' + ); + const data = await res.json(); + return data.status.indicator ?? 'none'; + }, + statusPage: 'https://status.openai.com/', + }; + default: + return { + getStatus: async () => 'none', + statusPage: '', + }; + } +}; + +const CompletionProviderStatus = ({ forceStatus, provider }: Props) => { + const { t } = useTranslation(); + const [status, setStatus] = useState(forceStatus ?? 'none'); + + const providerStatus = initProviderStatus(provider); + + useEffect(() => { + if (forceStatus) return; + + providerStatus.getStatus().then(status => setStatus(status)); + }, [forceStatus, provider]); + + return status !== 'none' ? ( + +

+ {t('completionProviderDown', { + provider: provider ?? t('completionProviderFallbackName'), + })} +

+ {!!providerStatus.statusPage?.length && ( +

+ + {t('completionProviderCheckStatusPage')} + +

+ )} + + } + > + +
+ ) : null; +}; + +export default CompletionProviderStatus; diff --git a/src/components/CompletionProviderStatus/__snapshots__/CompletionProviderStatus.test.tsx.snap b/src/components/CompletionProviderStatus/__snapshots__/CompletionProviderStatus.test.tsx.snap new file mode 100644 index 00000000..f57fecea --- /dev/null +++ b/src/components/CompletionProviderStatus/__snapshots__/CompletionProviderStatus.test.tsx.snap @@ -0,0 +1,82 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders CompletionProviderStatus errored unchanged 1`] = ` +
+
+
+
+

+ completionProviderDown +

+
+
+
+ +
+
+
+`; + +exports[`renders CompletionProviderStatus errored with provider specified unchanged 1`] = ` +
+
+
+
+

+ completionProviderDown +

+

+ + completionProviderCheckStatusPage + +

+
+
+
+ +
+
+
+`; + +exports[`renders CompletionProviderStatus unchanged 1`] = `
`; + +exports[`renders CompletionProviderStatus with provider specified unchanged 1`] = `
`; diff --git a/src/components/StartPanel/StartPanel.css b/src/components/StartPanel/StartPanel.css index e0966b28..7458e82b 100644 --- a/src/components/StartPanel/StartPanel.css +++ b/src/components/StartPanel/StartPanel.css @@ -109,6 +109,10 @@ opacity: 0.85; } +.memori--start-button { + margin-right: 1em; +} + .memori--language-chooser { margin-bottom: 1rem; } diff --git a/src/components/StartPanel/StartPanel.stories.tsx b/src/components/StartPanel/StartPanel.stories.tsx index 75fe2341..d22e8471 100644 --- a/src/components/StartPanel/StartPanel.stories.tsx +++ b/src/components/StartPanel/StartPanel.stories.tsx @@ -237,3 +237,22 @@ WithIntegration.args = { clickedStart: false, onClickStart: () => {}, }; + +export const WithCompletionProviderDown = Template.bind({}); +WithCompletionProviderDown.args = { + memori: { + ...memori, + enableCompletions: false, + completionProvider: 'OpenAI', + }, + tenant, + language: 'it', + userLang: 'en', + setUserLang: () => {}, + openPositionDrawer: () => {}, + instruct: false, + sessionId: sessionID, + clickedStart: false, + onClickStart: () => {}, + _TEST_forceProviderStatus: 'major', +}; diff --git a/src/components/StartPanel/StartPanel.test.tsx b/src/components/StartPanel/StartPanel.test.tsx index f89e2846..23b66845 100644 --- a/src/components/StartPanel/StartPanel.test.tsx +++ b/src/components/StartPanel/StartPanel.test.tsx @@ -132,3 +132,25 @@ it('renders StartPanel with integrationConfig unchanged', () => { ); expect(container).toMatchSnapshot(); }); + +it('renders StartPanel with completion provider down unchanged', () => { + const { container } = render( + {}} + openPositionDrawer={() => {}} + instruct={false} + sessionId={sessionID} + clickedStart={false} + onClickStart={() => {}} + _TEST_forceProviderStatus="major" + /> + ); + expect(container).toMatchSnapshot(); +}); diff --git a/src/components/StartPanel/StartPanel.tsx b/src/components/StartPanel/StartPanel.tsx index b85b7966..89ead07b 100644 --- a/src/components/StartPanel/StartPanel.tsx +++ b/src/components/StartPanel/StartPanel.tsx @@ -14,6 +14,7 @@ import Translation from '../icons/Translation'; import { chatLanguages } from '../../helpers/constants'; import BlockedMemoriBadge from '../BlockedMemoriBadge/BlockedMemoriBadge'; import AI from '../icons/AI'; +import CompletionProviderStatus from '../CompletionProviderStatus/CompletionProviderStatus'; export interface Props { memori: Memori; @@ -32,6 +33,7 @@ export interface Props { clickedStart?: boolean; onClickStart?: () => void; initializeTTS?: () => void; + _TEST_forceProviderStatus?: string; } const StartPanel: React.FC = ({ @@ -50,6 +52,7 @@ const StartPanel: React.FC = ({ clickedStart, onClickStart, initializeTTS, + _TEST_forceProviderStatus, }) => { const { t, i18n } = useTranslation(); const [translatedDescription, setTranslatedDescription] = useState( @@ -254,6 +257,11 @@ const StartPanel: React.FC = ({ )} + +

{instruct ? t('write_and_speak.pageInstructExplanation') diff --git a/src/components/StartPanel/__snapshots__/StartPanel.test.tsx.snap b/src/components/StartPanel/__snapshots__/StartPanel.test.tsx.snap index eef85737..e74315ce 100644 --- a/src/components/StartPanel/__snapshots__/StartPanel.test.tsx.snap +++ b/src/components/StartPanel/__snapshots__/StartPanel.test.tsx.snap @@ -224,6 +224,145 @@ exports[`renders StartPanel unchanged 1`] = `

`; +exports[`renders StartPanel with completion provider down unchanged 1`] = ` +
+
+
+
+
+
+ completionsEnabled +
+
+ + + +
+
+
+
+ + + Memori + +

+ Memori +

+
+

+ + Lorem ipsum. + +

+ +
+
+
+

+ completionProviderDown +

+

+ + completionProviderCheckStatusPage + +

+
+
+
+ +
+
+

+ write_and_speak.pageTryMeExplanation +

+
+
+
+`; + exports[`renders StartPanel with completions enabled unchanged 1`] = `