Skip to content

Commit 12b8a2a

Browse files
authored
Badボタンとフィードバックの充実 (#660)
* feat: feedback form UI * feat: backend api for feedback form * fix: lint issue of type any * fix: use understandable word and use Button component
1 parent 7e16e53 commit 12b8a2a

File tree

7 files changed

+162
-28
lines changed

7 files changed

+162
-28
lines changed

packages/cdk/lambda/repository.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ShareId,
66
UserIdAndChatId,
77
SystemContext,
8+
UpdateFeedbackRequest,
89
} from 'generative-ai-use-cases-jp';
910
import * as crypto from 'crypto';
1011
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
@@ -267,21 +268,39 @@ export const setChatTitle = async (
267268

268269
export const updateFeedback = async (
269270
_chatId: string,
270-
createdDate: string,
271-
feedback: string
271+
feedbackData: UpdateFeedbackRequest
272272
): Promise<RecordedMessage> => {
273273
const chatId = `chat#${_chatId}`;
274+
const {createdDate, feedback, reasons, detailedFeedback} = feedbackData;
275+
let updateExpression = 'set feedback = :feedback';
276+
const expressionAttributeValues: {
277+
':feedback': string;
278+
':reasons'?: string[];
279+
':detailedFeedback'?: string;
280+
} = {
281+
':feedback': feedback
282+
};
283+
284+
if (reasons && reasons.length > 0) {
285+
updateExpression += ', reasons = :reasons';
286+
expressionAttributeValues[':reasons'] = reasons;
287+
}
288+
289+
if (detailedFeedback) {
290+
updateExpression += ', detailedFeedback = :detailedFeedback';
291+
expressionAttributeValues[':detailedFeedback'] = detailedFeedback;
292+
}
293+
294+
274295
const res = await dynamoDbDocument.send(
275296
new UpdateCommand({
276297
TableName: TABLE_NAME,
277298
Key: {
278299
id: chatId,
279300
createdDate,
280301
},
281-
UpdateExpression: 'set feedback = :feedback',
282-
ExpressionAttributeValues: {
283-
':feedback': feedback,
284-
},
302+
UpdateExpression: updateExpression,
303+
ExpressionAttributeValues: expressionAttributeValues,
285304
ReturnValues: 'ALL_NEW',
286305
})
287306
);

packages/cdk/lambda/updateFeedback.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const handler = async (
99
const chatId = event.pathParameters!.chatId!;
1010
const req: UpdateFeedbackRequest = JSON.parse(event.body!);
1111

12-
const message = await updateFeedback(chatId, req.createdDate, req.feedback);
12+
const message = await updateFeedback(chatId, req);
1313

1414
return {
1515
statusCode: 200,

packages/types/src/protocol.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export type UpdateSystemContextTitleResponse = {
6161
export type UpdateFeedbackRequest = {
6262
createdDate: string;
6363
feedback: string;
64+
reasons?: string[];
65+
detailedFeedback?: string;
6466
};
6567

6668
export type UpdateFeedbackResponse = {

packages/web/src/components/ButtonFeedback.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,4 @@ const ButtonFeedback: React.FC<Props> = (props) => {
5555
);
5656
};
5757

58-
export default ButtonFeedback;
58+
export default ButtonFeedback;

packages/web/src/components/ChatMessage.tsx

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import ButtonFeedback from './ButtonFeedback';
66
import ZoomUpImage from './ZoomUpImage';
77
import { PiUserFill, PiChalkboardTeacher } from 'react-icons/pi';
88
import { BaseProps } from '../@types/common';
9-
import { ShownMessage } from 'generative-ai-use-cases-jp';
9+
import { ShownMessage, UpdateFeedbackRequest } from 'generative-ai-use-cases-jp';
1010
import BedrockIcon from '../assets/bedrock.svg?react';
1111
import useChat from '../hooks/useChat';
1212
import useTyping from '../hooks/useTyping';
1313
import useFileApi from '../hooks/useFileApi';
1414
import FileCard from './FileCard';
15+
import FeedbackForm from './FeedbackForm';
1516

1617
type Props = BaseProps & {
1718
idx?: number;
@@ -28,6 +29,8 @@ const ChatMessage: React.FC<Props> = (props) => {
2829
const { pathname } = useLocation();
2930
const { sendFeedback } = useChat(pathname);
3031
const [isSendingFeedback, setIsSendingFeedback] = useState(false);
32+
const [showFeedbackForm, setShowFeedbackForm] = useState(false);
33+
const [showThankYouMessage, setShowThankYouMessage] = useState(false);
3134
const { getFileDownloadSignedUrl } = useFileApi();
3235

3336
const { setTypingTextInput, typingTextOutput } = useTyping(
@@ -61,18 +64,54 @@ const ChatMessage: React.FC<Props> = (props) => {
6164
return isSendingFeedback || !props.chatContent?.id;
6265
}, [isSendingFeedback, props]);
6366

64-
const onSendFeedback = async (feedback: string) => {
67+
const onSendFeedback = async (feedbackData: UpdateFeedbackRequest) => {
6568
if (!disabled) {
6669
setIsSendingFeedback(true);
67-
if (feedback !== chatContent?.feedback) {
68-
await sendFeedback(props.chatContent!.createdDate!, feedback);
70+
if (feedbackData.feedback !== chatContent?.feedback) {
71+
if (feedbackData.feedback !== 'bad') {
72+
setShowFeedbackForm(false);
73+
}
74+
await sendFeedback(feedbackData);
6975
} else {
70-
await sendFeedback(props.chatContent!.createdDate!, 'none');
76+
await sendFeedback({
77+
createdDate: props.chatContent!.createdDate!,
78+
feedback: 'none'
79+
});
80+
setShowFeedbackForm(false);
7181
}
7282
setIsSendingFeedback(false);
7383
}
7484
};
7585

86+
const handleFeedbackClick = (feedback: string) => {
87+
// ボタン押した際、ユーザーからの詳細フィードバック前にDBに送る。
88+
onSendFeedback({
89+
createdDate: props.chatContent!.createdDate!,
90+
feedback: feedback
91+
});
92+
if (feedback === 'bad' && chatContent?.feedback !== 'bad') {
93+
setShowFeedbackForm(true);
94+
}
95+
};
96+
97+
const handleFeedbackFormSubmit = async (reasons: string[], detailedFeedback: string) => {
98+
await sendFeedback({
99+
createdDate: props.chatContent!.createdDate!,
100+
feedback: 'bad',
101+
reasons: reasons,
102+
detailedFeedback: detailedFeedback
103+
});
104+
setShowFeedbackForm(false);
105+
setShowThankYouMessage(true);
106+
setTimeout(() => {
107+
setShowThankYouMessage(false);
108+
}, 3000);
109+
};
110+
111+
const handleFeedbackFormCancel = () => {
112+
setShowFeedbackForm(false);
113+
};
114+
76115
return (
77116
<div
78117
className={`flex justify-center ${
@@ -83,7 +122,7 @@ const ChatMessage: React.FC<Props> = (props) => {
83122
<div
84123
className={`${
85124
props.className ?? ''
86-
} flex w-full flex-col justify-between p-3 md:w-11/12 lg:w-5/6 xl:w-4/6 2xl:flex-row`}>
125+
} flex w-full flex-col justify-between p-3 md:w-11/12 lg:w-5/6 xl:w-4/6`}>
87126
<div className="flex w-full">
88127
{chatContent?.role === 'user' && (
89128
<div className="bg-aws-sky h-min rounded p-2 text-xl text-white">
@@ -199,21 +238,34 @@ const ChatMessage: React.FC<Props> = (props) => {
199238
message={chatContent}
200239
disabled={disabled}
201240
onClick={() => {
202-
onSendFeedback('good');
241+
handleFeedbackClick('good');
203242
}}
204243
/>
205244
<ButtonFeedback
206245
className="ml-0.5"
207246
feedback="bad"
208247
message={chatContent}
209248
disabled={disabled}
210-
onClick={() => onSendFeedback('bad')}
249+
onClick={() => handleFeedbackClick('bad')}
211250
/>
212251
</>
213252
)}
214253
</>
215254
)}
216255
</div>
256+
<div>
257+
{showFeedbackForm && (
258+
<FeedbackForm
259+
onSubmit={handleFeedbackFormSubmit}
260+
onCancel={handleFeedbackFormCancel}
261+
/>
262+
)}
263+
{showThankYouMessage && (
264+
<div className="mt-2 p-2 bg-green-100 text-center text-green-700 rounded-md">
265+
フィードバックを受け付けました。ありがとうございます。
266+
</div>
267+
)}
268+
</div>
217269
</div>
218270
</div>
219271
);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, { useState } from 'react';
2+
import Button from './Button';
3+
4+
type Props = {
5+
onSubmit: (reasons: string[], feedback: string) => void;
6+
onCancel: () => void;
7+
};
8+
9+
const FeedbackForm: React.FC<Props> = ({ onSubmit, onCancel }) => {
10+
const [selectedReasons, setSelectedReasons] = useState<string[]>([]);
11+
const [feedback, setFeedback] = useState<string>('');
12+
const [error, setError] = useState<string>('');
13+
14+
const reasons = ['不正確', '情報が古い', '有害または攻撃的', 'その他'];
15+
16+
const handleReasonChange = (reason: string) => {
17+
setSelectedReasons(prev =>
18+
prev.includes(reason)
19+
? prev.filter(r => r !== reason)
20+
: [...prev, reason]
21+
);
22+
setError('');
23+
};
24+
25+
const handleSubmit = () => {
26+
if (selectedReasons.length === 0) {
27+
setError('理由を選択してください。');
28+
return;
29+
}
30+
onSubmit(selectedReasons, feedback);
31+
};
32+
33+
return (
34+
<div className="mt-2 bg-white p-4 border rounded-lg shadow-sm">
35+
<h3 className="text-base font-medium mb-3">この回答を評価した理由をお聞かせください。</h3>
36+
<div className="mb-3 flex flex-wrap gap-2">
37+
{reasons.map((reason) => (
38+
<button
39+
key={reason}
40+
onClick={() => handleReasonChange(reason)}
41+
className={`px-3 py-1 text-sm rounded-full border ${
42+
selectedReasons.includes(reason)
43+
? 'bg-blue-100 border-blue-500 text-blue-700'
44+
: 'bg-white border-gray-300 text-gray-700'
45+
}`}
46+
>
47+
{reason}
48+
</button>
49+
))}
50+
</div>
51+
{error && <p className="text-red-500 text-sm mb-2">{error}</p>}
52+
<textarea
53+
className="w-full p-2 text-sm border rounded-md mb-1"
54+
placeholder="他にフィードバックがありましたら、入力してください。(optional)"
55+
value={feedback}
56+
onChange={(e) => setFeedback(e.target.value)}
57+
rows={3}
58+
/>
59+
<div className="flex justify-end gap-2">
60+
<Button onClick={onCancel} outlined={true}>キャンセル</Button>
61+
<Button onClick={handleSubmit} outlined={false}>送信</Button>
62+
</div>
63+
</div>
64+
);
65+
};
66+
67+
export default FeedbackForm;

packages/web/src/hooks/useChat.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
UploadedFileType,
1212
ExtraData,
1313
Model,
14+
UpdateFeedbackRequest,
1415
} from 'generative-ai-use-cases-jp';
1516
import { useEffect, useMemo } from 'react';
1617
import { v4 as uuid } from 'uuid';
@@ -69,11 +70,7 @@ const useChatState = create<{
6970
sessionId: string | undefined,
7071
extraData: UploadedFileType[] | undefined
7172
) => void;
72-
sendFeedback: (
73-
id: string,
74-
createdDate: string,
75-
feedback: string
76-
) => Promise<void>;
73+
sendFeedback: (id: string, feedbackData: UpdateFeedbackRequest) => Promise<void>;
7774
getStopReason: (id: string) => string;
7875
}>((set, get) => {
7976
const {
@@ -633,14 +630,11 @@ const useChatState = create<{
633630
},
634631

635632
continueGeneration: generateMessage,
636-
sendFeedback: async (id: string, createdDate: string, feedback: string) => {
633+
sendFeedback: async (id: string, feedbackData: UpdateFeedbackRequest) => {
637634
const chat = get().chats[id].chat;
638635

639636
if (chat) {
640-
const { message } = await updateFeedback(chat.chatId, {
641-
createdDate,
642-
feedback,
643-
});
637+
const { message } = await updateFeedback(chat.chatId, feedbackData);
644638
replaceMessages(id, [message]);
645639
}
646640
},
@@ -783,8 +777,8 @@ const useChat = (id: string, chatId?: string) => {
783777
extraData
784778
);
785779
},
786-
sendFeedback: async (createdDate: string, feedback: string) => {
787-
await sendFeedback(id, createdDate, feedback);
780+
sendFeedback: async (feedbackData: UpdateFeedbackRequest) => {
781+
await sendFeedback(id, feedbackData);
788782
},
789783
getStopReason: () => {
790784
return getStopReason(id);

0 commit comments

Comments
 (0)