Add dialog to report AI message feedback

This commit is contained in:
Florian Rival
2025-04-01 00:16:31 +02:00
parent 4e9556e948
commit 03dce1d90a
5 changed files with 190 additions and 30 deletions

View File

@@ -28,9 +28,10 @@ import { type MessageDescriptor } from '../../../Utils/i18n/MessageDescriptor.fl
import Link from '../../../UI/Link';
import { getHelpLink } from '../../../Utils/HelpLink';
import Window from '../../../Utils/Window';
import { DislikeFeedbackDialog } from './DislikeFeedbackDialog';
const TOO_MANY_MESSAGES_WARNING_COUNT = 9;
const TOO_MANY_MESSAGES_ERROR_COUNT = 14;
const TOO_MANY_USER_MESSAGES_WARNING_COUNT = 5;
const TOO_MANY_USER_MESSAGES_ERROR_COUNT = 10;
type Props = {
aiRequest: AiRequest | null,
@@ -40,7 +41,8 @@ type Props = {
onSendFeedback: (
aiRequestId: string,
messageIndex: number,
feedback: 'like' | 'dislike'
feedback: 'like' | 'dislike',
reason?: string
) => Promise<void>,
hasOpenedProject: boolean,
@@ -107,6 +109,11 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
const scrollViewRef = React.useRef<ScrollViewInterface | null>(null);
const [messageFeedbacks, setMessageFeedbacks] = React.useState({});
const theme = React.useContext(GDevelopThemeContext);
const [
dislikeFeedbackDialogOpenedFor,
setDislikeFeedbackDialogOpenedFor,
] = React.useState(null);
const [newChatPlaceholder] = React.useState(() => {
const newChatPlaceholders: Array<MessageDescriptor> = [
t`How to add a leaderboard?`,
@@ -281,6 +288,10 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
);
}
const userMessagesCount = aiRequest.output.filter(
message => message.role === 'user'
).length;
return (
<ColumnStackLayout
expand
@@ -350,11 +361,10 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
...messageFeedbacks,
[feedbackKey]: 'dislike',
});
onSendFeedback(
aiRequest.id,
setDislikeFeedbackDialogOpenedFor({
aiRequestId: aiRequest.id,
messageIndex,
'dislike'
);
});
}}
>
<Dislike
@@ -415,10 +425,10 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
</Line>
) : null}
</ScrollView>
{aiRequest.output.length >= TOO_MANY_MESSAGES_WARNING_COUNT ? (
{userMessagesCount >= TOO_MANY_USER_MESSAGES_WARNING_COUNT ? (
<AlertMessage
kind={
aiRequest.output.length >= TOO_MANY_MESSAGES_ERROR_COUNT
userMessagesCount >= TOO_MANY_USER_MESSAGES_ERROR_COUNT
? 'error'
: 'warning'
}
@@ -463,6 +473,20 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
{isMobile && errorOrQuotaOrCreditsExplanation}
</ResponsiveLineStackLayout>
</Column>
{dislikeFeedbackDialogOpenedFor && (
<DislikeFeedbackDialog
open
onClose={() => setDislikeFeedbackDialogOpenedFor(null)}
onSendFeedback={(reason: string) => {
onSendFeedback(
dislikeFeedbackDialogOpenedFor.aiRequestId,
dislikeFeedbackDialogOpenedFor.messageIndex,
'dislike',
reason
);
}}
/>
)}
</ColumnStackLayout>
);
}

View File

@@ -0,0 +1,100 @@
// @flow
import * as React from 'react';
import { ColumnStackLayout } from '../../../UI/Layout';
import Text from '../../../UI/Text';
import { Trans } from '@lingui/macro';
import Dialog, { DialogPrimaryButton } from '../../../UI/Dialog';
import Radio from '@material-ui/core/Radio';
import RadioGroup from '@material-ui/core/RadioGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FlatButton from '../../../UI/FlatButton';
type DislikeFeedbackDialogProps = {|
open: boolean,
onClose: () => void,
onSendFeedback: (reason: string) => void,
|};
export const DislikeFeedbackDialog = ({
open,
onClose,
onSendFeedback,
}: DislikeFeedbackDialogProps) => {
const [selectedReason, setSelectedReason] = React.useState<?string>(null);
const handleChange = (event: { target: { value: string } }) => {
setSelectedReason(event.target.value);
};
const handleSendFeedback = () => {
if (selectedReason) {
onSendFeedback(selectedReason);
onClose();
}
};
return (
<Dialog
title={<Trans>What could be improved?</Trans>}
actions={[
<FlatButton
key="cancel"
label={<Trans>Cancel</Trans>}
onClick={onClose}
/>,
<DialogPrimaryButton
key="send"
primary
label={<Trans>Send feedback</Trans>}
onClick={handleSendFeedback}
disabled={!selectedReason}
/>,
]}
open={open}
onRequestClose={onClose}
maxWidth="sm"
>
<ColumnStackLayout noMargin>
<Text>
<Trans>
Help us improve by telling us what was wrong with the answer:
</Trans>
</Text>
<RadioGroup value={selectedReason || ''} onChange={handleChange}>
<FormControlLabel
value="not-in-my-language"
control={<Radio color="secondary" />}
label={<Trans>The answer is not in my language</Trans>}
/>
<FormControlLabel
value="non-existing-things"
control={<Radio color="secondary" />}
label={
<Trans>Some things in the answer don't exist in GDevelop</Trans>
}
/>
<FormControlLabel
value="not-as-good-as-it-could-be"
control={<Radio color="secondary" />}
label={<Trans>The answer is not as good as it could be</Trans>}
/>
<FormControlLabel
value="very-wrong-answer"
control={<Radio color="secondary" />}
label={<Trans>The answer is entirely wrong</Trans>}
/>
<FormControlLabel
value="out-of-scope"
control={<Radio color="secondary" />}
label={<Trans>The answer is out of scope for GDevelop</Trans>}
/>
<FormControlLabel
value="other"
control={<Radio color="secondary" />}
label={<Trans>Other reason</Trans>}
/>
</RadioGroup>
</ColumnStackLayout>
</Dialog>
);
};

View File

@@ -244,7 +244,7 @@ export const AskAi = React.memo<Props>(
);
const onSendFeedback = React.useCallback(
async (aiRequestId, messageIndex, feedback) => {
async (aiRequestId, messageIndex, feedback, reason) => {
if (!profile) return;
try {
await retryIfFailed({ times: 2 }, () =>
@@ -253,6 +253,7 @@ export const AskAi = React.memo<Props>(
aiRequestId,
messageIndex,
feedback,
reason,
})
);
} catch (error) {

View File

@@ -287,11 +287,13 @@ export const sendAiRequestFeedback = async (
aiRequestId,
messageIndex,
feedback,
reason,
}: {|
userId: string,
aiRequestId: string,
messageIndex: number,
feedback: 'like' | 'dislike',
reason?: string,
|}
): Promise<AiRequest> => {
const authorizationHeader = await getAuthorizationHeader();
@@ -302,6 +304,7 @@ export const sendAiRequestFeedback = async (
{
messageIndex,
feedback,
reason,
},
{
params: {

View File

@@ -231,36 +231,68 @@ const fakeOutputWithAiResponses = [
const fakeOutputWithMoreAiResponses = [
...fakeOutputWithUserRequestOnly,
...new Array(10).fill({
type: 'message',
status: 'completed',
role: 'assistant',
content: [
...new Array(7)
.fill([
{
type: 'output_text',
type: 'message',
status: 'completed',
text: 'Some **answer** from the AI. Lorem ipsum AI.',
annotations: [],
role: 'user',
content: [
{
type: 'user_request',
status: 'completed',
text: 'Some follow up question. Lorem ipsum user.',
},
],
},
],
}),
{
type: 'message',
status: 'completed',
role: 'assistant',
content: [
{
type: 'output_text',
status: 'completed',
text: 'Some **answer** from the AI. Lorem ipsum AI.',
annotations: [],
},
],
},
])
.flat(),
];
const fakeOutputWithEvenMoreAiResponses = [
...fakeOutputWithUserRequestOnly,
...new Array(15).fill({
type: 'message',
status: 'completed',
role: 'assistant',
content: [
...new Array(15)
.fill([
{
type: 'output_text',
type: 'message',
status: 'completed',
text: 'Some **answer** from the AI. Lorem ipsum AI.',
annotations: [],
role: 'user',
content: [
{
type: 'user_request',
status: 'completed',
text: 'Some follow up question. Lorem ipsum user.',
},
],
},
],
}),
{
type: 'message',
status: 'completed',
role: 'assistant',
content: [
{
type: 'output_text',
status: 'completed',
text: 'Some **answer** from the AI. Lorem ipsum AI.',
annotations: [],
},
],
},
])
.flat(),
];
export const ReadyAiRequest = () => (