add header style

This commit is contained in:
Wenjie Zhang 2024-09-14 02:46:44 +08:00
parent f5def6df67
commit 89af621753
7 changed files with 824 additions and 554 deletions

View File

@ -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大于BB小于CA和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);}}

View 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>

View File

@ -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 /> &nbsp;{{ 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 {

View File

@ -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 }}&nbsp;&nbsp;&nbsp;</strong>
<span>{{ result.file.filename }}&nbsp;&nbsp;&nbsp;</span>
<span><strong>距离</strong>{{ result.distance.toFixed(4) }}&nbsp;&nbsp;&nbsp;</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 }}&nbsp;&nbsp;&nbsp;</strong>
<span>{{ result.file.filename }}&nbsp;&nbsp;&nbsp;</span>
<span><strong>距离</strong>{{ result.distance.toFixed(4) }}&nbsp;&nbsp;&nbsp;</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 {

View File

@ -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>

View File

@ -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;

View File

@ -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 }} &nbsp;
<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 }} &nbsp;
<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 }} &nbsp;
<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 }} &nbsp;
<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 }} &nbsp;
<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 }} &nbsp;
<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 }} &nbsp;
<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 }} &nbsp;
<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>