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
Conversation
TaoJiang98
commented
Oct 13, 2023
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
left some comments
{answer && <ContentAnswersGridCard>{answer}</ContentAnswersGridCard>} | ||
{!answer && isLoading && <LoadingElement />} |
There was a problem hiding this comment.
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
prompt: string; | ||
answer?: string; | ||
createdAt?: string; | ||
error?: string; |
There was a problem hiding this comment.
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]); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
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 }); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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>} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
const lastQuestion = updatedQuestions.pop(); | ||
if (lastQuestion && lastQuestion.prompt) { | ||
lastQuestion.error = error; | ||
setQuestions([...updatedQuestions, lastQuestion]); |
There was a problem hiding this comment.
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
@@ -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.', |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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:
<div className="bdl-InlineError-AlertIcon"> | |
<div className="bdl-InlineError-alertIcon"> |
.bdl-InlineError { | ||
margin-top: $bdl-grid-unit * 4; | ||
|
||
&-icon { |
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when
instead of which
?
const IntlWrapper = ({ children }: { children?: React.ReactNode }) => { | ||
return <IntlProvider locale="en">{children}</IntlProvider>; | ||
}; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 />} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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} />}
f0da546
to
1528b62
Compare
@@ -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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?