feat(chat): 添加对话反馈功能和置信度显示
- 新增对话记录反馈提交功能,支持用户提交反馈内容 - 添加多语言支持包括英文、简体中文、繁体中文的反馈相关文案 - 在AI聊天界面添加反馈按钮和对话框组件 - 实现反馈类型选择和内容输入验证功能 - 添加置信度分数显示功能,在答案内容区域展示综合评分 - 更新应用发布流程,支持内部发布和公开发布的模式选择 - 优化系统资源管理中的应用访问控制逻辑 - 扩展应用聊天记录数据结构,增加反馈内容字段 - 实现反馈列表和详情展示功能,支持历史对话中的反馈查看v3.2
parent
193361fd64
commit
c4361b158a
|
|
@ -42,6 +42,10 @@ class ApplicationChatResponseSerializers(serializers.Serializer):
|
|||
star_num = serializers.IntegerField(required=True, label=_("Number of Likes"))
|
||||
trample_num = serializers.IntegerField(required=True, label=_("Number of thumbs-downs"))
|
||||
mark_sum = serializers.IntegerField(required=True, label=_("Number of tags"))
|
||||
feedback_count = serializers.IntegerField(required=False, label=_("Feedback count"))
|
||||
feedback_content = serializers.CharField(required=False, allow_null=True, allow_blank=True,
|
||||
label=_("Feedback content"))
|
||||
feedback_list = serializers.ListField(required=False, label=_("Feedback list"))
|
||||
|
||||
|
||||
class ApplicationChatRecordExportRequest(serializers.Serializer):
|
||||
|
|
@ -136,11 +140,45 @@ class ApplicationChatQuerySerializers(serializers.Serializer):
|
|||
def list(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
return native_search(self.get_query_set(), select_string=get_file_content(
|
||||
result = native_search(self.get_query_set(), select_string=get_file_content(
|
||||
os.path.join(PROJECT_DIR, "apps", "application", 'sql',
|
||||
('list_application_chat_ee.sql' if ['PE', 'EE'].__contains__(
|
||||
edition) else 'list_application_chat.sql'))),
|
||||
with_table_name=False)
|
||||
return self.append_feedback_content(result)
|
||||
|
||||
@staticmethod
|
||||
def get_feedback_items(details):
|
||||
if not isinstance(details, dict):
|
||||
return []
|
||||
feedback = details.get('feedback')
|
||||
if isinstance(feedback, list):
|
||||
return feedback
|
||||
if isinstance(feedback, dict):
|
||||
items = feedback.get('items')
|
||||
return items if isinstance(items, list) else []
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def append_feedback_content(cls, records):
|
||||
if not records:
|
||||
return records
|
||||
chat_id_list = [row.get('id') for row in records if row.get('id') is not None]
|
||||
feedback_map = {}
|
||||
chat_records = QuerySet(ChatRecord).filter(chat_id__in=chat_id_list).order_by('create_time')
|
||||
for chat_record in chat_records:
|
||||
feedback_items = cls.get_feedback_items(chat_record.details)
|
||||
if not feedback_items:
|
||||
continue
|
||||
feedback_map[str(chat_record.chat_id)] = feedback_items
|
||||
|
||||
for row in records:
|
||||
feedback_items = feedback_map.get(str(row.get('id')), [])
|
||||
latest_feedback = feedback_items[-1] if feedback_items else {}
|
||||
row['feedback_count'] = len(feedback_items)
|
||||
row['feedback_content'] = latest_feedback.get('content') if isinstance(latest_feedback, dict) else ''
|
||||
row['feedback_list'] = feedback_items
|
||||
return records
|
||||
|
||||
@staticmethod
|
||||
def paragraph_list_to_string(paragraph_list):
|
||||
|
|
@ -239,11 +277,13 @@ class ApplicationChatQuerySerializers(serializers.Serializer):
|
|||
def page(self, current_page: int, page_size: int, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
return native_page_search(current_page, page_size, self.get_query_set(), select_string=get_file_content(
|
||||
page = native_page_search(current_page, page_size, self.get_query_set(), select_string=get_file_content(
|
||||
os.path.join(PROJECT_DIR, "apps", "application", 'sql',
|
||||
('list_application_chat_ee.sql' if ['PE', 'EE'].__contains__(
|
||||
edition) else 'list_application_chat.sql'))),
|
||||
with_table_name=False)
|
||||
page['records'] = self.append_feedback_content(page.get('records'))
|
||||
return page
|
||||
|
||||
|
||||
class ChatCountSerializer(serializers.Serializer):
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from functools import reduce
|
|||
from typing import Dict
|
||||
|
||||
import uuid_utils.compat as uuid
|
||||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
from django.db.models import QuerySet
|
||||
from django.db.models.aggregates import Max, Min
|
||||
|
|
@ -45,6 +46,8 @@ class ChatRecordOperateSerializer(serializers.Serializer):
|
|||
workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID"))
|
||||
application_id = serializers.UUIDField(required=True, label=_("Application ID"))
|
||||
chat_record_id = serializers.UUIDField(required=True, label=_("Conversation record id"))
|
||||
feedback_type = serializers.CharField(required=False, allow_blank=True, label=_("Feedback type"))
|
||||
content = serializers.CharField(required=False, allow_blank=True, label=_("Feedback content"))
|
||||
|
||||
def is_valid(self, *, debug=False, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
|
|
@ -85,6 +88,49 @@ class ChatRecordOperateSerializer(serializers.Serializer):
|
|||
return ApplicationChatRecordQuerySerializers.reset_chat_record(
|
||||
chat_record, True if debug else show_source, True if debug else show_exec)
|
||||
|
||||
def feedback(self, request=None, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
chat_record = self.get_chat_record()
|
||||
if chat_record is None:
|
||||
raise AppApiException(500, gettext('Conversation record does not exist'))
|
||||
|
||||
feedback_type = self.data.get('feedback_type')
|
||||
content = self.data.get('content')
|
||||
|
||||
if not feedback_type:
|
||||
raise AppApiException(500, gettext('Feedback type is required'))
|
||||
if not content:
|
||||
raise AppApiException(500, gettext('Feedback content is required'))
|
||||
|
||||
feedback_items = []
|
||||
feedback_details = chat_record.details.get('feedback')
|
||||
if isinstance(feedback_details, list):
|
||||
feedback_items = feedback_details
|
||||
elif isinstance(feedback_details, dict):
|
||||
feedback_items = feedback_details.get('items') or []
|
||||
|
||||
feedback_data = {
|
||||
'feedback_type': feedback_type,
|
||||
'content': content,
|
||||
'create_time': timezone.now().isoformat()
|
||||
}
|
||||
chat_record.details['feedback'] = {
|
||||
'type': 'feedback-node',
|
||||
'items': [*feedback_items, feedback_data]
|
||||
}
|
||||
chat_record.save()
|
||||
|
||||
application_access_token = QuerySet(ApplicationAccessToken).filter(
|
||||
application_id=self.data.get('application_id')).first()
|
||||
show_source = False
|
||||
show_exec = False
|
||||
if application_access_token is not None:
|
||||
show_exec = application_access_token.show_exec
|
||||
show_source = application_access_token.show_source
|
||||
return ApplicationChatRecordQuerySerializers.reset_chat_record(
|
||||
chat_record, show_source, show_exec)
|
||||
|
||||
|
||||
class ApplicationChatRecordQuerySerializers(serializers.Serializer):
|
||||
workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID"))
|
||||
|
|
@ -114,12 +160,14 @@ class ApplicationChatRecordQuerySerializers(serializers.Serializer):
|
|||
def reset_chat_record(chat_record, show_source, show_exec):
|
||||
knowledge_list = []
|
||||
paragraph_list = []
|
||||
if 'search_step' in chat_record.details and chat_record.details.get('search_step').get(
|
||||
if 'search_step' in chat_record.details and isinstance(chat_record.details.get('search_step'), dict) and chat_record.details.get('search_step').get(
|
||||
'paragraph_list') is not None:
|
||||
paragraph_list = chat_record.details.get('search_step').get(
|
||||
'paragraph_list')
|
||||
|
||||
for item in chat_record.details.values():
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if item.get('type') == 'search-knowledge-node' and item.get('show_knowledge', False):
|
||||
paragraph_list = paragraph_list + (item.get(
|
||||
'paragraph_list') or [])
|
||||
|
|
@ -151,6 +199,7 @@ class ApplicationChatRecordQuerySerializers(serializers.Serializer):
|
|||
show_source_dict = {'knowledge_list': knowledge_list,
|
||||
'paragraph_list': paragraph_list, }
|
||||
show_exec_dict = {'execution_details': [chat_record.details[key] for key in chat_record.details if
|
||||
isinstance(chat_record.details[key], dict) and chat_record.details[key].get('type') != 'feedback-node' and
|
||||
(True if show_exec else chat_record.details[key].get(
|
||||
'type') == 'start-node')]}
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ urlpatterns = [
|
|||
path('workspace/<str:workspace_id>/application/<str:application_id>/chat/<int:current_page>/<int:page_size>', views.ApplicationChat.Page.as_view()),
|
||||
path('workspace/<str:workspace_id>/application/<str:application_id>/chat/<str:chat_id>/chat_record', views.ApplicationChatRecord.as_view()),
|
||||
path('workspace/<str:workspace_id>/application/<str:application_id>/chat/<str:chat_id>/chat_record/<str:chat_record_id>', views.ApplicationChatRecordOperateAPI.as_view()),
|
||||
path('workspace/<str:workspace_id>/application/<str:application_id>/chat/<str:chat_id>/chat_record/<str:chat_record_id>/feedback', views.ApplicationChatRecordOperateAPI.as_view()),
|
||||
path('workspace/<str:workspace_id>/application/<str:application_id>/chat/<str:chat_id>/chat_record/<int:current_page>/<int:page_size>', views.ApplicationChatRecord.Page.as_view()),
|
||||
path('workspace/<str:workspace_id>/application/<str:application_id>/chat/<str:chat_id>/chat_record/<str:chat_record_id>/improve', views.ApplicationChatRecordImprove.as_view()),
|
||||
path('workspace/<str:workspace_id>/application/<str:application_id>/chat/<str:chat_id>/chat_record/<str:chat_record_id>/knowledge/<str:knowledge_id>/document/<str:document_id>/improve', views.ApplicationChatRecordImproveParagraph.as_view()),
|
||||
|
|
|
|||
|
|
@ -107,6 +107,30 @@ class ApplicationChatRecordOperateAPI(APIView):
|
|||
'chat_id': chat_id,
|
||||
'chat_record_id': chat_record_id}).one(True))
|
||||
|
||||
@extend_schema(
|
||||
methods=['POST'],
|
||||
description=_('Submit conversation feedback'),
|
||||
summary=_('Submit conversation feedback'),
|
||||
operation_id=_('Submit conversation feedback'), # type: ignore
|
||||
tags=[_('Application/Conversation Log')] # type: ignore
|
||||
)
|
||||
@has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_application_permission(),
|
||||
PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_permission_workspace_manage_role(),
|
||||
PermissionConstants.APPLICATION_READ.get_workspace_application_permission(),
|
||||
PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
|
||||
ViewPermission([RoleConstants.USER.get_workspace_role()],
|
||||
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
|
||||
CompareConstants.AND),
|
||||
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
|
||||
def post(self, request: Request, workspace_id: str, application_id: str, chat_id: str, chat_record_id: str):
|
||||
return result.success(ChatRecordOperateSerializer(
|
||||
data={
|
||||
'workspace_id': workspace_id,
|
||||
'application_id': application_id,
|
||||
'chat_id': chat_id,
|
||||
'chat_record_id': chat_record_id,
|
||||
**request.data}).feedback(request=request))
|
||||
|
||||
|
||||
class ApplicationChatRecordAddKnowledge(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ urlpatterns = [
|
|||
name='application/chat_completions'),
|
||||
path('vote/chat/<str:chat_id>/chat_record/<str:chat_record_id>', views.VoteView.as_view(), name='vote'),
|
||||
path('historical_conversation', views.HistoricalConversationView.as_view(), name='historical_conversation'),
|
||||
path('historical_conversation/<str:chat_id>/record/<str:chat_record_id>/feedback', views.ChatRecordView.as_view(), name='conversation_feedback'),
|
||||
path('historical_conversation/<str:chat_id>/record/<str:chat_record_id>',views.ChatRecordView.as_view(),name='conversation_details'),
|
||||
path('historical_conversation/<int:current_page>/<int:page_size>', views.HistoricalConversationView.PageView.as_view(), name='historical_conversation'),
|
||||
path('historical_conversation/clear',views.HistoricalConversationView.BatchDelete.as_view(), name='historical_conversation_clear'),
|
||||
|
|
|
|||
|
|
@ -198,3 +198,22 @@ class ChatRecordView(APIView):
|
|||
'application_id': request.auth.application_id,
|
||||
'chat_user_id': request.auth.chat_user_id,
|
||||
}).one(False))
|
||||
|
||||
@extend_schema(
|
||||
methods=['POST'],
|
||||
description=_("Submit conversation feedback"),
|
||||
summary=_("Submit conversation feedback"),
|
||||
operation_id=_("Submit conversation feedback"), # type: ignore
|
||||
parameters=PageHistoricalConversationRecordAPI.get_parameters(),
|
||||
responses=PageHistoricalConversationRecordAPI.get_response(),
|
||||
tags=[_('Chat')] # type: ignore
|
||||
)
|
||||
def post(self, request: Request, chat_id: str, chat_record_id: str):
|
||||
return result.success(ChatRecordOperateSerializer(
|
||||
data={
|
||||
'chat_id': chat_id,
|
||||
'chat_record_id': chat_record_id,
|
||||
'application_id': request.auth.application_id,
|
||||
'chat_user_id': request.auth.chat_user_id,
|
||||
**request.data
|
||||
}).feedback())
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@
|
|||
prefix: '/admin',
|
||||
chatPrefix: '/chat',
|
||||
}
|
||||
})()</script><script type="module" crossorigin src="./assets/admin-BHHIGtO5.js"></script><link rel="stylesheet" crossorigin href="./assets/admin-uFds98Rz.css"></head><body><div id="app"></div></body></html>
|
||||
})()</script><script type="module" crossorigin src="./assets/admin-Da392tT3.js"></script><link rel="stylesheet" crossorigin href="./assets/admin-D4RuFzCU.css"></head><body><div id="app"></div></body></html>
|
||||
|
|
@ -198,6 +198,30 @@ const getChatRecordDetails: (
|
|||
loading,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交对话记录反馈
|
||||
* @param application_id
|
||||
* @param chat_id
|
||||
* @param chat_record_id
|
||||
* @param data
|
||||
* @param loading
|
||||
*/
|
||||
const postChatRecordFeedback: (
|
||||
application_id: string,
|
||||
chat_id: string,
|
||||
chat_record_id: string,
|
||||
data: any,
|
||||
loading?: Ref<boolean>,
|
||||
) => Promise<Result<any>> = (application_id, chat_id, chat_record_id, data, loading) => {
|
||||
return post(
|
||||
`${prefix.value}/${application_id}/chat/${chat_id}/chat_record/${chat_record_id}/feedback`,
|
||||
data,
|
||||
undefined,
|
||||
loading,
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
postChatLogAddKnowledge,
|
||||
getChatLog,
|
||||
|
|
@ -207,4 +231,5 @@ export default {
|
|||
delMarkChatRecord,
|
||||
postExportChatLog,
|
||||
getChatRecordDetails,
|
||||
postChatRecordFeedback,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,6 +250,15 @@ const getChatRecord: (
|
|||
) => Promise<Result<any>> = (chat_id, chat_record_id, loading) => {
|
||||
return get(`historical_conversation/${chat_id}/record/${chat_record_id}`, {}, loading)
|
||||
}
|
||||
|
||||
const postChatRecordFeedback: (
|
||||
chat_id: string,
|
||||
chat_record_id: string,
|
||||
data: any,
|
||||
loading?: Ref<boolean>,
|
||||
) => Promise<Result<any>> = (chat_id, chat_record_id, data, loading) => {
|
||||
return post(`historical_conversation/${chat_id}/record/${chat_record_id}/feedback`, data, undefined, loading)
|
||||
}
|
||||
/**
|
||||
* 文本转语音
|
||||
*/
|
||||
|
|
@ -356,6 +365,7 @@ export default {
|
|||
resetCurrentPassword,
|
||||
getChatUserProfile,
|
||||
getChatRecord,
|
||||
postChatRecordFeedback,
|
||||
textToSpeech,
|
||||
speechToText,
|
||||
deleteChat,
|
||||
|
|
|
|||
|
|
@ -180,6 +180,22 @@ const getChatRecordDetails: (
|
|||
loading,
|
||||
)
|
||||
}
|
||||
|
||||
const postChatRecordFeedback: (
|
||||
application_id: string,
|
||||
chat_id: string,
|
||||
chat_record_id: string,
|
||||
data: any,
|
||||
loading?: Ref<boolean>,
|
||||
) => Promise<Result<any>> = (application_id, chat_id, chat_record_id, data, loading) => {
|
||||
return post(
|
||||
`${prefix}/${application_id}/chat/${chat_id}/chat_record/${chat_record_id}/feedback`,
|
||||
data,
|
||||
undefined,
|
||||
loading,
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
postChatLogAddKnowledge,
|
||||
getChatLog,
|
||||
|
|
@ -189,4 +205,5 @@ export default {
|
|||
delMarkChatRecord,
|
||||
postExportChatLog,
|
||||
getChatRecordDetails,
|
||||
postChatRecordFeedback,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ interface chatType {
|
|||
id: string
|
||||
problem_text: string
|
||||
answer_text: string
|
||||
comprehensive_score?: number | string | null
|
||||
buffer: Array<string>
|
||||
answer_text_list: Array<
|
||||
Array<{
|
||||
|
|
|
|||
|
|
@ -54,6 +54,17 @@
|
|||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="formattedComprehensiveScore !== null"
|
||||
class="content confidence-row"
|
||||
:style="{
|
||||
'padding-left': showAvatar ? 'var(--padding-left)' : '0',
|
||||
'padding-right': showUserAvatar ? 'var(--padding-left)' : '0',
|
||||
}"
|
||||
>
|
||||
<span class="confidence-row__label">{{ $t('chat.comprehensiveScore') }}</span>
|
||||
<span class="confidence-row__value">{{ formattedComprehensiveScore }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
:style="{
|
||||
|
|
@ -146,6 +157,18 @@ const answer_text_list = computed(() => {
|
|||
})
|
||||
})
|
||||
|
||||
const formattedComprehensiveScore = computed(() => {
|
||||
const score = props.chatRecord?.comprehensive_score
|
||||
if (score === undefined || score === null || score === '') {
|
||||
return null
|
||||
}
|
||||
const numericScore = Number(score)
|
||||
if (Number.isNaN(numericScore)) {
|
||||
return null
|
||||
}
|
||||
return numericScore.toFixed(2)
|
||||
})
|
||||
|
||||
function showSource(row: any) {
|
||||
if (props.type === 'log') {
|
||||
return true
|
||||
|
|
@ -182,4 +205,19 @@ onMounted(() => {
|
|||
})
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.confidence-row {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--app-text-color-light);
|
||||
}
|
||||
|
||||
.confidence-row__label {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.confidence-row__value {
|
||||
color: var(--app-text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -93,7 +93,15 @@
|
|||
<AppIcon iconName="app-oppose-color"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" />
|
||||
<el-tooltip effect="dark" :content="$t('views.chatLog.feedback.shortTitle')" placement="top">
|
||||
<el-button text @click="openFeedback(data)" class="feedback-trigger">
|
||||
<el-icon class="feedback-trigger__icon"><ChatDotRound /></el-icon>
|
||||
<span class="feedback-trigger__label">{{ $t('views.chatLog.feedback.shortTitle') }}</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<FeedbackDialog ref="FeedbackDialogRef" @refresh="refreshFeedback" />
|
||||
<div ref="audioCiontainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -101,12 +109,14 @@
|
|||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, ref, onBeforeUnmount, type Ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ChatDotRound } from '@element-plus/icons-vue'
|
||||
import { copyClick } from '@/utils/clipboard'
|
||||
import applicationApi from '@/api/application/application'
|
||||
import chatAPI from '@/api/chat/chat'
|
||||
import { datetimeFormat } from '@/utils/time'
|
||||
import { MsgError } from '@/utils/message'
|
||||
import bus from '@/bus'
|
||||
import FeedbackDialog from '@/views/chat-log/component/FeedbackDialog.vue'
|
||||
const copy = (data: any) => {
|
||||
try {
|
||||
const text = data.answer_text_list
|
||||
|
|
@ -143,6 +153,7 @@ const emit = defineEmits(['update:data', 'regeneration'])
|
|||
|
||||
const audioPlayer = ref<HTMLAudioElement[] | null>([])
|
||||
const audioCiontainer = ref<HTMLDivElement>()
|
||||
const FeedbackDialogRef = ref()
|
||||
const buttonData = ref(props.data)
|
||||
const loading = ref(false)
|
||||
|
||||
|
|
@ -152,6 +163,19 @@ function regeneration() {
|
|||
emit('regeneration')
|
||||
}
|
||||
|
||||
function openFeedback(data: any) {
|
||||
FeedbackDialogRef.value.open({
|
||||
...data,
|
||||
chat_id: data.chat_id || props.chatId,
|
||||
application_id: props.applicationId
|
||||
})
|
||||
}
|
||||
|
||||
function refreshFeedback(data: any) {
|
||||
buttonData.value = data
|
||||
emit('update:data', buttonData.value)
|
||||
}
|
||||
|
||||
function voteHandle(val: string) {
|
||||
chatAPI.vote(props.chatId, props.data.record_id, val, loading).then(() => {
|
||||
buttonData.value['vote_status'] = val
|
||||
|
|
@ -560,6 +584,36 @@ onBeforeUnmount(() => {
|
|||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.feedback-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.feedback-trigger__icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.feedback-trigger__label {
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
max-width 0.18s ease,
|
||||
opacity 0.18s ease,
|
||||
margin-left 0.18s ease;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.feedback-trigger:hover .feedback-trigger__label {
|
||||
max-width: 32px;
|
||||
opacity: 1;
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
@media only screen and (max-width: 430px) {
|
||||
.chat-operation-button {
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -49,8 +49,15 @@
|
|||
<el-button text disabled v-if="buttonData?.vote_status === '1'">
|
||||
<AppIcon iconName="app-oppose-color"></AppIcon>
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-tooltip effect="dark" :content="$t('views.chatLog.feedback.title')" placement="top">
|
||||
<el-button text @click="openFeedback(data)">
|
||||
<AppIcon iconName="app-feedback"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<EditContentDialog ref="EditContentDialogRef" @refresh="refreshContent" />
|
||||
<EditMarkDialog ref="EditMarkDialogRef" @refresh="refreshMark" />
|
||||
<FeedbackDialog ref="FeedbackDialogRef" @refresh="refreshFeedback" />
|
||||
<!-- 先渲染,不然不能播放 -->
|
||||
<audio ref="audioPlayer" v-for="item in audioList" :key="item" controls hidden="hidden"></audio>
|
||||
</div>
|
||||
|
|
@ -61,6 +68,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||
import { copyClick } from '@/utils/clipboard'
|
||||
import EditContentDialog from '@/views/chat-log/component/EditContentDialog.vue'
|
||||
import EditMarkDialog from '@/views/chat-log/component/EditMarkDialog.vue'
|
||||
import FeedbackDialog from '@/views/chat-log/component/FeedbackDialog.vue'
|
||||
import { datetimeFormat } from '@/utils/time'
|
||||
import applicationApi from '@/api/application/application'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
|
@ -103,6 +111,7 @@ const audioPlayer = ref<HTMLAudioElement[] | null>(null)
|
|||
|
||||
const EditContentDialogRef = ref()
|
||||
const EditMarkDialogRef = ref()
|
||||
const FeedbackDialogRef = ref()
|
||||
|
||||
const buttonData = ref(props.data)
|
||||
const loading = ref(false)
|
||||
|
|
@ -118,6 +127,19 @@ function editMark(data: any) {
|
|||
EditMarkDialogRef.value.open(data)
|
||||
}
|
||||
|
||||
function openFeedback(data: any) {
|
||||
FeedbackDialogRef.value.open({
|
||||
...data,
|
||||
chat_id: data.chat_id || '',
|
||||
application_id: props.applicationId,
|
||||
})
|
||||
}
|
||||
|
||||
function refreshFeedback(data: any) {
|
||||
buttonData.value = data
|
||||
emit('update:data', buttonData.value)
|
||||
}
|
||||
|
||||
const audioPlayerStatus = ref(false)
|
||||
|
||||
function markdownToPlainText(md: string) {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
:applicationId="application.id"
|
||||
:chatId="chatRecord.chat_id"
|
||||
:chat_loading="loading"
|
||||
@update:data="(event: any) => emit('update:chatRecord', event)"
|
||||
@regeneration="regenerationChart(chatRecord)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -495,6 +495,43 @@ export const iconMap: any = {
|
|||
])
|
||||
},
|
||||
},
|
||||
'app-feedback': {
|
||||
iconReader: () => {
|
||||
return h('i', [
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
style: { height: '100%', width: '100%' },
|
||||
viewBox: '0 0 1024 1024',
|
||||
version: '1.1',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
d: 'M512 106.666667c-224.853333 0-407.04 182.186667-407.04 407.04s182.186667 407.04 407.04 407.04 407.04-182.186667 407.04-407.04-182.186667-407.04-407.04-407.04z m0 768a360.96 360.96 0 1 1 0-721.92 360.96 360.96 0 0 1 0 721.92z',
|
||||
fill: 'currentColor',
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M341.333333 469.333333a42.666667 42.666667 0 1 0 0 85.333334 42.666667 42.666667 0 0 0 0-85.333334z',
|
||||
fill: 'currentColor',
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M512 469.333333a42.666667 42.666667 0 1 0 0 85.333334 42.666667 42.666667 0 0 0 0-85.333334z',
|
||||
fill: 'currentColor',
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M682.666667 469.333333a42.666667 42.666667 0 1 0 0 85.333334 42.666667 42.666667 0 0 0 0-85.333334z',
|
||||
fill: 'currentColor',
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M512 597.333333a128 128 0 1 0 0-256 128 128 0 0 0 0 256z m0 42.666667a170.666667 170.666667 0 1 1 0-341.333334 170.666667 170.666667 0 0 1 0 341.333334z',
|
||||
fill: 'currentColor',
|
||||
}),
|
||||
],
|
||||
),
|
||||
])
|
||||
},
|
||||
},
|
||||
// 动态加载的图标
|
||||
...dynamicIcons,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,10 @@ function getProvider() {
|
|||
model
|
||||
.asyncGetProvider()
|
||||
.then((res: any) => {
|
||||
providerOptions.value = res?.data
|
||||
const excludedProviders = ['OpenAI', 'Azure OpenAI']
|
||||
providerOptions.value = (res?.data || []).filter(
|
||||
(provider: Provider) => !excludedProviders.includes(provider.name),
|
||||
)
|
||||
loading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export default {
|
|||
chatId: 'Chat ID',
|
||||
chatUserId: 'Chat User ID',
|
||||
chatUserType: 'Chat User Type',
|
||||
comprehensiveScore: 'Confidence:',
|
||||
userInput: 'User Input',
|
||||
quote: 'Quote',
|
||||
download: 'Click to Download',
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ export default {
|
|||
toChat: 'Chat',
|
||||
publish: 'Publish',
|
||||
},
|
||||
publishDialog: {
|
||||
title: 'Select Publish Mode',
|
||||
internalTitle: 'Internal Publish',
|
||||
internalDesc: 'Publish the app and automatically turn off the public URL.',
|
||||
publicTitle: 'Public Publish',
|
||||
publicDesc: 'Publish the app and automatically turn on the public URL.',
|
||||
},
|
||||
delete: {
|
||||
confirmTitle: 'Are you sure you want to delete this APP: ',
|
||||
confirmMessage:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ export default {
|
|||
feedback: {
|
||||
label: 'User Feedback',
|
||||
star: 'Agree',
|
||||
trample: 'Disagree'
|
||||
trample: 'Disagree',
|
||||
countLabel: 'Feedback Count',
|
||||
contentLabel: 'Feedback Content'
|
||||
},
|
||||
mark: 'Marks',
|
||||
recenTimes: 'Last Chat Time'
|
||||
|
|
@ -38,5 +40,20 @@ export default {
|
|||
title: {
|
||||
placeholder: 'Please set a title for the current content for management and viewing'
|
||||
}
|
||||
},
|
||||
feedback: {
|
||||
title: 'Chat Feedback',
|
||||
shortTitle: 'Feedback',
|
||||
type: 'Feedback Type',
|
||||
typePlaceholder: 'Please select feedback type',
|
||||
typeSuggestion: 'Suggestion',
|
||||
typeQuestion: 'Question',
|
||||
typeSupplement: 'Supplement',
|
||||
typeOther: 'Other',
|
||||
content: 'Feedback Content',
|
||||
listTitle: 'Feedback Details',
|
||||
contentPlaceholder: 'Please enter your feedback content',
|
||||
typeRequired: 'Please select feedback type',
|
||||
contentRequired: 'Please enter feedback content'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export default {
|
|||
chatId: '对话 ID',
|
||||
chatUserId: '对话用户 ID',
|
||||
chatUserType: '对话用户类型',
|
||||
comprehensiveScore: '置信度:',
|
||||
userInput: '用户输入',
|
||||
quote: '引用',
|
||||
download: '点击下载文件',
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ export default {
|
|||
toChat: '去对话',
|
||||
publish: '发布',
|
||||
},
|
||||
publishDialog: {
|
||||
title: '选择发布方式',
|
||||
internalTitle: '内部发布',
|
||||
internalDesc: '发布后自动关闭公开访问链接,仅保留内部使用。',
|
||||
publicTitle: '公开发布',
|
||||
publicDesc: '发布后自动打开公开访问链接,允许外部访问。',
|
||||
},
|
||||
delete: {
|
||||
confirmTitle: '是否删除应用:',
|
||||
confirmMessage: '删除后该应用将不再提供服务,请谨慎操作。',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ export default {
|
|||
feedback: {
|
||||
label: '用户反馈',
|
||||
star: '赞同',
|
||||
trample: '反对'
|
||||
trample: '反对',
|
||||
countLabel: '反馈条数',
|
||||
contentLabel: '反馈内容'
|
||||
},
|
||||
mark: '改进标注',
|
||||
recenTimes: '最近对话时间'
|
||||
|
|
@ -38,5 +40,20 @@ export default {
|
|||
title: {
|
||||
placeholder: '请给当前内容设置一个标题,以便管理查看'
|
||||
}
|
||||
},
|
||||
feedback: {
|
||||
title: '对话反馈',
|
||||
shortTitle: '反馈',
|
||||
type: '反馈类型',
|
||||
typePlaceholder: '请选择反馈类型',
|
||||
typeSuggestion: '建议',
|
||||
typeQuestion: '疑问',
|
||||
typeSupplement: '补充',
|
||||
typeOther: '其他',
|
||||
content: '反馈内容',
|
||||
listTitle: '反馈详情',
|
||||
contentPlaceholder: '请输入您的反馈内容',
|
||||
typeRequired: '请选择反馈类型',
|
||||
contentRequired: '请输入反馈内容'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export default {
|
|||
chatId: '對話 ID',
|
||||
chatUserId: '對話用戶 ID',
|
||||
chatUserType: '對話用戶類型',
|
||||
comprehensiveScore: '置信度:',
|
||||
userInput: '用戶輸入',
|
||||
quote: '引用',
|
||||
download: '點擊下載文件',
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@ export default {
|
|||
publish: '發布',
|
||||
addModel: '新增模型',
|
||||
},
|
||||
publishDialog: {
|
||||
title: '選擇發布方式',
|
||||
internalTitle: '內部發布',
|
||||
internalDesc: '發布後自動關閉公開訪問連結,僅保留內部使用。',
|
||||
publicTitle: '公開發布',
|
||||
publicDesc: '發布後自動開啟公開訪問連結,允許外部訪問。',
|
||||
},
|
||||
delete: {
|
||||
confirmTitle: '是否刪除應用:',
|
||||
confirmMessage: '刪除後該應用將不再提供服務,請謹慎操作。',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ export default {
|
|||
feedback: {
|
||||
label: '用戶反饋',
|
||||
star: '贊同',
|
||||
trample: '反對'
|
||||
trample: '反對',
|
||||
countLabel: '反饋條數',
|
||||
contentLabel: '反饋內容'
|
||||
},
|
||||
mark: '改進標註',
|
||||
recenTimes: '最近對話時間'
|
||||
|
|
@ -38,5 +40,20 @@ export default {
|
|||
title: {
|
||||
placeholder: '請給當前內容設定一個標題,以便管理查看'
|
||||
}
|
||||
},
|
||||
feedback: {
|
||||
title: '對話反饋',
|
||||
shortTitle: '反饋',
|
||||
type: '反饋類型',
|
||||
typePlaceholder: '請選擇反饋類型',
|
||||
typeSuggestion: '建議',
|
||||
typeQuestion: '疑問',
|
||||
typeSupplement: '補充',
|
||||
typeOther: '其他',
|
||||
content: '反饋內容',
|
||||
listTitle: '反饋詳情',
|
||||
contentPlaceholder: '請輸入您的反饋內容',
|
||||
typeRequired: '請選擇反饋類型',
|
||||
contentRequired: '請輸入反饋內容'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
|
|||
import { type Ref } from 'vue'
|
||||
const useApplicationStore = defineStore('application', {
|
||||
state: () => ({
|
||||
location: `${window.location.origin}${window.MaxKB.chatPrefix ? window.MaxKB.chatPrefix : window.MaxKB.prefix}/`,
|
||||
location: `${window.location.origin}${window.MaxKB.chatPrefix ? window.MaxKB.chatPrefix : '/chat'}/`,
|
||||
}),
|
||||
actions: {},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="publish(applicationFormRef)"
|
||||
@click="openPublishDialog(applicationFormRef)"
|
||||
:disabled="loading"
|
||||
v-if="permissionPrecise.edit(id)"
|
||||
>
|
||||
|
|
@ -633,6 +633,46 @@
|
|||
/>
|
||||
<McpServersDialog ref="mcpServersDialogRef" @refresh="submitMcpServersDialog" />
|
||||
<ToolDialog ref="toolDialogRef" @refresh="submitToolDialog" />
|
||||
<el-dialog
|
||||
v-model="publishDialogVisible"
|
||||
:title="$t('views.application.publishDialog.title')"
|
||||
width="520"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<div class="publish-mode-list">
|
||||
<div
|
||||
class="publish-mode-card"
|
||||
:class="{ active: publishMode === 'internal' }"
|
||||
@click="publishMode = 'internal'"
|
||||
>
|
||||
<div class="publish-mode-card__title">
|
||||
{{ $t('views.application.publishDialog.internalTitle') }}
|
||||
</div>
|
||||
<div class="publish-mode-card__desc">
|
||||
{{ $t('views.application.publishDialog.internalDesc') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="publish-mode-card"
|
||||
:class="{ active: publishMode === 'public' }"
|
||||
@click="publishMode = 'public'"
|
||||
>
|
||||
<div class="publish-mode-card__title">
|
||||
{{ $t('views.application.publishDialog.publicTitle') }}
|
||||
</div>
|
||||
<div class="publish-mode-card__desc">
|
||||
{{ $t('views.application.publishDialog.publicDesc') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="publishDialogVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirmPublish(applicationFormRef)">
|
||||
{{ $t('common.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
|
@ -703,6 +743,8 @@ const AddKnowledgeDialogRef = ref()
|
|||
|
||||
const loading = ref(false)
|
||||
const knowledgeLoading = ref(false)
|
||||
const publishDialogVisible = ref(false)
|
||||
const publishMode = ref<'internal' | 'public'>('public')
|
||||
|
||||
const applicationForm = ref<ApplicationFormType>({
|
||||
name: '',
|
||||
|
|
@ -791,10 +833,32 @@ const publish = (formEl: FormInstance | undefined) => {
|
|||
)
|
||||
})
|
||||
.then(() => {
|
||||
return loadSharedApi({ type: 'application', systemType: apiType.value }).putAccessToken(
|
||||
id,
|
||||
{
|
||||
is_active: publishMode.value === 'public',
|
||||
},
|
||||
loading,
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
publishDialogVisible.value = false
|
||||
MsgSuccess(t('views.application.tip.publishSuccess'))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const openPublishDialog = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate().then(() => {
|
||||
publishMode.value = 'public'
|
||||
publishDialogVisible.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const confirmPublish = (formEl: FormInstance | undefined) => {
|
||||
publish(formEl)
|
||||
}
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, fields) => {
|
||||
|
|
@ -1127,6 +1191,39 @@ onMounted(() => {
|
|||
color: var(--app-text-color);
|
||||
}
|
||||
|
||||
.publish-mode-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.publish-mode-card {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.publish-mode-card.active {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.publish-mode-card__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--app-text-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.publish-mode-card__desc {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--app-text-color-light);
|
||||
}
|
||||
|
||||
.dialog-bg {
|
||||
border-radius: 8px;
|
||||
background: var(--dialog-bg-gradient-color);
|
||||
|
|
|
|||
|
|
@ -572,7 +572,19 @@ function toChat(row: any) {
|
|||
aips = aips ? aips : []
|
||||
const apiParams = mapToUrlParams(aips) ? '?' + mapToUrlParams(aips) : ''
|
||||
ApplicationApi.getAccessToken(row.id, loading).then((res: any) => {
|
||||
window.open(application.location + res?.data?.access_token + apiParams)
|
||||
if (res?.data?.is_active) {
|
||||
window.open(application.location + res?.data?.access_token + apiParams)
|
||||
return
|
||||
}
|
||||
const debugPreviewUrl = router.resolve({
|
||||
name: 'AppSetting',
|
||||
params: {
|
||||
from: 'workspace',
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
},
|
||||
}).href
|
||||
window.open(debugPreviewUrl)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="$t('views.chatLog.feedback.title')"
|
||||
v-model="dialogVisible"
|
||||
width="600"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
:rules="rules"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form-item :label="$t('views.chatLog.feedback.type')" prop="feedback_type">
|
||||
<el-select v-model="form.feedback_type" :placeholder="$t('views.chatLog.feedback.typePlaceholder')">
|
||||
<el-option :label="$t('views.chatLog.feedback.typeSuggestion')" value="SUGGESTION"></el-option>
|
||||
<el-option :label="$t('views.chatLog.feedback.typeQuestion')" value="QUESTION"></el-option>
|
||||
<el-option :label="$t('views.chatLog.feedback.typeSupplement')" value="SUPPLEMENT"></el-option>
|
||||
<el-option :label="$t('views.chatLog.feedback.typeOther')" value="OTHER"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('views.chatLog.feedback.content')" prop="content">
|
||||
<el-input
|
||||
v-model="form.content"
|
||||
type="textarea"
|
||||
:placeholder="$t('views.chatLog.feedback.contentPlaceholder')"
|
||||
:rows="6"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
|
||||
<el-button type="primary" native-type="button" @click.prevent="submitForm(formRef)" :loading="loading">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, reactive, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { t } from '@/locales'
|
||||
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
|
||||
import chatAPI from '@/api/chat/chat'
|
||||
|
||||
const route = useRoute()
|
||||
const isChatRoute = computed(() => {
|
||||
return route.name === 'chat' || typeof route.params?.accessToken === 'string'
|
||||
})
|
||||
const apiType = computed(() => {
|
||||
if (route.path.includes('resource-management')) {
|
||||
return 'systemManage'
|
||||
} else {
|
||||
return 'workspace'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
const form = ref<any>({
|
||||
chat_id: '',
|
||||
record_id: '',
|
||||
application_id: '',
|
||||
feedback_type: '',
|
||||
content: '',
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
feedback_type: [
|
||||
{ required: true, message: t('views.chatLog.feedback.typeRequired'), trigger: 'blur' },
|
||||
],
|
||||
content: [
|
||||
{ required: true, message: t('views.chatLog.feedback.contentRequired'), trigger: 'blur' },
|
||||
],
|
||||
})
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
form.value = {
|
||||
chat_id: '',
|
||||
record_id: '',
|
||||
application_id: '',
|
||||
feedback_type: '',
|
||||
content: '',
|
||||
}
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
})
|
||||
|
||||
const open = (data: any) => {
|
||||
form.value.chat_id = data.chat_id
|
||||
form.value.record_id = data.record_id || data.id
|
||||
form.value.application_id = data.application_id
|
||||
formRef.value?.clearValidate()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submitForm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
const obj = {
|
||||
feedback_type: form.value.feedback_type,
|
||||
content: form.value.content,
|
||||
}
|
||||
const request = isChatRoute.value
|
||||
? chatAPI.postChatRecordFeedback(form.value.chat_id, form.value.record_id, obj, loading)
|
||||
: loadSharedApi({ type: 'chatLog', systemType: apiType.value }).postChatRecordFeedback(
|
||||
form.value.application_id,
|
||||
form.value.chat_id,
|
||||
form.value.record_id,
|
||||
obj,
|
||||
loading,
|
||||
)
|
||||
|
||||
request
|
||||
.then((res: any) => {
|
||||
emit('refresh', res.data)
|
||||
dialogVisible.value = false
|
||||
})
|
||||
.catch(() => undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -163,6 +163,14 @@
|
|||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="mark_sum" :label="$t('views.chatLog.table.mark')" align="right" />
|
||||
<el-table-column prop="feedback_count" :label="$t('views.chatLog.table.feedback.countLabel')" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.feedback_count" link type="primary" @click.stop="openFeedbackList(row)">
|
||||
{{ row.feedback_count }}
|
||||
</el-button>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="asker" :label="$t('views.chatLog.table.user')">
|
||||
<template #default="{ row }">
|
||||
{{ row.asker?.username }}
|
||||
|
|
@ -238,6 +246,23 @@
|
|||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
:title="$t('views.chatLog.feedback.listTitle')"
|
||||
v-model="feedbackListDialogVisible"
|
||||
width="720px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div v-if="currentFeedbackList.length" class="feedback-list">
|
||||
<div v-for="(item, index) in currentFeedbackList" :key="index" class="feedback-list__item">
|
||||
<div class="feedback-list__meta">
|
||||
<span>{{ feedbackTypeLabelMap[item.feedback_type] || item.feedback_type }}</span>
|
||||
<span>{{ datetimeFormat(item.create_time) }}</span>
|
||||
</div>
|
||||
<div class="feedback-list__content">{{ item.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>-</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
|
@ -321,6 +346,7 @@ const multipleSelection = ref<any[]>([])
|
|||
const ChatRecordRef = ref()
|
||||
const loading = ref(false)
|
||||
const documentLoading = ref(false)
|
||||
const feedbackListDialogVisible = ref(false)
|
||||
const paginationConfig = reactive({
|
||||
current_page: 1,
|
||||
page_size: 20,
|
||||
|
|
@ -330,6 +356,13 @@ const dialogVisible = ref(false)
|
|||
const documentDialogVisible = ref(false)
|
||||
const days = ref<number>(180)
|
||||
const tableData = ref<any[]>([])
|
||||
const currentFeedbackList = ref<any[]>([])
|
||||
const feedbackTypeLabelMap = {
|
||||
SUGGESTION: t('views.chatLog.feedback.typeSuggestion'),
|
||||
QUESTION: t('views.chatLog.feedback.typeQuestion'),
|
||||
SUPPLEMENT: t('views.chatLog.feedback.typeSupplement'),
|
||||
OTHER: t('views.chatLog.feedback.typeOther'),
|
||||
} as Record<string, string>
|
||||
const tableIndexMap = computed<Dict<number>>(() => {
|
||||
return tableData.value
|
||||
.map((row, index) => ({
|
||||
|
|
@ -570,6 +603,11 @@ function openDocumentDialog() {
|
|||
documentDialogVisible.value = true
|
||||
}
|
||||
|
||||
function openFeedbackList(row: any) {
|
||||
currentFeedbackList.value = row.feedback_list || []
|
||||
feedbackListDialogVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
changeDayHandle(history_day.value)
|
||||
getDetail()
|
||||
|
|
@ -581,4 +619,32 @@ onMounted(() => {
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-list {
|
||||
max-height: 480px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.feedback-list__item {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.feedback-list__item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.feedback-list__meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.feedback-list__content {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ const checkModelType = (model_type: string) => {
|
|||
const excludedProviders = [
|
||||
'Anthropic',
|
||||
'Amazon Bedrock',
|
||||
'OpenAI',
|
||||
'Azure OpenAI',
|
||||
'Gemini',
|
||||
'SILICONFLOW',
|
||||
'Xorbits Inference',
|
||||
|
|
|
|||
|
|
@ -223,6 +223,8 @@ onMounted(() => {
|
|||
const excludedProviders = [
|
||||
'Anthropic',
|
||||
'Amazon Bedrock',
|
||||
'OpenAI',
|
||||
'Azure OpenAI',
|
||||
'Gemini',
|
||||
'SILICONFLOW',
|
||||
'Xorbits Inference',
|
||||
|
|
|
|||
|
|
@ -120,7 +120,17 @@
|
|||
</el-dropdown>
|
||||
</el-card>
|
||||
</div>
|
||||
<h2 class="mb-16">{{ data.title || '-' }}</h2>
|
||||
<div class="paragraph-box__header mb-16">
|
||||
<h2 class="paragraph-box__title">{{ data.title || '-' }}</h2>
|
||||
<div class="paragraph-box__meta" v-if="data.create_time || data.update_time">
|
||||
<span v-if="data.create_time">
|
||||
{{ $t('common.createTime') }}:{{ datetimeFormat(data.create_time) }}
|
||||
</span>
|
||||
<span v-if="data.update_time">
|
||||
{{ $t('common.updateTime') }}:{{ datetimeFormat(data.update_time) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<MdPreview
|
||||
ref="editorRef"
|
||||
editorId="preview-only"
|
||||
|
|
@ -154,6 +164,7 @@ import { MsgSuccess, MsgConfirm } from '@/utils/message'
|
|||
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
|
||||
import permissionMap from '@/permission'
|
||||
import { t } from '@/locales'
|
||||
import { datetimeFormat } from '@/utils/time'
|
||||
const props = defineProps<{
|
||||
data: any
|
||||
disabled?: boolean
|
||||
|
|
@ -337,6 +348,25 @@ watch(dialogVisible, (val: boolean) => {
|
|||
overflow: inherit;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.paragraph-box__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.paragraph-box__title {
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.paragraph-box__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
font-size: 12px;
|
||||
color: var(--app-text-color-light);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
|||
|
|
@ -385,7 +385,19 @@ function toChat(row: any) {
|
|||
? '?' + mapToUrlParams(apiInputParams.value)
|
||||
: ''
|
||||
ApplicationResourceApi.getAccessToken(row.id, loading).then((res: any) => {
|
||||
window.open(application.location + res?.data?.access_token + apiParams)
|
||||
if (res?.data?.is_active) {
|
||||
window.open(application.location + res?.data?.access_token + apiParams)
|
||||
return
|
||||
}
|
||||
const debugPreviewUrl = router.resolve({
|
||||
name: 'AppSetting',
|
||||
params: {
|
||||
from: 'resource-management',
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
},
|
||||
}).href
|
||||
window.open(debugPreviewUrl)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue