升级了设置页面/聊天布局/数据库展示等,新增了参考资料查询

This commit is contained in:
Wenjie Zhang 2024-07-25 20:30:28 +08:00
parent 28e2b4bd0d
commit 84f6761a67
15 changed files with 631 additions and 396 deletions

3
.gitignore vendored
View File

@ -30,4 +30,5 @@ cache
*.pdf
src/data
neo4j*
*/package-lock.json
*/package-lock.json
src/config/base.yaml

View File

@ -1,4 +1,4 @@
python -m vllm.entrypoints.openai.api_server \
CUDA_VISIBLE_DEVICES=0 python -m vllm.entrypoints.openai.api_server \
--model="/home/zwj/workspace/models/chatglm3-6b" \
--tensor-parallel-size 1 \
--trust-remote-code \

View File

@ -26,43 +26,56 @@ class SimpleConfig(dict):
def __setitem__(self, key, value):
return super().__setitem__(self.__key(key), value)
def __dict__(self):
return {k: v for k, v in self.items()}
class Config(SimpleConfig):
def __init__(self, filename=None):
super().__init__()
self.filename = filename or "config/base.yaml"
self._config_items = {}
### >>> 默认配置
# 可以在 config/base.yaml 中覆盖
self.mode = "cli"
self.stream = True
self.add_item("mode", default="cli", des="运行模式", choices=["cli", "api"])
self.add_item("stream", default=True, des="是否开启流式输出")
# 功能选项
self.enable_query_rewrite = True
self.enable_knowledge_base = True
self.enable_knowledge_graph = True
self.enable_search_engine = True
self.add_item("enable_query_rewrite", default=True, des="是否开启查询重写")
self.add_item("enable_knowledge_base", default=True, des="是否开启知识库")
self.add_item("enable_knowledge_graph", default=True, des="是否开启知识图谱")
self.add_item("enable_search_engine", default=True, des="是否开启搜索引擎")
# 模型配置
## 注意这里是模型名,而不是具体的模型路径,默认使用 HuggingFace 的路径
## 如果需要自定义路径,则在 config/base.yaml 中配置 model_local_paths
self.model_provider = "qianfan"
self.model_name = None # 默认使用 provider 的默认模型
self.embed_model = "bge-large-zh-v1.5"
self.reranker = "bge-reranker-v2-m3"
self.add_item("model_provider", default="qianfan", des="模型提供商", choices=["qianfan", "vllm", "zhipu", "deepseek"])
self.add_item("model_name", default=None, des="模型名称,为空则表示使用默认值")
self.add_item("embed_model", default="bge-large-zh-v1.5", des="Embedding 模型", choices=["bge-large-zh-v1.5", "zhipu"])
self.add_item("reranker", default="bge-reranker-v2-m3", des="Re-Ranker 模型", choices=["bge-reranker-v2-m3"])
self.add_item("model_local_paths", default={}, des="本地模型路径")
### <<< 默认配置结束
self.load()
self.handle_self()
def add_item(self, key, default, des=None, choices=None):
self.__setattr__(key, default)
self._config_items[key] = {
"default": default,
"des": des,
"choices": choices
}
def handle_self(self):
### handle local model
model_root_dir = os.getenv("MODEL_ROOT_DIR", "pretrained_models")
for model, model_rel_path in self.model_local_paths.items():
# 如果 model_rel_path 不是绝对路径,那么拼接 model_root_dir
if not model_rel_path.startswith("/"):
self.model_local_paths[model] = os.path.join(model_root_dir, model_rel_path)
if self.model_local_paths is not None:
for model, model_rel_path in self.model_local_paths.items():
# 如果 model_rel_path 不是绝对路径,那么拼接 model_root_dir
if not model_rel_path.startswith("/"):
self.model_local_paths[model] = os.path.join(model_root_dir, model_rel_path)
def load(self):
@ -71,10 +84,20 @@ class Config(SimpleConfig):
if self.filename is not None and os.path.exists(self.filename):
if self.filename.endswith(".json"):
with open(self.filename, 'r') as f:
self.update(json.load(f))
content = f.read()
if content:
self.update(json.loads(content))
else:
print(f"{self.filename} is empty.")
elif self.filename.endswith(".yaml"):
with open(self.filename, 'r') as f:
self.update(yaml.safe_load(f))
content = f.read()
if content:
self.update(yaml.safe_load(content))
else:
print(f"{self.filename} is empty.")
else:
logger.warning(f"Unknown config file type {self.filename}")
else:
logger.warning(f"Config file {self.filename} not found")
@ -86,10 +109,10 @@ class Config(SimpleConfig):
if self.filename.endswith(".json"):
with open(self.filename, 'w+') as f:
json.dump(self, f, indent=4, ensure_ascii=False)
json.dump(self.__dict__(), f, indent=4, ensure_ascii=False)
elif self.filename.endswith(".yaml"):
with open(self.filename, 'w+') as f:
yaml.dump(self, f, indent=2)
yaml.dump(self.__dict__(), f, indent=2, allow_unicode=True)
else:
logger.warning(f"Unknown config file type {self.filename}, save as json")
with open(self.filename, 'w+') as f:

