From cfb488e705bd6b1ecbd505708474c6a08f5aebb8 Mon Sep 17 00:00:00 2001 From: zhangzhanwei Date: Wed, 24 Dec 2025 17:57:39 +0800 Subject: [PATCH] feat: Allow users to provide a reason when giving a thumbs-up or thumbs-down to a response --- .../ai_chat_step_node/impl/base_chat_node.py | 1 - ...te_other_content_chatrecord_vote_reason.py | 23 ++++ apps/application/models/application_chat.py | 8 ++ .../serializers/application_chat_record.py | 2 +- apps/chat/serializers/chat_record.py | 18 ++- ui/src/api/chat/chat.ts | 14 ++- .../operation-button/ChatOperationButton.vue | 91 ++++++++++---- .../operation-button/LogOperationButton.vue | 44 ++++++- .../operation-button/VoteReasonContent.vue | 113 ++++++++++++++++++ ui/src/locales/lang/en-US/ai-chat.ts | 11 ++ ui/src/locales/lang/zh-CN/ai-chat.ts | 10 ++ ui/src/locales/lang/zh-Hant/ai-chat.ts | 10 ++ 12 files changed, 307 insertions(+), 38 deletions(-) create mode 100644 apps/application/migrations/0005_chatrecord_vote_other_content_chatrecord_vote_reason.py create mode 100644 ui/src/components/ai-chat/component/operation-button/VoteReasonContent.vue diff --git a/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py b/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py index cc646c642..ed7336625 100644 --- a/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py +++ b/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py @@ -35,7 +35,6 @@ def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, wo node.context['message_tokens'] = message_tokens node.context['answer_tokens'] = answer_tokens node.context['answer'] = answer - node.context['history_message'] = node_variable['history_message'] node.context['question'] = node_variable['question'] node.context['run_time'] = time.time() - node.context['start_time'] node.context['reasoning_content'] = reasoning_content diff --git a/apps/application/migrations/0005_chatrecord_vote_other_content_chatrecord_vote_reason.py b/apps/application/migrations/0005_chatrecord_vote_other_content_chatrecord_vote_reason.py new file mode 100644 index 000000000..9f8f0de72 --- /dev/null +++ b/apps/application/migrations/0005_chatrecord_vote_other_content_chatrecord_vote_reason.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.8 on 2025-12-23 10:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('application', '0004_application_application_enable_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='chatrecord', + name='vote_other_content', + field=models.CharField(default='', max_length=1024, verbose_name='其他原因'), + ), + migrations.AddField( + model_name='chatrecord', + name='vote_reason', + field=models.CharField(blank=True, choices=[('accurate', '内容准确'), ('complete', '内容完善'), ('inaccurate', '内容不准确'), ('incomplete', '内容不完善'), ('other', '其他')], max_length=50, null=True, verbose_name='投票原因'), + ), + ] diff --git a/apps/application/models/application_chat.py b/apps/application/models/application_chat.py index d17515157..66d3b66dd 100644 --- a/apps/application/models/application_chat.py +++ b/apps/application/models/application_chat.py @@ -54,6 +54,12 @@ class VoteChoices(models.TextChoices): STAR = "0", '赞同' TRAMPLE = "1", '反对' +class VoteReasonChoices(models.TextChoices): + ACCURATE = 'accurate', '内容准确' + COMPLETE = 'complete', '内容完善' + INACCURATE = 'inaccurate', '内容不准确' + INCOMPLETE = 'incomplete', '内容不完善' + OTHER = 'other', '其他' class ChatRecord(AppModelMixin): """ @@ -63,6 +69,8 @@ class ChatRecord(AppModelMixin): chat = models.ForeignKey(Chat, on_delete=models.CASCADE) vote_status = models.CharField(verbose_name='投票', max_length=10, choices=VoteChoices.choices, default=VoteChoices.UN_VOTE) + vote_reason =models.CharField(verbose_name='投票原因', max_length=50,choices=VoteReasonChoices.choices, null=True, blank=True) + vote_other_content = models.CharField(verbose_name='其他原因', max_length=1024, default='') problem_text = models.CharField(max_length=10240, verbose_name="问题") answer_text = models.CharField(max_length=40960, verbose_name="答案") answer_text_list = ArrayField(verbose_name="改进标注列表", diff --git a/apps/application/serializers/application_chat_record.py b/apps/application/serializers/application_chat_record.py index 04fb4a609..540d9310d 100644 --- a/apps/application/serializers/application_chat_record.py +++ b/apps/application/serializers/application_chat_record.py @@ -34,7 +34,7 @@ from knowledge.task.embedding import embedding_by_paragraph, embedding_by_paragr class ChatRecordSerializerModel(serializers.ModelSerializer): class Meta: model = ChatRecord - fields = ['id', 'chat_id', 'vote_status', 'problem_text', 'answer_text', + fields = ['id', 'chat_id', 'vote_status','vote_reason','vote_other_content', 'problem_text', 'answer_text', 'message_tokens', 'answer_tokens', 'const', 'improve_paragraph_id_list', 'run_time', 'index', 'answer_text_list', 'create_time', 'update_time'] diff --git a/apps/chat/serializers/chat_record.py b/apps/chat/serializers/chat_record.py index 838f55d62..91fba2913 100644 --- a/apps/chat/serializers/chat_record.py +++ b/apps/chat/serializers/chat_record.py @@ -13,7 +13,7 @@ from django.db.models import QuerySet from django.utils.translation import gettext_lazy as _, gettext from rest_framework import serializers -from application.models import VoteChoices, ChatRecord, Chat, ApplicationAccessToken +from application.models import VoteChoices, ChatRecord, Chat, ApplicationAccessToken, VoteReasonChoices from application.serializers.application_chat import ChatCountSerializer from application.serializers.application_chat_record import ChatRecordSerializerModel, \ ApplicationChatRecordQuerySerializers @@ -25,7 +25,9 @@ from common.utils.lock import RedisLock class VoteRequest(serializers.Serializer): vote_status = serializers.ChoiceField(choices=VoteChoices.choices, label=_("Bidding Status")) + vote_reason = serializers.ChoiceField(choices=VoteReasonChoices.choices, label=_("Vote Reason"), required=False, allow_null=True) + vote_other_content = serializers.CharField(required=False, allow_blank=True,label=_("Vote other content")) class HistoryChatModel(serializers.ModelSerializer): class Meta: @@ -59,19 +61,33 @@ class VoteSerializer(serializers.Serializer): if chat_record_details_model is None: raise AppApiException(500, gettext("Non-existent conversation chat_record_id")) vote_status = instance.get("vote_status") + + # 未投票状态,可以进行投票 if chat_record_details_model.vote_status == VoteChoices.UN_VOTE: + # 投票时获取字段 + vote_reason = instance.get("vote_reason") + vote_other_content = instance.get("vote_other_content") or '' + if vote_status == VoteChoices.STAR: # 点赞 chat_record_details_model.vote_status = VoteChoices.STAR + chat_record_details_model.vote_reason = vote_reason + chat_record_details_model.vote_other_content = vote_other_content if vote_status == VoteChoices.TRAMPLE: # 点踩 chat_record_details_model.vote_status = VoteChoices.TRAMPLE + chat_record_details_model.vote_reason = vote_reason + chat_record_details_model.vote_other_content = vote_other_content + chat_record_details_model.save() + # 已投票状态 else: if vote_status == VoteChoices.UN_VOTE: # 取消点赞 chat_record_details_model.vote_status = VoteChoices.UN_VOTE + chat_record_details_model.vote_reason = None + chat_record_details_model.vote_other_content = '' chat_record_details_model.save() else: raise AppApiException(500, gettext("Already voted, please cancel first and then vote again")) diff --git a/ui/src/api/chat/chat.ts b/ui/src/api/chat/chat.ts index 5333dd01a..4eb745230 100644 --- a/ui/src/api/chat/chat.ts +++ b/ui/src/api/chat/chat.ts @@ -182,13 +182,19 @@ const vote: ( chat_id: string, chat_record_id: string, vote_status: string, + vote_reason?: string, + vote_other_content?: string, loading?: Ref, -) => Promise> = (chat_id, chat_record_id, vote_status, loading) => { +) => Promise> = (chat_id, chat_record_id, vote_status, vote_reason, vote_other_content, loading) => { + + const data = { + vote_status, + ...(vote_reason !== undefined && { vote_reason }), + ...(vote_other_content !== undefined && { vote_other_content }) +} return put( `/vote/chat/${chat_id}/chat_record/${chat_record_id}`, - { - vote_status, - }, + data, undefined, loading, ) diff --git a/ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue b/ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue index c666cfdbd..173c4bc78 100644 --- a/ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue +++ b/ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue @@ -52,44 +52,73 @@ - - - - - + + + + + + - + - - - - - + + + + + - + @@ -106,6 +135,7 @@ import applicationApi from '@/api/application/application' import chatAPI from '@/api/chat/chat' import { datetimeFormat } from '@/utils/time' import { MsgError } from '@/utils/message' +import VoteReasonContent from '@/components/ai-chat/component/operation-button/VoteReasonContent.vue' import bus from '@/bus' const copy = (data: any) => { try { @@ -117,6 +147,12 @@ const copy = (data: any) => { copyClick(removeFormRander(data?.answer_text.trim())) } } +const likePopoverRef = ref() +const opposePopoverRef = ref() +const closePopover = () => { + likePopoverRef.value.hide() + opposePopoverRef.value.hide() +} const route = useRoute() const { params: { id }, @@ -145,15 +181,20 @@ const audioPlayer = ref([]) const audioCiontainer = ref() const buttonData = ref(props.data) const loading = ref(false) - const audioList = ref([]) function regeneration() { emit('regeneration') } -function voteHandle(val: string) { - chatAPI.vote(props.chatId, props.data.record_id, val, loading).then(() => { +function handleVoteSuccess(voteStatus: string) { + buttonData.value['vote_status'] = voteStatus + emit('update:data', buttonData.value) + closePopover() +} + +function cancelVoteHandle(val: string) { + chatAPI.vote(props.chatId, props.data.record_id, val, undefined, '', loading).then(() => { buttonData.value['vote_status'] = val emit('update:data', buttonData.value) }) diff --git a/ui/src/components/ai-chat/component/operation-button/LogOperationButton.vue b/ui/src/components/ai-chat/component/operation-button/LogOperationButton.vue index 399655d6a..03ffe32e8 100644 --- a/ui/src/components/ai-chat/component/operation-button/LogOperationButton.vue +++ b/ui/src/components/ai-chat/component/operation-button/LogOperationButton.vue @@ -8,7 +8,12 @@
- + @@ -27,8 +32,12 @@ + + diff --git a/ui/src/locales/lang/en-US/ai-chat.ts b/ui/src/locales/lang/en-US/ai-chat.ts index 045d0acfa..6dc62abff 100644 --- a/ui/src/locales/lang/en-US/ai-chat.ts +++ b/ui/src/locales/lang/en-US/ai-chat.ts @@ -36,6 +36,17 @@ export default { continue: 'Continue', stopChat: 'Stop Response', startChat: 'Start Response', + likeTitle: 'What do you think makes you satisfied?', + opposeTitle: 'Please tell us the reason for your dissatisfaction.', + vote: { + accurate: 'Content is accurate', + inaccurate: 'Answer is inaccurate', + complete: 'Content is complete', + irrelevantAnswer: 'Answer is irrelevant', + other: 'Other', + placeholder: 'Tell us more about your relevant experiences', +}, + }, tip: { error500Message: 'Sorry, the service is currently under maintenance. Please try again later!', diff --git a/ui/src/locales/lang/zh-CN/ai-chat.ts b/ui/src/locales/lang/zh-CN/ai-chat.ts index f95ca8c87..23c17b095 100644 --- a/ui/src/locales/lang/zh-CN/ai-chat.ts +++ b/ui/src/locales/lang/zh-CN/ai-chat.ts @@ -36,6 +36,16 @@ export default { continue: '继续', stopChat: '停止回答', startChat: '开始对话', + likeTitle: '你觉得什么让你满意?', + opposeTitle: '请告诉我们不满意的原因', + vote: { + accurate: '内容准确', + inaccurate: '回答不准确', + complete: '内容完善', + irrelevantAnswer: '回答不相关', + other: '其他', + placeholder: '告诉我们更多关于你的相关体验', + }, }, tip: { error500Message: '抱歉,当前正在维护,无法提供服务,请稍后再试!', diff --git a/ui/src/locales/lang/zh-Hant/ai-chat.ts b/ui/src/locales/lang/zh-Hant/ai-chat.ts index 506f5d487..090930d48 100644 --- a/ui/src/locales/lang/zh-Hant/ai-chat.ts +++ b/ui/src/locales/lang/zh-Hant/ai-chat.ts @@ -36,6 +36,16 @@ export default { continue: '繼續', stopChat: '停止回答', startChat: '開始回答', + likeTitle: '你覺得什麼讓你滿意?', + opposeTitle: '請告訴我們不滿意的原因', + vote: { + accurate: '內容準確', + inaccurate: '回答不準確', + complete: '內容完善', + irrelevantAnswer: '回答不相關', + other: '其他', + placeholder: '告訴我們更多關於你的相關體驗', + }, }, tip: { error500Message: '抱歉,當前正在維護,無法提供服務,請稍後再試!',