This commit is contained in:
xerrors 2024-08-27 22:00:46 +08:00
commit 65a8b06313
23 changed files with 674 additions and 318 deletions

View File

@ -51,7 +51,7 @@ class Config(SimpleConfig):
## 如果需要自定义路径,则在 config/base.yaml 中配置 model_local_paths
self.add_item("model_provider", default="zhipu", des="模型提供商", choices=["qianfan", "vllm", "zhipu", "deepseek", "dashscope"])
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("embed_model", default="zhipu-embedding-3", des="Embedding 模型", choices=list(EMBED_MODEL_INFO.keys()))
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="本地模型路径")
### <<< 默认配置结束
@ -92,22 +92,29 @@ class Config(SimpleConfig):
"""根据传入的文件覆盖掉默认配置"""
logger.info(f"Loading config from {self.filename}")
if self.filename is not None and os.path.exists(self.filename):
if self.filename.endswith(".json"):
with open(self.filename, 'r') as f:
content = f.read()
if content:
self.update(json.loads(content))
local_config = json.loads(content)
local_config.pop("_config_items")
self.update(local_config)
else:
print(f"{self.filename} is empty.")
elif self.filename.endswith(".yaml"):
with open(self.filename, 'r') as f:
content = f.read()
if content:
self.update(yaml.safe_load(content))
local_config = yaml.safe_load(content)
local_config.pop("_config_items")
self.update(local_config)
else:
print(f"{self.filename} is empty.")
else:
logger.warning(f"Unknown config file type {self.filename}")
else:
logger.warning(f"\n\n{'='*70}\n{'Config file not found':^70}\n{'You can custum your config in `' + self.filename + '`':^70}\n{'='*70}\n\n")
@ -154,7 +161,7 @@ MODEL_NAMES = {
"ERNIE-Speed-128K",
"ERNIE-Tiny-8K",
"ERNIE-Lite-8K",
"ERNIE-4.0-8K-Latest"
"ERNIE-4.0-8K-Latest",
"Yi-34B-Chat",
],
@ -170,7 +177,30 @@ MODEL_NAMES = {
"llama3.1-8b-instruct",
"llama3-8b-instruct",
"llama3.1-405b-instruct",
"baichuan2-7b-chat-v1",
"qwen2-0.5b-instruct"
]
}
EMBED_MODEL_INFO = {
"bge-large-zh-v1.5": SimpleConfig({
"name": "bge-large-zh-v1.5",
"default_path": "BAAI/bge-large-zh-v1.5",
"dimension": 1024,
"query_instruction": "为这个句子生成表示以用于检索相关文章:",
}),
"zhipu-embedding-2": SimpleConfig({
"name": "zhipu-embedding-2",
"default_path": "embedding-2",
"dimension": 1024,
}),
"zhipu-embedding-3": SimpleConfig({
"name": "zhipu-embedding-3",
"default_path": "embedding-3",
"dimension": 2048,
}),
}
RERANKER_LIST = {
"bge-reranker-v2-m3": "BAAI/bge-reranker-v2-m3",
}

View File

@ -9,10 +9,11 @@ logger = setup_logger("DataBaseManager")
class DataBaseLite:
def __init__(self, name, description, db_type, **kwargs) -> None:
def __init__(self, name, description, db_type, dimension=None, **kwargs) -> None:
self.name = name
self.description = description
self.db_type = db_type
self.dimension = dimension
self.db_id = kwargs.get("db_id", hashstr(name))
self.metaname = kwargs.get("metaname", f"{db_type[:1]}{hashstr(name)}")
self.metadata = kwargs.get("metaname", {})
@ -37,7 +38,8 @@ class DataBaseLite:
"embed_model": self.embed_model,
"metaname": self.metaname,
"metadata": self.metadata,
"files": self.files
"files": self.files,
"dimension": self.dimension
}
def to_json(self):
@ -77,6 +79,12 @@ class DataBaseManager:
"graph": data["graph"]
}
# 检查所有文件,如果出现状态是 processing 的,那么设置为 failed
for db in self.data["databases"]:
for file in db.files:
if file["status"] == "processing" or file["status"] == "waiting":
file["status"] = "failed"
def _save_databases(self):
"""将数据库的信息保存到本地的文件里面"""
self._update_database()
@ -109,10 +117,14 @@ class DataBaseManager:
else:
return {"message": "Graph base not enabled", "graph": {}}
def create_database(self, database_name, description, db_type):
new_database = DataBaseLite(database_name, description, db_type, embed_model=self.config.embed_model)
def create_database(self, database_name, description, db_type, dimension):
new_database = DataBaseLite(database_name,
description,
db_type,
embed_model=self.config.embed_model,
dimension=dimension)
self.knowledge_base.add_collection(new_database.metaname)
self.knowledge_base.add_collection(new_database.metaname, dimension)
self.data["databases"].append(new_database)
self._save_databases()
return self.get_databases()
@ -129,29 +141,36 @@ class DataBaseManager:
# filenames = [f["filename"] for f in db.files]
# if os.path.basename(file) in filenames:
# continue
db.files.append({
new_file = {
"file_id": "file_" + hashstr(file + str(time.time())),
"filename": os.path.basename(file),
"path": file,
"type": file.split(".")[-1],
"status": "waiting",
"created_at": time.time()
})
new_files.append((len(db.files) - 1, file))
}
db.files.append(new_file)
new_files.append(new_file)
for idx, file in new_files:
for new_file in new_files:
file_id = new_file["file_id"]
idx = [idx for idx, f in enumerate(db.files) if f["file_id"] == file_id][0]
db.files[idx]["status"] = "processing"
try:
text = self.read_text(file)
text = self.read_text(new_file["path"])
chunks = self.chunking(text)
self.knowledge_base.add_documents(
docs=chunks,
collection_name=db.metaname,
file_id=db.files[idx]["file_id"])
file_id=file_id)
idx = [idx for idx, f in enumerate(db.files) if f["file_id"] == file_id][0]
db.files[idx]["status"] = "done"
except Exception as e:
logger.error(f"Failed to add documents to collection {db.metaname}, {e}")
idx = [idx for idx, f in enumerate(db.files) if f["file_id"] == file_id][0]
db.files[idx]["status"] = "failed"
self._save_databases()

