add header style
This commit is contained in:
parent
f5def6df67
commit
89af621753
@ -14,7 +14,7 @@
|
||||
class="newchat nav-btn"
|
||||
@click="$emit('newconv')"
|
||||
>
|
||||
<PlusCircleOutlined /> <span class="text">新对话:{{ configStore.config?.model_name }}</span>
|
||||
<PlusCircleOutlined /> <span class="text">{{ configStore.config?.model_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header__right">
|
||||
@ -81,13 +81,13 @@
|
||||
重写查询 <a-segmented v-model:value="meta.rewriteQuery" :options="['off', 'on', 'hyde']"/>
|
||||
</div>
|
||||
<div class="flex-center">
|
||||
最大历史轮数 <a-input-number id="inputNumber" v-model:value="meta.history_round" :min="1" :max="50" />
|
||||
最大历史轮数 <a-input-number id="inputNumber" v-model:value="meta.history_round" :min="1" :max="50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="conv.messages.length == 0" class="chat-examples">
|
||||
<h1>你好,我是 Athena 😊</h1>
|
||||
<h1>你好,我是语析,一个基于知识图谱的智能助手</h1>
|
||||
<div class="opts">
|
||||
<div
|
||||
class="opt__button"
|
||||
@ -157,6 +157,7 @@ import {
|
||||
FolderOpenOutlined,
|
||||
GlobalOutlined,
|
||||
FileTextOutlined,
|
||||
RobotOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { Marked } from 'marked';
|
||||
@ -186,6 +187,7 @@ const examples = ref([
|
||||
'A大于B,B小于C,A和C哪个大?',
|
||||
'今天天气怎么样?',
|
||||
'吃饭吃出苍蝇可以索赔吗?',
|
||||
'帮我写一个请假条',
|
||||
])
|
||||
|
||||
const opts = reactive({
|
||||
@ -267,7 +269,8 @@ const renameTitle = () => {
|
||||
const firstAiMessage = conv.value.messages[1].text
|
||||
const context = `${prompt}\n\n问题: ${firstUserMessage}\n\n回复: ${firstAiMessage},主题是(一句话):`
|
||||
simpleCall(context).then((data) => {
|
||||
emit('rename-title', data.response.split(":")[0])
|
||||
const response = data.response.split(":")[0].replace(/^["'"']/g, '').replace(/["'"']$/g, '')
|
||||
emit('rename-title', response)
|
||||
})
|
||||
} else {
|
||||
emit('rename-title', conv.value.messages[0].text)
|
||||
@ -306,7 +309,6 @@ const appendAiMessage = (message, refs=null) => {
|
||||
text: message,
|
||||
refs,
|
||||
status: "querying",
|
||||
model_name: configStore.config.model_name
|
||||
})
|
||||
scrollToBottom()
|
||||
}
|
||||
@ -317,6 +319,7 @@ const updateMessage = (text, id, refs, status) => {
|
||||
message.refs = refs;
|
||||
message.status = status;
|
||||
message.text = text;
|
||||
message.model_name = refs.model_name
|
||||
} else {
|
||||
console.error('Message not found');
|
||||
}
|
||||
@ -613,19 +616,19 @@ watch(
|
||||
/* animation: slideInUp 0.1s ease-in; */
|
||||
|
||||
.err-msg {
|
||||
color: red;
|
||||
border: 1px solid red;
|
||||
color: #FF6B6B;
|
||||
border: 1px solid #FF6B6B;
|
||||
padding: 0.2rem 1rem;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
background: #FFEBEE;
|
||||
background: #FFF0F0;
|
||||
}
|
||||
}
|
||||
|
||||
.message-box.sent {
|
||||
line-height: 24px;
|
||||
max-width: 95%;
|
||||
background: var(--main-light-3);
|
||||
background: var(--main-light-4);
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
@ -654,9 +657,6 @@ watch(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.bottom {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
@ -665,26 +665,29 @@ watch(
|
||||
padding: 4px 2rem 0 2rem;
|
||||
background: white;
|
||||
|
||||
|
||||
.input-box {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
align-items: flex-end;
|
||||
background-color: var(--main-light-4);
|
||||
border-radius: 2rem;
|
||||
height: auto;
|
||||
padding: 0.5rem;
|
||||
|
||||
box-shadow: rgba(42, 60, 79, 0.1) 0px 6px 10px 0px;
|
||||
border: 1px solid #E5E5E5;
|
||||
border-radius: 2rem;
|
||||
background: #fafafa;
|
||||
transition: background 0.3s, box-shadow 0.3s;
|
||||
&:focus-within {
|
||||
background-color: #ECF1F2;
|
||||
border: 1px solid #BABABA;
|
||||
background: white;
|
||||
box-shadow: rgba(42, 60, 79, 0.1) 0px 6px 10px 0px;
|
||||
}
|
||||
|
||||
.user-input {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
padding: 0.5rem 1rem;
|
||||
padding: 0.5rem 0.5rem;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
@ -703,6 +706,25 @@ watch(
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 1rem;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--main-light-3);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: #DCDCDC;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note {
|
||||
@ -712,6 +734,7 @@ watch(
|
||||
padding: 0rem;
|
||||
color: #ccc;
|
||||
margin: 4px 0;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -777,24 +800,32 @@ button:disabled {
|
||||
}
|
||||
|
||||
.loading-dots div {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin: 0 5px;
|
||||
background-color: #333;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin: 0 4px;
|
||||
background-color: #666;
|
||||
border-radius: 50%;
|
||||
animation: loading 0.8s infinite ease-in-out both;
|
||||
opacity: 0.3;
|
||||
animation: pulse 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
.loading-dots div:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.loading-dots div:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.loading-dots div:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
@keyframes pulse {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.3;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading {0%,80%,100%{transform:scale(0.5);}40%{transform:scale(1);}}
|
||||
|
||||
67
web/src/components/HeaderComponent.vue
Normal file
67
web/src/components/HeaderComponent.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<div class="header-content">
|
||||
<div class="header-title">
|
||||
<h1>{{ title }}</h1>
|
||||
<slot name="description">
|
||||
<p v-if="description">{{ description }}</p>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="header-actions" v-if="$slots.actions">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.header-container {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@ -12,7 +12,7 @@
|
||||
:key="index"
|
||||
:class="{ active: curConvId === index }"
|
||||
@click="goToConversation(index)">
|
||||
<div class="conversation__title">{{ state.title }}</div>
|
||||
<div class="conversation__title"><CommentOutlined /> {{ state.title }}</div>
|
||||
<div class="conversation__delete" @click.stop="delConv(index)"><DeleteOutlined /></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, watch, onMounted } from 'vue'
|
||||
import { FormOutlined, MenuOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import { FormOutlined, MenuOutlined, DeleteOutlined, CommentOutlined } from '@ant-design/icons-vue'
|
||||
import ChatComponent from '@/components/ChatComponent.vue'
|
||||
|
||||
const convs = reactive(JSON.parse(localStorage.getItem('chat-convs')) || [
|
||||
@ -201,8 +201,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-left: 3px solid var(--main-600);
|
||||
padding-left: 13px;
|
||||
border-right: 3px solid var(--main-500);
|
||||
padding-right: 13px;
|
||||
background-color: var(--main-light-3);
|
||||
|
||||
& .conversation__title {
|
||||
|
||||
@ -1,178 +1,205 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="sider">
|
||||
<div class="sider-top">
|
||||
<div class="header-actions">
|
||||
<a-button type="text" @click="backToDatabase"><LeftOutlined /></a-button>
|
||||
<a-button type="text" danger class="del-db" @click="deleteDatabse"><DeleteOutlined /></a-button>
|
||||
</div>
|
||||
<div class="top">
|
||||
<!-- <div class="icon"><ReadFilled /></div> -->
|
||||
<div class="info">
|
||||
<h3>{{ database.name }}</h3>
|
||||
<p><span>{{ database.metadata?.row_count }}行 · {{ database.files?.length || 0 }}文件</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="description">{{ database.description }}</p>
|
||||
<div class="tags">
|
||||
<div>
|
||||
<HeaderComponent
|
||||
:title="database.name"
|
||||
>
|
||||
<template #description>
|
||||
<div class="database-info">
|
||||
<a-tag color="blue" v-if="database.embed_model">{{ database.embed_model }}</a-tag>
|
||||
<a-tag color="green" v-if="database.dimension">{{ database.dimension }}</a-tag>
|
||||
<span class="row-count">{{ database.metadata?.row_count }} 行 · {{ database.files?.length || 0 }} 文件</span>
|
||||
</div>
|
||||
<a-divider/>
|
||||
<div class="query-params" v-if="state.curPage == 'query-test'">
|
||||
<!-- <p style="text-align: center; margin: 0;"><strong>参数配置</strong></p> -->
|
||||
<div class="params-item">
|
||||
<p>检索数量:</p>
|
||||
<a-input-number size="small" v-model:value="meta.maxQueryCount" :min="1" :max="20" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="backToDatabase">
|
||||
<LeftOutlined /> 返回
|
||||
</a-button>
|
||||
<a-button type="primary" danger @click="deleteDatabse">
|
||||
<DeleteOutlined /> 删除数据库
|
||||
</a-button>
|
||||
</template>
|
||||
</HeaderComponent>
|
||||
<a-alert v-if="database.embed_model != configStore.config.embed_model" message="向量模型不匹配,请重新选择" type="warning" style="margin: 10px 20px;" />
|
||||
<div class="db-main-container">
|
||||
<a-tabs v-model:activeKey="state.curPage" class="atab-container" type="card">
|
||||
<a-tab-pane key="add">
|
||||
<template #tab><span><CloudUploadOutlined />添加文件</span></template>
|
||||
<div class="db-tab-container">
|
||||
<div class="upload">
|
||||
<a-upload-dragger
|
||||
class="upload-dragger"
|
||||
v-model:fileList="fileList"
|
||||
name="file"
|
||||
:multiple="true"
|
||||
:disabled="state.loading"
|
||||
action="/api/database/upload"
|
||||
@change="handleFileUpload"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<p class="ant-upload-text">点击或者把文件拖拽到这里上传</p>
|
||||
<p class="ant-upload-hint">
|
||||
目前仅支持上传文本文件,如 .pdf, .txt, .md。且同名文件无法重复添加
|
||||
</p>
|
||||
</a-upload-dragger>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="addDocumentByFile"
|
||||
:loading="state.loading"
|
||||
:disabled="fileList.length === 0"
|
||||
style="margin: 0px 20px 20px 0;"
|
||||
>
|
||||
添加到知识库
|
||||
</a-button>
|
||||
<a-button @click="handleRefresh" :loading="state.refrashing">刷新状态</a-button>
|
||||
</div>
|
||||
<a-table :columns="columns" :data-source="database.files" row-key="filename" class="my-table">
|
||||
<template #bodyCell="{ column, text, record }">
|
||||
<template v-if="column.key === 'filename'">
|
||||
<a-button class="main-btn" type="link" @click="openFileDetail(record)">{{ text }}</a-button>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'type'"><span :class="text">{{ text.toUpperCase() }}</span></template>
|
||||
<template v-else-if="column.key === 'status' && text === 'done'">
|
||||
<CheckCircleFilled style="color: #41A317;"/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status' && text === 'failed'">
|
||||
<CloseCircleFilled style="color: #FF4D4F ;"/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status' && text === 'processing'">
|
||||
<HourglassFilled style="color: #1677FF;"/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status' && text === 'waiting'">
|
||||
<ClockCircleFilled style="color: #FFCD43;"/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button class="del-btn" type="link"
|
||||
@click="deleteFile(text)"
|
||||
:disabled="state.lock || record.status === 'processing' || record.status === 'waiting' "
|
||||
>删除
|
||||
</a-button>
|
||||
</template>
|
||||
<span v-else-if="column.key === 'created_at'">{{ formatRelativeTime(Math.round(text*1000)) }}</span>
|
||||
<span v-else>{{ text }}</span>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-drawer
|
||||
width="50%"
|
||||
v-model:open="state.drawer"
|
||||
class="custom-class"
|
||||
:title="selectedFile?.filename"
|
||||
placement="right"
|
||||
@after-open-change="afterOpenChange"
|
||||
>
|
||||
<h2>共 {{ selectedFile?.lines.length }} 个片段</h2>
|
||||
<p v-for="line in selectedFile?.lines" :key="line.id">
|
||||
<strong>Chunk #{{ line.id }}</strong> {{ line.text }}
|
||||
</p>
|
||||
</a-drawer>
|
||||
</div>
|
||||
<div class="params-item">
|
||||
<p>TopK:</p>
|
||||
<a-input-number size="small" v-model:value="meta.topK" :min="1" :max="meta.maxQueryCount" />
|
||||
</div>
|
||||
<div class="params-item">
|
||||
<p>过滤低质量:</p>
|
||||
<a-switch v-model:checked="meta.filter" />
|
||||
</div>
|
||||
<div class="params-item w100" v-if="configStore.config.enable_reranker">
|
||||
<p>重排序阈值:</p>
|
||||
<a-slider v-model:value="meta.rerankThreshold" :min="0" :max="1" :step="0.01" />
|
||||
</div>
|
||||
<div class="params-item w100">
|
||||
<p>距离阈值:</p>
|
||||
<a-slider v-model:value="meta.distanceThreshold" :min="0" :max="1" :step="0.01" />
|
||||
</div>
|
||||
<div class="params-item col">
|
||||
<p>重写查询<small>(修改后需重新检索)</small>:</p>
|
||||
<a-segmented v-model:value="meta.rewriteQuery" :options="rewriteQueryOptions" >
|
||||
<template #label="{ payload }">
|
||||
<div style="padding: 4px 4px">
|
||||
<p>{{ payload.title }}</p>
|
||||
<small>{{ payload.subTitle }}</small>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="query-test" force-render>
|
||||
<template #tab><span><SearchOutlined />检索测试</span></template>
|
||||
<div class="query-test-container db-tab-container">
|
||||
<div class="sider">
|
||||
<div class="sider-top">
|
||||
<div class="query-params" v-if="state.curPage == 'query-test'">
|
||||
<h3 class="params-title">参数配置</h3>
|
||||
<div class="params-group">
|
||||
<div class="params-item">
|
||||
<p>检索数量:</p>
|
||||
<a-input-number size="small" v-model:value="meta.maxQueryCount" :min="1" :max="20" />
|
||||
</div>
|
||||
<div class="params-item">
|
||||
<p>TopK:</p>
|
||||
<a-input-number size="small" v-model:value="meta.topK" :min="1" :max="meta.maxQueryCount" />
|
||||
</div>
|
||||
<div class="params-item">
|
||||
<p>过滤低质量:</p>
|
||||
<a-switch v-model:checked="meta.filter" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="params-group">
|
||||
<div class="params-item w100" v-if="configStore.config.enable_reranker">
|
||||
<p>重排序阈值:</p>
|
||||
<a-slider v-model:value="meta.rerankThreshold" :min="0" :max="1" :step="0.01" />
|
||||
</div>
|
||||
<div class="params-item w100">
|
||||
<p>距离阈值:</p>
|
||||
<a-slider v-model:value="meta.distanceThreshold" :min="0" :max="1" :step="0.01" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="params-group">
|
||||
<div class="params-item col">
|
||||
<p>重写查询<small>(修改后需重新检索)</small>:</p>
|
||||
<a-segmented v-model:value="meta.rewriteQuery" :options="rewriteQueryOptions">
|
||||
<template #label="{ payload }">
|
||||
<div>
|
||||
<p style="margin: 4px 0">{{ payload.subTitle }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</a-segmented>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-segmented>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sider-bottom">
|
||||
</div>
|
||||
</div>
|
||||
<a-tabs v-model:activeKey="state.curPage" class="atab-container" type="card">
|
||||
<a-tab-pane key="add">
|
||||
<template #tab><span><CloudUploadOutlined />添加文件</span></template>
|
||||
<div class="db-info-container">
|
||||
<a-alert v-if="database.embed_model != configStore.config.embed_model" message="向量模型不匹配,请重新选择" type="warning" />
|
||||
<!-- <h3>向知识库中添加文件</h3> -->
|
||||
<div class="upload">
|
||||
<a-upload-dragger
|
||||
class="upload-dragger"
|
||||
v-model:fileList="fileList"
|
||||
name="file"
|
||||
:multiple="true"
|
||||
:disabled="state.loading"
|
||||
action="/api/database/upload"
|
||||
@change="handleFileUpload"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<p class="ant-upload-text">点击或者把文件拖拽到这里上传</p>
|
||||
<p class="ant-upload-hint">
|
||||
目前仅支持上传文本文件,如 .pdf, .txt, .md。且同名文件无法重复添加。
|
||||
</p>
|
||||
</a-upload-dragger>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="addDocumentByFile"
|
||||
:loading="state.loading"
|
||||
:disabled="fileList.length === 0"
|
||||
style="margin: 0px 20px 20px 0;"
|
||||
>
|
||||
添加到知识库
|
||||
</a-button>
|
||||
<a-button @click="handleRefresh" :loading="state.refrashing">刷新状态</a-button>
|
||||
</div>
|
||||
<a-table :columns="columns" :data-source="database.files" row-key="filename" class="my-table">
|
||||
<template #bodyCell="{ column, text, record }">
|
||||
<template v-if="column.key === 'filename'">
|
||||
<a-button class="main-btn" type="link" @click="openFileDetail(record)">{{ text }}</a-button>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'type'"><span :class="text">{{ text.toUpperCase() }}</span></template>
|
||||
<template v-else-if="column.key === 'status' && text === 'done'">
|
||||
<CheckCircleFilled style="color: #41A317;"/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status' && text === 'failed'">
|
||||
<CloseCircleFilled style="color: #FF4D4F ;"/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status' && text === 'processing'">
|
||||
<HourglassFilled style="color: #1677FF;"/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status' && text === 'waiting'">
|
||||
<ClockCircleFilled style="color: #FFCD43;"/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button class="del-btn" type="link"
|
||||
@click="deleteFile(text)"
|
||||
:disabled="state.lock || record.status === 'processing' || record.status === 'waiting' "
|
||||
>删除
|
||||
</div>
|
||||
<div class="sider-bottom">
|
||||
</div>
|
||||
</div>
|
||||
<div class="query-result-container">
|
||||
<div class="query-action">
|
||||
<a-textarea
|
||||
v-model:value="queryText"
|
||||
placeholder="填写需要查询的句子"
|
||||
:auto-size="{ minRows: 2, maxRows: 10 }"
|
||||
/>
|
||||
<a-button class="btn-query" @click="onQuery" :disabled="queryText.length == 0">
|
||||
<span v-if="!state.searchLoading"><SearchOutlined /> 检索</span>
|
||||
<span v-else><LoadingOutlined /></span>
|
||||
</a-button>
|
||||
</template>
|
||||
<span v-else-if="column.key === 'created_at'">{{ formatRelativeTime(Math.round(text*1000)) }}</span>
|
||||
<span v-else>{{ text }}</span>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-drawer
|
||||
width="50%"
|
||||
v-model:open="state.drawer"
|
||||
class="custom-class"
|
||||
:title="selectedFile?.filename"
|
||||
placement="right"
|
||||
@after-open-change="afterOpenChange"
|
||||
>
|
||||
<h2>共 {{ selectedFile?.lines.length }} 个片段</h2>
|
||||
<p v-for="line in selectedFile?.lines" :key="line.id">
|
||||
<strong>Chunk #{{ line.id }}</strong> {{ line.text }}
|
||||
</p>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="query-test" force-render>
|
||||
<template #tab><span><SearchOutlined />检索测试</span></template>
|
||||
<div class="db-info-container">
|
||||
<a-alert v-if="database.embed_model != configStore.config.embed_model" message="向量模型不匹配,请重新选择" type="warning" />
|
||||
<!-- <h3>检索测试</h3> -->
|
||||
<div class="query-action">
|
||||
<a-textarea
|
||||
v-model:value="queryText"
|
||||
placeholder="填写需要查询的句子"
|
||||
:auto-size="{ minRows: 2, maxRows: 10 }"
|
||||
/>
|
||||
<!-- :loading="state.searchLoading" -->
|
||||
<a-button class="btn-query" @click="onQuery" :disabled="queryText.length == 0">
|
||||
<span v-if="!state.searchLoading"><SearchOutlined /> 检索</span>
|
||||
<span v-else><LoadingOutlined /></span>
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="query-test" v-if="queryResult">
|
||||
<div class="results-overview">
|
||||
<p>总数:{{ queryResult.all_results.length }},过滤后:{{ filteredResults.length }} (TopK:{{ meta.topK }})</p>
|
||||
<p>重写后查询:{{ queryResult.rw_query }}</p>
|
||||
</div>
|
||||
<div class="query-card" v-for="(result, idx) in (filteredResults)" :key="idx">
|
||||
<p>
|
||||
<strong>#{{ idx + 1 }} </strong>
|
||||
<span>{{ result.file.filename }} </span>
|
||||
<span><strong>距离</strong>:{{ result.distance.toFixed(4) }} </span>
|
||||
<span v-if="result.rerank_score"><strong>重排序</strong>:{{ result.rerank_score.toFixed(4) }}</span>
|
||||
</p>
|
||||
<p class="query-text">{{ result.entity.text }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 新增示例按钮 -->
|
||||
<div class="query-examples">
|
||||
<a-button v-for="example in queryExamples" :key="example" @click="useQueryExample(example)">
|
||||
{{ example }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="query-test" v-if="queryResult">
|
||||
<div class="results-overview">
|
||||
<div class="results-stats">
|
||||
<span class="stat-item">
|
||||
<strong>总数:</strong> {{ queryResult.all_results.length }}
|
||||
</span>
|
||||
<span class="stat-item">
|
||||
<strong>过滤后:</strong> {{ filteredResults.length }}
|
||||
</span>
|
||||
<span class="stat-item">
|
||||
<strong>TopK:</strong> {{ meta.topK }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="rewritten-query">
|
||||
<strong>重写后查询:</strong>
|
||||
<span class="query-text">{{ queryResult.rw_query }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="query-result-card" v-for="(result, idx) in (filteredResults)" :key="idx">
|
||||
<p>
|
||||
<strong>#{{ idx + 1 }} </strong>
|
||||
<span>{{ result.file.filename }} </span>
|
||||
<span><strong>距离</strong>:{{ result.distance.toFixed(4) }} </span>
|
||||
<span v-if="result.rerank_score"><strong>重排序</strong>:{{ result.rerank_score.toFixed(4) }}</span>
|
||||
</p>
|
||||
<p class="query-text">{{ result.entity.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<!-- <a-tab-pane key="3" tab="Tab 3">Content of Tab Pane 3</a-tab-pane> -->
|
||||
</a-tabs>
|
||||
</a-tab-pane>
|
||||
<!-- <a-tab-pane key="3" tab="Tab 3">Content of Tab Pane 3</a-tab-pane> -->
|
||||
</a-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -180,6 +207,7 @@ import { onMounted, reactive, ref, watch, toRaw } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import HeaderComponent from '@/components/HeaderComponent.vue';
|
||||
import {
|
||||
ReadFilled,
|
||||
LeftOutlined,
|
||||
@ -219,11 +247,12 @@ const state = reactive({
|
||||
});
|
||||
|
||||
const meta = reactive({
|
||||
mode: 'search',
|
||||
maxQueryCount: 30,
|
||||
filter: true,
|
||||
rewriteQuery: 'off',
|
||||
rerankThreshold: 0.1,
|
||||
distanceThreshold: 0.5,
|
||||
distanceThreshold: 0.3,
|
||||
topK: 10,
|
||||
});
|
||||
|
||||
@ -499,6 +528,19 @@ watch(() => meta, () => {
|
||||
filterQueryResults()
|
||||
}, { deep: true })
|
||||
|
||||
// 添加示例查询
|
||||
const queryExamples = ref([
|
||||
'食品添加剂的安全性如何?',
|
||||
'如何识别和预防食物中毒?',
|
||||
'转基因食品对人体健康有什么影响?',
|
||||
'如何正确储存和处理生鲜食品?'
|
||||
]);
|
||||
|
||||
// 使用示例查询的方法
|
||||
const useQueryExample = (example) => {
|
||||
queryText.value = example;
|
||||
onQuery();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getDatabaseInfo();
|
||||
@ -512,120 +554,111 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.sider {
|
||||
.database-info {
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
.db-main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 275px;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border-right: 1px solid #E0EAFF;
|
||||
flex: 0 0 275px;
|
||||
|
||||
.sider-top {
|
||||
|
||||
& > * {
|
||||
padding: 0 20px;
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
background-color: var(--main-light-5);
|
||||
border-bottom: 1px solid #E0EAFF;
|
||||
|
||||
button {
|
||||
height: auto;
|
||||
font-size: 16px;
|
||||
color: var(--c-text-light-1);
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .pagebtns {
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// gap: 16px;
|
||||
|
||||
// > div {
|
||||
// gap: 1rem;
|
||||
// width: 100%;
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
// padding: 10px 16px;
|
||||
// height: auto;
|
||||
// border-radius: 4px;
|
||||
// border: none;
|
||||
// background: var(--main-light-5);
|
||||
// letter-spacing: 4px;
|
||||
// border-radius: 8px;
|
||||
// border: 1px solid var(--main-light-2);
|
||||
|
||||
// &:hover {
|
||||
// cursor: pointer;
|
||||
// background: var(--main-light-3);
|
||||
// }
|
||||
// }
|
||||
|
||||
// .active {
|
||||
// color: var(--main-color);
|
||||
// background: var(--main-light-3);
|
||||
// font-weight: bold;
|
||||
// }
|
||||
// }
|
||||
|
||||
.query-params {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
box-sizing: border-box;
|
||||
gap: 12px;
|
||||
|
||||
.params-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.params-item.col {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.params-item.w100 > *{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-slider {
|
||||
margin: 6px 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.atab-container {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.db-info-container {
|
||||
flex: 1 1 auto;
|
||||
.db-tab-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.query-test-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
|
||||
.sider {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 325px;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
flex: 0 0 325px;
|
||||
|
||||
.sider-top {
|
||||
.query-params {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
font-size: 15px;
|
||||
gap: 12px;
|
||||
background-color: var(--main-light-6);
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid var(--main-light-3);
|
||||
|
||||
.params-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: var(--main-color);
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.params-group {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--main-light-3);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.params-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
color: var(--c-text-light-1);
|
||||
}
|
||||
|
||||
&.col {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.w100,
|
||||
&.col {
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-slider {
|
||||
margin: 6px 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-result-container {
|
||||
flex: 1;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.query-action {
|
||||
display: flex;
|
||||
@ -654,73 +687,101 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.query-examples {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.ant-btn {
|
||||
font-size: 14px;
|
||||
padding: 4px 12px;
|
||||
height: auto;
|
||||
background-color: var(--main-light-4);
|
||||
border: none;
|
||||
color: var(--main-600);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--main-light-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-test {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
gap: 16px;
|
||||
border-radius: 12px;
|
||||
gap: 20px;
|
||||
|
||||
.query-card {
|
||||
padding: 20px 16px;
|
||||
.results-overview {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
background: var(--main-light-5);
|
||||
padding: 16px;
|
||||
border: 1px solid var(--main-light-3);
|
||||
}
|
||||
|
||||
.query-text {
|
||||
font-size: 14px;
|
||||
margin-bottom: 0;
|
||||
color: var(--c-text-light-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
.results-stats {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
margin-bottom: 10px;
|
||||
.stat-item {
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
margin-right: 24px;
|
||||
padding: 4px 8px;
|
||||
strong {
|
||||
color: var(--main-color);
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 28px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #F5F8FF;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #E0EAFF;
|
||||
}
|
||||
|
||||
.info {
|
||||
h3, p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background: var(--main-color);
|
||||
display: block;
|
||||
opacity: 0.5;
|
||||
z-index: -1;
|
||||
.rewritten-query {
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 4px 8px;
|
||||
strong {
|
||||
color: var(--main-color);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.query-text {
|
||||
font-style: italic;
|
||||
color: var(--c-text-light-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--c-text-light-1);
|
||||
font-size: small;
|
||||
.query-result-card {
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--main-light-3);
|
||||
transition: box-shadow 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
color: var(--c-text-light-1);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--main-color);
|
||||
}
|
||||
|
||||
.query-text {
|
||||
font-size: 15px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--main-light-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -731,11 +792,11 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.my-table button.ant-btn-link {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.my-table {
|
||||
button.ant-btn-link {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pdf, .txt, .md {
|
||||
color: white;
|
||||
padding: 2px 4px;
|
||||
@ -751,7 +812,6 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.txt {
|
||||
|
||||
background: #068033;
|
||||
}
|
||||
|
||||
@ -775,19 +835,54 @@ onMounted(() => {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.atab-container div.ant-tabs-nav {
|
||||
background: var(--main-light-5);
|
||||
padding: 1rem;
|
||||
padding-bottom: 0;
|
||||
.atab-container {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
div.ant-tabs-nav {
|
||||
background: var(--main-light-5);
|
||||
padding: 8px 20px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.atab-container .ant-tabs-content-holder {
|
||||
padding: 0 1rem;
|
||||
.params-item.col .ant-segmented {
|
||||
width: 100%;
|
||||
|
||||
div.ant-segmented-group {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.atab-container {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
div.ant-tabs-nav {
|
||||
background: var(--main-light-5);
|
||||
padding: 8px 20px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.params-item.col .ant-segmented {
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
<template>
|
||||
<div class="database-container layout-container" v-if="configStore.config.enable_knowledge_base">
|
||||
<h2>文档知识库</h2>
|
||||
<p>知识型数据库,主要是非结构化的文本组成,使用向量检索使用。</p>
|
||||
<HeaderComponent
|
||||
title="文档知识库"
|
||||
description="知识型数据库,主要是非结构化的文本组成,使用向量检索使用。"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="newDatabase.open=true">新建数据库</a-button>
|
||||
</template>
|
||||
</HeaderComponent>
|
||||
|
||||
<a-modal :open="newDatabase.open" title="新建数据库" @ok="createDatabase">
|
||||
<h3>数据库名称<span style="color: red">*</span></h3>
|
||||
<a-input v-model:value="newDatabase.name" placeholder="新建数据库名称" />
|
||||
@ -41,7 +48,7 @@
|
||||
<p><span>{{ database.metaname }}</span> · <span>{{ database.metadata.row_count }}行</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="description">{{ database.description }}</p>
|
||||
<p class="description">{{ database.description || '暂无描述' }}</p>
|
||||
<div class="tags">
|
||||
<a-tag color="blue" v-if="database.embed_model">{{ database.embed_model }}</a-tag>
|
||||
<a-tag color="green" v-if="database.dimension">{{ database.dimension }}</a-tag>
|
||||
@ -84,6 +91,7 @@ import { useRouter, useRoute } from 'vue-router';
|
||||
import { message, Button } from 'ant-design-vue'
|
||||
import { ReadFilled, PlusOutlined, AppstoreFilled, LoadingOutlined } from '@ant-design/icons-vue'
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import HeaderComponent from '@/components/HeaderComponent.vue';
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@ -188,6 +196,7 @@ onMounted(() => {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.databases {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
@ -281,4 +290,8 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
color: var(--c-text-light-1);
|
||||
}
|
||||
|
||||
.database-container {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -9,48 +9,20 @@
|
||||
</a-empty>
|
||||
</div>
|
||||
<div class="graph-container layout-container" v-else>
|
||||
<div class="info">
|
||||
<h2>图数据库 {{ graphInfo?.database_name }}</h2>
|
||||
<p>
|
||||
<span v-if="state.loadingGraphInfo">加载中</span>
|
||||
<span class="green-dot" v-if="graphInfo?.status == 'open'"></span>
|
||||
<span class="red-dot" v-else></span>
|
||||
<span>{{ graphInfo?.status }}</span> ·
|
||||
<span>共 {{ graphInfo?.entity_count }} 实体,{{ graphInfo?.relationship_count }} 个关系</span>
|
||||
</p>
|
||||
</div>
|
||||
<HeaderComponent
|
||||
title="图数据库"
|
||||
:description="`${graphInfo?.database_name || ''} - 共 ${graphInfo?.entity_count || 0} 实体,${graphInfo?.relationship_count || 0} 个关系`"
|
||||
>
|
||||
<template #actions>
|
||||
<div class="status-wrapper">
|
||||
<div class="status-indicator" :class="graphStatusClass"></div>
|
||||
</div>
|
||||
<a-button type="primary" @click="state.showModal = true" ><UploadOutlined/> 上传文件</a-button>
|
||||
</template>
|
||||
</HeaderComponent>
|
||||
|
||||
<div class="actions">
|
||||
<div class="actions-left">
|
||||
<a-button @click="state.showModal = true"><UploadOutlined /> 上传文件</a-button>
|
||||
<a-modal
|
||||
:open="state.showModal" title="上传文件"
|
||||
@ok="addDocumentByFile"
|
||||
@cancel="() => state.showModal = false"
|
||||
ok-text="添加到图数据库" cancel-text="取消"
|
||||
:confirm-loading="state.precessing">
|
||||
<div class="upload">
|
||||
<a-upload-dragger
|
||||
class="upload-dragger"
|
||||
v-model:fileList="fileList"
|
||||
name="file"
|
||||
:fileList="fileList"
|
||||
:max-count="1"
|
||||
:disabled="state.precessing"
|
||||
action="/api/database/upload"
|
||||
@change="handleFileUpload"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<p class="ant-upload-text">点击或者把文件拖拽到这里上传</p>
|
||||
<p class="ant-upload-hint">
|
||||
目前仅支持上传 jsonl 文件。且同名文件无法重复添加。
|
||||
</p>
|
||||
</a-upload-dragger>
|
||||
</div>
|
||||
</a-modal>
|
||||
<input v-model="sampleNodeCount">
|
||||
<a-button @click="loadSampleNodes" :loading="state.fetching">获取节点</a-button>
|
||||
</div>
|
||||
<div class="actions-right">
|
||||
<input
|
||||
v-model="state.searchInput"
|
||||
placeholder="输入要查询的实体"
|
||||
@ -65,18 +37,49 @@
|
||||
检索实体
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="actions-right">
|
||||
<input v-model="sampleNodeCount">
|
||||
<a-button @click="loadSampleNodes" :loading="state.fetching">获取节点</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main" id="container" ref="container" v-show="graphData.nodes.length > 0"></div>
|
||||
<a-empty v-show="graphData.nodes.length === 0" style="padding: 4rem 0;"/>
|
||||
|
||||
<a-modal
|
||||
:open="state.showModal" title="上传文件"
|
||||
@ok="addDocumentByFile"
|
||||
@cancel="() => state.showModal = false"
|
||||
ok-text="添加到图数据库" cancel-text="取消"
|
||||
:confirm-loading="state.precessing">
|
||||
<div class="upload">
|
||||
<a-upload-dragger
|
||||
class="upload-dragger"
|
||||
v-model:fileList="fileList"
|
||||
name="file"
|
||||
:fileList="fileList"
|
||||
:max-count="1"
|
||||
:disabled="state.precessing"
|
||||
action="/api/database/upload"
|
||||
@change="handleFileUpload"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<p class="ant-upload-text">点击或者把文件拖拽到这里上传</p>
|
||||
<p class="ant-upload-hint">
|
||||
目前仅支持上传 jsonl 文件。且同名文件无法重复添加。
|
||||
</p>
|
||||
</a-upload-dragger>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Graph } from "@antv/g6";
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { message } from "ant-design-vue";
|
||||
import { message, Button as AButton } from 'ant-design-vue';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import HeaderComponent from '@/components/HeaderComponent.vue';
|
||||
|
||||
const configStore = useConfigStore()
|
||||
|
||||
@ -198,13 +201,17 @@ const onSearch = () => {
|
||||
state.searchLoading = true
|
||||
fetch(`/api/database/graph/node?entity_name=${state.searchInput}`)
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
} else {
|
||||
throw new Error("查询失败");
|
||||
if (!res.ok) {
|
||||
return res.json().then(errorData => {
|
||||
throw new Error(errorData.message || `查询失败:${res.status} ${res.statusText}`);
|
||||
});
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (!data.result || !data.result.nodes || !data.result.edges) {
|
||||
throw new Error('返回数据格式不正确');
|
||||
}
|
||||
graphData.nodes = data.result.nodes
|
||||
graphData.edges = data.result.edges
|
||||
if (graphData.nodes.length === 0) {
|
||||
@ -215,7 +222,8 @@ const onSearch = () => {
|
||||
randerGraph()
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error(error.message);
|
||||
console.error('查询错误:', error);
|
||||
message.error(`查询出错:${error.message}`);
|
||||
})
|
||||
.finally(() => state.searchLoading = false)
|
||||
};
|
||||
@ -286,37 +294,71 @@ const handleDrop = (event) => {
|
||||
console.log(fileList.value)
|
||||
}
|
||||
|
||||
const graphStatusClass = computed(() => {
|
||||
if (state.loadingGraphInfo) return 'loading';
|
||||
return graphInfo.value?.status === 'open' ? 'open' : 'closed';
|
||||
});
|
||||
|
||||
const graphStatusText = computed(() => {
|
||||
if (state.loadingGraphInfo) return '加载中';
|
||||
return graphInfo.value?.status === 'open' ? '已连接' : '已关闭';
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.graph-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.info span.green-dot, .info span.red-dot {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
margin: 0 5px;
|
||||
.status-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 16px;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
|
||||
&.loading {
|
||||
background-color: #faad14;
|
||||
animation: pulse 1.5s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.info span.green-dot {
|
||||
background: #52c41a;
|
||||
&.open {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.info span.red-dot {
|
||||
background: #f5222d;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-bottom: 20px;
|
||||
&.closed {
|
||||
background-color: #f5222d;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
margin: 20px 0;
|
||||
padding: 0 24px;
|
||||
|
||||
.actions-left, .actions-right {
|
||||
display: flex;
|
||||
@ -355,9 +397,9 @@ const handleDrop = (event) => {
|
||||
|
||||
#container {
|
||||
background: #F7F7F7;
|
||||
margin: 20px 0;
|
||||
margin: 20px 24px;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
width: calc(100% - 48px);
|
||||
height: calc(100vh - 200px);
|
||||
resize: horizontal;
|
||||
overflow: hidden;
|
||||
|
||||
@ -1,123 +1,134 @@
|
||||
<template>
|
||||
<div class="setting-container layout-container">
|
||||
<div class="setting">
|
||||
<h2>设置</h2>
|
||||
<h3>模型配置</h3>
|
||||
<p>请在 <code>src/.env</code> 文件中配置对应的 APIKEY,可参考 <code>src/.env.template</code></p>
|
||||
<div class="section">
|
||||
<div class="card">
|
||||
<span class="label">
|
||||
{{ items?.model_provider.des }}
|
||||
<a-button small v-if="needRestart.model_provider" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-select ref="select" style="width: 200px"
|
||||
:value="configStore.config?.model_provider"
|
||||
@change="handleChange('model_provider', $event)"
|
||||
>
|
||||
<div class="">
|
||||
<HeaderComponent title="设置" class="setting-header">
|
||||
<template #description>
|
||||
<p>配置文件也可以在 <code>saves/config/config.yaml</code> 中修改</p>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="sendRestart">
|
||||
<ReloadOutlined />重启服务
|
||||
</a-button>
|
||||
</template>
|
||||
</HeaderComponent>
|
||||
<div class="setting-container layout-container">
|
||||
<div class="setting">
|
||||
<h3>模型配置</h3>
|
||||
<p>请在 <code>src/.env</code> 文件中配置对应的 APIKEY,可参考 <code>src/.env.template</code></p>
|
||||
<div class="section">
|
||||
<div class="card">
|
||||
<span class="label">
|
||||
{{ items?.model_provider.des }}
|
||||
<a-button small v-if="needRestart.model_provider" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-select ref="select" style="width: 200px"
|
||||
:value="configStore.config?.model_provider"
|
||||
@change="handleChange('model_provider', $event)"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(name, idx) in items?.model_provider.choices" :key="idx"
|
||||
:value="name">{{ name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="label">
|
||||
{{ items?.model_name.des }}
|
||||
<a-button small v-if="needRestart.model_name" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-select ref="select" style="width: 200px"
|
||||
:value="configStore.config?.model_name"
|
||||
@change="handleChange('model_name', $event)"
|
||||
v-if="configStore.config?.model_names && configStore.config?.model_provider && configStore.config?.model_names[configStore.config?.model_provider]"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(name, idx) in items?.model_provider.choices" :key="idx"
|
||||
v-for="(name, idx) in configStore.config.model_names[configStore.config.model_provider]"
|
||||
:key="idx"
|
||||
:value="name">{{ name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="label">
|
||||
{{ items?.embed_model.des }}
|
||||
<a-button small v-if="needRestart.embed_model" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-select style="width: 200px"
|
||||
:value="configStore.config?.embed_model"
|
||||
@change="handleChange('embed_model', $event)"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(name, idx) in items?.embed_model.choices" :key="idx"
|
||||
:value="name">{{ name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="label">
|
||||
{{ items?.reranker.des }}
|
||||
<a-button small v-if="needRestart.reranker" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-select style="width: 200px"
|
||||
:value="configStore.config?.reranker"
|
||||
@change="handleChange('reranker', $event)"
|
||||
:disabled="!configStore.config.enable_reranker"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(name, idx) in items?.reranker.choices" :key="idx"
|
||||
:value="name">{{ name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="label">
|
||||
{{ items?.model_name.des }}
|
||||
<a-button small v-if="needRestart.model_name" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-select ref="select" style="width: 200px"
|
||||
:value="configStore.config?.model_name"
|
||||
@change="handleChange('model_name', $event)"
|
||||
v-if="configStore.config?.model_names && configStore.config?.model_provider && configStore.config?.model_names[configStore.config?.model_provider]"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(name, idx) in configStore.config.model_names[configStore.config.model_provider]"
|
||||
:key="idx"
|
||||
:value="name">{{ name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="label">
|
||||
{{ items?.embed_model.des }}
|
||||
<a-button small v-if="needRestart.embed_model" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-select style="width: 200px"
|
||||
:value="configStore.config?.embed_model"
|
||||
@change="handleChange('embed_model', $event)"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(name, idx) in items?.embed_model.choices" :key="idx"
|
||||
:value="name">{{ name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="label">
|
||||
{{ items?.reranker.des }}
|
||||
<a-button small v-if="needRestart.reranker" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-select style="width: 200px"
|
||||
:value="configStore.config?.reranker"
|
||||
@change="handleChange('reranker', $event)"
|
||||
:disabled="!configStore.config.enable_reranker"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(name, idx) in items?.reranker.choices" :key="idx"
|
||||
:value="name">{{ name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
<h3>功能配置</h3>
|
||||
<div class="section">
|
||||
<div class="card">
|
||||
<span class="label">{{ items?.enable_knowledge_base.des }}
|
||||
<a-button small v-if="needRestart.enable_knowledge_base" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-switch
|
||||
:checked="configStore.config.enable_knowledge_base"
|
||||
@change="handleChange('enable_knowledge_base', !configStore.config.enable_knowledge_base)"
|
||||
/>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="label">{{ items?.enable_knowledge_graph.des }}
|
||||
<a-button small v-if="needRestart.enable_knowledge_graph" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-switch
|
||||
:checked="configStore.config.enable_knowledge_graph"
|
||||
@change="handleChange('enable_knowledge_graph', !configStore.config.enable_knowledge_graph)"
|
||||
/>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="label">{{ items?.enable_search_engine.des }}</span>
|
||||
<a-switch
|
||||
:checked="configStore.config.enable_search_engine"
|
||||
@change="handleChange('enable_search_engine', !configStore.config.enable_search_engine)"
|
||||
/>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="label">{{ items?.enable_reranker.des }}
|
||||
<a-button small v-if="needRestart.enable_reranker" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-switch
|
||||
:checked="configStore.config.enable_reranker"
|
||||
@change="handleChange('enable_reranker', !configStore.config.enable_reranker)"
|
||||
/>
|
||||
<h3>功能配置</h3>
|
||||
<div class="section">
|
||||
<div class="card">
|
||||
<span class="label">{{ items?.enable_knowledge_base.des }}
|
||||
<a-button small v-if="needRestart.enable_knowledge_base" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-switch
|
||||
:checked="configStore.config.enable_knowledge_base"
|
||||
@change="handleChange('enable_knowledge_base', !configStore.config.enable_knowledge_base)"
|
||||
/>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="label">{{ items?.enable_knowledge_graph.des }}
|
||||
<a-button small v-if="needRestart.enable_knowledge_graph" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-switch
|
||||
:checked="configStore.config.enable_knowledge_graph"
|
||||
@change="handleChange('enable_knowledge_graph', !configStore.config.enable_knowledge_graph)"
|
||||
/>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="label">{{ items?.enable_search_engine.des }}</span>
|
||||
<a-switch
|
||||
:checked="configStore.config.enable_search_engine"
|
||||
@change="handleChange('enable_search_engine', !configStore.config.enable_search_engine)"
|
||||
/>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="label">{{ items?.enable_reranker.des }}
|
||||
<a-button small v-if="needRestart.enable_reranker" @click="sendRestart">
|
||||
<ReloadOutlined />需要重启
|
||||
</a-button>
|
||||
</span>
|
||||
<a-switch
|
||||
:checked="configStore.config.enable_reranker"
|
||||
@change="handleChange('enable_reranker', !configStore.config.enable_reranker)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -129,6 +140,7 @@ import { message } from 'ant-design-vue';
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { ReloadOutlined } from '@ant-design/icons-vue';
|
||||
import HeaderComponent from '@/components/HeaderComponent.vue';
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const needRestart = reactive({
|
||||
@ -172,10 +184,22 @@ const sendRestart = () => {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.setting-header p {
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
.setting-container {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.setting {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
|
||||
h3 {
|
||||
margin-top: 20px;
|
||||
@ -211,6 +235,4 @@ const sendRestart = () => {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user