Skip to content

Commit 5f5e77d

Browse files
committed
feat: Allow users to provide a reason when giving a thumbs-up or thumbs-down to a response
1 parent 12c66ae commit 5f5e77d

File tree

12 files changed

+307
-38
lines changed

12 files changed

+307
-38
lines changed

apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, wo
3535
node.context['message_tokens'] = message_tokens
3636
node.context['answer_tokens'] = answer_tokens
3737
node.context['answer'] = answer
38-
node.context['history_message'] = node_variable['history_message']
3938
node.context['question'] = node_variable['question']
4039
node.context['run_time'] = time.time() - node.context['start_time']
4140
node.context['reasoning_content'] = reasoning_content
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.2.8 on 2025-12-23 10:28
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('application', '0004_application_application_enable_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='chatrecord',
15+
name='vote_other_content',
16+
field=models.CharField(default='', max_length=1024, verbose_name='其他原因'),
17+
),
18+
migrations.AddField(
19+
model_name='chatrecord',
20+
name='vote_reason',
21+
field=models.CharField(blank=True, choices=[('accurate', '内容准确'), ('complete', '内容完善'), ('inaccurate', '内容不准确'), ('incomplete', '内容不完善'), ('other', '其他')], max_length=50, null=True, verbose_name='投票原因'),
22+
),
23+
]

apps/application/models/application_chat.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ class VoteChoices(models.TextChoices):
5454
STAR = "0", '赞同'
5555
TRAMPLE = "1", '反对'
5656

57+
class VoteReasonChoices(models.TextChoices):
58+
ACCURATE = 'accurate', '内容准确'
59+
COMPLETE = 'complete', '内容完善'
60+
INACCURATE = 'inaccurate', '内容不准确'
61+
INCOMPLETE = 'incomplete', '内容不完善'
62+
OTHER = 'other', '其他'
5763

