Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(content-answers): add inline error #3444

Merged
merged 9 commits into from Oct 24, 2023
Merged

feat(content-answers): add inline error #3444

merged 9 commits into from Oct 24, 2023

Conversation

TaoJiang98
Copy link
Contributor

Screenshot 2023-10-13 at 2 33 59 PM

@TaoJiang98 TaoJiang98 requested a review from a team as a code owner October 13, 2023 21:40
@TaoJiang98 TaoJiang98 marked this pull request as draft October 13, 2023 21:46
@TaoJiang98 TaoJiang98 marked this pull request as ready for review October 16, 2023 15:28
Copy link
Contributor

@greg-in-a-box greg-in-a-box left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left some comments

Comment on lines 21 to 17
{answer && <ContentAnswersGridCard>{answer}</ContentAnswersGridCard>}
{!answer && isLoading && <LoadingElement />}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to prevent these were rendering if there is an error, definitely the first line but the second line is debatable depending on how isLoading is handle but I think its safe to say both should not render if there is an error

Comment on lines 26 to 29
prompt: string;
answer?: string;
createdAt?: string;
error?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didnt notice this before but abc

}

setQuestions([...updatedQuestions, lastQuestion]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we still update state if there isnt a lastQuestion?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be guaranteed that there is always a lastQuestion in the question array. The reason is that whenever we submit a question, the new question object with the prompt only will be appended into the question array.

@@ -53,11 +54,15 @@ const ContentAnswersModal = ({ api, currentUser, file, isOpen, onRequestClose }:
setQuestions([...updatedQuestions, lastQuestion]);
}, []);

const handleErrorCallback = useCallback((error: ElementsXhrError): void => {
const handleErrorCallback = useCallback((error: ElementsXhrError, updatedQuestions): void => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we're already adding the error type, can we add the updatedQuestions type?

error: ElementsXhrError;
};

const InlineError = ({ error }: Props) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have to pass in the error? you're already controlling when to render component in the parent component

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed error prop, but in the future we could possibly pass error and make the inlineError component show different error messages based on the error prop


const loadingElement = screen.getByTestId('LoadingElement');
expect(loadingElement).toBeInTheDocument();
});

test('should render inline error', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should check if the other two have rendered or not


const loadingElement = screen.getByTestId('LoadingElement');
expect(loadingElement).toBeInTheDocument();
});

