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

Add discover summary error info and button click logic enhancement #8352

Merged
merged 5 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/8352.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fix:
- Add discover summary error info and button click logic enhancement ([#8352](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8352))
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,12 @@ describe('query assist summary', () => {
YES: true,
NO: false,
};
const FEEDBACK = {
YES: true,
NO: false,
};

enum FEEDBACK {
NONE,
THUMB_UP,
THUMB_DOWN,
}

const renderQueryAssistSummary = (isCollapsed: boolean) => {
const component = render(
Expand Down Expand Up @@ -136,7 +138,7 @@ describe('query assist summary', () => {
};

const defaultUseStateMock = () => {
mockUseState(null, LOADING.NO, FEEDBACK.NO);
mockUseState(null, LOADING.NO, FEEDBACK.NONE);
};

it('should not show if collapsed is true', () => {
Expand All @@ -147,14 +149,14 @@ describe('query assist summary', () => {
});

it('should not show if assistant is disabled by capability', () => {
mockUseState(null, LOADING.NO, FEEDBACK.NO, false);
mockUseState(null, LOADING.NO, FEEDBACK.NONE, false);
renderQueryAssistSummary(COLLAPSED.NO);
const summaryPanels = screen.queryAllByTestId('queryAssist__summary');
expect(summaryPanels).toHaveLength(0);
});

it('should not show if query assistant is collapsed', () => {
mockUseState(null, LOADING.NO, FEEDBACK.NO, true, COLLAPSED.YES);
mockUseState(null, LOADING.NO, FEEDBACK.NONE, true, COLLAPSED.YES);
renderQueryAssistSummary(COLLAPSED.NO);
const summaryPanels = screen.queryAllByTestId('queryAssist__summary');
expect(summaryPanels).toHaveLength(0);
Expand All @@ -168,15 +170,15 @@ describe('query assist summary', () => {
});

it('should display loading view if loading state is true', () => {
mockUseState(null, LOADING.YES, FEEDBACK.NO);
mockUseState(null, LOADING.YES, FEEDBACK.NONE);
renderQueryAssistSummary(COLLAPSED.NO);
expect(screen.getByTestId('queryAssist_summary_loading')).toBeInTheDocument();
expect(screen.queryAllByTestId('queryAssist_summary_result')).toHaveLength(0);
expect(screen.queryAllByTestId('queryAssist_summary_empty_text')).toHaveLength(0);
});

it('should display loading view if loading state is true even with summary', () => {
mockUseState('summary', LOADING.YES, FEEDBACK.NO);
mockUseState('summary', LOADING.YES, FEEDBACK.NONE);
renderQueryAssistSummary(COLLAPSED.NO);
expect(screen.getByTestId('queryAssist_summary_loading')).toBeInTheDocument();
expect(screen.queryAllByTestId('queryAssist_summary_result')).toHaveLength(0);
Expand All @@ -192,7 +194,7 @@ describe('query assist summary', () => {
});

it('should display summary result', () => {
mockUseState('summary', LOADING.NO, FEEDBACK.NO);
mockUseState('summary', LOADING.NO, FEEDBACK.NONE);
renderQueryAssistSummary(COLLAPSED.NO);
expect(screen.getByTestId('queryAssist_summary_result')).toBeInTheDocument();
expect(screen.getByTestId('queryAssist_summary_result')).toHaveTextContent('summary');
Expand All @@ -201,12 +203,12 @@ describe('query assist summary', () => {
});

it('should report metric for thumbup click', async () => {
mockUseState('summary', LOADING.NO, FEEDBACK.NO);
mockUseState('summary', LOADING.NO, FEEDBACK.NONE);
renderQueryAssistSummary(COLLAPSED.NO);
expect(screen.getByTestId('queryAssist_summary_result')).toBeInTheDocument();
await screen.getByTestId('queryAssist_summary_buttons_thumbup');
fireEvent.click(screen.getByTestId('queryAssist_summary_buttons_thumbup'));
expect(setFeedback).toHaveBeenCalledWith(true);
expect(setFeedback).toHaveBeenCalledWith(FEEDBACK.THUMB_UP);
expect(reportUiStatsMock).toHaveBeenCalledWith(
'query-assist',
'click',
Expand All @@ -215,21 +217,37 @@ describe('query assist summary', () => {
});

it('should report metric for thumbdown click', async () => {
mockUseState('summary', LOADING.NO, FEEDBACK.NO);
mockUseState('summary', LOADING.NO, FEEDBACK.NONE);
renderQueryAssistSummary(COLLAPSED.NO);
expect(screen.getByTestId('queryAssist_summary_result')).toBeInTheDocument();
await screen.getByTestId('queryAssist_summary_buttons_thumbdown');
fireEvent.click(screen.getByTestId('queryAssist_summary_buttons_thumbdown'));
expect(setFeedback).toHaveBeenCalledWith(true);
expect(setFeedback).toHaveBeenCalledWith(FEEDBACK.THUMB_DOWN);
expect(reportUiStatsMock).toHaveBeenCalledWith(
'query-assist',
'click',
expect.stringMatching(/^thumbdown/)
);
});

it('should hide thumbdown button if thumbup button is clicked', async () => {
mockUseState('summary', LOADING.NO, FEEDBACK.THUMB_UP);
renderQueryAssistSummary(COLLAPSED.NO);
expect(screen.getByTestId('queryAssist_summary_result')).toBeInTheDocument();
await screen.getByTestId('queryAssist_summary_buttons_thumbup');
expect(screen.queryByTestId('queryAssist_summary_buttons_thumbdown')).not.toBeInTheDocument();
});

it('should hide thumbup button if thumbdown button is clicked', async () => {
mockUseState('summary', LOADING.NO, FEEDBACK.THUMB_DOWN);
renderQueryAssistSummary(COLLAPSED.NO);
expect(screen.getByTestId('queryAssist_summary_result')).toBeInTheDocument();
await screen.getByTestId('queryAssist_summary_buttons_thumbdown');
expect(screen.queryByTestId('queryAssist_summary_buttons_thumbup')).not.toBeInTheDocument();
});

it('should not fetch summary if data is empty', async () => {
mockUseState(null, LOADING.NO, FEEDBACK.NO);
mockUseState(null, LOADING.NO, FEEDBACK.NONE);
renderQueryAssistSummary(COLLAPSED.NO);
question$.next(question);
query$.next({ query: PPL, language: 'PPL' });
Expand All @@ -238,7 +256,7 @@ describe('query assist summary', () => {
});

it('should fetch summary with expected payload and response', async () => {
mockUseState('summary', LOADING.NO, FEEDBACK.NO);
mockUseState('summary', LOADING.NO, FEEDBACK.NONE);
const RESPONSE_TEXT = 'response';
httpMock.post.mockResolvedValue(RESPONSE_TEXT);
renderQueryAssistSummary(COLLAPSED.NO);
Expand All @@ -258,27 +276,27 @@ describe('query assist summary', () => {
dataSourceId: undefined,
},
});
expect(setSummary).toHaveBeenNthCalledWith(1, null);
expect(setSummary).toHaveBeenNthCalledWith(1, '');
expect(setSummary).toHaveBeenNthCalledWith(2, RESPONSE_TEXT);
expect(setLoading).toHaveBeenNthCalledWith(1, true);
expect(setLoading).toHaveBeenNthCalledWith(2, false);
});

it('should handle fetch summary error', async () => {
mockUseState('summary', LOADING.NO, FEEDBACK.NO);
mockUseState('summary', LOADING.NO, FEEDBACK.NONE);
httpMock.post.mockRejectedValueOnce({});
renderQueryAssistSummary(COLLAPSED.NO);
question$.next(question);
query$.next({ query: PPL, language: 'PPL' });
data$.next(dataFrame as IDataFrame);
await sleep(WAIT_TIME);
expect(setSummary).toBeCalledTimes(1);
expect(setSummary).toBeCalledTimes(2);
expect(setLoading).toHaveBeenNthCalledWith(1, true);
expect(setLoading).toHaveBeenNthCalledWith(2, false);
});

it('should not update queryResults if subscription changed not in order', async () => {
mockUseState('summary', LOADING.NO, FEEDBACK.NO);
mockUseState('summary', LOADING.NO, FEEDBACK.NONE);
const RESPONSE_TEXT = 'response';
httpMock.post.mockResolvedValue(RESPONSE_TEXT);
renderQueryAssistSummary(COLLAPSED.NO);
Expand All @@ -290,7 +308,7 @@ describe('query assist summary', () => {
});

it('should update queryResults if subscriptions changed in order', async () => {
mockUseState('summary', LOADING.NO, FEEDBACK.NO);
mockUseState('summary', LOADING.NO, FEEDBACK.NONE);
const RESPONSE_TEXT = 'response';
httpMock.post.mockResolvedValue(RESPONSE_TEXT);
renderQueryAssistSummary(COLLAPSED.NO);
Expand All @@ -308,14 +326,14 @@ describe('query assist summary', () => {
});

it('should reset feedback state if re-fetch summary', async () => {
mockUseState('summary', LOADING.NO, FEEDBACK.YES);
mockUseState('summary', LOADING.NO, FEEDBACK.THUMB_UP);
const RESPONSE_TEXT = 'response';
httpMock.post.mockResolvedValue(RESPONSE_TEXT);
renderQueryAssistSummary(COLLAPSED.NO);
question$.next(question);
query$.next({ query: PPL, language: 'PPL' });
data$.next(dataFrame as IDataFrame);
await sleep(WAIT_TIME);
expect(setFeedback).toHaveBeenCalledWith(FEEDBACK.NO);
expect(setFeedback).toHaveBeenCalledWith(FEEDBACK.NONE);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,17 @@ export const convertResult = (body: IDataFrame) => {
return hits;
};

enum FeedbackStatus {
chishui marked this conversation as resolved.
Show resolved Hide resolved
NONE,
THUMB_UP,
THUMB_DOWN,
}

export const QueryAssistSummary: React.FC<QueryAssistSummaryProps> = (props) => {
const { query, search } = props.data;
const [summary, setSummary] = useState(null); // store fetched data
Copy link
Member

Choose a reason for hiding this comment

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

do we know for sure the summary will never be ''. null for me implies no summary was retrieved where as if the summary was an empty string the user might not be able to give feedback on the summary even though the information can be used to capture cases where query assist didn't output any summary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In case LLM fails to generate summary, we would render an error message. But if LLM is able to generate summary, I believe there's always some outputs.

const [summary, setSummary] = useState(''); // store fetched data
const [loading, setLoading] = useState(false); // track loading state
const [feedback, setFeedback] = useState(false);
const [feedback, setFeedback] = useState(FeedbackStatus.NONE);
const [isEnabledByCapability, setIsEnabledByCapability] = useState(false);
const selectedDataset = useRef(query.queryString.getQuery()?.dataset);
const { question$, isQueryAssistCollapsed } = useQueryAssist();
Expand All @@ -76,6 +82,9 @@ export const QueryAssistSummary: React.FC<QueryAssistSummaryProps> = (props) =>
defaultMessage:
'Thank you for the feedback. Try again by adjusting your question so that I have the opportunity to better assist you.',
});
const errorPrompt = i18n.translate('queryEnhancements.queryAssist.summary.errorPrompt', {
Copy link
Member

Choose a reason for hiding this comment

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

should we consider adding : Try again by adjusting your question so that I have the opportunity to better assist you.

Or do we have an error message that we can display to the user for more insight?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Some errors are not caused by questions, it might be frustrating when there's backend issue but we keep asking users to change questions.

defaultMessage: 'I am unable to respond to this query. Try another question.',
});

const sampleSize = 10;

Expand Down Expand Up @@ -117,8 +126,8 @@ export const QueryAssistSummary: React.FC<QueryAssistSummaryProps> = (props) =>
async (queryContext: QueryContext) => {
if (isEmpty(queryContext?.queryResults)) return;
setLoading(true);
setSummary(null);
setFeedback(false);
setSummary('');
setFeedback(FeedbackStatus.NONE);
const SUCCESS_METRIC = 'fetch_summary_success';
try {
const actualSampleSize = Math.min(sampleSize, queryContext?.queryResults?.length);
Expand All @@ -140,11 +149,12 @@ export const QueryAssistSummary: React.FC<QueryAssistSummaryProps> = (props) =>
reportCountMetric(SUCCESS_METRIC, 1);
} catch (error) {
reportCountMetric(SUCCESS_METRIC, 0);
setSummary(errorPrompt);
} finally {
setLoading(false);
}
},
[props.http, reportCountMetric]
[props.http, reportCountMetric, errorPrompt]
);

useEffect(() => {
Expand Down Expand Up @@ -203,8 +213,8 @@ export const QueryAssistSummary: React.FC<QueryAssistSummaryProps> = (props) =>

const onFeedback = useCallback(
(satisfied: boolean) => {
if (feedback) return;
setFeedback(true);
if (feedback !== FeedbackStatus.NONE) return;
chishui marked this conversation as resolved.
Show resolved Hide resolved
setFeedback(satisfied ? FeedbackStatus.THUMB_UP : FeedbackStatus.THUMB_DOWN);
reportMetric(satisfied ? 'thumbup' : 'thumbdown');
chishui marked this conversation as resolved.
Show resolved Hide resolved
},
[feedback, reportMetric]
Expand Down Expand Up @@ -246,38 +256,42 @@ export const QueryAssistSummary: React.FC<QueryAssistSummaryProps> = (props) =>
})}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSmallButtonIcon
aria-label="feedback thumbs up"
color="text"
iconType="thumbsUp"
title={
!feedback
? i18n.translate('queryEnhancements.queryAssist.summary.goodResponse', {
defaultMessage: `Good response`,
})
: afterFeedbackTip
}
onClick={() => onFeedback(true)}
data-test-subj="queryAssist_summary_buttons_thumbup"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSmallButtonIcon
aria-label="feedback thumbs down"
color="text"
title={
!feedback
? i18n.translate('queryEnhancements.queryAssist.summary.badResponse', {
defaultMessage: `Bad response`,
})
: afterFeedbackTip
}
iconType="thumbsDown"
onClick={() => onFeedback(false)}
data-test-subj="queryAssist_summary_buttons_thumbdown"
/>
</EuiFlexItem>
{feedback !== FeedbackStatus.THUMB_DOWN && (
<EuiFlexItem grow={false}>
<EuiSmallButtonIcon
aria-label="feedback thumbs up"
color={feedback === FeedbackStatus.THUMB_UP ? 'subdued' : 'text'}
iconType="thumbsUp"
title={
!feedback
? i18n.translate('queryEnhancements.queryAssist.summary.goodResponse', {
defaultMessage: `Good response`,
})
: afterFeedbackTip
}
onClick={() => onFeedback(true)}
data-test-subj="queryAssist_summary_buttons_thumbup"
Comment on lines +269 to +270
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we disable the button when feedback === FeedbackStatus.THUMB_UP, so that we don't need to check if (feedback !== FeedbackStatus.NONE) return; for onFeedback()

/>
</EuiFlexItem>
)}
{feedback !== FeedbackStatus.THUMB_UP && (
<EuiFlexItem grow={false}>
<EuiSmallButtonIcon
aria-label="feedback thumbs down"
color={feedback === FeedbackStatus.THUMB_DOWN ? 'subdued' : 'text'}
title={
!feedback
? i18n.translate('queryEnhancements.queryAssist.summary.badResponse', {
defaultMessage: `Bad response`,
})
: afterFeedbackTip
}
iconType="thumbsDown"
onClick={() => onFeedback(false)}
data-test-subj="queryAssist_summary_buttons_thumbdown"
/>
</EuiFlexItem>
)}
<EuiSpacer size="m" style={{ borderLeft: '1px solid #D3DAE6', height: '20px' }} />
<EuiFlexItem grow={false}>
<EuiCopy textToCopy={summary ?? ''}>
Expand Down
Loading