5864
class ChatRecord(AppModelMixin):
5965
"""
@@ -63,6 +69,8 @@ class ChatRecord(AppModelMixin):
6369
chat = models.ForeignKey(Chat, on_delete=models.CASCADE)
6470
vote_status = models.CharField(verbose_name='投票', max_length=10, choices=VoteChoices.choices,
6571
default=VoteChoices.UN_VOTE)
72+
vote_reason =models.CharField(verbose_name='投票原因', max_length=50,choices=VoteReasonChoices.choices, null=True, blank=True)
73+
vote_other_content = models.CharField(verbose_name='其他原因', max_length=1024, default='')
6674
problem_text = models.CharField(max_length=10240, verbose_name="问题")
6775
answer_text = models.CharField(max_length=40960, verbose_name="答案")
6876
answer_text_list = ArrayField(verbose_name="改进标注列表",

apps/application/serializers/application_chat_record.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
class ChatRecordSerializerModel(serializers.ModelSerializer):
3535
class Meta:
3636
model = ChatRecord
37-
fields = ['id', 'chat_id', 'vote_status', 'problem_text', 'answer_text',
37+
fields = ['id', 'chat_id', 'vote_status','vote_reason','vote_other_content', 'problem_text', 'answer_text',
3838
'message_tokens', 'answer_tokens', 'const', 'improve_paragraph_id_list', 'run_time', 'index',
3939
'answer_text_list',
4040
'create_time', 'update_time']

apps/chat/serializers/chat_record.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from django.utils.translation import gettext_lazy as _, gettext
1414
from rest_framework import serializers
1515

16-
from application.models import VoteChoices, ChatRecord, Chat, ApplicationAccessToken
16+
from application.models import VoteChoices, ChatRecord, Chat, ApplicationAccessToken, VoteReasonChoices
1717
from application.serializers.application_chat import ChatCountSerializer
1818
from application.serializers.application_chat_record import ChatRecordSerializerModel, \
1919
ApplicationChatRecordQuerySerializers
@@ -25,7 +25,9 @@
2525
class VoteRequest(serializers.Serializer):
2626
vote_status = serializers.ChoiceField(choices=VoteChoices.choices,
2727
label=_("Bidding Status"))
28+
vote_reason = serializers.ChoiceField(choices=VoteReasonChoices.choices, label=_("Vote Reason"), required=False, allow_null=True)
2829

30+
vote_other_content = serializers.CharField(required=False, allow_blank=True,label=_("Vote other content"))
2931

3032
class HistoryChatModel(serializers.ModelSerializer):
3133
class Meta:
@@ -59,19 +61,33 @@ def vote(self, instance: Dict, with_valid=True):
5961
if chat_record_details_model is None:
6062
raise AppApiException(500, gettext("Non-existent conversation chat_record_id"))
6163
vote_status = instance.get("vote_status")
64+
65+
# 未投票状态,可以进行投票
6266
if chat_record_details_model.vote_status == VoteChoices.UN_VOTE:
67+
# 投票时获取字段
68+
vote_reason = instance.get("vote_reason")
69+
vote_other_content = instance.get("vote_other_content") or ''
70+
6371
if vote_status == VoteChoices.STAR:
6472
# 点赞
6573
chat_record_details_model.vote_status = VoteChoices.STAR
74+
chat_record_details_model.vote_reason = vote_reason
75+
chat_record_details_model.vote_other_content = vote_other_content
6676

6777
if vote_status == VoteChoices.TRAMPLE:
6878
# 点踩
6979
chat_record_details_model.vote_status = VoteChoices.TRAMPLE
80+
chat_record_details_model.vote_reason = vote_reason
81+
chat_record_details_model.vote_other_content = vote_other_content
82+
7083
chat_record_details_model.save()
84+
# 已投票状态
7185
else:
7286
if vote_status == VoteChoices.UN_VOTE:
7387
# 取消点赞
7488
chat_record_details_model.vote_status = VoteChoices.UN_VOTE
89+
chat_record_details_model.vote_reason = None
90+
chat_record_details_model.vote_other_content = ''
7591
chat_record_details_model.save()
7692
else:
7793
raise AppApiException(500, gettext("Already voted, please cancel first and then vote again"))

ui/src/api/chat/chat.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,19 @@ const vote: (
182182
chat_id: string,
183183
chat_record_id: string,
184184
vote_status: string,
185+
vote_reason?: string,
186+
vote_other_content?: string,
185187
loading?: Ref<boolean>,
186-
) => Promise<Result<boolean>> = (chat_id, chat_record_id, vote_status, loading) => {
188+
) => Promise<Result<boolean>> = (chat_id, chat_record_id, vote_status, vote_reason, vote_other_content, loading) => {
189+
190+
const data = {
191+
vote_status,
192+
...(vote_reason !== undefined && { vote_reason }),
193+
...(vote_other_content !== undefined && { vote_other_content })
194+
}
187195
return put(
188196
`/vote/chat/${chat_id}/chat_record/${chat_record_id}`,
189-
{
190-
vote_status,
191-
},
197+
data,
192198
undefined,
193199
loading,
194200
)

ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -52,44 +52,73 @@
5252
</el-button>
5353
</el-tooltip>
5454
<el-divider direction="vertical" />
55-
<el-tooltip
56-
effect="dark"
57-
:content="$t('chat.operation.like')"
58-
placement="top"
59-
v-if="buttonData?.vote_status === '-1'"
60-
>
61-
<el-button text @click="voteHandle('0')" :disabled="loading">
62-
<AppIcon iconName="app-like"></AppIcon>
63-
</el-button>
64-
</el-tooltip>
55+
<el-popover ref="likePopoverRef" trigger="click" placement="bottom-start" :width="400">
56+
<template #reference>
57+
<span>
58+
<el-tooltip
59+
effect="dark"
60+
:content="$t('chat.operation.like')"
61+
placement="top"
62+
v-if="buttonData?.vote_status === '-1'"
63+
>
64+
<el-button text :disabled="loading">
65+
<AppIcon iconName="app-like"></AppIcon>
66+
</el-button>
67+
</el-tooltip>
68+
</span>
69+
</template>
70+
<VoteReasonContent
71+
vote-type="0"
72+
:chat-id="props.chatId"
73+
:record-id="props.data.record_id"
74+
@success="handleVoteSuccess"
75+
@close="closePopover"
76+
>
77+
</VoteReasonContent>
78+
</el-popover>
79+
6580
<el-tooltip
6681
effect="dark"
6782
:content="$t('chat.operation.cancelLike')"
6883
placement="top"
6984
v-if="buttonData?.vote_status === '0'"
7085
>
71-
<el-button text @click="voteHandle('-1')" :disabled="loading">
86+
<el-button text @click="cancelVoteHandle('-1')" :disabled="loading">
7287
<AppIcon iconName="app-like-color"></AppIcon>
7388
</el-button>
7489
</el-tooltip>
7590
<el-divider direction="vertical" v-if="buttonData?.vote_status === '-1'" />
76-
<el-tooltip
77-
effect="dark"
78-
:content="$t('chat.operation.oppose')"
79-
placement="top"
80-
v-if="buttonData?.vote_status === '-1'"
81-
>
82-
<el-button text @click="voteHandle('1')" :disabled="loading">
83-
<AppIcon iconName="app-oppose"></AppIcon>
84-
</el-button>
85-
</el-tooltip>
91+
<el-popover ref="opposePopoverRef" trigger="click" placement="bottom-start" :width="400">
92+
<template #reference>
93+
<span>
94+
<el-tooltip
95+
effect="dark"
96+
:content="$t('chat.operation.oppose')"
97+
placement="top"
98+
v-if="buttonData?.vote_status === '-1'"
99+
>
100+
<el-button text :disabled="loading">
101+
<AppIcon iconName="app-oppose"></AppIcon>
102+
</el-button>
103+
</el-tooltip>
104+
</span>
105+
</template>
106+
<VoteReasonContent
107+
vote-type="1"
108+
:chat-id="props.chatId"
109+
:record-id="props.data.record_id"
110+
@success="handleVoteSuccess"
111+
@close="closePopover"
112+
>
113+
</VoteReasonContent>
114+
</el-popover>
86115
<el-tooltip
87116
effect="dark"
88117
:content="$t('chat.operation.cancelOppose')"
89118
placement="top"
90119
v-if="buttonData?.vote_status === '1'"
91120
>
92-
<el-button text @click="voteHandle('-1')" :disabled="loading">
121+
<el-button text @click="cancelVoteHandle('-1')" :disabled="loading">
93122
<AppIcon iconName="app-oppose-color"></AppIcon>
94123
</el-button>
95124
</el-tooltip>
@@ -106,6 +135,7 @@ import applicationApi from '@/api/application/application'
106135
import chatAPI from '@/api/chat/chat'
107136
import { datetimeFormat } from '@/utils/time'
108137
import { MsgError } from '@/utils/message'
138+
import VoteReasonContent from '@/components/ai-chat/component/operation-button/VoteReasonContent.vue'
109139
import bus from '@/bus'
110140
const copy = (data: any) => {
111141
try {
@@ -117,6 +147,12 @@ const copy = (data: any) => {
117147
copyClick(removeFormRander(data?.answer_text.trim()))
118148
}
119149
}
150+
const likePopoverRef = ref()
151+
const opposePopoverRef = ref()
152+
const closePopover = () => {
153+
likePopoverRef.value.hide()
154+
opposePopoverRef.value.hide()
155+
}
120156
const route = useRoute()
121157
const {
122158
params: { id },
@@ -145,15 +181,20 @@ const audioPlayer = ref<HTMLAudioElement[] | null>([])
145181
const audioCiontainer = ref<HTMLDivElement>()
146182
const buttonData = ref(props.data)
147183
const loading = ref(false)
148-
149184
const audioList = ref<string[]>([])
150185
151186
function regeneration() {
152187
emit('regeneration')
153188
}
154189
155-
function voteHandle(val: string) {
156-
chatAPI.vote(props.chatId, props.data.record_id, val, loading).then(() => {
190+
function handleVoteSuccess(voteStatus: string) {
191+
buttonData.value['vote_status'] = voteStatus
192+
emit('update:data', buttonData.value)
193+
closePopover()
194+
}
195+
196+
function cancelVoteHandle(val: string) {
197+
chatAPI.vote(props.chatId, props.data.record_id, val, undefined, '', loading).then(() => {
157198
buttonData.value['vote_status'] = val
158199
emit('update:data', buttonData.value)
159200
})

ui/src/components/ai-chat/component/operation-button/LogOperationButton.vue

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
<div>
99
<!-- 语音播放 -->
1010
<span v-if="tts">
11-
<el-tooltip effect="dark" :content="$t('chat.operation.play')" placement="top" v-if="!audioPlayerStatus">
11+
<el-tooltip
12+
effect="dark"
13+
:content="$t('chat.operation.play')"
14+
placement="top"
15+
v-if="!audioPlayerStatus"
16+
>
1217
<el-button text @click="playAnswerText(data?.answer_text)">
1318
<AppIcon iconName="app-video-play"></AppIcon>
1419
</el-button>
@@ -27,8 +32,12 @@
2732
</el-tooltip>
2833
<el-divider direction="vertical" />
2934
<template v-if="permissionPrecise.chat_log_add_knowledge(id)">
30-
<el-tooltip v-if="buttonData.improve_paragraph_id_list.length === 0" effect="dark"
31-
:content="$t('views.chatLog.editContent')" placement="top">
35+
<el-tooltip
36+
v-if="buttonData.improve_paragraph_id_list.length === 0"
37+
effect="dark"
38+
:content="$t('views.chatLog.editContent')"
39+
placement="top"
40+
>
3241
<el-button text @click="editContent(data)">
3342
<AppIcon iconName="app-edit"></AppIcon>
3443
</el-button>
@@ -52,9 +61,32 @@
5261
<EditContentDialog ref="EditContentDialogRef" @refresh="refreshContent" />
5362
<EditMarkDialog ref="EditMarkDialogRef" @refresh="refreshMark" />
5463
<!-- 先渲染,不然不能播放 -->
55-
<audio ref="audioPlayer" v-for="item in audioList" :key="item" controls hidden="hidden"></audio>
64+
<audio
65+
ref="audioPlayer"
66+
v-for="item in audioList"
67+
:key="item"
68+
controls
69+
hidden="hidden"
70+
></audio>
5671
</div>
5772
</div>
73+
<div>
74+
<el-card
75+
class="mt-16"
76+
shadow="always"
77+
v-if="buttonData?.vote_status !== '-1' && data.vote_reason"
78+
>
79+
<VoteReasonContent
80+
:vote-type="buttonData?.vote_status"
81+
:chat-id="buttonData?.chat_id"
82+
:record-id="buttonData?.id"
83+
readonly
84+
:default-reason="data.vote_reason"
85+
:default-other-content="data.vote_other_content"
86+
>
87+
</VoteReasonContent>
88+
</el-card>
89+
</div>
5890
</template>
5991
<script setup lang="ts">
6092
import { computed, onMounted, ref } from 'vue'
@@ -67,16 +99,16 @@ import { useRoute } from 'vue-router'
6799
import permissionMap from '@/permission'
68100
import { MsgError } from '@/utils/message'
69101
import { t } from '@/locales'
102+
import VoteReasonContent from '@/components/ai-chat/component/operation-button/VoteReasonContent.vue'
70103
const route = useRoute()
71104
const {
72105
params: { id },
73106
} = route as any
74107
75-
76108
const props = defineProps({
77109
data: {
78110
type: Object,
79-
default: () => { },
111+
default: () => {},
80112
},
81113
applicationId: {
82114
type: String,

0 commit comments

Comments
 (0)