View File

@ -18,7 +18,6 @@ class KnowledgeBase:
self.client = MilvusClient(self.milvus_path)
def _init_config(self, config):
self.vector_dim = 1024 # 暂时不知道这个和 embedding model 的 embedding 大小有什么关系
self.milvus_path = os.path.join(config.save_dir, "data/vector_base/milvus.db")
os.makedirs(os.path.dirname(self.milvus_path), exist_ok=True)
@ -40,14 +39,14 @@ class KnowledgeBase:
# collection["id"] = hashstr(collection_name)
return collection
def add_collection(self, collection_name):
def add_collection(self, collection_name, dimension=None):
if self.client.has_collection(collection_name=collection_name):
logger.warning(f"Collection {collection_name} already exists, drop it")
self.client.drop_collection(collection_name=collection_name)
self.client.create_collection(
collection_name=collection_name,
dimension=self.vector_dim, # The vectors we will use in this demo has 768 dimensions
dimension= dimension, # The vectors we will use in this demo has 768 dimensions
)
def add_documents(self, docs, collection_name, **kwargs):
@ -55,8 +54,8 @@ class KnowledgeBase:
# 检查 collection 是否存在
import random
if not self.client.has_collection(collection_name=collection_name):
logger.warning(f"Collection {collection_name} not found, create it")
self.add_collection(collection_name)
logger.error(f"Collection {collection_name} not found, create it")
# self.add_collection(collection_name)
vectors = self.embed_model.encode(docs)

View File

@ -66,29 +66,31 @@ class Retriever:
def query_knowledgebase(self, query, history, refs):
"""查询知识库"""
rw_query = self.rewrite_query(query, history, refs)
kb_res = []
final_res = []
if refs["meta"].get("db_name") and self.config.enable_knowledge_base:
if not refs["meta"].get("db_name") or not self.config.enable_knowledge_base:
return {"results": final_res, "all_results": kb_res, "rw_query": query, "message": "Knowledge base is disabled"}
db_name = refs["meta"]["db_name"]
kb = self.dbm.metaname2db[db_name]
limit = refs["meta"].get("queryCount", 10)
rw_query = self.rewrite_query(query, history, refs)
kb_res = self.dbm.knowledge_base.search(rw_query, db_name, limit=limit)
db_name = refs["meta"]["db_name"]
kb = self.dbm.metaname2db[db_name]
limit = refs["meta"].get("queryCount", 10)
kb_res = self.dbm.knowledge_base.search(rw_query, db_name, limit=limit)
for r in kb_res:
r["file"] = kb.id2file(r["entity"]["file_id"])
if self.config.enable_reranker:
RERANK_THRESHOLD = 0.1
for r in kb_res:
r["file"] = kb.id2file(r["entity"]["file_id"])
r["rerank_score"] = self.reranker.compute_score([query, r["entity"]["text"]], normalize=True)
kb_res.sort(key=lambda x: x["rerank_score"], reverse=True)
final_res = [_res for _res in kb_res if _res["rerank_score"] > RERANK_THRESHOLD]
if self.config.enable_reranker:
RERANK_THRESHOLD = 0.1
for r in kb_res:
r["rerank_score"] = self.reranker.compute_score([query, r["entity"]["text"]], normalize=True)
kb_res.sort(key=lambda x: x["rerank_score"], reverse=True)
final_res = [_res for _res in kb_res if _res["rerank_score"] > RERANK_THRESHOLD]
else:
final_res = kb_res[:5]
else:
final_res = kb_res[:5]
return {"results": final_res, "all_results": kb_res, "rw_query": rw_query}

View File

@ -11,8 +11,6 @@ class OpenAIBase():
self.model_name = model_name
def predict(self, message, stream=False):
logger.debug(message)
if isinstance(message, str):
messages=[{"role": "user", "content": message}]
else:

View File

@ -1,37 +1,24 @@
import os
from FlagEmbedding import FlagModel, FlagReranker
from src.config import EMBED_MODEL_INFO, RERANKER_LIST
from src.utils.logging_config import setup_logger
logger = setup_logger("EmbeddingModel")
SUPPORT_LIST = {
"bge-large-zh-v1.5": "BAAI/bge-large-zh-v1.5",
"zhipu": "embedding-2",
}
RERANKER_LIST = {
"bge-reranker-v2-m3": "BAAI/bge-reranker-v2-m3",
}
QUERY_INSTRUCTION = {
"bge-large-zh-v1.5": "为这个句子生成表示以用于检索相关文章:",
}
class EmbeddingModel(FlagModel):
def __init__(self, config, **kwargs):
assert config.embed_model in SUPPORT_LIST.keys(), f"Unsupported embed model: {config.embed_model}, only support {SUPPORT_LIST}"
model_name_or_path = config.model_local_paths.get(config.embed_model, SUPPORT_LIST[config.embed_model])
logger.info(f"Loading embedding model {config.embed_model} from {model_name_or_path}")
def __init__(self, model_info, config, **kwargs):
self.info = model_info
model_name_or_path = config.model_local_paths.get(model_info.name, model_info.default_path)
logger.info(f"Loading embedding model {model_info.name} from {model_name_or_path}")
super().__init__(model_name_or_path,
query_instruction_for_retrieval=QUERY_INSTRUCTION[config.embed_model],
query_instruction_for_retrieval=model_info.get("query_instruction", None),
use_fp16=False, **kwargs)
logger.info(f"Embedding model {config.embed_model} loaded")
logger.info(f"Embedding model {model_info.name} loaded")
class Reranker(FlagReranker):
@ -50,18 +37,27 @@ from zhipuai import ZhipuAI
class ZhipuEmbedding:
def __init__(self, config) -> None:
def __init__(self, model_info, config) -> None:
self.config = config
self.model_info = model_info
self.client = ZhipuAI(api_key=os.getenv("ZHIPUAPI"))
logger.info("Zhipu Embedding model loaded")
self.query_instruction_for_retrieval = "为这个句子生成表示以用于检索相关文章:"
def predict(self, message):
response = self.client.embeddings.create(
model=SUPPORT_LIST[self.config.embed_model],
input=message
)
return [a["embedding"] for a in response["data"]]
data = []
for i in range(0, len(message), 10):
group_msg = message[i:i+10]
response = self.client.embeddings.create(
model=self.model_info.default_path,
input=group_msg,
)
data.extend([a.embedding for a in response.data])
return data
def encode(self, message):
return self.predict(message)
@ -75,7 +71,12 @@ def get_embedding_model(config):
if not config.enable_knowledge_base:
return None
if config.embed_model == "zhipu":
return ZhipuEmbedding(config)
else:
return EmbeddingModel(config)
assert config.embed_model in EMBED_MODEL_INFO.keys(), f"Unsupported embed model: {config.embed_model}, only support {EMBED_MODEL_INFO.keys()}"
if config.embed_model in ["bge-large-zh-v1.5"]:
model = EmbeddingModel(EMBED_MODEL_INFO[config.embed_model], config)
if config.embed_model in ["zhipu-embedding-2", "zhipu-embedding-3"]:
model = ZhipuEmbedding(EMBED_MODEL_INFO[config.embed_model], config)
return model

