feat: 对话日志

This commit is contained in:
wangdan-fit2cloud 2023-12-07 19:28:38 +08:00
parent 717cf97758
commit eb0b630fbb
16 changed files with 482 additions and 41 deletions

View File

@ -25,7 +25,7 @@ const listSplitPattern: (
}
/**
*
*
* @param dataset_id,
* page {
"current_page": "string",
@ -49,6 +49,13 @@ const getDocument: (
)
}
const getAllDocument: (dataset_id: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
dataset_id,
loading
) => {
return get(`${prefix}/${dataset_id}/document`, undefined, loading)
}
/**
*
* @param
@ -117,6 +124,7 @@ const getDocumentDetail: (dataset_id: string, document_id: string) => Promise<Re
export default {
postSplitDocument,
getDocument,
getAllDocument,
postDocument,
putDocument,
delDocument,

View File

@ -64,8 +64,43 @@ const getChatRecordLog: (
)
}
/**
*
* @param
* application_id, chart_id, chart_record_id, dataset_id, document_id
* data {
"title": "string",
"content": "string",
}
*/
const putChatRecordLog: (
applicaiton_id: String,
chart_id: String,
chart_record_id: String,
dataset_id: String,
document_id: String,
data: any,
loading?: Ref<boolean>
) => Promise<Result<any>> = (
applicaiton_id,
chart_id,
chart_record_id,
dataset_id,
document_id,
data,
loading
) => {
return put(
`${prefix}/${applicaiton_id}/chat/${chart_id}/chat_record/${chart_record_id}/dataset/${dataset_id}/document_id/${document_id}/improve`,
data,
undefined,
loading
)
}
export default {
getChatLog,
delChatLog,
getChatRecordLog
getChatRecordLog,
putChatRecordLog
}

View File

