mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: Allow users to provide a reason when giving a thumbs-up or thumbs-down to a response
This commit is contained in:
parent
12c66ae135
commit
cfb488e705
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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='投票原因'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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="改进标注列表",
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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<boolean>,
|
||||
) => Promise<Result<boolean>> = (chat_id, chat_record_id, vote_status, loading) => {
|
||||
) => Promise<Result<boolean>> = (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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -52,44 +52,73 @@
|
|||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" />
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('chat.operation.like')"
|
||||
placement="top"
|
||||
v-if="buttonData?.vote_status === '-1'"
|
||||
>
|
||||
<el-button text @click="voteHandle('0')" :disabled="loading">
|
||||
<AppIcon iconName="app-like"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-popover ref="likePopoverRef" trigger="click" placement="bottom-start" :width="400">
|
||||
<template #reference>
|
||||
<span>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('chat.operation.like')"
|
||||
placement="top"
|
||||
v-if="buttonData?.vote_status === '-1'"
|
||||
>
|
||||
<el-button text :disabled="loading">
|
||||
<AppIcon iconName="app-like"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<VoteReasonContent
|
||||
vote-type="0"
|
||||
:chat-id="props.chatId"
|
||||
:record-id="props.data.record_id"
|
||||
@success="handleVoteSuccess"
|
||||
@close="closePopover"
|
||||
>
|
||||
</VoteReasonContent>
|
||||
</el-popover>
|
||||
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('chat.operation.cancelLike')"
|
||||
placement="top"
|
||||
v-if="buttonData?.vote_status === '0'"
|
||||
>
|
||||
<el-button text @click="voteHandle('-1')" :disabled="loading">
|
||||
<el-button text @click="cancelVoteHandle('-1')" :disabled="loading">
|
||||
<AppIcon iconName="app-like-color"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" v-if="buttonData?.vote_status === '-1'" />
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('chat.operation.oppose')"
|
||||
placement="top"
|
||||
v-if="buttonData?.vote_status === '-1'"
|
||||
>
|
||||
<el-button text @click="voteHandle('1')" :disabled="loading">
|
||||
<AppIcon iconName="app-oppose"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-popover ref="opposePopoverRef" trigger="click" placement="bottom-start" :width="400">
|
||||
<template #reference>
|
||||
<span>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('chat.operation.oppose')"
|
||||
placement="top"
|
||||
v-if="buttonData?.vote_status === '-1'"
|
||||
>
|
||||
<el-button text :disabled="loading">
|
||||
<AppIcon iconName="app-oppose"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<VoteReasonContent
|
||||
vote-type="1"
|
||||
:chat-id="props.chatId"
|
||||
:record-id="props.data.record_id"
|
||||
@success="handleVoteSuccess"
|
||||
@close="closePopover"
|
||||
>
|
||||
</VoteReasonContent>
|
||||
</el-popover>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('chat.operation.cancelOppose')"
|
||||
placement="top"
|
||||
v-if="buttonData?.vote_status === '1'"
|
||||
>
|
||||
<el-button text @click="voteHandle('-1')" :disabled="loading">
|
||||
<el-button text @click="cancelVoteHandle('-1')" :disabled="loading">
|
||||
<AppIcon iconName="app-oppose-color"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
|
@ -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<HTMLAudioElement[] | null>([])
|
|||
const audioCiontainer = ref<HTMLDivElement>()
|
||||
const buttonData = ref(props.data)
|
||||
const loading = ref(false)
|
||||
|
||||
const audioList = ref<string[]>([])
|
||||
|
||||
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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@
|
|||
<div>
|
||||
<!-- 语音播放 -->
|
||||
<span v-if="tts">
|
||||
<el-tooltip effect="dark" :content="$t('chat.operation.play')" placement="top" v-if="!audioPlayerStatus">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('chat.operation.play')"
|
||||
placement="top"
|
||||
v-if="!audioPlayerStatus"
|
||||
>
|
||||
<el-button text @click="playAnswerText(data?.answer_text)">
|
||||
<AppIcon iconName="app-video-play"></AppIcon>
|
||||
</el-button>
|
||||
|
|
@ -27,8 +32,12 @@
|
|||
</el-tooltip>
|
||||
<el-divider direction="vertical" />
|
||||
<template v-if="permissionPrecise.chat_log_add_knowledge(id)">
|
||||
<el-tooltip v-if="buttonData.improve_paragraph_id_list.length === 0" effect="dark"
|
||||
:content="$t('views.chatLog.editContent')" placement="top">
|
||||
<el-tooltip
|
||||
v-if="buttonData.improve_paragraph_id_list.length === 0"
|
||||
effect="dark"
|
||||
:content="$t('views.chatLog.editContent')"
|
||||
placement="top"
|
||||
>
|
||||
<el-button text @click="editContent(data)">
|
||||
<AppIcon iconName="app-edit"></AppIcon>
|
||||
</el-button>
|
||||
|
|
@ -52,9 +61,32 @@
|
|||
<EditContentDialog ref="EditContentDialogRef" @refresh="refreshContent" />
|
||||
<EditMarkDialog ref="EditMarkDialogRef" @refresh="refreshMark" />
|
||||
<!-- 先渲染,不然不能播放 -->
|
||||
<audio ref="audioPlayer" v-for="item in audioList" :key="item" controls hidden="hidden"></audio>
|
||||
<audio
|
||||
ref="audioPlayer"
|
||||
v-for="item in audioList"
|
||||
:key="item"
|
||||
controls
|
||||
hidden="hidden"
|
||||
></audio>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-card
|
||||
class="mt-16"
|
||||
shadow="always"
|
||||
v-if="buttonData?.vote_status !== '-1' && data.vote_reason"
|
||||
>
|
||||
<VoteReasonContent
|
||||
:vote-type="buttonData?.vote_status"
|
||||
:chat-id="buttonData?.chat_id"
|
||||
:record-id="buttonData?.id"
|
||||
readonly
|
||||
:default-reason="data.vote_reason"
|
||||
:default-other-content="data.vote_other_content"
|
||||
>
|
||||
</VoteReasonContent>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
|
@ -67,16 +99,16 @@ import { useRoute } from 'vue-router'
|
|||
import permissionMap from '@/permission'
|
||||
import { MsgError } from '@/utils/message'
|
||||
import { t } from '@/locales'
|
||||
import VoteReasonContent from '@/components/ai-chat/component/operation-button/VoteReasonContent.vue'
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id },
|
||||
} = route as any
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => { },
|
||||
default: () => {},
|
||||
},
|
||||
applicationId: {
|
||||
type: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<div class="p-4">
|
||||
<h3>{{ title }}</h3>
|
||||
</div>
|
||||
<div class="p-4 mt-16">
|
||||
<el-button
|
||||
v-for="reason in reasons"
|
||||
:key="reason.value"
|
||||
:type="selectedReason === reason.value ? 'primary' : 'info'"
|
||||
@click="selectReason(reason.value)"
|
||||
:disabled="readonly"
|
||||
plain
|
||||
bg
|
||||
>
|
||||
{{ reason.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="selectedReason === 'other'" class="mt-16">
|
||||
<el-input
|
||||
v-model="feedBack"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 4, maxRows: 20 }"
|
||||
:placeholder="$t('chat.operation.vote.placeholder')"
|
||||
:readonly="readonly"
|
||||
>
|
||||
</el-input>
|
||||
</div>
|
||||
<div v-if="!readonly" class="dialog-footer mt-24 text-right">
|
||||
<el-button @click="emit('close')"> {{ $t('common.cancel') }}</el-button>
|
||||
<el-button :disabled="isSubmitDisabled" type="primary" @click="voteHandle()">
|
||||
{{ $t('common.confirm') }}</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { t } from '@/locales'
|
||||
import chatAPI from '@/api/chat/chat'
|
||||
|
||||
const props = defineProps<{
|
||||
voteType: '0' | '1'
|
||||
chatId: string
|
||||
recordId: string
|
||||
readonly?: boolean
|
||||
defaultReason?: string
|
||||
defaultOtherContent?: string
|
||||
}>()
|
||||
|
||||
const selectedReason = ref<string>(props.readonly ? props.defaultReason || '' : '')
|
||||
const feedBack = ref<string>(props.readonly ? props.defaultOtherContent || '' : '')
|
||||
const loading = ref(false)
|
||||
|
||||
const selectReason = (value: string) => {
|
||||
if (props.readonly) {
|
||||
return
|
||||
}
|
||||
selectedReason.value = value
|
||||
}
|
||||
|
||||
const isSubmitDisabled = computed(() => {
|
||||
if (!selectedReason.value) {
|
||||
return true
|
||||
}
|
||||
if (selectedReason.value === 'other' && !feedBack.value.trim()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const LIKE_REASONS = [
|
||||
{ label: t('chat.operation.vote.accurate'), value: 'accurate' },
|
||||
{ label: t('chat.operation.vote.complete'), value: 'complete' },
|
||||
{ label: t('chat.operation.vote.other'), value: 'other' },
|
||||
]
|
||||
|
||||
const OPPOSE_REASONS = [
|
||||
{ label: t('chat.operation.vote.inaccurate'), value: 'inaccurate' },
|
||||
{ label: t('chat.operation.vote.irrelevantAnswer'), value: 'incomplete' },
|
||||
{ label: t('chat.operation.vote.other'), value: 'other' },
|
||||
]
|
||||
|
||||
const title = computed(() => {
|
||||
return props.voteType === '0' ? t('chat.operation.likeTitle') : t('chat.operation.opposeTitle')
|
||||
})
|
||||
|
||||
const reasons = computed(() => {
|
||||
return props.voteType === '0' ? LIKE_REASONS : OPPOSE_REASONS
|
||||
})
|
||||
|
||||
function voteHandle() {
|
||||
chatAPI
|
||||
.vote(
|
||||
props.chatId,
|
||||
props.recordId,
|
||||
props.voteType,
|
||||
selectedReason.value,
|
||||
feedBack.value,
|
||||
loading,
|
||||
)
|
||||
.then(() => {
|
||||
emit('success', props.voteType)
|
||||
emit('close')
|
||||
})
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [voteStatus: string]
|
||||
close: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -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!',
|
||||
|
|
|
|||
|
|
@ -36,6 +36,16 @@ export default {
|
|||
continue: '继续',
|
||||
stopChat: '停止回答',
|
||||
startChat: '开始对话',
|
||||
likeTitle: '你觉得什么让你满意?',
|
||||
opposeTitle: '请告诉我们不满意的原因',
|
||||
vote: {
|
||||
accurate: '内容准确',
|
||||
inaccurate: '回答不准确',
|
||||
complete: '内容完善',
|
||||
irrelevantAnswer: '回答不相关',
|
||||
other: '其他',
|
||||
placeholder: '告诉我们更多关于你的相关体验',
|
||||
},
|
||||
},
|
||||
tip: {
|
||||
error500Message: '抱歉,当前正在维护,无法提供服务,请稍后再试!',
|
||||
|
|
|
|||
|
|
@ -36,6 +36,16 @@ export default {
|
|||
continue: '繼續',
|
||||
stopChat: '停止回答',
|
||||
startChat: '開始回答',
|
||||
likeTitle: '你覺得什麼讓你滿意?',
|
||||
opposeTitle: '請告訴我們不滿意的原因',
|
||||
vote: {
|
||||
accurate: '內容準確',
|
||||
inaccurate: '回答不準確',
|
||||
complete: '內容完善',
|
||||
irrelevantAnswer: '回答不相關',
|
||||
other: '其他',
|
||||
placeholder: '告訴我們更多關於你的相關體驗',
|
||||
},
|
||||
},
|
||||
tip: {
|
||||
error500Message: '抱歉,當前正在維護,無法提供服務,請稍後再試!',
|
||||
|
|
|
|||
Loading…
Reference in New Issue