View File

@ -3,21 +3,23 @@ import os
from datetime import datetime
# DATETIME = datetime.now().strftime('%Y-%m-%d-%H%M%S')
DATETIME = "debug" # 为了方便,调试的时候输出到 debug.log 文件
DATETIME = datetime.now().strftime('%Y-%m-%d-%H%M%S')
# DATETIME = "debug" # 为了方便,调试的时候输出到 debug.log 文件
LOG_FILE = f'log/project-{DATETIME}.log'
def setup_logger(name, log_file=None, level=logging.DEBUG, console=False):
if log_file is None:
log_file = f'log/project-{DATETIME}.log'
def setup_logger(name, level=logging.DEBUG, console=False):
os.makedirs("log", exist_ok=True)
"""Function to setup logger with the given name and log file."""
logger = logging.getLogger(name)
logger.setLevel(level)
# 清除已有的 Handler防止重复添加
if logger.hasHandlers():
logger.handlers.clear()
# File handler for logging to a file
file_handler = logging.FileHandler(log_file)
file_handler = logging.FileHandler(LOG_FILE)
file_handler.setLevel(level)
# Formatter for the logs
@ -34,6 +36,7 @@ def setup_logger(name, log_file=None, level=logging.DEBUG, console=False):
return logger
# Setup the root logger
logger = setup_logger('Athena')

View File