test('should render inline error', () => {
renderComponent({ answer: '', error: new Error('error'), isLoading: true });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to do something like a test.each to check all the possibilities and then verify the expected behavior in each of them

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since each check has special case such as checking only renders LoadingElement would have to check the length of testId 'content-answers-grid-card' to be exact 1 so that I separate those three test cases

@@ -24,11 +24,11 @@ const ContentAnswersModalContent = ({ currentUser, fileName, isLoading, question
<WelcomeMessage fileName={fileName} />
<ul>
{questions &&
questions.map(({ prompt, answer = '' }, index) => {
questions.map(({ prompt, answer = '', error }, index) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaulted props always come at the end of the args

isLoading: boolean;
};

const Answer = ({ answer, isLoading }: Props) => {
const Answer = ({ answer, error, isLoading }: Props) => {
return (
<div className="bdl-Answer">
{answer && <ContentAnswersGridCard>{answer}</ContentAnswersGridCard>}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think we should give the opportunity to provide an answer and an error at the same time where it renders both

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you suggest that only pass answer prop here and if the error is not undefined, we display InlineError directly in ContentAnswersModalContent component? so that Answer component won't have the answer and error prop appeared at the same time

renderComponent({ answer: '', isLoading: true });

const loadingElement = screen.getByTestId('LoadingElement');
expect(loadingElement).toBeInTheDocument();
expect(screen.queryAllByTestId('content-answers-grid-card')).toHaveLength(1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should assert if the answer is there not the card, its easier to understand in terms of readability. noone is going to know that the card is actually under the LoadingElement too unless they check in that component too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the answer prop here is given as ''. I think it does not make sense to check if text '' not in the document. Instead, I checked length of children list under Answer component is 1 which should be loading element.

Comment on lines 59 to 62
const lastQuestion = updatedQuestions.pop();
if (lastQuestion && lastQuestion.prompt) {
lastQuestion.error = error;
setQuestions([...updatedQuestions, lastQuestion]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need a test to handle this logic

@TaoJiang98 TaoJiang98 requested a review from a team as a code owner October 23, 2023 19:42
@@ -32,6 +32,11 @@ const messages = defineMessages({
'Disabled tooltip message for Content Answers entry point button when the file type is not supported',
id: 'boxui.contentAnswers.disabledTooltipFileNotCompatible',
},
inlineErrorText: {
defaultMessage: 'The Box AI service was unavailable.',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it say, is instead of was?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the error comes out, there would be a retry button for user to click. And I think it would make sense saying that Box AI service was unavailable and it is worth to retry.

</div>
</Media.Figure>
<Media.Body>
<div className="bdl-InlineError-AlertIcon">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the descendant element should be camelcase:

Suggested change
<div className="bdl-InlineError-AlertIcon">
<div className="bdl-InlineError-alertIcon">

.bdl-InlineError {
margin-top: $bdl-grid-unit * 4;

&-icon {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the past we've tried avoiding using & for descendant elements and modifiers so it doesn't hinder searching

e.g. searching bdl-InlineError-icon in GitHub or in an IDE would not show results because it's written &-icon

@@ -59,4 +60,11 @@ describe('features/content-answers/ContentAnswersModalContent', () => {
const loadingElement = screen.queryAllByTestId('LoadingElement');
expect(loadingElement.length).toEqual(1);
});

test('should render only the inline error which not loading and there is an error', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when instead of which?

Comment on lines 12 to 14
const IntlWrapper = ({ children }: { children?: React.ReactNode }) => {
return <IntlProvider locale="en">{children}</IntlProvider>;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this work:

jest.mock('react-intl', () => ({
    ...jest.requireActual('react-intl'),
    FormattedMessage: ({ defaultMessage }: { defaultMessage: string }) => <span>{defaultMessage}</span>,
}));

or does it still need the whole wrapper? when does the console error occur? why does a test like ContentAnalyticsErrorState.test.tsx not need it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I think this works. The console error occurs when I only mocks 'react-intl'. The
test ContentAnalyticsErrorState.test.tsx did not need it because it was testing the element instead of the text.

if (error) {
// TODO: render error component

const lastQuestion = updatedQuestions.pop();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably avoid object mutation here since the reference to updatedQuestions is used in multiple places which could cause unexpected issues

maybe a slice() or length solution:

const [lastQuestion] = updatedQuestions.slice(-1);

// or

const lastQuestion = updatedQuestions[updatedQuestions.length - 1];

}, []);

const handleErrorCallback = useCallback((error: ElementsXhrError): void => {
const handleErrorCallback = useCallback((error: ElementsXhrError, updatedQuestions: QuestionType[]): void => {
setIsLoading(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is setIsLoading called at the beginning of the function instead of the end of the logic or at the end of handleOnAsk?

return (
<li key={index}>
<Question currentUser={currentUser} prompt={prompt} />
<Answer answer={answer} isLoading={isLoading} />
{!hasError && <Answer answer={answer} isLoading={isLoading} />}
{hasError && !isLoading && <InlineError />}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to remove the isLoading check for InlineError? wouldn't this affect all "error answers"?

Stage 1

Q: summarize this document

Stage 2

Q: summarize this document
A: ERROR

Stage 3

Q: summarize this document
A: ERROR
Q: summarize this document in 200 words

Stage 4

Q: summarize this document
A: LOADING
Q: summarize this document in 200 words
A: LOADING

Stage 5

Q: summarize this document
A: ERROR
Q: summarize this document in 200 words
A: blah blah blah

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{hasError ? <InlineError /> : <Answer answer={answer} isLoading={isLoading} />}

@@ -76,8 +75,9 @@ const ContentAnswersModal = ({ api, currentUser, file, isOpen, onRequestClose }:
const response = await api.getIntelligenceAPI(true).ask(prompt, items);
handleSuccessCallback(response, updatedQuestions);
} catch (e) {
handleErrorCallback(e);
handleErrorCallback(e, updatedQuestions);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry it might be out of scope but I didn't realize this before: are these "callbacks" necessary?

why not something like:

const handleOnAsk = useCallback(
    ...
    let nextQuestion = { prompt };
    setQuestions([...questions, nextQuestion]);
    setIsLoading(true);
    try {
        const response = await api.getIntelligenceAPI(true).ask(prompt, items);
        nextQuestion = {
            answer: response.data.answer,
            createdAt: response.data.created_at,
            prompt,
        };
    } catch (error) {
        nextQuestion = {
            error,
            prompt,
        };
    }
    setQuestions([...questions, nextQuestion]);
    setIsLoading(false);

that way you don't need handleSuccessCallback and handleErrorCallback and avoids the object mutation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very great point! I think previously it is written as handleSuccessCallback and handleErrorCallback to better address which part need to work on for following small tasks.

When bring everything back to try catch, it still needs to update the last question either in place or pop up and then append it back since we need to append the question with prompt into the array at the very beginning.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed the object mutation logic


test('should render the header icon', () => {
renderComponent();
renderComponent(mockApi);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cant mockApi be defaulted since you have to use it more than once?

@mergify mergify bot merged commit 405cdb4 into master Oct 24, 2023
6 checks passed
@mergify mergify bot deleted the inline-error branch October 24, 2023 22:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants