feat: chat log

This commit is contained in:
wangdan-fit2cloud 2025-06-11 13:13:10 +08:00
parent fa818fc6e8
commit e9527bdd2b
17 changed files with 1645 additions and 58 deletions

View File

@ -3,7 +3,6 @@ import { get, post, postStream, del, put, request, download, exportFile } from '
import type { pageRequest } from '@/api/type/common'
import type { ApplicationFormType } from '@/api/type/application'
import { type Ref } from 'vue'
import type { FormField } from '@/components/dynamics-form/type'
const prefix = '/workspace/' + localStorage.getItem('workspace_id') + '/application'

View File

@ -0,0 +1,200 @@
import { Result } from '@/request/Result'
import {
get,
post,
exportExcelPost,
postStream,
del,
put,
request,
download,
exportFile,
} from '@/request/index'
import type { pageRequest } from '@/api/type/common'
import type { ApplicationFormType } from '@/api/type/application'
import { type Ref } from 'vue'
const prefix = '/workspace/' + localStorage.getItem('workspace_id') + '/application'
/**
*
* @param data
* @param loading
* @param application_id
* @param knowledge_id
*/
const postChatLogAddKnowledge: (
application_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (application_id, data, loading) => {
return post(`${prefix}/${application_id}/add_knowledge`, data, undefined, loading)
}
/**
*
* @param
* application_id
* param {
"start_time": "string",
"end_time": "string",
}
*/
const getChatLog: (
application_id: String,
page: pageRequest,
param: any,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (application_id, page, param, loading) => {
return get(
`${prefix}/${application_id}/chat/${page.current_page}/${page.page_size}`,
param,
loading,
)
}
/**
*
* @param
* application_id, chart_id,order_asc
*/
const getChatRecordLog: (
application_id: String,
chart_id: String,
page: pageRequest,
loading?: Ref<boolean>,
order_asc?: boolean,
) => Promise<Result<any>> = (application_id, chart_id, page, loading, order_asc) => {
return get(
`${prefix}/${application_id}/chat/${chart_id}/chat_record/${page.current_page}/${page.page_size}`,
{ order_asc: order_asc !== undefined ? order_asc : true },
loading,
)
}
/**
*
* @param
* application_id, chart_id, chart_record_id, knowledge_id, document_id
*/
const getMarkChatRecord: (
application_id: String,
chart_id: String,
chart_record_id: String,
knowledge_id: String,
document_id: String,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (
application_id,
chart_id,
chart_record_id,
knowledge_id,
document_id,
loading,
) => {
return get(
`${prefix}/${application_id}/chat/${chart_id}/chat_record/${chart_record_id}/knowledge/${knowledge_id}/document/${document_id}/improve`,
undefined,
loading,
)
}
/**
*
* @param
* application_id, chart_id, chart_record_id, knowledge_id, document_id
* data {
"title": "string",
"content": "string",
"problem_text": "string"
}
*/
const putChatRecordLog: (
application_id: String,
chart_id: String,
chart_record_id: String,
knowledge_id: String,
document_id: String,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (
application_id,
chart_id,
chart_record_id,
knowledge_id,
document_id,
data,
loading,
) => {
return put(
`${prefix}/${application_id}/chat/${chart_id}/chat_record/${chart_record_id}/dataset/${knowledge_id}/document/${document_id}/improve`,
data,
undefined,
loading,
)
}
/**
*
* @param
* application_id, chart_id, chart_record_id, dataset_id, document_id,paragraph_id
*/
const delMarkChatRecord: (
application_id: String,
chart_id: String,
chart_record_id: String,
knowledge_id: String,
document_id: String,
paragraph_id: String,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (
application_id,
chart_id,
chart_record_id,
knowledge_id,
document_id,
paragraph_id,
loading,
) => {
return del(
`${prefix}/${application_id}/chat/${chart_id}/chat_record/${chart_record_id}/knowledge/${knowledge_id}/document/${document_id}/paragraph/${paragraph_id}/improve`,
undefined,
{},
loading,
)
}
/**
*
* @param
* application_id
* param {
"start_time": "string",
"end_time": "string",
}
*/
const postExportChatLog: (
application_id: string,
application_name: string,
param: any,
data: any,
loading?: Ref<boolean>,
) => void = (application_id, application_name, param, data, loading) => {
exportExcelPost(
application_name + '.xlsx',
`${prefix}/${application_id}/chat/export`,
param,
data,
loading,
)
}
export default {
postChatLogAddKnowledge,
getChatLog,
getChatRecordLog,
getMarkChatRecord,
putChatRecordLog,
delMarkChatRecord,
postExportChatLog,
}

View File

@ -22,10 +22,10 @@ export default {
mark: 'Marks',
recenTimes: 'Last Chat Time'
},
addToDataset: 'Add to Knowledge',
addToKnowledge: 'Add to Knowledge',
daysText: 'Days ago',
selectDataset: 'Select Knowledge',
selectDatasetPlaceholder: 'Please select a knowledge',
selectKnowledge: 'Select Knowledge',
selectKnowledgePlaceholder: 'Please select a knowledge',
saveToDocument: 'Save to Document',
documentPlaceholder: 'Please select a document',
editContent: 'Edit Content',

View File

@ -12,7 +12,7 @@ import model from './model'
import document from './document'
import paragraph from './paragraph'
import problem from './problem'
import log from './log'
import chatLog from './chat-log'
import applicationWorkflow from './application-workflow'
import login from './login'
import operateLog from './operate-log'
@ -31,7 +31,7 @@ export default {
document,
paragraph,
problem,
log,
chatLog,
login,
operateLog,
role

View File

@ -22,10 +22,10 @@ export default {
mark: '改进标注',
recenTimes: '最近对话时间'
},
addToDataset: '添加至知识库',
addToKnowledge: '添加至知识库',
daysText: '天之前的对话记录',
selectDataset: '选择知识库',
selectDatasetPlaceholder: '请选择知识库',
selectKnowledge: '选择知识库',
selectKnowledgePlaceholder: '请选择知识库',
saveToDocument: '保存至文档',
documentPlaceholder: '请选择文档',
editContent: '修改内容',

View File

@ -12,7 +12,7 @@ import problem from './problem'
import applicationOverview from './application-overview'
import applicationWorkflow from './application-workflow'
import paragraph from './paragraph'
import log from './log'
import chatLog from './chat-log'
// import notFound from './404'
// import operateLog from './operate-log'
@ -31,7 +31,7 @@ export default {
applicationOverview,
applicationWorkflow,
paragraph,
log,
chatLog,
// notFound,
// operateLog

View File

@ -22,10 +22,10 @@ export default {
mark: '改進標註',
recenTimes: '最近對話時間'
},
addToDataset: '添加至知識庫',
addToKnowledge: '添加至知識庫',
daysText: '天之前的對話記錄',
selectDataset: '選擇知識庫',
selectDatasetPlaceholder: '請選擇知識庫',
selectKnowledge: '選擇知識庫',
selectKnowledgePlaceholder: '請選擇知識庫',
saveToDocument: '保存至文件',
documentPlaceholder: '請選擇文件',
editContent: '修改內容',

View File

@ -12,7 +12,7 @@ import model from './model'
import document from './document'
import paragraph from './paragraph'
import problem from './problem'
import log from './log'
import chatLog from './chat-log'
import applicationWorkflow from './application-workflow'
import login from './login'
import operateLog from './operate-log'
@ -31,7 +31,7 @@ export default {
document,
paragraph,
problem,
log,
chatLog,
login,
operateLog,
role

View File

@ -57,19 +57,19 @@ const ApplicationDetailRouter = {
},
component: () => import('@/views/hit-test/index.vue'),
},
// {
// path: 'log',
// name: 'Log',
// meta: {
// icon: 'app-document',
// iconActive: 'app-document-active',
// title: 'views.log.title',
// active: 'log',
// parentPath: '/application/:id/:type',
// parentName: 'ApplicationDetail'
// },
// component: () => import('@/views/log/index.vue')
// }
{
path: 'chat-log',
name: 'ChatLog',
meta: {
icon: 'app-document',
iconActive: 'app-document-active',
title: 'views.chatLog.title',
active: 'log',
parentPath: '/application/:id/:type',
parentName: 'ApplicationDetail'
},
component: () => import('@/views/chat-log/index.vue')
}
],
}

View File

@ -10,7 +10,7 @@ import useProblemStore from './modules/problem'
import useParagraphStore from './modules/paragraph'
import useDocumentStore from './modules/document'
import useApplicationStore from './modules/application'
import useChatLogStore from './modules/chat-log'
const useStore = () => ({
common: useCommonStore(),
login: useLoginStore(),
@ -24,6 +24,7 @@ const useStore = () => ({
paragraph: useParagraphStore(),
document: useDocumentStore(),
application: useApplicationStore(),
chatLog: useChatLogStore(),
})
export default useStore

View File

@ -0,0 +1,78 @@
import { defineStore } from 'pinia'
import chatLogApi from '@/api/application/chat-log'
import { type Ref } from 'vue'
import type { pageRequest } from '@/api/type/common'
const useChatLogStore = defineStore('chatLog',{
state: () => ({}),
actions: {
async asyncGetChatLog(id: string, page: pageRequest, param: any, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
chatLogApi
.getChatLog(id, page, param, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
async asyncChatRecordLog(
id: string,
chatId: string,
page: pageRequest,
loading?: Ref<boolean>,
order_asc?: boolean
) {
return new Promise((resolve, reject) => {
chatLogApi
.getChatRecordLog(id, chatId, page, loading, order_asc)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
async asyncGetChatLogClient(id: string, page: pageRequest, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
chatLogApi
.getChatLogClient(id, page, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
async asyncDelChatClientLog(id: string, chatId: string, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
chatLogApi
.delChatClientLog(id, chatId, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
async asyncPutChatClientLog(id: string, chatId: string, data: any, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
chatLogApi
.putChatClientLog(id, chatId, data, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
}
}
})
export default useChatLogStore

View File

@ -156,7 +156,7 @@
<AppIcon iconName="app-operation" class="mr-4"></AppIcon>
{{ $t('common.paramSetting') }}
</el-button>
<el-button type="primary" link @click="openDatasetDialog">
<el-button type="primary" link @click="openKnowledgeDialog">
<el-icon class="mr-4">
<Plus />
</el-icon>
@ -180,11 +180,11 @@
v-for="(item, index) in applicationForm.knowledge_id_list"
:key="index"
>
<el-card class="relate-dataset-card border-r-4" shadow="never">
<el-card class="relate-knowledge-card border-r-4" shadow="never">
<div class="flex-between">
<div class="flex align-center" style="width: 80%">
<el-avatar
v-if="relatedObject(datasetList, item, 'id')?.type === '1'"
v-if="relatedObject(knowledgeList, item, 'id')?.type === '1'"
class="mr-8 avatar-purple"
shape="square"
:size="32"
@ -196,7 +196,7 @@
/>
</el-avatar>
<el-avatar
v-else-if="relatedObject(datasetList, item, 'id')?.type === '2'"
v-else-if="relatedObject(knowledgeList, item, 'id')?.type === '2'"
class="mr-8 avatar-purple"
shape="square"
:size="32"
@ -218,12 +218,12 @@
<span
class="ellipsis cursor"
:title="relatedObject(datasetList, item, 'id')?.name"
:title="relatedObject(knowledgeList, item, 'id')?.name"
>
{{ relatedObject(datasetList, item, 'id')?.name }}</span
{{ relatedObject(knowledgeList, item, 'id')?.name }}</span
>
</div>
<el-button text @click="removeDataset(item)">
<el-button text @click="removeKnowledge(item)">
<el-icon>
<Close />
</el-icon>
@ -470,12 +470,12 @@
<AIModeParamSettingDialog ref="AIModeParamSettingDialogRef" @refresh="refreshForm" />
<TTSModeParamSettingDialog ref="TTSModeParamSettingDialogRef" @refresh="refreshTTSForm" />
<ParamSettingDialog ref="ParamSettingDialogRef" @refresh="refreshParam" />
<AddDatasetDialog
ref="AddDatasetDialogRef"
@addData="addDataset"
:data="datasetList"
<AddKnowledgeDialog
ref="AddKnowledgeDialogRef"
@addData="addKnowledge"
:data="knowledgeList"
@refresh="refresh"
:loading="datasetLoading"
:loading="knowledgeLoading"
/>
<EditAvatarDialog ref="EditAvatarDialogRef" @refresh="refreshIcon" />
@ -491,7 +491,7 @@ import { useRoute } from 'vue-router'
import { groupBy } from 'lodash'
import AIModeParamSettingDialog from './component/AIModeParamSettingDialog.vue'
import ParamSettingDialog from './component/ParamSettingDialog.vue'
import AddDatasetDialog from './component/AddDatasetDialog.vue'
import AddKnowledgeDialog from './component/AddKnowledgeDialog.vue'
import EditAvatarDialog from '@/views/application-overview/component/EditAvatarDialog.vue'
import applicationApi from '@/api/application/application'
import { isAppIcon } from '@/utils/common'
@ -529,11 +529,11 @@ const TTSModeParamSettingDialogRef = ref<InstanceType<typeof TTSModeParamSetting
const ParamSettingDialogRef = ref<InstanceType<typeof ParamSettingDialog>>()
const applicationFormRef = ref<FormInstance>()
const AddDatasetDialogRef = ref()
const AddKnowledgeDialogRef = ref()
const EditAvatarDialogRef = ref()
const loading = ref(false)
const datasetLoading = ref(false)
const knowledgeLoading = ref(false)
const applicationForm = ref<ApplicationFormType>({
name: '',
desc: '',
@ -578,7 +578,7 @@ const rules = reactive<FormRules<ApplicationFormType>>({
],
})
const modelOptions = ref<any>(null)
const datasetList = ref([])
const knowledgeList = ref([])
const sttModelOptions = ref<any>(null)
const ttsModelOptions = ref<any>(null)
const showEditIcon = ref(false)
@ -660,7 +660,7 @@ function refreshTTSForm(data: any) {
applicationForm.value.tts_model_params_setting = data
}
function removeDataset(id: any) {
function removeKnowledge(id: any) {
if (applicationForm.value.knowledge_id_list) {
applicationForm.value.knowledge_id_list.splice(
applicationForm.value.knowledge_id_list.indexOf(id),
@ -669,12 +669,12 @@ function removeDataset(id: any) {
}
}
function addDataset(val: Array<string>) {
function addKnowledge(val: Array<string>) {
applicationForm.value.knowledge_id_list = val
}
function openDatasetDialog() {
AddDatasetDialogRef.value.open(applicationForm.value.knowledge_id_list)
function openKnowledgeDialog() {
AddKnowledgeDialogRef.value.open(applicationForm.value.knowledge_id_list)
}
function getDetail() {
@ -692,9 +692,9 @@ function getDetail() {
})
}
function getDataset() {
application.asyncGetApplicationDataset(id, datasetLoading).then((res: any) => {
datasetList.value = res.data
function getKnowledge() {
application.asyncGetApplicationKnowledge(id, knowledgeLoading).then((res: any) => {
knowledgeList.value = res.data
})
}
@ -766,12 +766,12 @@ function refreshIcon() {
}
function refresh() {
getDataset()
getKnowledge()
}
onMounted(() => {
// getModel()
// getDataset()
// getKnowledge()
// getDetail()
// getSTTModel()
// getTTSModel()
@ -779,7 +779,7 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
.application-setting {
.relate-dataset-card {
.relate-knowledge-card {
color: var(--app-text-color);
}

View File

@ -0,0 +1,166 @@
<template>
<el-drawer v-model="visible" size="60%" @close="closeHandle" class="chat-record-drawer">
<template #header>
<h4 class="single-line">{{ currentAbstract }}</h4>
</template>
<div
v-loading="paginationConfig.current_page === 1 && loading"
class="h-full"
style="padding: 24px 0"
>
<AiChat
ref="AiChatRef"
:application-details="application"
type="log"
:record="recordList"
@scroll="handleScroll"
>
</AiChat>
</div>
<template #footer>
<div>
<el-button @click="pre" :disabled="pre_disable || loading">{{
$t('views.log.buttons.prev')
}}</el-button>
<el-button @click="next" :disabled="next_disable || loading">{{
$t('views.log.buttons.next')
}}</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, reactive, watch, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { type ApplicationFormType, type chatType } from '@/api/type/application'
import useStore from '@/stores'
const AiChatRef = ref()
const { log } = useStore()
const props = withDefaults(
defineProps<{
/**
* 应用信息
*/
application?: ApplicationFormType
/**
* 对话 记录id
*/
chatId: string
currentAbstract: string
/**
* 下一条
*/
next: () => void
/**
* 上一条
*/
pre: () => void
pre_disable: boolean
next_disable: boolean
}>(),
{}
)
const emit = defineEmits(['update:chatId', 'update:currentAbstract', 'refresh'])
const route = useRoute()
const {
params: { id }
} = route
const loading = ref(false)
const visible = ref(false)
const recordList = ref<chatType[]>([])
const paginationConfig = reactive({
current_page: 1,
page_size: 20,
total: 0
})
function closeHandle() {
recordList.value = []
paginationConfig.total = 0
paginationConfig.current_page = 1
}
function getChatRecord() {
return log
.asyncChatRecordLog(id as string, props.chatId, paginationConfig, loading)
.then((res: any) => {
paginationConfig.total = res.data.total
const list = res.data.records
recordList.value = [...list, ...recordList.value].sort((a, b) =>
a.create_time.localeCompare(b.create_time)
)
if (paginationConfig.current_page === 1) {
nextTick(() => {
//
AiChatRef.value.setScrollBottom()
})
}
})
}
watch(
() => props.chatId,
() => {
recordList.value = []
paginationConfig.total = 0
paginationConfig.current_page = 1
if (props.chatId) {
getChatRecord()
}
}
)
watch(visible, (bool) => {
if (!bool) {
emit('update:chatId', '')
emit('update:currentAbstract', '')
emit('refresh')
}
})
function handleScroll(event: any) {
if (
props.chatId !== 'new' &&
event.scrollTop === 0 &&
paginationConfig.total > recordList.value.length
) {
const history_height = event.dialogScrollbar.offsetHeight
paginationConfig.current_page += 1
getChatRecord().then(() => {
event.scrollDiv.setScrollTop(event.dialogScrollbar.offsetHeight - history_height)
})
}
}
const open = () => {
visible.value = true
}
defineExpose({
open
})
</script>
<style lang="scss">
.single-line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-record-drawer {
.el-drawer__body {
background: var(--app-layout-bg-color);
padding: 0;
}
:deep(.el-divider__text) {
background: var(--app-layout-bg-color);
}
}
</style>

View File

@ -0,0 +1,307 @@
<template>
<el-dialog
:title="$t('views.log.editContent')"
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.paragraph.relatedProblem.title')">
<el-input
v-model="form.problem_text"
:placeholder="$t('views.paragraph.relatedProblem.title')"
maxlength="256"
show-word-limit
>
</el-input>
</el-form-item>
<el-form-item :label="$t('common.content')" prop="content">
<MdEditor
v-model="form.content"
:placeholder="$t('views.log.form.content.placeholder')"
:maxLength="100000"
:preview="false"
:toolbars="toolbars"
style="height: 300px"
@onUploadImg="onUploadImg"
:footers="footers"
>
<template #defFooters>
<span style="margin-left: -6px">/ 100000</span>
</template>
</MdEditor>
</el-form-item>
<el-form-item :label="$t('common.title')">
<el-input
show-word-limit
v-model="form.title"
:placeholder="$t('views.log.form.title.placeholder')"
maxlength="256"
>
</el-input>
</el-form-item>
<el-form-item :label="$t('views.log.selectDataset')" prop="dataset_id">
<el-select
v-model="form.dataset_id"
filterable
:placeholder="$t('views.log.selectDatasetPlaceholder')"
:loading="optionLoading"
@change="changeDataset"
>
<el-option v-for="item in datasetList" :key="item.id" :label="item.name" :value="item.id">
<span class="flex align-center">
<AppAvatar
v-if="!item.dataset_id && item.type === '1'"
class="mr-12 avatar-purple"
shape="square"
:size="24"
>
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="!item.dataset_id && item.type === '2'"
class="mr-8 avatar-purple"
shape="square"
:size="24"
style="background: none"
>
<img src="@/assets/logo_lark.svg" style="width: 100%" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="!item.dataset_id && item.type === '0'"
class="mr-12 avatar-blue"
shape="square"
:size="24"
>
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
{{ item.name }}
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('views.log.saveToDocument')" prop="document_id">
<el-select
v-model="form.document_id"
filterable
:placeholder="$t('views.log.documentPlaceholder')"
:loading="optionLoading"
@change="changeDocument"
>
<el-option
v-for="item in documentList"
:key="item.id"
:label="item.name"
:value="item.id"
>
{{ item.name }}
</el-option>
</el-select>
</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" @click="submitForm(formRef)" :loading="loading">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, reactive } from 'vue'
import { useRoute } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import logApi from '@/api/log'
import imageApi from '@/api/image'
import useStore from '@/stores'
import { t } from '@/locales'
const { application, document, user } = useStore()
const route = useRoute()
const {
params: { id }
} = route as any
const emit = defineEmits(['refresh'])
const formRef = ref()
const toolbars = [
'bold',
'underline',
'italic',
'-',
'title',
'strikeThrough',
'sub',
'sup',
'quote',
'unorderedList',
'orderedList',
'task',
'-',
'codeRow',
'code',
'link',
'image',
'table',
'mermaid',
'katex',
'-',
'revoke',
'next',
'=',
'pageFullscreen',
'preview',
'htmlPreview'
] as any[]
const footers = ['markdownTotal', 0, '=', 1, 'scrollSwitch']
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
const form = ref<any>({
chat_id: '',
record_id: '',
problem_text: '',
title: '',
content: '',
dataset_id: '',
document_id: ''
})
const rules = reactive<FormRules>({
content: [{ required: true, message: t('views.log.form.content.placeholder'), trigger: 'blur' }],
dataset_id: [
{ required: true, message: t('views.log.selectDatasetPlaceholder'), trigger: 'change' }
],
document_id: [{ required: true, message: t('views.log.documentPlaceholder'), trigger: 'change' }]
})
const datasetList = ref<any[]>([])
const documentList = ref<any[]>([])
const optionLoading = ref(false)
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
chat_id: '',
record_id: '',
problem_text: '',
title: '',
content: '',
dataset_id: '',
document_id: ''
}
datasetList.value = []
documentList.value = []
formRef.value?.clearValidate()
}
})
const onUploadImg = async (files: any, callback: any) => {
const res = await Promise.all(
files.map((file: any) => {
return new Promise((rev, rej) => {
const fd = new FormData()
fd.append('file', file)
imageApi
.postImage(fd)
.then((res: any) => {
rev(res)
})
.catch((error) => rej(error))
})
})
)
callback(res.map((item) => item.data))
}
function changeDataset(dataset_id: string) {
localStorage.setItem(id + 'chat_dataset_id', dataset_id)
form.value.document_id = ''
getDocument(dataset_id)
}
function changeDocument(document_id: string) {
localStorage.setItem(id + 'chat_document_id', document_id)
}
function getDocument(dataset_id: string) {
document.asyncGetAllDocument(dataset_id, loading).then((res: any) => {
documentList.value = res.data
if (localStorage.getItem(id + 'chat_document_id')) {
form.value.document_id = localStorage.getItem(id + 'chat_document_id') as string
}
if (!documentList.value.find((v) => v.id === form.value.document_id)) {
form.value.document_id = ''
}
})
}
function getDataset() {
application.asyncGetApplicationDataset(id, loading).then((res: any) => {
datasetList.value = res.data
if (localStorage.getItem(id + 'chat_dataset_id')) {
form.value.dataset_id = localStorage.getItem(id + 'chat_dataset_id') as string
if (!datasetList.value.find((v) => v.id === form.value.dataset_id)) {
form.value.dataset_id = ''
form.value.document_id = ''
} else {
getDocument(form.value.dataset_id)
}
}
})
}
const open = (data: any) => {
getDataset()
form.value.chat_id = data.chat_id
form.value.record_id = data.id
form.value.problem_text = data.problem_text ? data.problem_text.substring(0, 256) : ''
form.value.content = data.answer_text
formRef.value?.clearValidate()
dialogVisible.value = true
}
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
const obj = {
title: form.value.title,
content: form.value.content,
problem_text: form.value.problem_text
}
logApi
.putChatRecordLog(
id,
form.value.chat_id,
form.value.record_id,
form.value.dataset_id,
form.value.document_id,
obj,
loading
)
.then((res: any) => {
emit('refresh', res.data)
dialogVisible.value = false
})
}
})
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,174 @@
<template>
<el-dialog
:title="$t('views.log.editMark')"
v-model="dialogVisible"
width="600"
class="edit-mark-dialog"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<template #header="{ titleId, titleClass }">
<div class="flex-between">
<h4 :id="titleId" :class="titleClass">{{ $t('views.chatLog.editMark') }}</h4>
<div class="text-right">
<el-button text @click="isEdit = true" v-if="!isEdit">
<el-icon><EditPen /></el-icon>
</el-button>
<el-button text style="margin-left: 4px" @click="deleteMark">
<el-icon><Delete /></el-icon>
</el-button>
<el-divider direction="vertical" />
</div>
</div>
</template>
<el-scrollbar>
<div style="min-height: 250px; max-height: 350px" v-loading="loading">
<el-form
v-if="isEdit"
ref="formRef"
:model="form"
label-position="top"
require-asterisk-position="right"
:rules="rules"
@submit.prevent
>
<el-form-item prop="content">
<el-input
v-model="form.content"
:placeholder="$t('views.chatLog.form.content.placeholder')"
:maxlength="100000"
show-word-limit
:rows="15"
type="textarea"
>
</el-input>
</el-form-item>
</el-form>
<span v-else class="pre-wrap">{{ form?.content }}</span>
</div>
</el-scrollbar>
<template #footer>
<span class="dialog-footer" v-if="isEdit">
<el-button @click.prevent="isEdit = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit(formRef)" :loading="loading">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, reactive } from 'vue'
import { useRoute } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import chatLogApi from '@/api/application/chat-log'
import useStore from '@/stores'
import { t } from '@/locales'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
const route = useRoute()
const {
params: { id },
} = route as any
const { paragraph } = useStore()
const emit = defineEmits(['refresh'])
const formRef = ref()
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
const form = ref<any>({})
const isEdit = ref(false)
const detail = ref<any>({})
const rules = reactive<FormRules>({
content: [
{ required: true, message: t('views.chatLog.form.content.placeholder'), trigger: 'blur' },
],
})
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {}
isEdit.value = false
}
})
function deleteMark() {
chatLogApi
.delMarkChatRecord(
id as string,
detail.value.chat_id,
detail.value.id,
form.value.dataset,
form.value.document,
form.value.id,
loading,
)
.then(() => {
emit('refresh')
MsgSuccess(t('common.deleteSuccess'))
dialogVisible.value = false
})
}
function getMark(data: any) {
chatLogApi
.getMarkChatRecord(
id as string,
data.chat_id,
data.id,
data.dataset,
data.document,
loading,
)
.then((res: any) => {
if (res.data.length > 0) {
form.value = res.data[0]
}
})
}
const open = (data: any) => {
detail.value = data
getMark(data)
dialogVisible.value = true
}
const submit = async (formEl: FormInstance) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
paragraph
.asyncPutParagraph(
form.value.dataset,
form.value.document,
form.value.id,
{
content: form.value.content,
},
loading,
)
.then((res) => {
dialogVisible.value = false
})
}
})
}
defineExpose({ open })
</script>
<style lang="scss" scoped>
.edit-mark-dialog {
.el-dialog__header.show-close {
padding-right: 15px;
}
.el-dialog__headerbtn {
top: 13px;
}
}
</style>

View File

@ -0,0 +1,662 @@
<template>
<LayoutContainer :header="$t('views.chatLog.title')">
<div class="p-24">
<div class="mb-16">
<el-select
v-model="history_day"
class="mr-12"
@change="changeDayHandle"
style="width: 180px"
>
<el-option
v-for="item in dayOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-date-picker
v-if="history_day === 'other'"
v-model="daterangeValue"
type="daterange"
:start-placeholder="$t('views.applicationOverview.monitor.startDatePlaceholder')"
:end-placeholder="$t('views.applicationOverview.monitor.endDatePlaceholder')"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="changeDayRangeHandle"
/>
<el-input
v-model="search"
@change="getList"
:placeholder="$t('common.search')"
prefix-icon="Search"
class="w-240"
clearable
/>
<div style="display: flex; align-items: center" class="float-right">
<el-button @click="dialogVisible = true">{{
$t('views.chatLog.buttons.clearStrategy')
}}</el-button>
<el-button @click="exportLog">{{ $t('common.export') }}</el-button>
<el-button @click="openDocumentDialog" :disabled="multipleSelection.length === 0"
>{{ $t('views.chatLog.addToKnowledge') }}
</el-button>
</div>
</div>
<app-table
:data="tableData"
:pagination-config="paginationConfig"
@sizeChange="getList"
@changePage="getList"
@row-click="rowClickHandle"
v-loading="loading"
:row-class-name="setRowClass"
@selection-change="handleSelectionChange"
class="log-table"
ref="multipleTableRef"
>
<el-table-column type="selection" width="55" />
<el-table-column
prop="abstract"
:label="$t('views.chatLog.table.abstract')"
show-overflow-tooltip
/>
<el-table-column
prop="chat_record_count"
:label="$t('views.chatLog.table.chat_record_count')"
align="right"
/>
<el-table-column prop="star_num" align="right">
<template #header>
<div>
<span>{{ $t('views.chatLog.table.feedback.label') }}</span>
<el-popover :width="200" trigger="click" :visible="popoverVisible">
<template #reference>
<el-button
style="margin-top: -2px"
:type="filter.min_star || filter.min_trample ? 'primary' : ''"
link
@click="popoverVisible = !popoverVisible"
>
<el-icon>
<Filter />
</el-icon>
</el-button>
</template>
<div class="filter">
<div class="form-item mb-16">
<div @click.stop>
{{ $t('views.chatLog.table.feedback.star') }} >=
<el-input-number
v-model="filter.min_star"
:min="0"
:step="1"
:value-on-clear="0"
controls-position="right"
style="width: 80px"
size="small"
step-strictly
/>
</div>
</div>
<div class="form-item mb-16">
<div @click.stop>
{{ $t('views.chatLog.table.feedback.trample') }} >=
<el-input-number
v-model="filter.min_trample"
:min="0"
:step="1"
:value-on-clear="0"
controls-position="right"
style="width: 80px"
size="small"
step-strictly
/>
</div>
</div>
</div>
<div class="text-right">
<el-button size="small" @click="filterChange('clear')">{{
$t('common.clear')
}}</el-button>
<el-button type="primary" @click="filterChange" size="small">{{
$t('common.confirm')
}}</el-button>
</div>
</el-popover>
</div>
</template>
<template #default="{ row }">
<span class="mr-8" v-if="!row.trample_num && !row.star_num"> - </span>
<span class="mr-8" v-else>
<span v-if="row.star_num">
<AppIcon iconName="app-like-color"></AppIcon>
{{ row.star_num }}
</span>
<span v-if="row.trample_num" class="ml-8">
<AppIcon iconName="app-oppose-color"></AppIcon>
{{ row.trample_num }}
</span>
</span>
</template>
</el-table-column>
<el-table-column prop="mark_sum" :label="$t('views.chatLog.table.mark')" align="right" />
<el-table-column prop="asker" :label="$t('views.chatLog.table.user')">
<template #default="{ row }">
{{ row.asker?.user_name }}
</template>
</el-table-column>
<el-table-column :label="$t('views.chatLog.table.recenTimes')" width="180">
<template #default="{ row }">
{{ datetimeFormat(row.update_time) }}
</template>
</el-table-column>
</app-table>
</div>
<ChatRecordDrawer
:next="nextChatRecord"
:pre="preChatRecord"
ref="ChatRecordRef"
v-model:chatId="currentChatId"
v-model:currentAbstract="currentAbstract"
:application="detail"
:pre_disable="pre_disable"
:next_disable="next_disable"
@refresh="refresh"
/>
<el-dialog
:title="$t('views.chatLog.buttons.clearStrategy')"
v-model="dialogVisible"
width="25%"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<span>{{ $t('common.delete') }}</span>
<el-input-number
v-model="days"
controls-position="right"
:min="1"
:max="100000"
:value-on-clear="0"
step-strictly
style="width: 110px; margin-left: 8px; margin-right: 8px"
></el-input-number>
<span>{{ $t('views.chatLog.daysText') }}</span>
<template #footer>
<div class="dialog-footer" style="margin-top: 16px">
<el-button @click="dialogVisible = false">{{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="saveCleanTime">
{{ $t('common.save') }}
</el-button>
</div>
</template>
</el-dialog>
<el-dialog
:title="$t('views.chatLog.addToKnowledge')"
v-model="documentDialogVisible"
width="50%"
: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.selectKnowledge')" prop="knowledge_id">
<el-select
v-model="form.knowledge_id"
filterable
:placeholder="$t('views.chatLog.selectKnowledgePlaceholder')"
:loading="optionLoading"
@change="changeKnowledge"
>
<el-option
v-for="item in knowledgeList"
:key="item.id"
:label="item.name"
:value="item.id"
>
<span class="flex align-center">
<AppAvatar
v-if="!item.knowledge_id && item.type === '1'"
class="mr-12 avatar-purple"
shape="square"
:size="24"
>
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="!item.knowledge_id && item.type === '2'"
class="mr-12 avatar-purple"
shape="square"
:size="24"
style="background: none"
>
<img src="@/assets/logo_lark.svg" style="width: 100%" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="!item.knowledge_id && item.type === '0'"
class="mr-12 avatar-blue"
shape="square"
:size="24"
>
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
{{ item.name }}
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('views.chatLog.saveToDocument')" prop="document_id">
<el-select
v-model="form.document_id"
filterable
:placeholder="$t('views.chatLog.documentPlaceholder')"
:loading="optionLoading"
@change="changeDocument"
>
<el-option
v-for="item in documentList"
:key="item.id"
:label="item.name"
:value="item.id"
>
{{ item.name }}
</el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="documentDialogVisible = false">
{{ $t('common.cancel') }}
</el-button>
<el-button type="primary" @click="submitForm(formRef)" :loading="documentLoading">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</LayoutContainer>
</template>
<script setup lang="ts">
import { ref, type Ref, onMounted, reactive, computed } from 'vue'
import { useRoute } from 'vue-router'
import { cloneDeep } from 'lodash'
import ChatRecordDrawer from './component/ChatRecordDrawer.vue'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import chatLogApi from '@/api/application/chat-log'
import { beforeDay, datetimeFormat, nowDate } from '@/utils/time'
import useStore from '@/stores'
import type { Dict } from '@/api/type/common'
import { t } from '@/locales'
import type { FormInstance, FormRules } from 'element-plus'
import { ElTable } from 'element-plus'
const { application, chatLog, document, user } = useStore()
const route = useRoute()
const {
params: { id },
} = route as any
const emit = defineEmits(['refresh'])
const formRef = ref()
const dayOptions = [
{
value: 7,
// @ts-ignore
label: t('views.applicationOverview.monitor.pastDayOptions.past7Days'), // 使 t
},
{
value: 30,
label: t('views.applicationOverview.monitor.pastDayOptions.past30Days'),
},
{
value: 90,
label: t('views.applicationOverview.monitor.pastDayOptions.past90Days'),
},
{
value: 183,
label: t('views.applicationOverview.monitor.pastDayOptions.past183Days'),
},
{
value: 'other',
label: t('views.applicationOverview.monitor.pastDayOptions.other'),
},
]
const daterangeValue = ref('')
//
const daterange = ref({
start_time: '',
end_time: '',
})
const multipleTableRef = ref<InstanceType<typeof ElTable>>()
const multipleSelection = ref<any[]>([])
const ChatRecordRef = ref()
const loading = ref(false)
const documentLoading = ref(false)
const paginationConfig = reactive({
current_page: 1,
page_size: 20,
total: 0,
})
const dialogVisible = ref(false)
const documentDialogVisible = ref(false)
const days = ref<number>(180)
const tableData = ref<any[]>([])
const tableIndexMap = computed<Dict<number>>(() => {
return tableData.value
.map((row, index) => ({
[row.id]: index,
}))
.reduce((pre, next) => ({ ...pre, ...next }), {})
})
const history_day = ref<number | string>(7)
const search = ref('')
const detail = ref<any>(null)
const currentChatId = ref<string>('')
const currentAbstract = ref<string>('')
const popoverVisible = ref(false)
const defaultFilter = {
min_star: 0,
min_trample: 0,
comparer: 'and',
}
const filter = ref<any>({
min_star: 0,
min_trample: 0,
comparer: 'and',
})
const form = ref<any>({
knowledge_id: '',
document_id: '',
})
const rules = reactive<FormRules>({
knowledge_id: [
{ required: true, message: t('views.chatLog.selectKnowledgePlaceholder'), trigger: 'change' },
],
document_id: [
{
required: true,
message: t('views.chatLog.documentPlaceholder'),
trigger: 'change',
},
],
})
const optionLoading = ref(false)
const documentList = ref<any[]>([])
function filterChange(val: string) {
if (val === 'clear') {
filter.value = cloneDeep(defaultFilter)
}
getList()
popoverVisible.value = false
}
/**
* 下一页
*/
const nextChatRecord = () => {
let index = tableIndexMap.value[currentChatId.value] + 1
if (index >= tableData.value.length) {
if (
index + (paginationConfig.current_page - 1) * paginationConfig.page_size >=
paginationConfig.total - 1
) {
return
}
paginationConfig.current_page = paginationConfig.current_page + 1
getList().then(() => {
index = 0
currentChatId.value = tableData.value[index].id
currentAbstract.value = tableData.value[index].abstract
})
} else {
currentChatId.value = tableData.value[index].id
currentAbstract.value = tableData.value[index].abstract
}
}
const pre_disable = computed(() => {
let index = tableIndexMap.value[currentChatId.value] - 1
return index < 0 && paginationConfig.current_page <= 1
})
const next_disable = computed(() => {
let index = tableIndexMap.value[currentChatId.value] + 1
return (
index >= tableData.value.length &&
index + (paginationConfig.current_page - 1) * paginationConfig.page_size >=
paginationConfig.total - 1
)
})
/**
* 上一页
*/
const preChatRecord = () => {
let index = tableIndexMap.value[currentChatId.value] - 1
if (index < 0) {
if (paginationConfig.current_page <= 1) {
return
}
paginationConfig.current_page = paginationConfig.current_page - 1
getList().then(() => {
index = paginationConfig.page_size - 1
currentChatId.value = tableData.value[index].id
currentAbstract.value = tableData.value[index].abstract
})
} else {
currentChatId.value = tableData.value[index].id
currentAbstract.value = tableData.value[index].abstract
}
}
function rowClickHandle(row: any, column?: any) {
if (column && column.type === 'selection') {
return
}
currentChatId.value = row.id
currentAbstract.value = row.abstract
ChatRecordRef.value.open()
}
const setRowClass = ({ row }: any) => {
return currentChatId.value === row?.id ? 'highlight' : ''
}
const handleSelectionChange = (val: any[]) => {
multipleSelection.value = val
}
// function deleteLog(row: any) {
// MsgConfirm(`${row.abstract} ?`, ``, {
// confirmButtonText: t('common.delete'),
// confirmButtonClass: 'danger'
// })
// .then(() => {
// loading.value = true
// logApi.delChatLog(id as string, row.id, loading).then(() => {
// MsgSuccess(t('common.deleteSuccess'))
// getList()
// })
// })
// .catch(() => {})
// }
function getList() {
let obj: any = {
start_time: daterange.value.start_time,
end_time: daterange.value.end_time,
...filter.value,
}
if (search.value) {
obj = { ...obj, abstract: search.value }
}
return chatLog.asyncGetChatLog(id as string, paginationConfig, obj, loading).then((res: any) => {
tableData.value = res.data.records
if (currentChatId.value) {
currentChatId.value = tableData.value[0]?.id
}
paginationConfig.total = res.data.total
})
}
function getDetail(isLoading = false) {
application
.asyncGetApplicationDetail(id as string, isLoading ? loading : undefined)
.then((res: any) => {
detail.value = res.data
days.value = res.data.clean_time
})
}
const exportLog = () => {
const arr: string[] = []
multipleSelection.value.map((v) => {
if (v) {
arr.push(v.id)
}
})
if (detail.value) {
let obj: any = {
start_time: daterange.value.start_time,
end_time: daterange.value.end_time,
...filter.value,
}
if (search.value) {
obj = { ...obj, abstract: search.value }
}
chatLogApi.postExportChatLog(detail.value.id, detail.value.name, obj, { select_ids: arr }, loading)
}
}
function refresh() {
getList()
}
function changeDayRangeHandle(val: string) {
daterange.value.start_time = val[0]
daterange.value.end_time = val[1]
getList()
}
function changeDayHandle(val: number | string) {
if (val !== 'other') {
daterange.value.start_time = beforeDay(val)
daterange.value.end_time = nowDate
getList()
}
}
function saveCleanTime() {
const obj = {
clean_time: days.value,
}
application
.asyncPutApplication(id as string, obj, loading)
.then(() => {
MsgSuccess(t('common.saveSuccess'))
dialogVisible.value = false
getDetail(true)
})
.catch(() => {
dialogVisible.value = false
})
}
function changeKnowledge(knowledge_id: string) {
localStorage.setItem(id + 'chat_knowledge_id', knowledge_id)
form.value.document_id = ''
getDocument(knowledge_id)
}
function changeDocument(document_id: string) {
localStorage.setItem(id + 'chat_document_id', document_id)
}
const knowledgeList = ref<any[]>([])
function getKnowledge() {
application.asyncGetApplicationKnowledge(id, documentLoading).then((res: any) => {
knowledgeList.value = res.data
if (localStorage.getItem(id + 'chat_knowledge_id')) {
form.value.knowledge_id = localStorage.getItem(id + 'chat_knowledge_id') as string
if (!knowledgeList.value.find((v) => v.id === form.value.knowledge_id)) {
form.value.knowledge_id = ''
form.value.document_id = ''
} else {
getDocument(form.value.knowledge_id)
}
}
})
}
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
const arr: string[] = []
multipleSelection.value.map((v) => {
if (v) {
arr.push(v.id)
}
})
await formEl.validate((valid) => {
if (valid) {
const obj = {
document_id: form.value.document_id,
knowledge_id: form.value.knowledge_id,
chat_ids: arr,
}
chatLogApi.postChatLogAddKnowledge(id, obj, documentLoading).then((res: any) => {
multipleTableRef.value?.clearSelection()
documentDialogVisible.value = false
})
}
})
}
function getDocument(knowledge_id: string) {
document.asyncGetAllDocument(knowledge_id, documentLoading).then((res: any) => {
documentList.value = res.data
if (localStorage.getItem(id + 'chat_document_id')) {
form.value.document_id = localStorage.getItem(id + 'chat_document_id') as string
}
if (!documentList.value.find((v) => v.id === form.value.document_id)) {
form.value.document_id = ''
}
})
}
function openDocumentDialog() {
getKnowledge()
formRef.value?.clearValidate()
documentDialogVisible.value = true
}
onMounted(() => {
changeDayHandle(history_day.value)
getDetail()
})
</script>
<style lang="scss" scoped>
.log-table {
:deep(tr) {
cursor: pointer;
}
}
</style>

View File

@ -10,10 +10,10 @@
label-width="auto"
ref="DatasetNodeFormRef"
>
<el-form-item :label="$t('views.log.selectDataset')">
<el-form-item :label="$t('views.chatLog.selectDataset')">
<template #label>
<div class="flex-between">
<span>{{ $t('views.log.selectDataset') }}</span>
<span>{{ $t('views.chatLog.selectDataset') }}</span>
<el-button type="primary" link @click="openDatasetDialog">
<el-icon><Plus /></el-icon>
</el-button>