@ -23,7 +23,7 @@ interface chatType {
*/
is_stop?: boolean
record_id: string
vote_status: string
vote_status: string,
}
export class ChatRecordManage {

View File

@ -1 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5 2.5H12.5V6.25H13.75V8.75H6.875C5.49429 8.75 4.375 9.86929 4.375 11.25V25C4.375 26.3807 5.49429 27.5 6.875 27.5H23.125C24.5057 27.5 25.625 26.3807 25.625 25V11.25C25.625 9.86929 24.5057 8.75 23.125 8.75H16.25V6.25H17.5V2.5ZM8.75 16.25H12.5V20H8.75V16.25ZM21.25 16.25V20H17.5V16.25H21.25Z" fill="url(#paint0_linear_1514_13484)"/>
<path d="M2.5 15H0V21.25H2.5V15Z" fill="url(#paint1_linear_1514_13484)"/>
<path d="M27.5 15H30V21.25H27.5V15Z" fill="url(#paint2_linear_1514_13484)"/>
<defs>
<linearGradient id="paint0_linear_1514_13484" x1="15" y1="2.5" x2="15" y2="27.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#3370FF"/>
<stop offset="1" stop-color="#7F3BF5"/>
</linearGradient>
<linearGradient id="paint1_linear_1514_13484" x1="15" y1="2.5" x2="15" y2="27.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#3370FF"/>
<stop offset="1" stop-color="#7F3BF5"/>
</linearGradient>
<linearGradient id="paint2_linear_1514_13484" x1="15" y1="2.5" x2="15" y2="27.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#3370FF"/>
<stop offset="1" stop-color="#7F3BF5"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,65 @@
<template>
<div>
<el-tooltip effect="dark" content="修改内容" placement="top">
<el-button text @click="editContent(data)">
<el-icon><EditPen /></el-icon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
<el-tooltip effect="dark" content="复制" placement="top">
<el-button text @click="copyClick(data?.answer_text)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" v-if="buttonData?.vote_status !== '-1'" />
<el-button text disabled v-if="buttonData?.vote_status === '0'">
<AppIcon iconName="app-like-color"></AppIcon>
</el-button>
<el-button text disabled v-if="buttonData?.vote_status === '1'">
<AppIcon iconName="app-oppose-color"></AppIcon>
</el-button>
<EditContentDialog
ref="EditContentDialogRef"
:chartId="chartId"
@updateContent="updateContent"
/>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, watch, onMounted } from 'vue'
import { copyClick } from '@/utils/clipboard'
import EditContentDialog from '@/views/log/component/EditContentDialog.vue'
const props = defineProps({
data: {
type: Object,
default: () => {}
},
applicationId: {
type: String,
default: ''
},
chartId: {
type: String,
default: ''
},
log: Boolean
})
const emit = defineEmits(['update:data'])
const EditContentDialogRef = ref()
const buttonData = ref(props.data)
const loading = ref(false)
function editContent(data: any) {
EditContentDialogRef.value.open(data)
}
function updateContent(data: any) {
emit('update:data', data)
}
</script>
<style lang="scss" scoped></style>

View File

@ -71,7 +71,8 @@ const props = defineProps({
chartId: {
type: String,
default: ''
}
},
log: Boolean
})
const emit = defineEmits(['update:data', 'regeneration'])
@ -79,7 +80,7 @@ const emit = defineEmits(['update:data', 'regeneration'])
const buttonData = ref(props.data)
const loading = ref(false)
function regeneration() {
function regeneration() {
emit('regeneration')
}

View File

@ -22,7 +22,8 @@
<template v-for="(item, index) in data?.example" :key="index">
<div
@click="quickProblemHandel(item)"
class="problem-button cursor ellipsis-2"
class="problem-button ellipsis-2"
:class="log ? 'disabled' : 'cursor'"
v-if="item"
>
<el-icon><EditPen /></el-icon>
@ -66,7 +67,14 @@
:inner_suffix="false"
></MarkdownRenderer>
</el-card>
<div class="flex-between mt-8">
<div class="flex-between mt-8" v-if="log">
<el-text type="info">
消耗 {{ item?.message_tokens + item?.answer_tokens }} tokens
</el-text>
<LogOperationButton :data="item" :applicationId="appId" :chartId="item.id" />
</div>
<div class="flex-between mt-8" v-else>
<div>
<el-button
type="primary"
@ -94,7 +102,7 @@
</template>
</div>
</el-scrollbar>
<div class="ai-chat__operate p-24" v-if="!record">
<div class="ai-chat__operate p-24" v-if="!log">
<div class="operate-textarea flex">
<el-input
ref="quickInputRef"
@ -124,8 +132,9 @@
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onUpdated, computed } from 'vue'
import { ref, nextTick, onUpdated, computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import LogOperationButton from './LogOperationButton.vue'
import OperationButton from './OperationButton.vue'
import applicationApi from '@/api/application'
import { ChatManagement, type chatType } from '@/api/type/application'
@ -142,7 +151,11 @@ const props = defineProps({
default: () => {}
},
appId: String,
record: Boolean
log: Boolean,
record: {
type: Array<chatType[]>,
default: () => []
}
})
const { application } = useStore()
@ -152,17 +165,31 @@ const dialogScrollbar = ref()
const loading = ref(false)
const inputValue = ref('')
const chartOpenId = ref('')
const chatList = ref<chatType[]>([])
const chatList = ref<any[]>([])
const isDisabledChart = computed(
() => !(inputValue.value && (props.appId || (props.data?.name && props.data?.model_id)))
)
watch(
() => props.record,
(value) => {
if (props.log) {
chatList.value = value
}
},
{
immediate: true
}
)
function quickProblemHandel(val: string) {
inputValue.value = val
nextTick(() => {
quickInputRef.value?.focus()
})
if (!props.log) {
inputValue.value = val
nextTick(() => {
quickInputRef.value?.focus()
})
}
}
function sendChatHandle(event: any) {
@ -286,7 +313,9 @@ function handleScrollBottom() {
}
onUpdated(() => {
handleScrollBottom()
if (!props.log) {
handleScrollBottom()
}
})
</script>
<style lang="scss" scoped>
@ -330,6 +359,11 @@ onUpdated(() => {
&:hover {
background: var(--el-color-primary-light-9);
}
&.disabled {
&:hover {
background: var(--app-layout-bg-color);
}
}
:deep(.el-icon) {
color: var(--el-color-primary);
}

View File

@ -3,7 +3,9 @@
<div class="flex-center h-full">
<div class="app-title-container flex-center">
<div class="app-title-icon"></div>
<div class="app-title-text app-logo-font">{{ defaultTitle }}</div>
<div class="app-title-text app-logo-font ml-4">
{{ defaultTitle }}
</div>
</div>
<TopMenu></TopMenu>
</div>
@ -26,10 +28,11 @@ const defaultTitle = import.meta.env.VITE_APP_TITLE
.app-title-container {
margin-right: 45px;
.app-title-icon {
background-image: url('@/assets/logo.png');
background-image: url('@/assets/logo.svg');
background-size: 100% 100%;
width: 34px;
height: 34px;
width: 30px;
height: 30px;
background-position: center -1px;
}
.app-title-text {

View File

@ -7,6 +7,7 @@ import useDatasetStore from './modules/dataset'
import useParagraphStore from './modules/paragraph'
import useModelStore from './modules/model'
import useApplicationStore from './modules/application'
import useDocumentStore from './modules/document'
const useStore = () => ({
common: useCommonStore(),
@ -14,7 +15,8 @@ const useStore = () => ({
dataset: useDatasetStore(),
paragraph: useParagraphStore(),
model: useModelStore(),
application: useApplicationStore()
application: useApplicationStore(),
document: useDocumentStore()
})
export default useStore

View File

@ -34,6 +34,19 @@ const useApplicationStore = defineStore({
})
},
async asyncGetApplicationDataset(id: string, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
applicationApi
.getApplicationDataset(id, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
async asyncGetAccessToken(id: string, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
applicationApi

View File

@ -0,0 +1,24 @@
import { defineStore } from 'pinia'
import documentApi from '@/api/document'
import { type Ref } from 'vue'
const useDocumentStore = defineStore({
id: 'documents',
state: () => ({}),
actions: {
async asyncGetAllDocument(id: string, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
documentApi
.getAllDocument(id, loading)
.then((res) => {
resolve(res)
})
.catch((error) => {
reject(error)
})
})
}
}
})
export default useDocumentStore

View File

@ -195,3 +195,11 @@
padding: 16px 24px;
}
}
.el-cascader-node {
padding-left: 2px;
}
.el-cascader-node__prefix {
right: 10px;
left: auto;
}

View File

@ -191,7 +191,7 @@ import type { Provider } from '@/api/type/model'
import { realatedObject } from '@/utils/utils'
import { MsgSuccess } from '@/utils/message'
import useStore from '@/stores'
import type Result from '@/request/Result'
const { model, dataset, application, user } = useStore()
const router = useRouter()
@ -293,12 +293,12 @@ function getDetail() {
function getDataset() {
if (id) {
applicationApi.getApplicationDataset(id, datasetLoading).then((res) => {
application.asyncGetApplicationDataset(id, datasetLoading).then((res: any) => {
datasetList.value = res.data
})
} else {
dataset.asyncGetAllDateset(datasetLoading).then((res:any) => {
datasetList.value = res.data?.filter((v:any) => v.user_id === user.userInfo?.id)
dataset.asyncGetAllDateset(datasetLoading).then((res: any) => {
datasetList.value = res.data?.filter((v: any) => v.user_id === user.userInfo?.id)
})
}
}

View File

@ -7,29 +7,51 @@
class="chat-record-drawer"
>
<template #header>
<h4>应用标题</h4>
<h4>{{ data?.name }}</h4>
</template>
<AiChat record></AiChat>
<div v-loading="paginationConfig.current_page === 1 && loading">
<div v-infinite-scroll="loadDataset" :infinite-scroll-disabled="disabledScroll">
<AiChat :data="data" :record="recordList" log></AiChat>
</div>
<div style="padding: 16px 10px">
<el-divider class="custom-divider" v-if="recordList.length > 0 && loading">
<el-text type="info"> 加载中...</el-text>
</el-divider>
<el-divider class="custom-divider" v-if="noMore">
<el-text type="info"> 到底啦</el-text>
</el-divider>
</div>
</div>
<template #footer>
<div>
<el-button>上一条</el-button>
<el-button>下一条</el-button>
<el-button :disabled="loading">上一条</el-button>
<el-button :disabled="loading">下一条</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ref, reactive, computed } from 'vue'
import { useRoute } from 'vue-router'
import logApi from '@/api/log'
import { type chatType } from '@/api/type/application'
const props = defineProps({
data: {
type: Object,
default: () => {}
}
})
const route = useRoute()
const {
params: { id }
} = route
const loading = ref(false)
const visible = ref(false)
const recordList = ref<chatType[]>([])
const currentChatId = ref('')
const paginationConfig = reactive({
current_page: 1,
@ -37,17 +59,43 @@ const paginationConfig = reactive({
total: 0
})
function closeHandel() {}
const noMore = computed(
() =>
recordList.value.length > 0 &&
recordList.value.length === paginationConfig.total &&
paginationConfig.total > 20 &&
!loading.value
)
const disabledScroll = computed(
() => recordList.value.length > 0 && (loading.value || noMore.value)
)
function getChatRecord(chatId:string) {
logApi.getChatRecordLog(id as string, chatId, paginationConfig, loading).then((res) => {
// tableData.value = res.data.records
paginationConfig.total = res.data.total
})
function closeHandel() {
recordList.value = []
currentChatId.value = ''
paginationConfig.total = 0
paginationConfig.current_page = 1
}
const open = (id:string) => {
getChatRecord(id)
function loadDataset() {
if (paginationConfig.total > paginationConfig.page_size) {
paginationConfig.current_page += 1
getChatRecord()
}
}
function getChatRecord() {
logApi
.getChatRecordLog(id as string, currentChatId.value, paginationConfig, loading)
.then((res) => {
paginationConfig.total = res.data.total
recordList.value = [...recordList.value, ...res.data.records]
})
}
const open = (id: string) => {
currentChatId.value = id
getChatRecord()
visible.value = true
}
defineExpose({

View File

@ -0,0 +1,172 @@
<template>
<el-dialog title="修改内容" v-model="dialogVisible" width="600">
<el-form
ref="formRef"
:model="form"
label-position="top"
require-asterisk-position="right"
:rules="rules"
@submit.prevent
>
<el-form-item label="关联问题">
<span>{{ form.problem_text }}</span>
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input
v-model="form.content"
placeholder="请输入内容"
maxlength="1024"
show-word-limit
:rows="8"
type="textarea"
>
</el-input>
</el-form-item>
<el-form-item label="标题">
<el-input v-model="form.title" placeholder="请给当前内容设置一个标题,以便管理查看">
</el-input>
</el-form-item>
<el-form-item label="保存至文档" prop="document">
<el-cascader v-model="form.document" :props="LoadDocument" placeholder="请选择文档">
<template #default="{ node, data }">
<span class="flex align-center">
<AppAvatar v-if="!node.isLeaf" class="mr-12" shape="square" :size="24">
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
{{ data.name }}
</span>
</template>
</el-cascader>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
<el-button type="primary" @click="submitForm(formRef)"> 保存 </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 type { CascaderProps } from 'element-plus'
import useStore from '@/stores'
const { application, document } = useStore()
const props = defineProps({
chartId: {
type: String,
default: ''
}
})
const route = useRoute()
const {
params: { id }
} = route as any
const emit = defineEmits(['updateContent'])
const formRef = ref()
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
const form = ref<any>({
record_id: '',
problem_text: '',
title: '',
content: '',
document: []
})
const rules = reactive<FormRules>({
content: [{ required: true, message: '请输入内容', trigger: 'blur' }],
document: [{ type: 'array', required: true, message: '请选择文档', trigger: 'change' }]
})
const datasetList = ref([])
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
record_id: '',
problem_text: '',
title: '',
content: '',
document: []
}
}
})
const LoadDocument: CascaderProps = {
lazy: true,
value: 'id',
label: 'name',
leaf: 'dataset_id',
lazyLoad(node, resolve: any) {
const { level, data } = node
if (data?.id) {
getDocument(data?.id as string, resolve)
} else {
getDataset(resolve)
}
}
}
function getDocument(id: string, resolve: any) {
document.asyncGetAllDocument(id, loading).then((res: any) => {
datasetList.value = res.data
resolve(datasetList.value)
})
}
function getDataset(resolve: any) {
application.asyncGetApplicationDataset(id, loading).then((res: any) => {
datasetList.value = res.data
resolve(datasetList.value)
})
}
const open = (data: any) => {
form.value.record_id = data.id
form.value.problem_text = data.problem_text
form.value.content = data.answer_text
dialogVisible.value = true
}
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
const obj = {
title: form.value.title,
content: form.value.content
}
logApi
.putChatRecordLog(
id,
props.chartId,
form.value.record_id,
form.value.document[0],
form.value.document[1],
obj,
loading
)
.then((res: any) => {
emit('updateContent', res.data)
loading.value = false
})
} else {
console.log('error submit!', fields)
}
})
}
defineExpose({ open })
</script>
<style lang="scss" scope></style>

View File

@ -62,7 +62,7 @@
</el-table-column>
</app-table>
</div>
<ChatRecordDrawer ref="ChatRecordRef" />
<ChatRecordDrawer ref="ChatRecordRef" :data="detail" />
</LayoutContainer>
</template>
<script setup lang="ts">
@ -72,6 +72,8 @@ import ChatRecordDrawer from './component/ChatRecordDrawer.vue'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import logApi from '@/api/log'
import { datetimeFormat } from '@/utils/time'
import useStore from '@/stores'
const { application } = useStore()
const route = useRoute()
const {
params: { id }
@ -107,6 +109,7 @@ const tableData = ref([])
const history_day = ref(7)
const search = ref('')
const detail = ref<any>(null)
function rowClickHandle(row: any) {
// router.push({ path: `/dataset/${id}/${row.id}` })
@ -152,8 +155,15 @@ function getList() {
})
}
function getDetail() {
application.asyncGetApplicationDetail(id as string, loading).then((res: any) => {
detail.value = res.data
})
}
onMounted(() => {
getList()
getDetail()
})
</script>
<style lang="scss" scoped></style>