View File

@ -1,13 +0,0 @@
# 默认配置请参考 config/__init__.py
name: base
## model
### model_provider, option in deepseek, zhipu
model_provider: vllm
model_name: null # for default
## model dir 可以写相对路径和绝对路径
### 相对路径是相对于环境变量 (.env) 中 MODEL_ROOT_DIR 的路径
model_local_paths:
bge-large-zh-v1.5: bge-large-zh-v1.5
oneke: oneke

View File

@ -15,7 +15,6 @@ class Retriever:
refs = {}
# TODO: 查询分类、查询重写、查询分解、查询伪文档生成HyDE)
refs["meta"] = meta
refs["rewrite_query"] = self.rewrite_query(query, history, meta)
refs["knowledge_base"] = self.query_knowledgebase(query, history, meta)
@ -35,7 +34,7 @@ class Retriever:
external += f"知识库信息: \n\n{kb_text}"
db_res = refs.get("graph_base").get("results", [])
if len(db_res) > 0:
if len(db_res["nodes"]) > 0:
db_text = '\n'.join([f"{edge['source_name']}{edge['target_name']}的关系是{edge['type']}" for edge in db_res['edges']])
external += f"图数据库信息: \n\n{db_text}"
@ -164,4 +163,5 @@ class Retriever:
def __call__(self, query, history, meta):
refs = self.retrieval(query, history, meta)
query = self.construct_query(query, refs, meta)
logger.debug(f"Retriever query: {query}")
return query, refs

View File