@ -29,7 +29,6 @@ def chat():
request_data = json.loads(request.data)
query = request_data['query']
meta = request_data.get('meta')
logger.debug(f"Web query: {query}")
history_manager = HistoryManager(request_data['history'])
new_query, refs = startup.retriever(query, history_manager.messages, meta)
@ -63,7 +62,7 @@ def call():
request_data = json.loads(request.data)
query = request_data['query']
response = startup.model.predict(query)
logger.debug(f"\n\n\nCall query: \n{query} \n\nResponse: \n{response.content}\n\n")
logger.debug({"query": query, "response": response.content})
return jsonify({
"response": response.content,
@ -83,4 +82,13 @@ def update_config():
@common.route('/restart', methods=['POST'])
def restart():
startup.restart()
return jsonify({"message": "Restarted!"})
return jsonify({"message": "Restarted!"})
@common.route('/log', methods=['GET'])
def get_log():
from src.utils.logging_config import LOG_FILE
with open(LOG_FILE, 'r') as f:
log = f.read()
return jsonify({"log": log})

View File

@ -27,8 +27,9 @@ def create_database():
database_name = data.get('database_name')
description = data.get('description')
db_type = data.get('db_type')
dimension = data.get('dimension')
logger.debug(f"Create database {database_name}")
database = startup.dbm.create_database(database_name, description, db_type)
database = startup.dbm.create_database(database_name, description, db_type, dimension=dimension)
return jsonify(database)
@db.route('/', methods=['DELETE'])
@ -103,7 +104,7 @@ def upload_file():
if file:
upload_dir = os.path.join(startup.config.save_dir, "data/uploads")
os.makedirs(upload_dir, exist_ok=True)
filename = f"{hashstr(file.filename, 6, with_salt=True)}_{file.filename}"
filename = f"{hashstr(file.filename, 4, with_salt=True)}_{file.filename}"
file_path = os.path.join(upload_dir, filename)
file.save(file_path)
return jsonify({'message': 'File successfully uploaded', 'file_path': file_path}), 200

View File

@ -19,8 +19,10 @@
"d3": "^7.8.3",
"echarts": "^5.4.2",
"echarts-gl": "^2.0.9",
"highlight.js": "^11.10.0",
"less": "^4.1.3",
"marked": "^13.0.2",
"marked-highlight": "^2.1.4",
"pinia": "^2.0.32",
"vue": "^3.2.47",
"vue-router": "^4.1.6"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -1,5 +1,17 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--main-900: #003A51;
--main-800: #004F69;
--main-700: #00637F;
--main-600: #1B7796;
--main-500: #2C86A8;
--main-400: #4e99b9;
--main-300: #6AADCB;
--main-200: #8CC6E1;
--main-100: #ABE0F7;
--main-50: #CDF5FF;
--main-25: #E6FAFF;
--c-white: #ffffff;
--c-white-soft: #f8f8f8;
--c-white-mute: #f2f2f2;
@ -8,60 +20,28 @@
--c-black-soft: #222222;
--c-black-mute: #282828;
--c-indigo: #2c3e50;
--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);
--c-text-dark-1: var(--c-white);
--c-text-light-1: var(--c-black);
--c-text-dark-1: #0D0D0D;
--c-text-dark-2: #b8b8b8;
}
/* semantic color variables for this project */
:root {
--color-background: var(--c-white);
--color-background-soft: var(--c-white-soft);
--color-background-mute: var(--c-white-mute);
--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-black);
--section-gap: 160px;
--main-color: #005f77;
--main-color: var(--main-700);
--main-color-dark: #004d5c;
--main-light-1: #007f96;
--main-light-2: #D3EAED;
--main-light-3: #EDF4F5;
--main-light-4: #F2F6F7;
--main-light-1: #0076AB;
--main-light-2: #DAEAED;
--main-light-3: #EDF0F1;
--main-light-4: #F2F5F5;
--main-light-5: #F7FAFB;
--main-light-6: #FAFDFD;
--min-width: 400px;
--min-header-width: 80px;
--error-color: #f50a0d;
}
/* @media (prefers-color-scheme: dark) {
:root {
--color-background: var(--c-black);
--color-background-soft: var(--c-black-soft);
--color-background-mute: var(--c-black-mute);
--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);
}
} */
*,
*::before,
*::after {

View File

@ -2,6 +2,6 @@ export const themeConfig = {
token: {
colorPrimary: '#005F77',
colorInfo: '#191919',
wireframe: true
fontFamily: "'HarmonyOS Sans SC', Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;"
},
}

View File

@ -14,7 +14,7 @@
class="newchat nav-btn"
@click="$emit('newconv')"
>
<PlusCircleOutlined /> <span class="text">新对话</span>
<PlusCircleOutlined /> <span class="text">新对话 {{ configStore.config?.model_name }}</span>
</div>
</div>
<div class="header__right">
@ -107,53 +107,17 @@
:class="message.role"
>
<p v-if="message.role=='sent'" style="white-space: pre-line" class="message-text">{{ message.text }}</p>
<div v-else-if="message.text.length == 0" class="loading-dots">
<div></div>
<div></div>
<div></div>
</div>
<p v-else
v-html="renderMarkdown(message.text)"
v-html="renderMarkdown(message)"
class="message-md"
@click="consoleMsg(message)"></p>
<div class="refs" v-if="message.role=='received' && message.groupedResults && message.status=='finished'">
<a-tag
class="filetag"
v-for="(results, filename) in message.groupedResults"
:key="filename"
@click="opts.openDetail = true"
:bordered="false"
>
{{ filename }}
<a-drawer
v-model:open="opts.openDetail"
title="检索详情"
width="800"
:contentWrapperStyle="{ maxWidth: '100%'}"
placement="right"
class="retrieval-detail"
rootClassName="root"
>
<div class="fileinfo">
<p><strong>文件名:</strong> {{ results[0].file.filename }}</p>
<p><strong>文件类型:</strong> {{ results[0].file.type }}</p>
<p><strong>创建时间:</strong> {{ new Date(results[0].file.created_at * 1000).toLocaleString() }}</p>
</div>
<div v-for="(res, idx) in results" :key="idx" class="result-item">
<p class="result-id"><strong>ID:</strong> #{{ res.id }}</p>
<p class="result-distance">
<strong>相似度距离:</strong>
<div class=scorebar>
<a-progress :percent="(res.distance * 100).toFixed(2)" stroke-color="#1677FF" :size="[200, 10]"/>
</div>
</p>
<p class="result-rerank-score">
<strong>重排序分数:</strong>
<div class=scorebar>
<a-progress :percent="(res.rerank_score * 100).toFixed(2)" stroke-color="#1677FF" :size="[200, 10]"/>
</div>
</p>
<p class="result-text">{{ res.entity.text }}</p>
</div>
</a-drawer>
</a-tag>
</div>
<RefsComponent v-if="message.role=='received' && message.status=='finished'" :message="message" />
</div>
</div>
<div class="bottom">
@ -169,7 +133,7 @@
<template #icon> <SendOutlined v-if="!isStreaming" /> <LoadingOutlined v-else/> </template>
</a-button>
</div>
<p class="note">即便强如雅典娜也可能会出错请注意辨别内容的可靠性 模型供应商{{ configStore.config?.model_provider }}:{{ configStore.config?.model_name }}</p>
<p class="note">请注意辨别内容的可靠性 模型供应商{{ configStore.config?.model_provider }}: {{ configStore.config?.model_name }}</p>
</div>
</div>
</template>
@ -190,10 +154,16 @@ import {
PlusCircleOutlined,
FolderOutlined,
FolderOpenOutlined,
GlobalOutlined,
FileTextOutlined,
} from '@ant-design/icons-vue'
import { onClickOutside } from '@vueuse/core'
import { marked } from 'marked';
import { Marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
import { useConfigStore } from '@/stores/config'
import RefsComponent from '@/components/RefsComponent.vue'
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';
const props = defineProps({
conv: Object,
@ -232,16 +202,30 @@ const meta = reactive(JSON.parse(localStorage.getItem('meta')) || {
history_round: 5,
})
// marked https://marked.js.org/
marked.setOptions({
gfm: true,
breaks: true,
tables: true,
});
const marked = new Marked(
{
gfm: true,
breaks: true,
tables: true,
},
markedHighlight({
langPrefix: 'hljs language-',
highlight(code) {
return hljs.highlightAuto(code).value;
}
})
);
const renderMarkdown = (text) => marked(text)
const consoleMsg = (message) => console.log(message)
onClickOutside(panel, () => setTimeout(() => opts.showPanel = false, 30))
const renderMarkdown = (message) => {
if (message.status === 'loading') {
return marked.parse(message.text + '🟢')
} else {
return marked.parse(message.text)
}
}
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
@ -307,22 +291,24 @@ const appendAiMessage = (message, refs=null) => {
role: 'received',
text: message,
refs,
status: "querying"
status: "querying",
model_name: configStore.config.model_name
})
scrollToBottom()
}
const updateMessage = (text, id, refs, status) => {
const message = conv.value.messages.find((message) => message.id === id)
const message = conv.value.messages.find((message) => message.id === id);
if (message) {
message.text = text
message.refs = refs
message.status = status
message.refs = refs;
message.status = status;
message.text = text;
} else {
console.error('Message not found')
console.error('Message not found');
}
scrollToBottom()
}
scrollToBottom();
};
const updateStatus = (id, status) => {
const message = conv.value.messages.find((message) => message.id === id)
@ -332,7 +318,6 @@ const updateStatus = (id, status) => {
console.error('Message not found')
}
console.log("updateStatus", message, message.refs.knowledge_base.results.length > 0)
if (message.refs.knowledge_base.results.length > 0) {
message.groupedResults = message.refs.knowledge_base.results.reduce((acc, result) => {
const { filename } = result.file;
@ -373,7 +358,7 @@ const sendMessage = () => {
if (user_input) {
isStreaming.value = true
appendUserMessage(user_input)
appendAiMessage("···", null)
appendAiMessage("", null)
const cur_res_id = conv.value.messages[conv.value.messages.length - 1].id
conv.value.inputText = ''
meta.db_name = opts.databases[meta.selectedKB]?.metaname
@ -585,7 +570,7 @@ watch(
.chat-box {
width: 100%;
max-width: 1100px;
max-width: 900px;
margin: 0 auto;
flex-grow: 1;
padding: 1rem;
@ -593,25 +578,24 @@ watch(
flex-direction: column;
.message-box {
max-width: 95%;
display: inline-block;
border-radius: 0.8rem;
border-radius: 1.5rem;
margin: 0.8rem 0;
padding: 1rem;
padding: 0.625rem 1.25rem;
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;
color: black;
/* 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;
max-width: 95%;
background: var(--main-light-3);
align-self: flex-end;
}
@ -619,12 +603,13 @@ watch(
.message-box.received {
color: initial;
width: fit-content;
padding-top: 16px;
background-color: #F5F7F8;
text-align: left;
word-wrap: break-word;
margin-bottom: 0;
margin: 0;
padding-bottom: 0;
padding-top: 16px;
padding-left: 0;
padding-right: 0;
text-align: justify;
}
@ -639,55 +624,9 @@ watch(
margin-bottom: 0;
}
.refs {
margin-bottom: 20px;
.filetag:hover {
cursor: pointer;
}
}
}
.retrieval-detail {
.fileinfo {
margin-bottom: 20px;
padding: 10px;
background: var(--main-light-3);
border-radius: 8px;
p {
margin: 0;
line-height: 1.5;
}
}
.result-item {
margin-bottom: 20px;
padding: 10px;
border: 1px solid #e8e8e8;
border-radius: 8px;
background: var(--main-light-4);
.result-id,
.result-distance,
.result-rerank-score,
.result-text-label,
.result-text {
margin: 5px 0;
}
.scorebar {
margin-left: 10px;
display: inline-block;
width: 200px;
padding-bottom: 2px;
& > * {
margin: 0;
}
}
}
}
@ -696,23 +635,23 @@ watch(
bottom: 0;
width: 100%;
margin: 0 auto;
padding: 0.5rem 2rem;
padding: 4px 2rem 0 2rem;
background: white;
.input-box {
display: flex;
width: 100%;
max-width: 1100px;
max-width: 900px;
margin: 0 auto;
align-items: flex-end;
background-color: #F5F7F8;
background-color: var(--main-light-4);
border-radius: 2rem;
height: auto;
padding: 0.5rem;
&:focus-within {
background-color: #F2F2F2;
background-color: #ECF1F2;
}
.user-input {
@ -762,9 +701,10 @@ watch(
background-color: transparent;
border: none;
height: 2.5rem;
background-color: black;
background-color: var(--main-color);
border-radius: 3rem;
color: white;
transition: background-color 0.3s;
&:hover {
color: white;
@ -772,7 +712,7 @@ watch(
}
button:disabled {
background: #D7D7D7;
background: #E0E0E0;
cursor: not-allowed;
}
@ -803,8 +743,37 @@ button:disabled {
border-radius: 4px;
}
.slide-out-left{-webkit-animation:slide-out-left .5s cubic-bezier(.55,.085,.68,.53) both;animation:slide-out-left .5s cubic-bezier(.55,.085,.68,.53) both}
.swing-in-top-fwd {-webkit-animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;}
.loading-dots {
display: inline-flex;
align-items: center;
justify-content: center;
}
.loading-dots div {
width: 10px;
height: 10px;
margin: 0 5px;
background-color: #333;
border-radius: 50%;
animation: loading 0.8s infinite ease-in-out both;
}
.loading-dots div:nth-child(1) {
animation-delay: 0s;
}
.loading-dots div:nth-child(2) {
animation-delay: 0.2s;
}
.loading-dots div:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes loading {0%,80%,100%{transform:scale(0.5);}40%{transform:scale(1);}}
.slide-out-left{-webkit-animation:slide-out-left .2s cubic-bezier(.55,.085,.68,.53) both;animation:slide-out-left .5s cubic-bezier(.55,.085,.68,.53) both}
.swing-in-top-fwd {-webkit-animation: swing-in-top-fwd 0.2s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;}
@-webkit-keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}}@keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}}
@-webkit-keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}}@keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}}
@ -851,3 +820,16 @@ button:disabled {
}
}
</style>
<style lang="less">
.message-box pre {
border-radius: 8px;
font-size: 14px;
border: 1px solid var(--main-light-3);
padding: 1rem;
&:has(code.hljs) {
padding: 0;
}
}
</style>

View File

@ -0,0 +1,85 @@
<template>
<div class="log-viewer">
<a-button @click="fetchLogs">刷新</a-button>
<div ref="logContainer" class="log-container">
<pre v-if="logs">{{ logs }}</pre>
</div>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref, onMounted, onActivated, nextTick } from 'vue';
// ref
const logs = ref('');
const error = ref('');
// ref DOM
const logContainer = ref(null);
//
const fetchLogs = async () => {
try {
//
error.value = '';
//
const response = await fetch('/api/log'); // API
if (!response.ok) {
throw new Error('Failed to fetch logs');
}
// JSON
const data = await response.json();
// logs
logs.value = data.log;
// DOM
await nextTick();
if (logContainer.value) {
logContainer.value.scrollTop = logContainer.value.scrollHeight;
}
} catch (err) {
//
error.value = `Error: ${err.message}`;
}
};
//
onMounted(() => {
fetchLogs();
setInterval(fetchLogs, 5000); // 5
});
// keep-alive
onActivated(() => {
fetchLogs();
});
</script>
<style scoped>
.log-viewer {}
.error {
color: red;
}
.log-container {
max-height: 80vh; /* 设置最大高度 */
overflow-y: auto; /* 启用垂直滚动 */
background-color: #f0f0f0;
margin: 20px 0;
padding: 10px;
padding-bottom: 0;
border-radius: 5px;
white-space: pre-wrap; /* 使日志内容自动换行 */
word-wrap: break-word;
background: #0C0C0C;
color: #D1D1D1;
}
</style>

View File

@ -0,0 +1,143 @@
<!-- RefsComponent.vue -->
<template>
<div class="refs" v-if="showRefs">
<span class="item"><GlobalOutlined /> {{ message.model_name }}</span>
<div class="tags">
<span class="filetag item"
v-for="(results, filename) in message.groupedResults"
:key="filename"
@click="openDetail = true"
>
<FileTextOutlined /> {{ filename }}
<a-drawer
v-model:open="openDetail"
title="检索详情"
width="800"
:contentWrapperStyle="{ maxWidth: '100%'}"
placement="right"
class="retrieval-detail"
rootClassName="root"
>
<div class="fileinfo">
<p><strong>文件名:</strong> {{ results[0].file.filename }}</p>
<p><strong>文件类型:</strong> {{ results[0].file.type }}</p>
<p><strong>创建时间:</strong> {{ new Date(results[0].file.created_at * 1000).toLocaleString() }}</p>
</div>
<div v-for="(res, idx) in results" :key="idx" class="result-item">
<p class="result-id"><strong>ID:</strong> #{{ res.id }}</p>
<p class="result-distance">
<strong>相似度距离:</strong>
<div class="scorebar">
<a-progress :percent="(res.distance * 100).toFixed(2)" stroke-color="#1677FF" :size="[200, 10]"/>
</div>
</p>
<p class="result-rerank-score">
<strong>重排序分数:</strong>
<div class="scorebar">
<a-progress :percent="(res.rerank_score * 100).toFixed(2)" stroke-color="#1677FF" :size="[200, 10]"/>
</div>
</p>
<a-divider />
<p class="result-text">{{ res.entity.text }}</p>
</div>
</a-drawer>
</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import {
GlobalOutlined,
FileTextOutlined,
} from '@ant-design/icons-vue'
const props = defineProps({
message: Object,
})
const message = ref(props.message)
const openDetail = ref(false)
const showRefs = computed(() => message.value.role=='received' && message.value.status=='finished')
</script>
<style lang="less" scoped>
.refs {
display: flex;
margin-bottom: 20px;
color: var(--c-text-light-4);
font-size: 14px;
gap: 10px;
.item {
background: var(--main-25);
color: var(--main-800);
border: 1px solid var(--main-100);
padding: 2px 8px;
border-radius: 8px;
font-size: 14px;
}
.tags {
display: flex;
gap: 10px;
.filetag {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
&:hover {
background: var(--main-100);
}
}
}
}
.retrieval-detail {
.fileinfo {
margin-bottom: 20px;
padding: 1rem;
background: var(--main-25);
color: var(--main-800);
border-radius: 8px;
border: 1px solid var(--main-100);
p {
margin: 10px;
line-height: 1.5;
}
}
.result-item {
margin-bottom: 20px;
padding: 24px 16px 10px 16px;
border: 1px solid #e8e8e8;
border-radius: 8px;
background: var(--main-light-6);
.result-id,
.result-distance,
.result-rerank-score,
.result-text-label,
.result-text {
margin: 5px 0;
}
.scorebar {
margin-left: 10px;
display: inline-block;
width: 200px;
padding-bottom: 2px;
& > * {
margin: 0;
}
}
}
}
</style>

View File

@ -1,5 +1,5 @@
<script setup>
import { KeepAlive, onMounted } from 'vue'
import { ref, KeepAlive, onMounted } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import {
MessageOutlined,
@ -9,14 +9,19 @@ import {
BookOutlined,
BookFilled,
GithubOutlined,
DatabaseOutlined,
DatabaseFilled,
} from '@ant-design/icons-vue'
import { themeConfig } from '@/assets/theme'
import { useConfigStore } from '@/stores/config'
import { useDatabaseStore } from '@/stores/database'
import DebugComponent from '@/components/DebugComponent.vue'
const configStore = useConfigStore()
const databaseStore = useDatabaseStore()
const showDebug = ref(false)
const getRemoteConfig = () => {
fetch('/api/config').then(res => res.json()).then(data => {
console.log(data)
@ -46,6 +51,18 @@ console.log(route)
<template>
<div class="app-layout">
<div class="debug-panel">
<div class="shown-btn" @click="showDebug=!showDebug">Debug</div>
<a-drawer
v-model:open="showDebug"
title="调试面板"
width="800"
:contentWrapperStyle="{ maxWidth: '100%'}"
placement="right"
>
<DebugComponent />
</a-drawer>
</div>
<div class="header">
<div class="logo">
<router-link to="/"><img src="/jnu.png"> </router-link>
@ -55,7 +72,7 @@ console.log(route)
<component class="icon" :is="route.path === '/chat' ? MessageFilled : MessageOutlined" />
</RouterLink>
<RouterLink to="/database" class="nav-item" active-class="active">
<component class="icon" :is="route.path.startsWith('/database') ? BookFilled : BookOutlined" />
<component class="icon" :is="route.path.startsWith('/database') ? DatabaseFilled : DatabaseOutlined" />
</RouterLink>
</div>
<div class="fill" style="flex-grow: 1;"></div>
@ -96,6 +113,19 @@ console.log(route)
.header-mobile {
display: none;
}
.debug-panel {
position: absolute;
z-index: 100;
right: 0;
top: 50px;
border-radius: 16px 0 0 16px;
background-color: var(--main-light-3);
padding: 8px 8px 8px 16px;
box-shadow: 0 0 20px 10px rgba(0, 0, 0, 0.1);
transition: right 0.3s ease-in-out;
cursor: pointer;
}
}
div.header, #app-router-view {
@ -111,13 +141,13 @@ div.header, #app-router-view {
.header {
display: flex;
flex-direction: column;
flex: 0 0 80px;
flex: 0 0 70px;
justify-content: flex-start;
align-items: center;
background-color: var(--main-light-4);
background-color: var(--main-light-3);
height: 100%;
width: 80px;
border-right: 1px solid var(--main-light-2);
width: 70px;
border-right: 1px solid var(--main-light-3);
.logo {
width: 40px;
@ -150,12 +180,12 @@ div.header, #app-router-view {
&.active {
font-weight: bold;
color: var(--main-color);
background-color: var(--main-light-2);
color: var(--main-600);
background-color: #E6E8E9;
}
&:hover {
background-color: var(--main-light-2);
background-color: #E6E8E9;
cursor: pointer;
}
}
@ -205,7 +235,7 @@ div.header, #app-router-view {
.nav-item {
text-decoration: none;
width: 40px;
color: var(--c-text-light-2);
color: var(--c-black-soft);
font-size: 1rem;
font-weight: bold;
transition: color 0.1s ease-in-out, font-size 0.1s ease-in-out;

View File

@ -67,6 +67,19 @@ const router = createRouter({
}
]
},
// {
// path: '/monitor',
// name: 'monitor',
// component: AppLayout,
// children: [
// {
// path: '',
// name: 'monitor',
// component: () => import('../views/MonitorView.vue'),
// meta: { keepAlive: true }
// }
// ]
// },
{
path: '/:pathMatch(.*)*',
name: 'NotFound',

View File

@ -6,13 +6,15 @@
<span style="font-weight: bold;">对话历史</span>
<div class="action close" @click="state.isSidebarOpen = false"><MenuOutlined /></div>
</div>
<div class="conversation"
v-for="(state, index) in convs"
:key="index"
:class="{ active: curConvId === index }"
@click="goToConversation(index)">
<div class="conversation__title">{{ state.title }}</div>
<div class="conversation__delete" @click.stop="delConv(index)"><DeleteOutlined /></div>
<div class="conversation-list">
<div class="conversation"
v-for="(state, index) in convs"
:key="index"
:class="{ active: curConvId === index }"
@click="goToConversation(index)">
<div class="conversation__title">{{ state.title }}</div>
<div class="conversation__delete" @click.stop="delConv(index)"><DeleteOutlined /></div>
</div>
</div>
</div>
<ChatComponent
@ -132,17 +134,12 @@ onMounted(() => {
flex: 1 1 auto; /* 当侧边栏打开时,占据可用空间 */
}
.conversations {
display: flex;
flex-direction: column;
width: 230px; /* 初始宽度 */
height: 100%;
max-width: 230px;
overflow-y: auto;
border-right: 1px solid var(--main-light-3);
background-color: #FAFCFD;
overflow: hidden; /* 确保内容不溢出 */
white-space: nowrap; /* 防止文本换行 */
transition: all 0.2s ease-out;
max-height: 100%;
background-color: #FAFCFD;
& .actions {
height: var(--header-height);
@ -150,10 +147,8 @@ onMounted(() => {
justify-content: space-between;
align-items: center;
padding: 16px;
position: sticky;
top: 0;
background-color: #FAFCFD;
z-index: 9;
border-bottom: 1px solid var(--main-light-3);
.action {
font-size: 1.2rem;
@ -172,7 +167,14 @@ onMounted(() => {
}
}
.conversation {
.conversation-list {
display: flex;
flex-direction: column;
overflow-y: auto;
max-height: 100%;
}
.conversation-list .conversation {
display: flex;
justify-content: space-between;
align-items: center;
@ -199,12 +201,12 @@ onMounted(() => {
}
&.active {
border-left: 3px solid var(--main-color);
border-left: 3px solid var(--main-600);
padding-left: 13px;
background-color: var(--main-light-3);
& .conversation__title {
color: var(--c-black-light-1);
color: var(--main-900);
}
}
@ -218,33 +220,33 @@ onMounted(() => {
}
}
.conversations::-webkit-scrollbar {
.conversation-list::-webkit-scrollbar {
position: absolute;
width: 4px;
}
.conversations::-webkit-scrollbar-track {
.conversation-list::-webkit-scrollbar-track {
background: transparent;
border-radius: 4px;
}
.conversations::-webkit-scrollbar-thumb {
.conversation-list::-webkit-scrollbar-thumb {
background: var(--c-text-dark-2);
border-radius: 4px;
}
.conversations::-webkit-scrollbar-thumb:hover {
.conversation-list::-webkit-scrollbar-thumb:hover {
background: rgb(100, 100, 100);
border-radius: 4px;
}
.conversations::-webkit-scrollbar-thumb:active {
.conversation-list::-webkit-scrollbar-thumb:active {
background: rgb(68, 68, 68);
border-radius: 4px;
}
@media (max-width: 520px) {
.conversations {
.conversation {
position: absolute;
z-index: 101;
width: 300px;

View File

@ -19,12 +19,12 @@
</div>
<a-divider/>
<div class="pagebtns">
<a-button @click="state.curPage='add'" :class="{ 'active': state.curPage === 'add' }">
<div @click="state.curPage='add'" :class="{ 'active': state.curPage === 'add' }">
<CloudUploadOutlined />添加文件
</a-button>
<a-button @click="state.curPage='query-test'" :class="{ 'active': state.curPage === 'query-test' }">
</div>
<div @click="state.curPage='query-test'" :class="{ 'active': state.curPage === 'query-test' }">
<SearchOutlined />检索测试
</a-button>
</div>
</div>
<div class="query-params" v-if="state.curPage == 'query-test'">
<p style="text-align: center; margin: 0;"><strong>参数配置</strong></p>
@ -494,18 +494,31 @@ onMounted(() => {
flex-direction: column;
gap: 16px;
button {
> div {
gap: 1rem;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 10px 16px;
height: auto;
border-radius: 8px;
border-radius: 4px;
border: none;
background: var(--main-light-4);
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 {
font-weight: bold;
color: var(--main-color);
background: var(--main-light-2);
background: var(--main-light-3);
font-weight: bold;
}
}

View File

@ -11,6 +11,9 @@
placeholder="新建数据库描述"
:auto-size="{ minRows: 2, maxRows: 5 }"
/>
<h3 style="margin-top: 20px;">向量维度</h3>
<p>必须与向量模型 {{ configStore.config.embed_model }} 一致</p>
<a-input v-model:value="newDatabase.dimension" placeholder="向量维度 (e.g. 768, 1024)" />
<template #footer>
<a-button key="back" @click="newDatabase.open=false">取消</a-button>
<a-button key="submit" type="primary" :loading="newDatabase.loading" @click="createDatabase">创建</a-button>
@ -40,7 +43,8 @@
</div>
<p class="description">{{ database.description }}</p>
<div class="tags">
<a-tag color="blue" v-if="database.embed_model">Embed: {{ database.embed_model }}</a-tag>
<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>
</div>
<!-- <button @click="deleteDatabase(database.collection_name)">删除</button> -->
</div>
@ -94,6 +98,7 @@ const configStore = useConfigStore()
const newDatabase = reactive({
name: '',
description: '',
dimension: '',
loading: false,
})
@ -123,7 +128,8 @@ const createDatabase = () => {
body: JSON.stringify({
database_name: newDatabase.name,
description: newDatabase.description,
db_type: "knowledge"
db_type: "knowledge",
dimension: newDatabase.dimension,
})
})
.then(response => response.json())
@ -132,7 +138,8 @@ const createDatabase = () => {
loadDatabases()
newDatabase.open = false
newDatabase.name = ''
newDatabase.description = ''
newDatabase.description = '',
newDatabase.dimension = ''
})
.finally(() => {
newDatabase.loading = false
@ -238,6 +245,7 @@ onMounted(() => {
background-color: #F5F8FF;
border-radius: 8px;
border: 1px solid #E0EAFF;
color: var(--main-color);
}
.info {
@ -246,6 +254,11 @@ onMounted(() => {
color: black;
}
h3 {
font-size: 16px;
font-weight: bold;
}
p {
color: var(--c-text-light-1);
font-size: small;

View File

@ -1,10 +1,11 @@
<template>
<div class="welcome">
<header>江南大学人工智能与计算机学院</header>
<header class="glass-header">江南大学人工智能与计算机学院</header>
<h1>{{ title }}</h1>
<p>大模型驱动的知识库管理工具</p>
<button class="home-btn" @click="goToChat">开始对话</button>
<img src="/home.png" alt="Placeholder Image" />
<footer>© 江南语析 2024</footer>
<footer>© 江南语析 2024 [WIP] v0.12.138</footer>
</div>
</template>
@ -12,7 +13,7 @@
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
const title = ref('Athena ✨')
const title = ref('📢 Athena ✨')
const router = useRouter()
const goToChat = () => {
@ -21,45 +22,46 @@ const goToChat = () => {
</script>
<style scoped>
<style lang="less" scoped>
.welcome {
display: flex;
flex-direction: column;
align-items: center;
/* margin-top: 50px; */
min-height: 100vh;
color: #333;
text-align: center;
background: linear-gradient(168deg, #ffd6eb, #ffe7ca, #d3fffb, #dbebff, #ffd8ff);
background-size: 1000% 1000%;
animation: animateBackground 20s ease infinite;
}
header {
background-color: var(--main-color);
font-size: 1.2rem;
font-weight: bold;
color: aliceblue;
color: var(--main-color);
width: 100%;
padding: 1rem 0;
backdrop-filter: blur(10px);
width: 100%;
background-color: rgba(255, 255, 255, 0.25);
border-bottom: 2px solid var(--main-color);
}
h1 {
font-size: 48px;
font-weight: bold;
font-weight: 600;
margin-top: calc(20vh - 80px);
margin-bottom: 0;
}
p {
font-size: 24px;
font-size: 18px;
text-align: center;
}
img {
width: 700px;
height: auto;
object-fit: cover;
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.05);
border-radius: 1rem;
max-width: 90%;
}
button.home-btn {
padding: 0.5rem 2rem;
font-size: 24px;
@ -70,12 +72,42 @@ button.home-btn {
border-radius: 3rem;
cursor: pointer;
transition: all 0.3s;
margin-top: 20px;
margin-bottom: calc(15vh - 80px);
transition: all 0.3s;
&:hover {
background-color: #555;
transform: translateY(-2px);
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.1);
}
}
img {
width: 700px;
height: auto;
object-fit: cover;
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.05);
border-radius: 1rem;
max-width: 90%;
}
footer {
font-size: 1rem;
color: #666;
margin-top: 20px;
margin-top: auto;
padding: 1rem 0;
}
/* 动态背景动画 */
@keyframes animateBackground {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
</style>

View File

@ -12,7 +12,7 @@
<ReloadOutlined />需要重启
</a-button>
</span>
<a-select ref="select" style="width: 160px"
<a-select ref="select" style="width: 200px"
:value="configStore.config?.model_provider"
@change="handleChange('model_provider', $event)"
>
@ -29,7 +29,7 @@
<ReloadOutlined />需要重启
</a-button>
</span>
<a-select ref="select" style="width: 160px"
<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]"
@ -47,7 +47,7 @@
<ReloadOutlined />需要重启
</a-button>
</span>
<a-select style="width: 160px"
<a-select style="width: 200px"
:value="configStore.config?.embed_model"
@change="handleChange('embed_model', $event)"
>
@ -63,7 +63,7 @@
<ReloadOutlined />需要重启
</a-button>
</span>
<a-select style="width: 160px"
<a-select style="width: 200px"
:value="configStore.config?.reranker"
@change="handleChange('reranker', $event)"
:disabled="!configStore.config.enable_reranker"