Merge branch 'dev' of https://github.com/xerrors/ProjectAthena into dev
This commit is contained in:
commit
65a8b06313
@ -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",
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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})
|
||||
@ -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
|
||||
|
||||
@ -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 |
@ -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 {
|
||||
|
||||
@ -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;"
|
||||
},
|
||||
}
|
||||
@ -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>
|
||||
85
web/src/components/DebugComponent.vue
Normal file
85
web/src/components/DebugComponent.vue
Normal 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>
|
||||
143
web/src/components/RefsComponent.vue
Normal file
143
web/src/components/RefsComponent.vue
Normal 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>
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user