升级了设置页面/聊天布局/数据库展示等,新增了参考资料查询
This commit is contained in:
parent
28e2b4bd0d
commit
84f6761a67
3
.gitignore
vendored
3
.gitignore
vendored
@ -30,4 +30,5 @@ cache
|
||||
*.pdf
|
||||
src/data
|
||||
neo4j*
|
||||
*/package-lock.json
|
||||
*/package-lock.json
|
||||
src/config/base.yaml
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -30,6 +30,8 @@ python -m vllm.entrypoints.openai.api_server \
|
||||
|
||||
### 2. 向量模型支持
|
||||
|
||||
需要注意,由于知识库和图数据库的构建都依赖于向量模型,如果中途更改向量模型,回导致知识库不可用。未来考虑增加一键迁移脚本。
|
||||
|
||||
|
||||
|模型名称(`config.embed_model`)|默认路径/模型|需要配置项目(`config.model_local_paths`)|
|
||||
|:-|:-|:-|
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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" />
|
||||
{{ 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" />
|
||||
{{ 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大于B,B小于C,A和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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
<!-- <button @click="deleteDatabase(database.collection_name)">删除</button> -->
|
||||
</div>
|
||||
</div>
|
||||
<h2>图数据库</h2>
|
||||
<h2>图数据库 <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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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 }}
|
||||
<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 }}
|
||||
<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 }}
|
||||
<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>
|
||||
Loading…
Reference in New Issue
Block a user