@ -30,6 +30,8 @@ python -m vllm.entrypoints.openai.api_server \
### 2. 向量模型支持
需要注意,由于知识库和图数据库的构建都依赖于向量模型,如果中途更改向量模型,回导致知识库不可用。未来考虑增加一键迁移脚本。
|模型名称(`config.embed_model`)|默认路径/模型|需要配置项目(`config.model_local_paths`|
|:-|:-|:-|

View File

@ -10,10 +10,10 @@
--c-indigo: #2c3e50;
--c-divider-light-1: rgba(60, 60, 60, 0.29);
--c-divider-light-2: rgba(60, 60, 60, 0.12);
--c-divider-dark-1: rgba(84, 84, 84, 0.65);
--c-divider-dark-2: rgba(84, 84, 84, 0.48);
--c-black-light-1: #333333;
--c-black-light-2: #454545;
--c-black-light-3: #666666;
--c-black-light-4: #999999;
--c-text-light-1: var(--c-indigo);
--c-text-light-2: rgba(60, 66, 70, 0.66);
@ -27,8 +27,8 @@
--color-background-soft: var(--c-white-soft);
--color-background-mute: var(--c-white-mute);
--color-border: var(--c-divider-light-2);
--color-border-hover: var(--c-divider-light-1);
--color-border: var(--c-black-light-2);
--color-border-hover: var(--c-black-light-1);
--color-heading: var(--c-text-light-1);
--color-text: var(--c-text-light-1);
@ -36,8 +36,10 @@
--section-gap: 160px;
--main-color: #005f77;
--main-color-light: #007f96;
--main-color-dark: #004d5c;
--main-light-1: #007f96;
--main-light-2: #F2F6F7;
--main-light-3: #E2EEF3;
--min-width: 400px;
--min-header-width: 80px;
--min-sider-width: 100px;
@ -50,8 +52,8 @@
--color-background-soft: var(--c-black-soft);
--color-background-mute: var(--c-black-mute);
--color-border: var(--c-divider-dark-2);
--color-border-hover: var(--c-divider-dark-1);
--color-border: var(--c-black-dark-2);
--color-border-hover: var(--c-black-dark-1);
--color-heading: var(--c-text-dark-1);
--color-text: var(--c-text-dark-2);

View File

@ -1,6 +1,6 @@
<!-- ChatComponent.vue -->
<template>
<div class="chat">
<div class="chat" ref="chatContainer">
<div class="header">
<div class="header__left">
<div
@ -11,11 +11,10 @@
<MenuOutlined />
</div>
<div
v-if="!state.isSidebarOpen"
class="newchat nav-btn"
@click="$emit('newconv')"
>
<FormOutlined />
<PlusCircleOutlined /> <span class="text">新对话</span>
</div>
</div>
<div class="header__right">
@ -24,14 +23,14 @@
<GoldenFilled v-if="meta.use_graph"/>
</div> -->
<a-dropdown v-if="state.selectedKB !== null">
<a class="ant-dropdown-link nav-btn text" @click.prevent>
<component :is="state.selectedKB === null ? BookOutlined : BookFilled" />&nbsp;
{{ state.selectedKB === null ? '未选择' : state.databases[state.selectedKB]?.name }}
<a class="ant-dropdown-link nav-btn" @click.prevent>
<component :is="state.selectedKB === null ? BookOutlined : BookFilled" />
<span class="text">{{ state.selectedKB === null ? '不使用' : state.databases[state.selectedKB]?.name }}</span>
</a>
<template #overlay>
<a-menu>
<a-menu-item v-for="(db, index) in state.databases" :key="index" @click="state.selectedKB=index">
<a href="javascript:;">{{ db.name }}</a>
<a href="javascript:;" >{{ db.name }}</a>
</a-menu-item>
<a-menu-item @click="state.selectedKB = null">
<a href="javascript:;">不使用</a>
@ -40,7 +39,7 @@
</template>
</a-dropdown>
<div class="nav-btn text" @click="state.showPanel = !state.showPanel">
<component :is="state.showPanel ? SettingFilled : SettingOutlined" />
<component :is="state.showPanel ? SettingFilled : SettingOutlined" /> <span class="text">选项</span>
</div>
<div v-if="state.showPanel" class="my-panal" ref="panel">
<div class="graphbase flex-center">
@ -49,7 +48,7 @@
<a-dropdown>
<a class="ant-dropdown-link " @click.prevent>
<component :is="state.selectedKB === null ? BookOutlined : BookFilled" />&nbsp;
{{ state.selectedKB === null ? '未选择' : state.databases[state.selectedKB]?.name }}
{{ state.selectedKB === null ? '不使用' : state.databases[state.selectedKB]?.name }}
</a>
<template #overlay>
<a-menu>
@ -86,7 +85,7 @@
</div>
</div>
</div>
<div ref="chatBox" class="chat-box">
<div class="chat-box">
<div
v-for="message in conv.messages"
:key="message.id"
@ -98,27 +97,33 @@
v-html="renderMarkdown(message.text)"
class="message-md"
@click="consoleMsg(message)"></p>
<div class="refs" v-if="message.role=='received' && message.refs?.knowledge_base.results.length > 0">
<a-tag
v-for="(ref, index) in message.refs?.knowledge_base.results"
:key="index"
color="blue"
>
{{ ref.id }}
</a-tag>
</div>
</div>
</div>
<div class="input-box">
<a-textarea
class="user-input"
v-model:value="conv.inputText"
@keydown="handleKeyDown"
placeholder="输入问题……"
:auto-size="{ minRows: 1, maxRows: 10 }"
/>
<!-- <input
class="user-input"
v-model="conv.inputText"
@keydown.enter="sendMessage"
placeholder="输入问题……"
/> -->
<a-button size="large" @click="sendMessage" :disabled="(!conv.inputText && !isStreaming)">
<template #icon> <SendOutlined v-if="!isStreaming" /> <LoadingOutlined v-else/> </template>
</a-button>
<div class="bottom">
<div class="input-box">
<a-textarea
class="user-input"
v-model:value="conv.inputText"
@keydown="handleKeyDown"
placeholder="输入问题……"
:auto-size="{ minRows: 1, maxRows: 10 }"
/>
<a-button size="large" @click="sendMessage" :disabled="(!conv.inputText && !isStreaming)">
<template #icon> <SendOutlined v-if="!isStreaming" /> <LoadingOutlined v-else/> </template>
</a-button>
</div>
<p class="note">即便强如雅典娜也可能会出错请注意辨别内容的可靠性</p>
</div>
<p class="note">即便强如雅典娜也可能会出错请注意辨别内容的可靠性</p>
</div>
</template>
@ -136,6 +141,7 @@ import {
GoldenFilled,
SettingOutlined,
SettingFilled,
PlusCircleOutlined,
} from '@ant-design/icons-vue'
import { marked } from 'marked';
@ -147,13 +153,13 @@ const props = defineProps({
const emit = defineEmits(['renameTitle'])
const { conv, state } = toRefs(props)
const chatBox = ref(null)
const chatContainer = ref(null)
const isStreaming = ref(false)
const panel = ref(null)
const examples = ref([
'写一个冒泡排序',
'肉碱是什么?',
'介绍一下江南大学',
'洋葱的功效是什么?',
'A大于BB小于CA和C哪个大',
'今天天气怎么样?'
])
@ -161,7 +167,7 @@ const examples = ref([
const meta = reactive({
db_name: computed(() => state.value.databases[state.value.selectedKB]?.metaname),
use_graph: false,
use_search: false,
use_web: false,
graph_name: "neo4j",
})
@ -210,7 +216,7 @@ const renderMarkdown = (text) => marked(text)
const scrollToBottom = () => {
setTimeout(() => {
chatBox.value.scrollTop = chatBox.value.scrollHeight - chatBox.value.clientHeight
chatContainer.value.scrollTop = chatContainer.value.scrollHeight - chatContainer.value.clientHeight
}, 10)
}
@ -253,10 +259,11 @@ const appendAiMessage = (message, refs=null) => {
scrollToBottom()
}
const updateMessage = (text, id) => {
const updateMessage = (text, id, refs) => {
const message = conv.value.messages.find((message) => message.id === id)
if (message) {
message.text = text
message.refs = refs
} else {
console.error('Message not found')
}
@ -320,7 +327,7 @@ const sendMessage = () => {
try {
const data = JSON.parse(message)
updateMessage(data.response, cur_res_id)
updateMessage(data.response, cur_res_id, data.refs)
conv.value.history = data.history
buffer = ''
} catch (e) {
@ -353,62 +360,64 @@ onMounted(() => {
<style lang="less" scoped>
.chat {
width: 200px;
max-width: 1100px;
margin: 0 auto;
position: relative;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
background: white;
position: relative;
box-sizing: border-box;
flex: 5 5 auto;
}
flex: 5 5 200px;
overflow-y: scroll;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
.chat div.header {
height: var(--header-height);
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
}
.chat div.header {
user-select: none;
.header__left, .header__right {
display: flex;
align-items: center;
gap: 1rem;
&::-webkit-scrollbar {
display: none; /* Chrome, Safari, and Opera */
}
}
.ant-dropdown-link {
color: var(--c-text-light-1);
cursor: pointer;
}
.header {
user-select: none;
position: sticky;
top: 0;
z-index: 10;
background-color: white;
height: var(--header-height);
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
.nav-btn {
font-size: 1.2rem;
width: 2.5rem;
height: 2.5rem;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
color: #6D6D6D;
cursor: pointer;
.header__left, .header__right {
display: flex;
align-items: center;
}
}
&.text {
.nav-btn {
height: 2.5rem;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
color: var(--c-text-light-1);
cursor: pointer;
font-size: 1rem;
width: auto;
padding: 0.5rem 1rem;
.text {
margin-left: 10px;
}
&:hover {
background-color: #EDF4F5;
}
}
&:hover {
background-color: #ECECEC;
}
}
.metas {
display: flex;
gap: 8px;
@ -424,7 +433,7 @@ onMounted(() => {
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.05);
border-radius: 12px;
padding: 12px;
z-index: 100;
z-index: 101;
width: 250px;
.flex-center {
@ -447,138 +456,168 @@ onMounted(() => {
}
div.chat-examples {
.chat-examples {
padding: 0 50px;
text-align: center;
position: absolute;
top: 20%;
width: 100%;
z-index: 100;
}
.chat-examples h1 {
margin-bottom: 20px;
font-size: 24px;
color: #333;
}
.opt {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.opt__button {
background-color: white;
color: #222;
padding: 4px 1rem;
border-radius: 1rem;
cursor: pointer;
border: 2px solid white;
transition: background-color 0.3s;
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.05);
&:hover {
background-color: #fcfcfc;
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.1);
h1 {
margin-bottom: 20px;
font-size: 24px;
color: #333;
}
.opt {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
.opt__button {
background-color: white;
color: #222;
padding: 6px 1rem;
border-radius: 1rem;
cursor: pointer;
// border: 2px solid var(--main-light-2);
transition: background-color 0.3s;
box-shadow: 0px 0px 10px 4px var(--main-light-2);
&:hover {
background-color: #fcfcfc;
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.1);
}
}
}
}
.chat-box {
flex: 1;
overflow-y: auto;
width: 100%;
max-width: 1100px;
margin: 0 auto;
flex-grow: 1;
padding: 1rem;
display: flex;
flex-direction: column;
.message-box {
max-width: 95%;
display: inline-block;
border-radius: 0.8rem;
margin: 0.8rem 0;
padding: 1rem;
user-select: text;
word-break: break-word;
font-size: 16px;
font-variation-settings: 'wght' 400, 'opsz' 10.5;
font-weight: 400;
box-sizing: border-box;
color: #0D0D0D;
/* box-shadow: 0px 0.3px 0.9px rgba(0, 0, 0, 0.12), 0px 1.6px 3.6px rgba(0, 0, 0, 0.16); */
/* animation: slideInUp 0.1s ease-in; */
}
.message-box.sent {
background-color: #efefef;
line-height: 24px;
background: #EDF4F5;
align-self: flex-end;
}
.message-box.received {
color: initial;
width: fit-content;
padding-top: 16px;
background-color: #F5F7F8;
text-align: left;
word-wrap: break-word;
margin-bottom: 0;
padding-bottom: 0;
text-align: justify;
}
p.message-text {
max-width: 100%;
word-wrap: break-word;
margin-bottom: 0;
}
p.message-md {
word-wrap: break-word;
margin-bottom: 0;
}
.refs {
margin-bottom: 20px;
}
}
.message-box {
max-width: 90%;
display: inline-block;
border-radius: 0.8rem;
margin: 0.8rem 0;
padding: 0.8rem;
user-select: text;
word-break: break-word;
font-size: 16px;
font-variation-settings: 'wght' 400, 'opsz' 10.5;
font-weight: 400;
box-sizing: border-box;
color: #0D0D0D;
/* box-shadow: 0px 0.3px 0.9px rgba(0, 0, 0, 0.12), 0px 1.6px 3.6px rgba(0, 0, 0, 0.16); */
/* animation: slideInUp 0.1s ease-in; */
}
.message-box.sent {
background-color: #efefef;
line-height: 24px;
background: #EDF4F5;
align-self: flex-end;
}
.message-box.received {
color: initial;
width: fit-content;
padding-top: 16px;
background-color: #f7f7f7;
text-align: left;
word-wrap: break-word;
margin-bottom: 0;
padding-bottom: 0;
text-align: justify;
}
p.message-text {
max-width: 100%;
word-wrap: break-word;
margin-bottom: 0;
}
p.message-md {
word-wrap: break-word;
margin-bottom: 0;
}
img.message-image {
max-width: 300px;
max-height: 50vh;
object-fit: contain;
}
.input-box {
width: calc(100% - 2rem);
max-width: calc(1100px - 2rem);
.bottom {
position: sticky;
bottom: 0;
width: 100%;
margin: 0 auto;
display: flex;
align-items: flex-end;
background-color: #F4F4F4;
border-radius: 2rem;
height: auto;
padding: 0.5rem;
padding: 0.5rem 2rem;
background: white;
.input-box {
display: flex;
width: 100%;
max-width: 1100px;
margin: 0 auto;
align-items: flex-end;
background-color: #F5F7F8;
border-radius: 2rem;
height: auto;
padding: 0.5rem;
.user-input {
flex: 1;
height: 40px;
padding: 0.5rem 1rem;
background-color: transparent;
border: none;
font-size: 1.2rem;
margin: 0 0.6rem;
color: #111111;
font-size: 16px;
font-variation-settings: 'wght' 400, 'opsz' 10.5;
outline: none;
&:focus {
outline: none;
box-shadow: none;
}
&:active {
outline: none;
}
}
}
.note {
width: 100%;
font-size: small;
text-align: center;
padding: 0rem;
color: #ccc;
margin: 4px 0;
}
}
.user-input {
flex: 1;
height: 40px;
padding: 0.5rem 1rem;
background-color: transparent;
border: none;
font-size: 1.2rem;
margin: 0 0.6rem;
color: #111111;
font-size: 16px;
font-variation-settings: 'wght' 400, 'opsz' 10.5;
outline: none;
&:focus {
outline: none;
box-shadow: none;
}
&:active {
outline: none;
}
.ant-dropdown-link {
color: var(--c-text-light-1);
cursor: pointer;
}
.ant-btn-icon-only {
@ -590,6 +629,10 @@ img.message-image {
background-color: black;
border-radius: 3rem;
color: white;
&:hover {
color: white;
}
}
button:disabled {
@ -597,25 +640,33 @@ button:disabled {
cursor: not-allowed;
}
p.note {
width: 100%;
font-size: small;
text-align: center;
padding: 0rem;
color: #ccc;
margin: 4px 0;
}
@media (max-width: 520px) {
.chat {
height: calc(100vh - 60px);
}
/*
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(100%);
}
to {
opacity: 1;
transform: translateY(0);
.chat-container .chat .header {
background: var(--main-light-2);
.header__left, .header__right {
gap: 20px;
}
.nav-btn {
font-size: 1.5rem;
padding: 0;
&:hover {
background-color: transparent;
color: black;
}
.text {
display: none;
}
}
}
}
*/
</style>

View File

@ -38,15 +38,17 @@ console.log(route)
</div>
<div class="nav">
<RouterLink to="/chat" class="nav-item" active-class="active">
<component :is="route.path === '/chat' ? MessageFilled : MessageOutlined" />
<component class="icon" :is="route.path === '/chat' ? MessageFilled : MessageOutlined" />
<span class="text">对话</span>
</RouterLink>
<RouterLink to="/database" class="nav-item" active-class="active">
<component :is="route.path.startsWith('/database') ? BookFilled : BookOutlined" />
<component class="icon" :is="route.path.startsWith('/database') ? BookFilled : BookOutlined" />
<span class="text">知识</span>
</RouterLink>
</div>
<div class="fill" style="flex-grow: 1;"></div>
<RouterLink to="/setting" class="setting" active-class="active">
<component :is="route.path === '/setting' ? SettingFilled : SettingOutlined" />
<RouterLink class="nav-item setting" to="/setting" active-class="active">
<component class="icon" :is="route.path === '/setting' ? SettingFilled : SettingOutlined" />
</RouterLink>
</div>
<a-config-provider :theme="themeConfig">
@ -86,15 +88,15 @@ div.header, #app-router-view {
flex: 0 0 80px;
justify-content: flex-start;
align-items: center;
background-color: #F2F6F7;
background-color: var(--main-light-2);
height: 100%;
width: 80px;
border-right: 1px solid #e2eef3;
& .logo {
width: 50px;
height: 50px;
margin: 20px 0;
.logo {
width: 40px;
height: 40px;
margin: 30px 0;
img {
width: 100%;
@ -111,6 +113,7 @@ div.header, #app-router-view {
}
.setting {
width: auto;
font-size: 20px;
color: #333;
margin: 20px 0;
@ -129,33 +132,37 @@ div.header, #app-router-view {
position: relative;
height: 45px;
gap: 16px;
}
.header .nav .nav-item {
padding: 8px 16px;
border: none;
border-radius: 8px;
background-color: transparent;
color: #222;
font-size: 20px;
transition: background-color 0.2s ease-in-out;
margin: 0 10px;
.nav-item {
padding: 8px 16px;
border: none;
border-radius: 8px;
background-color: transparent;
color: #222;
font-size: 20px;
transition: background-color 0.2s ease-in-out;
margin: 0 10px;
&.active {
font-weight: bold;
color: var(--main-color);
background-color: #e2eef3;
}
.text {
display: none;
}
&:hover {
background-color: #e2eef3;
cursor: pointer;
&.active {
font-weight: bold;
color: var(--main-color);
background-color: #e2eef3;
}
&:hover {
background-color: #e2eef3;
cursor: pointer;
}
}
}
@media (max-width: 520px) {
.app-layout {
flex-direction: column;
flex-direction: column-reverse;
}
.app-layout div.header {
@ -167,9 +174,10 @@ div.header, #app-router-view {
align-items: center;
flex: 0 0 60px;
border-right: none;
border-bottom: 1px solid #e2eef3;
border-top: 1px solid #e2eef3;
.logo {
display: none;
flex-shrink: 0;
width: 40px;
height: 40px;
@ -178,9 +186,12 @@ div.header, #app-router-view {
.setting {
margin: 0;
width: 60px;
}
}
.app-layout .nav {
flex-direction: row;
height: 100%;
@ -188,12 +199,29 @@ div.header, #app-router-view {
justify-content: center;
gap: 0;
.nav-item {
text-decoration: none;
span.text {
display: block;
color: #333;
}
span.icon {
display: none;
}
}
.nav-item:hover {
background-color: transparent;
}
.nav-item.active {
font-weight: bold;
background-color: transparent;
span.text {
font-weight: bold;
}
}
}
.app-layout .chat-box::webkit-scrollbar {

View File

@ -20,6 +20,14 @@ export const useConfigStore = defineStore('config', () => {
function setConfigValue(key, value) {
config.value[key] = value
fetch('/api/config', {
method: 'POST',
body: JSON.stringify(config.value),
})
.then(response => response.json())
.then(data => {
console.log('Success:', data)
})
}
return { config, setConfig, setConfigValue }

View File

@ -2,7 +2,8 @@
<div class="chat-container">
<div v-if="state.isSidebarOpen" class="conversations">
<div class="actions">
<div class="action new" @click="addNewConv"><FormOutlined /></div>
<!-- <div class="action new" @click="addNewConv"><FormOutlined /></div> -->
<span>对话历史</span>
<div class="action close" @click="state.isSidebarOpen = false"><MenuOutlined /></div>
</div>
<div class="conversation"
@ -153,7 +154,6 @@ onMounted(() => {
justify-content: space-between;
align-items: center;
padding: 16px;
// margin-bottom: 16px;
position: sticky;
top: 0;
background-color: #FAFCFD;
@ -167,7 +167,7 @@ onMounted(() => {
justify-content: center;
align-items: center;
border-radius: 8px;
color: #6D6D6D;
color: var(--c-black-light-2);
cursor: pointer;
&:hover {
@ -175,44 +175,49 @@ onMounted(() => {
}
}
}
}
.conversation {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
cursor: pointer;
width: 100%;
user-select: none;
transition: border-left 0.1s;
.conversation {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
cursor: pointer;
width: 100%;
user-select: none;
transition: border-left 0.1s;
&__title {
white-space: nowrap; /* 禁止换行 */
overflow: hidden; /* 超出部分隐藏 */
text-overflow: ellipsis; /* 显示省略号 */
}
&__title {
color: var(--c-black-light-3);
white-space: nowrap; /* 禁止换行 */
overflow: hidden; /* 超出部分隐藏 */
text-overflow: ellipsis; /* 显示省略号 */
}
&__delete {
display: none;
color: #7D7D7D;
&__delete {
display: none;
color: #7D7D7D;
&:hover {
color: #F93A37;
background-color: #EEE;
}
}
&.active {
border-left: 3px solid var(--main-color);
background-color: #EDF4F5;
& .conversation__title {
color: var(--c-black-light-1);
}
}
&:hover {
color: #F93A37;
background-color: #EEE;
}
}
background-color: #EDF4F5;
&.active {
border-left: 4px solid var(--main-color);
background-color: #EDF4F5;
}
&:hover {
background-color: #EDF4F5;
& .conversation__delete {
display: block;
& .conversation__delete {
display: block;
}
}
}
}

View File

@ -17,39 +17,43 @@
<div class="tags">
<a-tag color="blue" v-if="database.embed_model">Embed: {{ database.embed_model }}</a-tag>
</div>
<a-divider/>
<h3 style="margin-top: 20px;">向知识库中添加文件</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>
<a-button
type="primary"
@click="addDocumentByFile"
:loading="state.loading"
:disabled="fileList.length === 0"
style="margin: 0px 20px 20px 20px;"
>
添加到知识库
</a-button>
<a-button @click="handleRefresh" :loading="state.refrashing">刷新状态</a-button>
</div>
<div class="sider-bottom">
</div>
</div>
<div class="db-info-container">
<h2>向知识库中添加文件</h2>
<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 class="query-test">
用于测试检索
</div>
<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>
<a-table :columns="columns" :data-source="database.files" row-key="file_id" class="my-table">
<template #bodyCell="{ column, text, record }">
<template v-if="column.key === 'file_id'">
@ -398,6 +402,17 @@ onMounted(() => {
.db-info-container {
padding: 20px;
flex: 1 1 auto;
.query-test {
width: 100%;
height: 300px;
display: flex;
justify-content: center;
align-items: center;
background: var(--main-light-3);
margin-bottom: 20px;
border-radius: 8px;
}
}
.top {
@ -446,19 +461,19 @@ onMounted(() => {
.my-table {
.pdf, .txt, .md {
color: white;
padding: 4px 8px;
border-radius: 2px;
padding: 2px 4px;
border-radius: 4px;
font-size: small;
font-weight: bold;
}
.pdf {
background: #f17592;
background: #005F77;
}
.txt {
background: #31c989;
background: #068033;
}
button.main-btn {

View File

@ -45,7 +45,7 @@
<!-- <button @click="deleteDatabase(database.collection_name)">删除</button> -->
</div>
</div>
<h2>图数据库</h2>
<h2>图数据库 &nbsp; <a-spin v-if="graphloading" :indicator="indicator" /></h2>
<p>基于 neo4j 构建的图数据库</p>
<div :class="{'graphloading': graphloading}" v-if="graph">
<div class="dbcard graphbase" @click="navigateToGraph">
@ -67,10 +67,10 @@
</template>
<script setup>
import { ref, onMounted, reactive, watch } from 'vue'
import { ref, onMounted, reactive, watch, h } from 'vue'
import { useRouter, useRoute } from 'vue-router';
import { message, Button } from 'ant-design-vue'
import { ReadFilled, PlusOutlined, AppstoreFilled } from '@ant-design/icons-vue'
import { ReadFilled, PlusOutlined, AppstoreFilled, LoadingOutlined } from '@ant-design/icons-vue'
const route = useRoute()
const router = useRouter()
@ -78,6 +78,8 @@ const databases = ref([])
const graph = ref(null)
const graphloading = ref(false)
const indicator = h(LoadingOutlined, {spin: true});
const newDatabase = reactive({
name: '',
description: '',
@ -187,25 +189,25 @@ onMounted(() => {
}
.database, .graphbase {
background-color: white;
box-shadow: 0px 1px 2px 0px rgba(16,24,40,.06),0px 1px 3px 0px rgba(16,24,40,.1);
border: 2px solid white;
transition: box-shadow 0.2s ease-in-out;
background-color: white;
box-shadow: 0px 1px 2px 0px rgba(16,24,40,.06),0px 1px 3px 0px rgba(16,24,40,.1);
border: 2px solid white;
transition: box-shadow 0.2s ease-in-out;
&:hover {
box-shadow: 0px 4px 6px -2px rgba(16,24,40,.03),0px 12px 16px -4px rgba(16,24,40,.08);
}
&:hover {
box-shadow: 0px 4px 6px -2px rgba(16,24,40,.03),0px 12px 16px -4px rgba(16,24,40,.08);
}
}
.dbcard, .database {
padding: 10px;
border-radius: 12px;
width: 360px;
width: 340px;
height: 160px;
padding: 20px;
cursor: pointer;
flex: 1 1 380px;
max-width: 380px;
max-width: 450px;
.top {
display: flex;

View File

@ -7,12 +7,18 @@
<div class="actions">
<div class="actions-left">
<a-button @click="state.showModal = true">上传文件</a-button>
<a-modal v-model:open="state.showModal" title="上传文件" @ok="handleUpload">
<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"
@ -25,16 +31,6 @@
</p>
</a-upload-dragger>
</div>
<a-button
type="primary"
@click="addDocumentByFile"
:loading="state.precessing"
:disabled="fileList.length === 0"
style="margin: 0px 20px 20px 0;"
>
添加到图数据库
</a-button>
<a-button @click="handleRefresh" :loading="state.refrashing">刷新状态</a-button>
</a-modal>
</div>
<div class="action-right">
@ -42,6 +38,7 @@
v-model="state.searchInput"
placeholder="输入要查询的实体"
style="width: 200px"
@keydown.enter="onSearch"
/>
<a-button
type="primary"
@ -121,14 +118,15 @@ const addDocumentByFile = () => {
file_path: files[0]
}),
})
// .then(response => response.json())
// .then((data) => {
// message.success(data.message);
// })
// .catch((error) => {
// message.error(error.message);
// })
// .finally(() => state.precessing = false)
.then(response => response.json())
.then((data) => {
message.success(data.message);
state.showModal = false;
})
.catch((error) => {
message.error(error.message);
})
.finally(() => state.precessing = false)
};
const onSearch = () => {
@ -261,7 +259,7 @@ const handleDrop = (event) => {
margin: 20px 0;
border-radius: 16px;
width: 100%;
height: 400px;
height: calc(100% - 200px);
}

View File

@ -1,22 +1,116 @@
<template>
<div class="not-found">
<h1>404 - 页面还没做</h1>
<p>Sorry, Yemian has not been zuoed.</p>
<a-button @click="sendRestart" :loading="state.loading">重载</a-button>
<p>{{ configStore.config }}</p>
<div class="setting-container">
<div class="setting">
<h2>设置</h2>
<h3>模型配置</h3>
<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: 220px"
: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?.embed_model.des }} &nbsp;
<a-button small v-if="needRestart.embed_model" @click="sendRestart">
<ReloadOutlined />需要重启
</a-button>
</span>
<a-select style="width: 220px"
: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: 220px"
:value="configStore.config?.reranker"
@change="handleChange('reranker', $event)"
>
<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 }}</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 }}</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_query_rewrite.des }}</span>
<a-switch
:checked="configStore.config.enable_query_rewrite"
@change="handleChange('enable_query_rewrite', !configStore.config.enable_query_rewrite)"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { message } from 'ant-design-vue';
import { reactive, ref } from 'vue'
import { computed, reactive, ref } from 'vue'
import { useConfigStore } from '@/stores/counter';
import { ReloadOutlined } from '@ant-design/icons-vue';
const configStore = useConfigStore()
const needRestart = reactive({
model_provider: false,
embed_model: false,
reranker: false,
})
const items = computed(() => configStore.config._config_items)
const state = reactive({
loading: false,
})
const handleChange = (key, e) => {
console.log('Change', key, e)
needRestart[key] = true
configStore.setConfigValue(key, e)
}
const sendRestart = () => {
console.log('Restarting...')
state.loading = true
@ -33,24 +127,43 @@ const sendRestart = () => {
}
</script>
<style scoped>
.not-found {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 80vh;
text-align: center;
<style lang="less" scoped>
.setting-container {
width: 100%;
padding: 20px;
}
.not-found h1 {
font-size: 2rem;
margin-bottom: 1rem;
.setting {
max-width: 800px;
margin: 0 auto;
h3 {
margin-top: 20px;
}
.section {
margin-top: 20px;
background-color: #FAFCFD;
padding: 20px;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 16px;
// box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px solid #E2EEF3;
}
.card {
display: flex;
align-items: center;
justify-content: space-between;
.label {
margin-right: 20px;
}
}
}
.not-found p {
font-size: 1.5rem;
margin-bottom: 2rem;
}
</style>
</style>