MaxKB/ui/src/views/application/CreateAndSetting.vue
2024-04-15 16:52:41 +08:00

573 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<LayoutContainer
:header="id ? '设置' : '创建应用'"
:back-to="id ? '' : '-1'"
class="create-application"
>
<el-row v-loading="loading">
<el-col :span="10">
<div class="p-24 mb-16" style="padding-bottom: 0">
<h4 class="title-decoration-1">应用信息</h4>
</div>
<div class="scrollbar-height-left">
<el-scrollbar>
<el-form
hide-required-asterisk
ref="applicationFormRef"
:model="applicationForm"
:rules="rules"
label-position="top"
require-asterisk-position="right"
class="p-24"
style="padding-top: 0"
>
<el-form-item prop="name">
<template #label>
<div class="flex-between">
<span>应用名称 <span class="danger">*</span></span>
</div>
</template>
<el-input
v-model="applicationForm.name"
maxlength="64"
placeholder="请输入应用名称"
show-word-limit
/>
</el-form-item>
<el-form-item label="应用描述">
<el-input
v-model="applicationForm.desc"
type="textarea"
placeholder="描述该应用的应用场景及用途MaxKB 小助手回答用户提出的 MaxKB 产品使用问题"
:rows="3"
maxlength="256"
show-word-limit
/>
</el-form-item>
<el-form-item label="AI 模型" prop="model_id">
<template #label>
<div class="flex-between">
<span>AI 模型 <span class="danger">*</span></span>
</div>
</template>
<el-select
v-model="applicationForm.model_id"
placeholder="请选择 AI 模型"
class="w-full"
popper-class="select-model"
>
<el-option-group
v-for="(value, label) in modelOptions"
:key="value"
:label="realatedObject(providerOptions, label, 'provider')?.name"
>
<el-option
v-for="item in value.filter((v: any) => v.status === 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
>
<div class="flex">
<span
v-html="realatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
</div>
<el-icon class="check-icon" v-if="item.id === applicationForm.model_id"
><Check
/></el-icon>
</el-option>
<!-- 不可用 -->
<el-option
v-for="item in value.filter((v: any) => v.status !== 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
disabled
>
<div class="flex">
<span
v-html="realatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<span class="danger">(不可用)</span>
</div>
<el-icon class="check-icon" v-if="item.id === applicationForm.model_id"
><Check
/></el-icon>
</el-option>
</el-option-group>
<template #footer>
<div class="w-full text-left cursor" @click="openCreateModel()">
<el-button type="primary" link>
<el-icon class="mr-4"><Plus /></el-icon> 添加模型
</el-button>
</div>
</template>
</el-select>
</el-form-item>
<el-form-item label="提示词" prop="model_setting.prompt">
<template #label>
<div class="flex align-center">
<div class="flex-between mr-4">
<span>提示词 <span class="danger">*</span></span>
</div>
<el-tooltip effect="dark" placement="right">
<template #content
>通过调整提示词内容,可以引导大模型聊天方向,该提示词会被固定在上下文的开头。<br />可以使用变量:{data}
是携带知识库中已知信息;{question}是用户提出的问题。</template
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
</template>
<el-input
v-model="applicationForm.model_setting.prompt"
:rows="6"
type="textarea"
maxlength="2048"
:placeholder="defaultPrompt"
/>
</el-form-item>
<el-form-item label="多轮对话" @click.prevent>
<el-switch
size="small"
v-model="applicationForm.multiple_rounds_dialogue"
></el-switch>
</el-form-item>
<el-form-item label="关联知识库">
<template #label>
<div class="flex-between">
<span>关联知识库</span>
<div>
<el-popover :visible="popoverVisible" :width="214" trigger="click">
<template #reference>
<el-button type="primary" link @click="datasetSettingChange('open')">
<AppIcon iconName="app-operation" class="mr-4"></AppIcon>参数设置
</el-button>
</template>
<div class="dataset_setting">
<div class="form-item mb-16">
<div class="title flex align-center mb-8">
<span style="margin-right: 4px">相似度高于</span>
<el-tooltip
effect="dark"
content="相似度越高相关性越强。"
placement="right"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
<div @click.stop>
<el-input-number
v-model="dataset_setting.similarity"
:min="0"
:max="1"
:precision="3"
:step="0.1"
controls-position="right"
style="width: 180px"
/>
</div>
</div>
<div class="form-item mb-16">
<div class="title mb-8">引用分段数 TOP</div>
<div @click.stop>
<el-input-number
v-model="dataset_setting.top_n"
:min="1"
:max="10"
controls-position="right"
style="width: 180px"
/>
</div>
</div>
<div class="form-item mb-16">
<div class="title mb-8">最多引用字符数</div>
<div class="flex align-center">
<el-slider
v-model="dataset_setting.max_paragraph_char_number"
show-input
:show-input-controls="false"
:min="500"
:max="10000"
style="width: 180px"
class="custom-slider"
/>
</div>
</div>
</div>
<div class="text-right">
<el-button @click="popoverVisible = false">取消</el-button>
<el-button type="primary" @click="datasetSettingChange('close')"
>确认</el-button
>
</div>
</el-popover>
<el-button type="primary" link @click="openDatasetDialog"
><el-icon class="mr-4"><Plus /></el-icon>添加</el-button
>
</div>
</div>
</template>
<div class="w-full">
<el-text type="info" v-if="applicationForm.dataset_id_list?.length === 0"
>关联的知识库展示在这里</el-text
>
<el-row :gutter="12" v-else>
<!-- <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" class="mb-8">
<CardAdd
title="关联知识库"
@click="openDatasetDialog"
style="min-height: 50px; font-size: 14px"
/>
</el-col> -->
<el-col
:xs="24"
:sm="24"
:md="24"
:lg="12"
:xl="12"
class="mb-8"
v-for="(item, index) in applicationForm.dataset_id_list"
:key="index"
>
<el-card class="relate-dataset-card" shadow="never">
<div class="flex-between">
<div class="flex align-center">
<AppAvatar
v-if="realatedObject(datasetList, item, 'id')?.type === '1'"
class="mr-8 avatar-purple"
shape="square"
:size="32"
>
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar v-else class="mr-12" shape="square" :size="32">
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
<div class="ellipsis">
{{ realatedObject(datasetList, item, 'id')?.name }}
</div>
</div>
<el-button text @click="removeDataset(item)">
<el-icon><Close /></el-icon>
</el-button>
</div>
</el-card>
</el-col>
</el-row>
</div>
</el-form-item>
<el-form-item label="开场白">
<MdEditor
class="prologue-md-editor"
v-model="applicationForm.prologue"
:preview="false"
:toolbars="[]"
:footers="[]"
/>
</el-form-item>
<el-form-item @click.prevent>
<template #label>
<div class="flex align-center">
<span class="mr-4">问题优化</span>
<el-tooltip
effect="dark"
content="根据历史聊天优化完善当前问题,更利于匹配知识点。"
placement="right"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
</template>
<el-switch size="small" v-model="applicationForm.problem_optimization"></el-switch>
</el-form-item>
</el-form>
</el-scrollbar>
</div>
<div class="text-right border-t p-16">
<el-button v-if="!id" @click="router.push({ path: `/application` })"> 取消 </el-button>
<el-button type="primary" @click="submit(applicationFormRef)" :disabled="loading">
{{ id ? '保存' : '创建' }}
</el-button>
</div>
</el-col>
<el-col :span="14" class="p-24 border-l">
<h4 class="title-decoration-1 mb-16">调试预览</h4>
<div class="dialog-bg">
<h4 class="p-24">{{ applicationForm?.name || '应用名称' }}</h4>
<div class="scrollbar-height">
<AiChat :data="applicationForm"></AiChat>
</div>
</div>
</el-col>
</el-row>
<AddDatasetDialog
ref="AddDatasetDialogRef"
@addData="addDataset"
:data="datasetList"
@refresh="refresh"
:loading="datasetLoading"
/>
<CreateModelDialog
ref="createModelRef"
@submit="getModel"
@change="openCreateModel($event)"
></CreateModelDialog>
<SelectProviderDialog ref="selectProviderRef" @change="openCreateModel($event)" />
</LayoutContainer>
</template>
<script setup lang="ts">
import { reactive, ref, watch, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { groupBy, cloneDeep } from 'lodash'
import AddDatasetDialog from './components/AddDatasetDialog.vue'
import CreateModelDialog from '@/views/template/component/CreateModelDialog.vue'
import SelectProviderDialog from '@/views/template/component/SelectProviderDialog.vue'
import { MdEditor } from 'md-editor-v3'
import applicationApi from '@/api/application'
import type { FormInstance, FormRules } from 'element-plus'
import type { ApplicationFormType } from '@/api/type/application'
import type { Provider } from '@/api/type/model'
import { realatedObject } from '@/utils/utils'
import { MsgSuccess } from '@/utils/message'
import useStore from '@/stores'
const { model, dataset, application, user } = useStore()
const router = useRouter()
const route = useRoute()
const {
params: { id }
} = route as any
const defaultPrompt = `已知信息:
{data}
回答要求:
- 请使用简洁且专业的语言来回答用户的问题。
- 如果你不知道答案,请回答“没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作”。
- 避免提及你是从已知信息中获得的知识。
- 请保证答案与已知信息中描述的一致。
- 请使用 Markdown 语法优化答案的格式。
- 已知信息中的图片、链接地址和脚本语言请直接返回。
- 请使用与问题相同的语言来回答。
问题:
{question}
`
const createModelRef = ref<InstanceType<typeof CreateModelDialog>>()
const selectProviderRef = ref<InstanceType<typeof SelectProviderDialog>>()
const applicationFormRef = ref<FormInstance>()
const AddDatasetDialogRef = ref()
const loading = ref(false)
const datasetLoading = ref(false)
const applicationForm = ref<ApplicationFormType>({
name: '',
desc: '',
model_id: '',
multiple_rounds_dialogue: false,
prologue: `您好,我是 MaxKB 小助手,您可以向我提出 MaxKB 使用问题。
- MaxKB 主要功能有什么?
- MaxKB 支持哪些大语言模型?
- MaxKB 支持哪些文档类型?`,
dataset_id_list: [],
dataset_setting: {
top_n: 3,
similarity: 0.6,
max_paragraph_char_number: 5000
},
model_setting: {
prompt: defaultPrompt
},
problem_optimization: false
})
const popoverVisible = ref(false)
const rules = reactive<FormRules<ApplicationFormType>>({
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
model_id: [
{
required: true,
message: '请选择模型',
trigger: 'change'
}
],
'model_setting.prompt': [{ required: true, message: '请输入提示词', trigger: 'blur' }]
})
const modelOptions = ref<any>(null)
const providerOptions = ref<Array<Provider>>([])
const datasetList = ref([])
const dataset_setting = ref<any>({})
function datasetSettingChange(val: string) {
if (val === 'open') {
popoverVisible.value = true
dataset_setting.value = cloneDeep(applicationForm.value.dataset_setting)
} else if (val === 'close') {
popoverVisible.value = false
applicationForm.value.dataset_setting = cloneDeep(dataset_setting.value)
}
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
if (id) {
applicationApi.putApplication(id, applicationForm.value, loading).then((res) => {
MsgSuccess('保存成功')
})
} else {
applicationApi.postApplication(applicationForm.value, loading).then((res) => {
MsgSuccess('创建成功')
router.push({ path: `/application` })
})
}
}
})
}
const openCreateModel = (provider?: Provider) => {
if (provider && provider.provider) {
createModelRef.value?.open(provider)
} else {
selectProviderRef.value?.open()
}
}
function removeDataset(id: any) {
if (applicationForm.value.dataset_id_list) {
applicationForm.value.dataset_id_list.splice(
applicationForm.value.dataset_id_list.indexOf(id),
1
)
}
}
function addDataset(val: Array<string>) {
applicationForm.value.dataset_id_list = val
}
function openDatasetDialog() {
AddDatasetDialogRef.value.open(applicationForm.value.dataset_id_list)
}
function getDetail() {
application.asyncGetApplicationDetail(id, loading).then((res: any) => {
applicationForm.value = res.data
applicationForm.value.model_id = res.data.model
})
}
function getDataset() {
if (id) {
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)
})
}
}
function getModel() {
loading.value = true
if (id) {
applicationApi
.getApplicationModel(id)
.then((res: any) => {
modelOptions.value = groupBy(res?.data, 'provider')
loading.value = false
})
.catch(() => {
loading.value = false
})
} else {
model
.asyncGetModel()
.then((res: any) => {
modelOptions.value = groupBy(res?.data, 'provider')
loading.value = false
})
.catch(() => {
loading.value = false
})
}
}
function getProvider() {
loading.value = true
model
.asyncGetProvider()
.then((res: any) => {
providerOptions.value = res?.data
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function refresh() {
getDataset()
}
onMounted(() => {
getProvider()
getModel()
getDataset()
if (id) {
getDetail()
}
})
</script>
<style lang="scss" scoped>
.create-application {
.relate-dataset-card {
color: var(--app-text-color);
border-radius: 4px;
}
.dialog-bg {
border-radius: 8px;
background: var(--dialog-bg-gradient-color);
overflow: hidden;
box-sizing: border-box;
}
.scrollbar-height-left {
height: calc(var(--app-main-height) - 127px);
}
.scrollbar-height {
height: calc(var(--app-main-height) - 150px);
}
}
.model-icon {
width: 20px;
}
.check-icon {
position: absolute;
right: 10px;
}
.prologue-md-editor {
height: 150px;
}
.dataset_setting {
color: var(--el-text-color-regular);
font-weight: 400;
}
.custom-slider {
:deep(.el-input-number.is-without-controls .el-input__wrapper) {
padding: 0 !important;
}
}
</style>