feat:完成简易的的前端和后端api
1
.gitattributes
vendored
@ -1 +0,0 @@
|
||||
*.html linguist-language=Python
|
40
.gitignore
vendored
@ -1,3 +1,37 @@
|
||||
/__pycache__
|
||||
*.json
|
||||
APPData.db
|
||||
__pycache__
|
||||
.venv
|
||||
test.py
|
||||
data
|
||||
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
|
17
README.md
@ -1,17 +0,0 @@
|
||||
# PyGetGPT
|
||||
使用python构建的简易语言模型api调用系统
|
||||
使用web页面作为GUI易于部署
|
||||
---
|
||||
## 使用的外部库
|
||||
- flask
|
||||
- flask_cors
|
||||
- zhipuai
|
||||
- pymysql
|
||||
- requests
|
||||
- openai
|
||||
|
||||
## 使用pip安装依赖
|
||||
~~~ bash
|
||||
pip install pymysql requests flask zhipuai openai
|
||||
pip install pymysql requests flask zhipuai openai -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
~~~
|
@ -1,27 +0,0 @@
|
||||
import zhipuai , config
|
||||
|
||||
zhipuai.api_key = config.readConf()["chatglmturbo"]["Authorization"]
|
||||
|
||||
def service(prompt,history = ""):
|
||||
if history == "":
|
||||
response = zhipuai.model_api.invoke(
|
||||
model="chatglm_turbo",
|
||||
prompt=[
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
)
|
||||
else:
|
||||
response = zhipuai.model_api.invoke(
|
||||
model="chatglm_turbo",
|
||||
prompt=[
|
||||
{"role": "user", "content": history[1]["user"]},
|
||||
{"role": "assistant", "content": history[1]["bot"]},
|
||||
{"role": "user", "content": history[0]["user"]},
|
||||
{"role": "assistant", "content": history[0]["bot"]},
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
)
|
||||
if response["code"] == 200:
|
||||
return 200, str(response["data"]["choices"][0]["content"]).split('"')[1], response["data"]["usage"]['total_tokens']
|
||||
else:
|
||||
return 50 , str(response["code"])+response["msg"], 0
|
@ -1,28 +0,0 @@
|
||||
import openai , config
|
||||
|
||||
openai.api_key = config.readConf()["gpt3.5turbo"]["Authorization"]
|
||||
openai.base_url = config.readConf()["gpt3.5turbo"]["url"]
|
||||
|
||||
def service(prompt,history = ""):
|
||||
if history == "":
|
||||
response = openai.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
)
|
||||
else:
|
||||
response = openai.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[
|
||||
{"role": "user", "content": history[1]["user"]},
|
||||
{"role": "assistant", "content": history[1]["bot"]},
|
||||
{"role": "user", "content": history[0]["user"]},
|
||||
{"role": "assistant", "content": history[0]["bot"]},
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
)
|
||||
if response.choices[0].finish_reason == "stop":
|
||||
return 200, response.choices[0].message.content, int(response.usage.total_tokens*3) #三倍tokens消耗
|
||||
else:
|
||||
return 50 , "API Error!", 0
|
@ -1,28 +0,0 @@
|
||||
import openai , config
|
||||
|
||||
openai.api_key = config.readConf()["gpt4.0turbo"]["Authorization"]
|
||||
openai.base_url = config.readConf()["gpt4.0turbo"]["url"]
|
||||
|
||||
def service(prompt,history = ""):
|
||||
if history == "":
|
||||
response = openai.chat.completions.create(
|
||||
model="gpt-4-1106-preview",
|
||||
messages=[
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
)
|
||||
else:
|
||||
response = openai.chat.completions.create(
|
||||
model="gpt-4-1106-preview",
|
||||
messages=[
|
||||
{"role": "user", "content": history[1]["user"]},
|
||||
{"role": "assistant", "content": history[1]["bot"]},
|
||||
{"role": "user", "content": history[0]["user"]},
|
||||
{"role": "assistant", "content": history[0]["bot"]},
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
)
|
||||
if response.choices[0].finish_reason == "stop":
|
||||
return 200, response.choices[0].message.content, int(response.usage.total_tokens*45) #45倍tokens消耗
|
||||
else:
|
||||
return 50 , "API Error!", 0
|
@ -1,31 +0,0 @@
|
||||
import requests , json , config
|
||||
|
||||
# 设置请求的目标URL
|
||||
url = config.readConf()["qwenturbo"]["url"] # 替换为你的API端点URL
|
||||
header = {
|
||||
"Content-Type":"application/json",
|
||||
"Authorization":config.readConf()["qwenturbo"]["Authorization"]
|
||||
}
|
||||
|
||||
def service(prompt,history = ""):
|
||||
# 设置请求数据
|
||||
if history == "":
|
||||
data = {
|
||||
"model": "qwen-turbo",
|
||||
"input":{
|
||||
"prompt":f"{prompt}"
|
||||
}
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"model": "qwen-turbo",
|
||||
"input":{
|
||||
"prompt":f"{prompt}",
|
||||
"history":history
|
||||
}
|
||||
}
|
||||
# 发送POST请求
|
||||
response = json.loads(requests.post(url, json=data ,headers=header).text)
|
||||
if 'code' in response:
|
||||
return 50,response['code']+response['message'],0
|
||||
return 200,response['output']['text'],response["usage"]["total_tokens"]
|
19
config.py
@ -1,19 +0,0 @@
|
||||
import json
|
||||
|
||||
def readConf():
|
||||
with open('config.json') as f:
|
||||
return json.load(f)
|
||||
|
||||
def updateConf(data_dict):
|
||||
# 打开JSON文件并读取内容
|
||||
file_path = 'config.json'
|
||||
with open(file_path, 'r') as json_file:
|
||||
existing_data = json.load(json_file)
|
||||
|
||||
# 将新的数据合并到现有的数据中
|
||||
existing_data.update(data_dict)
|
||||
|
||||
# 再次打开文件(这次是以写模式),并将更新后的数据保存回文件
|
||||
with open(file_path, 'w') as json_file:
|
||||
json.dump(existing_data, json_file, indent=4, ensure_ascii=False)
|
||||
|
84
configUtil.py
Normal file
@ -0,0 +1,84 @@
|
||||
import configparser
|
||||
|
||||
class ConfigUtil:
|
||||
# 初始化ConfigParser对象
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
def __init__(self,path:str):
|
||||
# 读取并加载配置文件config.ini
|
||||
self.config.read(path,encoding="utf-8")
|
||||
|
||||
def get(self, section: str, key: str) -> str:
|
||||
"""
|
||||
获取配置项的字符串值
|
||||
|
||||
参数:
|
||||
section (str): 配置文件中的节名
|
||||
key (str): 节中的配置项键名
|
||||
|
||||
返回:
|
||||
str: 配置项的字符串值
|
||||
"""
|
||||
return self.config.get(section, key)
|
||||
|
||||
def getBool(self, section: str, key: str) -> bool:
|
||||
"""
|
||||
获取配置项的布尔值
|
||||
|
||||
参数:
|
||||
section (str): 配置文件中的节名
|
||||
key (str): 节中的配置项键名
|
||||
|
||||
返回:
|
||||
bool: 配置项的布尔值
|
||||
"""
|
||||
return self.config.getboolean(section, key)
|
||||
|
||||
def getInt(self, section: str, key: str) -> int:
|
||||
"""
|
||||
获取配置项的整数值
|
||||
|
||||
参数:
|
||||
section (str): 配置文件中的节名
|
||||
key (str): 节中的配置项键名
|
||||
|
||||
返回:
|
||||
int: 配置项的整数值
|
||||
"""
|
||||
return self.config.getint(section, key)
|
||||
|
||||
def getFloat(self, section: str, key: str) -> float:
|
||||
"""
|
||||
获取配置项的浮点数值
|
||||
|
||||
参数:
|
||||
section (str): 配置文件中的节名
|
||||
key (str): 节中的配置项键名
|
||||
|
||||
返回:
|
||||
float: 配置项的浮点数值
|
||||
"""
|
||||
return self.config.getfloat(section, key)
|
||||
|
||||
|
||||
def getSectionList(self) -> list:
|
||||
"""
|
||||
获取配置文件中的所有节名
|
||||
|
||||
返回:
|
||||
list: 所有节名的列表
|
||||
"""
|
||||
return self.config.sections()
|
||||
|
||||
|
||||
def getKeyList(self, section: str) -> list:
|
||||
"""
|
||||
获取指定节中的所有键名
|
||||
|
||||
参数:
|
||||
section (str): 配置文件中的节名
|
||||
|
||||
返回:
|
||||
list: 指定节中的所有键名的列表
|
||||
"""
|
||||
return self.config.options(section)
|
89
dao/db/user.py
Normal file
@ -0,0 +1,89 @@
|
||||
import sqlite3, time
|
||||
import configUtil
|
||||
|
||||
def getConnection() -> sqlite3.Connection:
|
||||
return sqlite3.connect(configUtil.ConfigUtil("config.ini").get("database","path"))
|
||||
|
||||
|
||||
def init():
|
||||
conn = getConnection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
'''
|
||||
CREATE TABLE User (
|
||||
uid TEXT,
|
||||
email TEXT,
|
||||
password_hash TEXT,
|
||||
created_at INT,
|
||||
surplus INT);
|
||||
'''
|
||||
)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
print("数据库初始化完成")
|
||||
|
||||
|
||||
def addUser(uid: str, email: str, password_hash: str) -> bool:
|
||||
conn = getConnection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
"INSERT INTO User (uid, email, password_hash, created_at, surplus) VALUES (?, ?, ?, ?, ?);",
|
||||
[uid, email, password_hash, int(time.time()), 0]
|
||||
)
|
||||
conn.commit()
|
||||
return True
|
||||
except sqlite3.IntegrityError:
|
||||
return False
|
||||
|
||||
|
||||
def checkUser(uid: str,password_hash) -> bool:
|
||||
"""检查用户与密码是否合法 可输入邮箱或uid"""
|
||||
conn = getConnection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM User WHERE ( uid = ? AND password_hash = ? ) OR ( email = ? AND password_hash = ? )",[uid,password_hash,uid,password_hash])
|
||||
result = cursor.fetchone()
|
||||
return result != None
|
||||
|
||||
|
||||
def getUser(uid: str) -> dict:
|
||||
"""获取用户信息"""
|
||||
conn = getConnection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM User WHERE uid = ?",[uid])
|
||||
result = cursor.fetchone()
|
||||
return {"uid":result[0],"email":result[1],"password_hash":result[2],"created_at":result[3],"surplus":result[4]}
|
||||
|
||||
|
||||
def updateUserSurplus(uid: str, surplus: int) -> bool:
|
||||
"""更新用户剩余额度"""
|
||||
conn = getConnection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute("UPDATE User SET surplus = ? WHERE uid = ?",[surplus,uid])
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
except: return False
|
||||
return True
|
||||
|
||||
def getUserSurplus(uid: str) -> int:
|
||||
"""获取用户剩余额度"""
|
||||
return getUser(uid)["surplus"]
|
||||
|
||||
|
||||
def updateUserPasswd(uid: str, password_hash: str) -> bool:
|
||||
"""更新用户密码"""
|
||||
conn = getConnection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute("UPDATE User SET password_hash = ? WHERE uid = ?",[password_hash,uid])
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
except: return False
|
||||
return True
|
||||
|
||||
|
||||
|
226
db.py
@ -1,226 +0,0 @@
|
||||
import config , uuid , pathlib
|
||||
|
||||
def getconn():
|
||||
try:
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(config.readConf()["db"]["path"])
|
||||
return conn
|
||||
except:
|
||||
print("DB ERROR")
|
||||
return 0
|
||||
|
||||
def dbIsOK():
|
||||
#打开数据库连接
|
||||
path = pathlib.Path(config.readConf()["db"]["path"])
|
||||
if not path.is_file():
|
||||
init()
|
||||
|
||||
try:
|
||||
getconn()
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def init():
|
||||
#打开数据库连接
|
||||
db = getconn()
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
#建表
|
||||
cursor.execute(
|
||||
'''
|
||||
CREATE TABLE usersurplus (
|
||||
userkey TEXT,
|
||||
surplus INT);
|
||||
''')
|
||||
cursor.execute(
|
||||
'''
|
||||
CREATE TABLE log (
|
||||
ip TEXT,
|
||||
time INT,
|
||||
tokens INT,
|
||||
model TEXT,
|
||||
userkey TEXT);
|
||||
''')
|
||||
# 提交事务
|
||||
db.commit()
|
||||
|
||||
# 关闭连接
|
||||
db.close()
|
||||
|
||||
def userSurplus(userkey): #查询userkey剩余配额
|
||||
#打开数据库连接
|
||||
db = getconn()
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
|
||||
# 使用 execute() 方法执行 SQL 查询
|
||||
cursor.execute("SELECT surplus FROM usersurplus WHERE userkey = ?;",[userkey])
|
||||
# 使用 fetchone() 方法获取单条数据.
|
||||
data = cursor.fetchone()
|
||||
|
||||
# 关闭连接
|
||||
db.close()
|
||||
|
||||
if data != None:
|
||||
return data[0]
|
||||
return -99999
|
||||
|
||||
def reduce_value(userkey, value): # 减去对应的值
|
||||
#打开数据库连接
|
||||
db = getconn()
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
|
||||
# 执行 SQL 查询以获取当前值
|
||||
cursor.execute("SELECT surplus FROM usersurplus WHERE userkey = ?;",[userkey])
|
||||
current_value = cursor.fetchone()[0]
|
||||
|
||||
# 如果没有找到用户,则返回错误信息
|
||||
if current_value is None:
|
||||
db.close()
|
||||
return -1
|
||||
|
||||
# 计算新的值
|
||||
new_value = current_value - value
|
||||
|
||||
# 更新数据库中的值
|
||||
cursor.execute("UPDATE usersurplus SET surplus= ? WHERE userkey= ?;",[new_value,userkey])
|
||||
|
||||
# 提交事务
|
||||
db.commit()
|
||||
|
||||
# 关闭连接
|
||||
db.close()
|
||||
|
||||
# 返回新值
|
||||
return 0
|
||||
|
||||
def getAllKey():
|
||||
#打开数据库连接
|
||||
db = getconn()
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
|
||||
# 使用 execute() 方法执行 SQL 查询
|
||||
cursor.execute("SELECT * FROM usersurplus ;")
|
||||
# 使用 fetchall() 方法获取结果集
|
||||
data = cursor.fetchall()
|
||||
|
||||
# 关闭连接
|
||||
db.close()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def delKey(userkey):
|
||||
#打开数据库连接
|
||||
db = getconn()
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
|
||||
# 使用 execute() 方法执行 SQL 查询
|
||||
cursor.execute("DELETE FROM usersurplus WHERE userkey = ?;", [userkey])
|
||||
|
||||
# 提交事务
|
||||
db.commit()
|
||||
|
||||
if cursor.rowcount > 0:
|
||||
db.close() # 使用 rowcount() 方法查询受影响行数
|
||||
return True
|
||||
db.close()
|
||||
return False
|
||||
|
||||
|
||||
def createKey(quota,number=1,key="null"):
|
||||
#打开数据库连接
|
||||
db = getconn()
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
|
||||
# 使用 execute() 方法执行 SQL 查询
|
||||
output = []
|
||||
|
||||
if key == "null":
|
||||
for i in range(int(number)):
|
||||
key = str(uuid.uuid1())
|
||||
output.append(key)
|
||||
cursor.execute("INSERT INTO usersurplus (userkey,surplus) VALUES (?, ?);", [key, quota])
|
||||
else:
|
||||
cursor.execute("INSERT INTO usersurplus (userkey,surplus) VALUES (?, ?);", [key, quota])
|
||||
output.append(key)
|
||||
|
||||
|
||||
# 提交事务
|
||||
db.commit()
|
||||
|
||||
db.close()
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def newLog(ip:str, time:int, tokens:int, model:str, userkey:str):
|
||||
#打开数据库连接
|
||||
db = getconn()
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
|
||||
# 使用 execute() 方法执行 SQL 查询
|
||||
|
||||
cursor.execute("INSERT INTO log (ip, time, tokens, model, userkey) VALUES (?, ?, ?, ?, ?);", [ip, time, tokens, model, userkey])
|
||||
|
||||
# 提交事务
|
||||
db.commit()
|
||||
|
||||
db.close()
|
||||
|
||||
|
||||
def getlog(num:int):
|
||||
#打开数据库连接
|
||||
db = getconn()
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
|
||||
# 使用 execute() 方法执行 SQL 查询
|
||||
cursor.execute("SELECT * FROM log order by time desc limit ?;", [num])
|
||||
# 使用 fetchall() 方法获取结果集
|
||||
data = cursor.fetchall()
|
||||
|
||||
# 关闭连接
|
||||
db.close()
|
||||
|
||||
return data
|
||||
|
||||
def getLogAllModel():
|
||||
#打开数据库连接
|
||||
db = getconn()
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
|
||||
# 使用 execute() 方法执行 SQL 查询
|
||||
cursor.execute("SELECT DISTINCT model FROM log ;")
|
||||
# 使用 fetchall() 方法获取结果集
|
||||
data = cursor.fetchall()
|
||||
|
||||
# 关闭连接
|
||||
db.close()
|
||||
|
||||
return data
|
||||
|
||||
def countLog(key:str, value:str):
|
||||
#打开数据库连接
|
||||
db = getconn()
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
# 使用 execute() 方法执行 SQL 查询
|
||||
cursor.execute(f"SELECT COUNT(*) FROM log WHERE {key} = ?;", [value])
|
||||
# 使用 fetchone() 方法获取结果
|
||||
data = cursor.fetchone()
|
||||
|
||||
# 关闭连接
|
||||
db.close()
|
||||
except Exception as e:
|
||||
return e
|
||||
|
||||
return data[0]
|
@ -1,7 +0,0 @@
|
||||
# 配置文件说明
|
||||
{"url": "<此处为api URL>", "userkey": "<此处为你的UserKey>", "context": <此处为1 开启上下位关联功能 默认0关闭>}
|
||||
注意:开启上下文关联会消耗大量的token
|
||||
# 什么是Token
|
||||
在大型语言模型中,"token"是指文本中的一个最小单位。 通常,一个token可以是一个单词、一个标点符号、一个数字、一个符号等。 在自然语言处理中,tokenization是将一个句子或文本分成tokens的过程。 在大型语言模型的训练和应用中,模型接收一串tokens作为输入,并尝试预测下一个最可能的token。
|
||||
# 上下文关联
|
||||
在使用上下文关联时,模型除了处理你当前的问题时还需要一并将历史对话的一部分共同处理。所以需要消耗更多的token。在本应用中token消耗平均值大约是不开启上下文功能的300%。
|
@ -1,17 +0,0 @@
|
||||
# server API 接口文档
|
||||
|
||||
## 请求格式(json)
|
||||
| 键 | 类型 | 必选 | 示例 |
|
||||
|---------|------|-----|-----|
|
||||
| prompt | str | yes | "你好" |
|
||||
| userkey | str | yes | "2b3j41b2xh1hz1" |
|
||||
| history | list | no | "history":[{"user":"XXXXXX","bot":"XXXXXX"},{"user":"XXXXXX""bot":"XXXXXX"}] |
|
||||
|
||||
|
||||
## 返回格式(json)
|
||||
| 键 | 类型 | 示例 |
|
||||
|----|------|------|
|
||||
| code | int | 200 |
|
||||
| output | str | "你好" |
|
||||
| surplus | int | 10000 |
|
||||
| |
|
3
frontend/env.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue"
|
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
1861
frontend/package-lock.json
generated
Normal file
30
frontend/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.7",
|
||||
"element-plus": "^2.8.5",
|
||||
"marked": "^14.1.3",
|
||||
"vue": "^3.5.11",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^20.16.11",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"npm-run-all2": "^6.2.3",
|
||||
"typescript": "~5.5.4",
|
||||
"vite": "^5.4.8",
|
||||
"vue-tsc": "^2.1.6"
|
||||
}
|
||||
}
|
BIN
frontend/public/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
34
frontend/src/App.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-container">
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 聊天框容器样式 */
|
||||
.chat-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 90vh; /* 高度占据视口的 90% */
|
||||
width: 90vw; /* 宽度占据视口的 90% */
|
||||
margin: auto;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden; /* 防止内容溢出 */
|
||||
}
|
||||
|
||||
/* 聊天框内部内容样式 */
|
||||
.chat-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto; /* 允许垂直滚动 */
|
||||
}
|
||||
</style>
|
86
frontend/src/assets/base.css
Normal file
@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
35
frontend/src/assets/main.css
Normal file
@ -0,0 +1,35 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
15
frontend/src/main.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
// import ElementPlus from 'element-plus';
|
||||
// import 'element-plus/dist/index.css';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router);
|
||||
// app.use(ElementPlus);
|
||||
|
||||
app.mount('#app');
|
||||
|
29
frontend/src/router/index.ts
Normal file
@ -0,0 +1,29 @@
|
||||
|
||||
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import ChatPage from '../views/ChatPage.vue';
|
||||
import LoginPage from '../views/LoginPage.vue';
|
||||
|
||||
const routes = [
|
||||
{ path: '/', redirect: '/chat' },
|
||||
{ path: '/login', component: LoginPage },
|
||||
{ path: '/chat', component: ChatPage }, // 聊天页面
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
// 路由守卫,确保用户登录后才能访问聊天页面
|
||||
router.beforeEach((to, from, next) => {
|
||||
const isAuthenticated = document.cookie.includes('auth_token'); // 假设后端返回的是这个Cookie
|
||||
if (to.path === '/chat' && !isAuthenticated) {
|
||||
next('/login'); // 未登录时跳转到登录页面
|
||||
} else {
|
||||
next(); // 允许进入
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
340
frontend/src/views/ChatPage.vue
Normal file
@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<div class="chat-app">
|
||||
<!-- 左侧对话列表 -->
|
||||
<aside :class="['conversation-list', { 'collapsed': isCollapsed }]">
|
||||
<div v-if="!isCollapsed" v-for="(conversation, index) in conversations" :key="index"
|
||||
class="conversation-item" @click="selectConversation(index)"
|
||||
:class="{ 'active': index === selectedConversation }">
|
||||
<span>对话 {{ index + 1 }}</span>
|
||||
<button @click.stop="deleteConversation(index)" class="delete-btn">删除</button>
|
||||
</div>
|
||||
<button v-if="!isCollapsed" @click="newConversation" class="new-conversation-btn">新建对话</button>
|
||||
<button @click="toggleCollapse" class="collapse-btn">{{ isCollapsed ? '>' : '折叠' }}</button>
|
||||
</aside>
|
||||
|
||||
<!-- 聊天内容区域 -->
|
||||
<section class="chat-container">
|
||||
<!-- 模型选择 -->
|
||||
<div class="model-selection">
|
||||
<select v-model="selectedModel" @change="fetchMessages" class="model-dropdown">
|
||||
<option v-for="model in models" :key="model.id" :value="model.id">
|
||||
{{ model.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 聊天窗口 -->
|
||||
<div ref="chatWindow" class="chat-window">
|
||||
<div v-for="(message, index) in conversations[selectedConversation].messages" :key="index"
|
||||
:class="['message', message.sender]">
|
||||
<div class="message-bubble" v-html="renderMarkdown(message.text)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<div class="input-area">
|
||||
<input v-model="userInput" @keyup.enter="sendMessage" placeholder="请输入您的消息..." class="chat-input" />
|
||||
<button @click="sendMessage" class="send-btn">发送</button>
|
||||
<button @click="clearHistory" class="clear-btn">清除记录</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { marked } from 'marked';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userInput: '',
|
||||
conversations: JSON.parse(localStorage.getItem('conversations')) || [
|
||||
{
|
||||
messages: [{ sender: 'bot', text: '你好!请选择一个模型开始聊天。' }],
|
||||
},
|
||||
],
|
||||
selectedConversation: 0,
|
||||
models: [],
|
||||
selectedModel: '',
|
||||
isCollapsed: false, // 控制菜单折叠的状态
|
||||
conversationLimit: 6, // 对话数量上限
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchModels();
|
||||
},
|
||||
methods: {
|
||||
async fetchModels() {
|
||||
try {
|
||||
const response = await axios.get('/api/chat/models');
|
||||
this.models = response.data.models;
|
||||
this.selectedModel = this.models[0].id;
|
||||
} catch (error) {
|
||||
console.error('获取模型列表时出错:', error);
|
||||
this.conversations[this.selectedConversation].messages.push({ sender: 'bot', text: '获取模型列表失败,请稍后再试。' });
|
||||
}
|
||||
},
|
||||
async sendMessage() {
|
||||
if (this.userInput.trim() === '' || !this.selectedModel) return;
|
||||
|
||||
const currentMessages = this.conversations[this.selectedConversation].messages;
|
||||
currentMessages.push({ sender: 'user', text: this.userInput });
|
||||
currentMessages.push({ sender: 'bot', text: '正在思考...' });
|
||||
|
||||
const userMessage = this.userInput;
|
||||
this.userInput = '';
|
||||
|
||||
try {
|
||||
const response = await axios.post('/api/chat', {
|
||||
message: userMessage,
|
||||
model: this.selectedModel,
|
||||
});
|
||||
|
||||
currentMessages.pop();
|
||||
currentMessages.push({ sender: 'bot', text: response.data.message });
|
||||
this.saveConversations();
|
||||
this.scrollToBottom();
|
||||
} catch (error) {
|
||||
console.error('发送消息出错:', error);
|
||||
currentMessages.push({ sender: 'bot', text: '抱歉!出现了错误。' });
|
||||
}
|
||||
},
|
||||
renderMarkdown(text) {
|
||||
return marked(text);
|
||||
},
|
||||
scrollToBottom() {
|
||||
const chatWindow = this.$refs.chatWindow;
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight;
|
||||
},
|
||||
saveConversations() {
|
||||
localStorage.setItem('conversations', JSON.stringify(this.conversations));
|
||||
},
|
||||
loadConversation() {
|
||||
this.scrollToBottom();
|
||||
},
|
||||
clearHistory() {
|
||||
this.conversations[this.selectedConversation].messages = [];
|
||||
this.saveConversations();
|
||||
},
|
||||
newConversation() {
|
||||
if (this.conversations.length >= this.conversationLimit) {
|
||||
alert(`对话数量已达到上限(${this.conversationLimit}条)。`);
|
||||
return;
|
||||
}
|
||||
this.conversations.push({
|
||||
messages: [{ sender: 'bot', text: '新对话已创建,请选择一个模型开始聊天。' }],
|
||||
});
|
||||
this.selectedConversation = this.conversations.length - 1;
|
||||
this.saveConversations();
|
||||
},
|
||||
deleteConversation(index) {
|
||||
if (this.conversations.length === 1) {
|
||||
alert('至少需要保留一个对话。');
|
||||
return;
|
||||
}
|
||||
if (confirm('确定要删除这个对话吗?')) {
|
||||
this.conversations.splice(index, 1);
|
||||
if (this.selectedConversation === index) {
|
||||
this.selectedConversation = 0;
|
||||
}
|
||||
this.saveConversations();
|
||||
}
|
||||
},
|
||||
selectConversation(index) {
|
||||
this.selectedConversation = index;
|
||||
this.scrollToBottom();
|
||||
},
|
||||
toggleCollapse() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.scrollToBottom();
|
||||
},
|
||||
updated() {
|
||||
this.scrollToBottom();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 全局容器样式 */
|
||||
.chat-app {
|
||||
display: flex;
|
||||
height: 90vh;
|
||||
width: 90vw;
|
||||
margin: auto;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 左侧对话列表 */
|
||||
.conversation-list {
|
||||
width: 20%;
|
||||
background-color: #f9f9f9;
|
||||
border-right: 1px solid #ddd;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.conversation-list.collapsed {
|
||||
width: 50px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.conversation-item.active {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.new-conversation-btn {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.new-conversation-btn:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background-color: #dc3545;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
margin-top: auto;
|
||||
padding: 5px 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.collapse-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
/* 右侧聊天窗口 */
|
||||
.chat-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.model-selection {
|
||||
padding: 10px;
|
||||
background-color: #f1f1f1;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.model-dropdown {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ddd;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message.bot {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
max-width: 60%;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message.user .message-bubble {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.message.bot .message-bubble {
|
||||
background-color: #f1f1f1;
|
||||
color: black;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
background-color: #f1f1f1;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex-grow: 1;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 20px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.send-btn,
|
||||
.clear-btn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
margin-left: 10px;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.send-btn:hover,
|
||||
.clear-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
136
frontend/src/views/LoginPage.vue
Normal file
@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<h2>登录</h2>
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div class="form-group">
|
||||
<label for="userinfo">UID或邮箱</label>
|
||||
<input
|
||||
type="text"
|
||||
id="userinfo"
|
||||
v-model="loginForm.userinfo"
|
||||
placeholder="请输入用户名"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
v-model="loginForm.password"
|
||||
placeholder="请输入密码"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="login-btn">登录</button>
|
||||
</div>
|
||||
</form>
|
||||
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loginForm: {
|
||||
userinfo: '',
|
||||
password: '',
|
||||
},
|
||||
errorMessage: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async handleLogin() {
|
||||
try {
|
||||
const response = await axios.post('/api/user/login', this.loginForm);
|
||||
if (response.status === 200) {
|
||||
// 登录成功,跳转到聊天页面
|
||||
this.$router.push('/chat');
|
||||
}
|
||||
} catch (error) {
|
||||
// 登录失败,显示错误信息
|
||||
this.errorMessage = '登录失败,请检查用户名或密码';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background-color: #fff;
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
14
frontend/tsconfig.app.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
11
frontend/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
19
frontend/tsconfig.node.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
16
frontend/vite.config.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
26
log.py
@ -1,26 +0,0 @@
|
||||
import json
|
||||
import time as times
|
||||
import db
|
||||
|
||||
def newLog(ip:str,tokens:int, model:str, userkey:str):
|
||||
db.newLog(ip, int(times.time()), tokens, model, userkey)
|
||||
|
||||
def getlog(num:int):
|
||||
if num < 0:
|
||||
num = 10
|
||||
rawdata = db.getlog(num)
|
||||
data = []
|
||||
for i in rawdata:
|
||||
item = list(i)
|
||||
item[1] = times.strftime("%Y-%m-%d %H:%M:%S",times.localtime(i[1]))
|
||||
data.append(item)
|
||||
|
||||
return data
|
||||
|
||||
def modelChartsData(): #按模型用量统计
|
||||
data = []
|
||||
model = db.getLogAllModel()
|
||||
for item in model:
|
||||
data.append({'value':db.countLog("model",item[0]),'name':item[0]})
|
||||
return data
|
||||
|
148
main.py
@ -1,142 +1,16 @@
|
||||
import flask
|
||||
from flask_cors import CORS
|
||||
import db , config , log
|
||||
from apiModule import qwenTurbo , chatglmTurbo , gpt35Turbo , gpt4Turbo
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
CORS(app,origins="*")
|
||||
app.secret_key = b'SQ-{kJE;m(jEBi|{yq]v'
|
||||
app.config['TRUSTED_PROXIES'] = ['proxy_ip']
|
||||
import router.chat
|
||||
import router.page
|
||||
import router.user
|
||||
|
||||
@app.route('/api/user', methods=['POST'])
|
||||
def post_data():
|
||||
userRequest = flask.request.json
|
||||
surplusToken = db.userSurplus(userRequest['userkey'])
|
||||
|
||||
if userRequest["prompt"] == "":
|
||||
return {"code":42,"output":"Input is empty"}
|
||||
|
||||
if userRequest["prompt"] == "":
|
||||
return {"code":42,"output":"UserKey is empty"}
|
||||
|
||||
if surplusToken == -99999: # 判断用户是否存在和余额
|
||||
return {"code":41,"output":"UserKey not found"}
|
||||
elif surplusToken <= 0:
|
||||
return {"code":40,"output":"Token has been use up"}
|
||||
|
||||
if userRequest["model"] == "qwen-turbo": # 调用qwen-Turbo
|
||||
if userRequest["context"] == 1: # 是否使用上文关联
|
||||
code , output , tokenUsed = qwenTurbo.service(userRequest['prompt'],userRequest['history'])
|
||||
elif userRequest["context"] == 0:
|
||||
code , output , tokenUsed = qwenTurbo.service(userRequest['prompt'])
|
||||
|
||||
if userRequest["model"] == "chatglm-turbo": # 调用chatglm-turbo
|
||||
if userRequest["context"] == 1: # 是否使用上文关联
|
||||
code , output , tokenUsed = chatglmTurbo.service(userRequest['prompt'],userRequest['history'])
|
||||
elif userRequest["context"] == 0:
|
||||
code , output , tokenUsed = chatglmTurbo.service(userRequest['prompt'])
|
||||
|
||||
if userRequest["model"] == "gpt3.5-turbo": # 调用gpt3.5-turbo
|
||||
if userRequest["context"] == 1: # 是否使用上文关联
|
||||
code , output , tokenUsed = gpt35Turbo.service(userRequest['prompt'],userRequest['history'])
|
||||
elif userRequest["context"] == 0:
|
||||
code , output , tokenUsed = gpt35Turbo.service(userRequest['prompt'])
|
||||
|
||||
if userRequest["model"] == "gpt4.0-turbo": # 调用gpt4.0-turbo
|
||||
if userRequest["context"] == 1: # 是否使用上文关联
|
||||
code , output , tokenUsed = gpt4Turbo.service(userRequest['prompt'],userRequest['history'])
|
||||
elif userRequest["context"] == 0:
|
||||
code , output , tokenUsed = gpt4Turbo.service(userRequest['prompt'])
|
||||
|
||||
db.reduce_value(userRequest['userkey'], tokenUsed)
|
||||
log.newLog(flask.request.headers.get('X-Forwarded-For').split(",")[0], tokenUsed, userRequest["model"], userRequest['userkey'])
|
||||
return {"code":code,"output":output,"surplus":surplusToken}
|
||||
app = FastAPI()
|
||||
app.include_router(router.chat.router)
|
||||
app.include_router(router.user.router)
|
||||
app.include_router(router.page.router)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return flask.render_template('index.html')
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
|
||||
@app.route('/login', methods=['POST','GET'])
|
||||
def login():
|
||||
if flask.request.method == 'GET':
|
||||
return flask.render_template('login.html')
|
||||
userRequest = flask.request.form
|
||||
if userRequest["password"] != config.readConf()["adminkey"]:
|
||||
return flask.render_template('login.html')
|
||||
flask.session["admin"] = True
|
||||
return flask.redirect(flask.url_for('admin'))
|
||||
|
||||
|
||||
@app.route('/admin')
|
||||
def admin():
|
||||
if "admin" in flask.session :
|
||||
status = {}
|
||||
status["db"] = db.dbIsOK()
|
||||
return flask.render_template("status.html" ,status=status)
|
||||
return flask.redirect('login')
|
||||
|
||||
|
||||
@app.route('/admin/list')
|
||||
def adminList():
|
||||
if "admin" in flask.session :
|
||||
data = db.getAllKey()
|
||||
return flask.render_template("keylist.html",data=data)
|
||||
return flask.redirect('login')
|
||||
|
||||
@app.route('/api/modelcount')
|
||||
def apiModelCount():
|
||||
if "admin" in flask.session :
|
||||
data = log.modelChartsData()
|
||||
return data
|
||||
return flask.abort(403)
|
||||
|
||||
@app.route('/admin/log', methods=['GET'])
|
||||
def adminListLog():
|
||||
if "admin" in flask.session :
|
||||
if 'show' in flask.request.args:
|
||||
try:
|
||||
shownum = int(flask.request.values.get('show'))
|
||||
except:
|
||||
return flask.abort(400)
|
||||
else:
|
||||
return flask.abort(400)
|
||||
data = log.getlog(shownum)
|
||||
return flask.render_template("loglist.html",data=data)
|
||||
return flask.redirect('login')
|
||||
|
||||
@app.route('/admin/createkey', methods=['POST','GET'])
|
||||
def createkey():
|
||||
if "admin" in flask.session :
|
||||
if flask.request.method == "GET":
|
||||
return flask.render_template("createKey.html",resq="null")
|
||||
if "number" in flask.request.form: # 创建单个还是多个
|
||||
resq = db.createKey(flask.request.form["quota"], flask.request.form["number"])
|
||||
elif "key" in flask.request.form:
|
||||
resq = db.createKey(flask.request.form["quota"],1,flask.request.form["key"])
|
||||
|
||||
return flask.render_template("createKey.html",resq=resq)
|
||||
return flask.redirect('login')
|
||||
|
||||
@app.route('/admin/lookupkey', methods=['POST','GET'])
|
||||
def lookupkey():
|
||||
if "admin" in flask.session :
|
||||
if flask.request.method == "GET":
|
||||
return flask.render_template("lookupKey.html",resq="null")
|
||||
resq = db.userSurplus(flask.request.form["key"])
|
||||
return flask.render_template("lookupKey.html",resq=resq)
|
||||
return flask.redirect('login')
|
||||
|
||||
@app.route('/admin/operate', methods=['POST','GET'])
|
||||
def operate():
|
||||
if "admin" in flask.session :
|
||||
if flask.request.args['type'] == "del":
|
||||
if db.delKey(flask.request.args['target']):
|
||||
return "成功 <a href='javascript:;' onclick='self.location=document.referrer;'>返回上一页并刷新</a>"
|
||||
return "失败 <a href='javascript:;' onclick='self.location=document.referrer;'>返回上一页并刷新</a>"
|
||||
return "拒绝访问"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=bool(config.readConf()["appconf"]["debug"]),host=config.readConf()["appconf"]["host"],port=config.readConf()["appconf"]["port"])
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
32
model/glm-4-flash.py
Normal file
@ -0,0 +1,32 @@
|
||||
from zhipuai import ZhipuAI
|
||||
from model.util import InputData, OutputData, getModelAPIKey
|
||||
|
||||
# client = ZhipuAI(api_key="") # 填写您自己的APIKey
|
||||
# response = client.chat.completions.create(
|
||||
# model="glm-4-0520", # 填写需要调用的模型编码
|
||||
# messages=[
|
||||
# {"role": "user", "content": "作为一名营销专家,请为我的产品创作一个吸引人的slogan"},
|
||||
# {"role": "assistant", "content": "当然,为了创作一个吸引人的slogan,请告诉我一些关于您产品的信息"},
|
||||
# {"role": "user", "content": "智谱AI开放平台"},
|
||||
# {"role": "assistant", "content": "智启未来,谱绘无限一智谱AI,让创新触手可及!"},
|
||||
# {"role": "user", "content": "创造一个更精准、吸引人的slogan"}
|
||||
# ],
|
||||
# )
|
||||
# print(response.choices[0].message)
|
||||
|
||||
|
||||
|
||||
def predict(input_data:InputData):
|
||||
client = ZhipuAI(api_key=getModelAPIKey("glm-4-flash"))
|
||||
response = client.chat.completions.create(
|
||||
model="glm-4-flash", # 填写需要调用的模型编码
|
||||
messages=[
|
||||
{"role": "user", "content": input_data.message}],
|
||||
)
|
||||
if response.choices[0].finish_reason == "stop":
|
||||
return OutputData(response.choices[0].message.content,200,response.usage.total_tokens)
|
||||
elif response.choices[0].finish_reason == "length":
|
||||
return OutputData(response.choices[0].message.content,201,response.usage.total_tokens)
|
||||
elif response.choices[0].finish_reason == "network_error":
|
||||
return OutputData("Server Network Error",500,0)
|
||||
else: return OutputData("Unknown Error",500,0)
|
57
model/util.py
Normal file
@ -0,0 +1,57 @@
|
||||
import importlib
|
||||
from configUtil import ConfigUtil
|
||||
|
||||
|
||||
class InputData:
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
class OutputData:
|
||||
def __init__(self, message,code,tokenUsed):
|
||||
self.message = message
|
||||
self.code = code
|
||||
self.tokenUsed = tokenUsed
|
||||
|
||||
|
||||
|
||||
def getModels() -> list:
|
||||
model_config = ConfigUtil("data/models.ini")
|
||||
out = []
|
||||
for model in model_config.getSectionList():
|
||||
if model_config.getBool(model, "enabled") == False:
|
||||
continue
|
||||
out.append(model)
|
||||
return out
|
||||
|
||||
|
||||
def getModelsInfo() -> list:
|
||||
model_config = ConfigUtil("data/models.ini")
|
||||
out = []
|
||||
for model in model_config.getSectionList():
|
||||
if model_config.getBool(model, "enabled") == False:
|
||||
continue
|
||||
util = {
|
||||
"name":model_config.get(model, "name"),
|
||||
"id":model,
|
||||
}
|
||||
out.append(util)
|
||||
return out
|
||||
|
||||
|
||||
def getModelAPIKey(model: str) -> str:
|
||||
model_config = ConfigUtil("data/models.ini")
|
||||
try:
|
||||
return model_config.get(model, "key")
|
||||
except:
|
||||
return "Model API key not found"
|
||||
|
||||
def requestModel(model: str, input_data: InputData) -> OutputData:
|
||||
if model not in getModels():
|
||||
ret = OutputData("Model not found",404,0)
|
||||
else:
|
||||
module = importlib.import_module(f"model.{model}")
|
||||
ret = module.predict(input_data)
|
||||
resq = {}
|
||||
for key in ret.__dict__: resq[key] = ret.__dict__[key]
|
||||
return resq
|
||||
|
@ -1,6 +1,3 @@
|
||||
pymysql
|
||||
requests
|
||||
flask
|
||||
fastapi[standard]==0.115.0
|
||||
zhipuai
|
||||
openai
|
||||
flask_cors
|
21
router/chat.py
Normal file
@ -0,0 +1,21 @@
|
||||
from fastapi import APIRouter
|
||||
from model.util import InputData, getModels, getModelsInfo, requestModel
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
model: str
|
||||
message: str
|
||||
@router.post("/api/chat")
|
||||
def chat(req: ChatRequest):
|
||||
model_name = req.model
|
||||
input_data = InputData(req.message)
|
||||
#调用模型
|
||||
ret = requestModel(model_name, input_data)
|
||||
return ret
|
||||
|
||||
|
||||
@router.get("/api/chat/models")
|
||||
def get_models():
|
||||
return {"models": getModelsInfo()}
|
30
router/page.py
Normal file
@ -0,0 +1,30 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 将 Vue 构建后的 dist 目录中的 assets 作为静态资源目录
|
||||
#router.mount("/assets", StaticFiles(directory="frontend/dist/assets"), name="assets")
|
||||
@router.get("/assets/{path:path}")
|
||||
async def serve_static(path: str):
|
||||
if path.endswith(".css"):
|
||||
return FileResponse(f"frontend/dist/assets/{path}",media_type="text/css")
|
||||
elif path.endswith(".js"):
|
||||
return FileResponse(f"frontend/dist/assets/{path}",media_type="text/javascript")
|
||||
elif path.endswith(".png"):
|
||||
return FileResponse(f"frontend/dist/assets/{path}",media_type="image/png")
|
||||
else:
|
||||
return FileResponse(f"frontend/dist/assets/{path}")
|
||||
|
||||
|
||||
# 根路径提供 index.html
|
||||
@router.get("/")
|
||||
async def serve_vue_app():
|
||||
return FileResponse("frontend/dist/index.html")
|
||||
|
||||
# 通用路由处理,确保 history 模式下的路由正常工作
|
||||
@router.get("/{full_path:path}")
|
||||
async def serve_vue_app_catch_all(full_path: str):
|
||||
return FileResponse("frontend/dist/index.html")
|
19
router/user.py
Normal file
@ -0,0 +1,19 @@
|
||||
from fastapi import APIRouter, Response
|
||||
from pydantic import BaseModel
|
||||
|
||||
import dao.db.user as user
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
userinfo: str | None = None
|
||||
password: str
|
||||
|
||||
@router.post("/api/user/login")
|
||||
def login(response: Response,LoginRequest: LoginRequest):
|
||||
#if not user.checkUser(uid, password_hash):
|
||||
if False:
|
||||
return {"code": 401, "message": "Invalid credentials"}
|
||||
response.set_cookie(key="auth_token", value="1234567890")
|
||||
return {"code": 200, "message": "Login successful"}
|
45
static/echarts.min.js
vendored
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 134 KiB |
@ -1,78 +0,0 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background-image: url("../static/img/bg_circles.png");
|
||||
background-repeat: repeat;
|
||||
}
|
||||
#chat-container {
|
||||
width: 80%; /* 使用百分比宽度 */
|
||||
max-width: 1300px; /* 最大宽度,防止界面变得过宽 */
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
#chat-messages {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
word-wrap: break-word; /* 自动换行 */
|
||||
white-space: pre-wrap; /* 保留空格并自动换行 */
|
||||
}
|
||||
.message-divider {
|
||||
border-top: 1px solid #ccc;
|
||||
margin: 10px 0;
|
||||
}
|
||||
#user-input {
|
||||
width: 97%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
#user-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
#user-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
#user-input-button {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 10px 20px;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#user-input-button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
/* 新增样式 */
|
||||
#additional-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between; /* 左右对齐 */
|
||||
margin-top: 10px;
|
||||
}
|
||||
#additional-controls input[type="checkbox"] {
|
||||
margin-right: 10px;
|
||||
}
|
||||
#additional-controls button {
|
||||
background-color: #466d2b;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
}
|
@ -1,270 +0,0 @@
|
||||
.loading {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
opacity: 1;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3;
|
||||
background-color: #fbfbfb;
|
||||
transition: opacity 1s ease;
|
||||
pointer-events: none; /* 确保遮罩不影响下方元素的交互 */
|
||||
}
|
||||
|
||||
.typewriter {
|
||||
--blue: #5C86FF;
|
||||
--blue-dark: #275EFE;
|
||||
--key: #fff;
|
||||
--paper: #EEF0FD;
|
||||
--text: #D3D4EC;
|
||||
--tool: #FBC56C;
|
||||
--duration: 3s;
|
||||
position: relative;
|
||||
-webkit-animation: bounce05 var(--duration) linear infinite;
|
||||
animation: bounce05 var(--duration) linear infinite;
|
||||
}
|
||||
|
||||
.typewriter .slide {
|
||||
width: 92px;
|
||||
height: 20px;
|
||||
border-radius: 3px;
|
||||
margin-left: 14px;
|
||||
transform: translateX(14px);
|
||||
background: linear-gradient(var(--blue), var(--blue-dark));
|
||||
-webkit-animation: slide05 var(--duration) ease infinite;
|
||||
animation: slide05 var(--duration) ease infinite;
|
||||
}
|
||||
|
||||
.typewriter .slide:before,
|
||||
.typewriter .slide:after,
|
||||
.typewriter .slide i:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: var(--tool);
|
||||
}
|
||||
|
||||
.typewriter .slide:before {
|
||||
width: 2px;
|
||||
height: 8px;
|
||||
top: 6px;
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.typewriter .slide:after {
|
||||
left: 94px;
|
||||
top: 3px;
|
||||
height: 14px;
|
||||
width: 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.typewriter .slide i {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
width: 6px;
|
||||
height: 4px;
|
||||
top: 4px;
|
||||
background: var(--tool);
|
||||
}
|
||||
|
||||
.typewriter .slide i:before {
|
||||
right: 100%;
|
||||
top: -2px;
|
||||
width: 4px;
|
||||
border-radius: 2px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.typewriter .paper {
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
top: -26px;
|
||||
width: 40px;
|
||||
height: 46px;
|
||||
border-radius: 5px;
|
||||
background: var(--paper);
|
||||
transform: translateY(46px);
|
||||
-webkit-animation: paper05 var(--duration) linear infinite;
|
||||
animation: paper05 var(--duration) linear infinite;
|
||||
}
|
||||
|
||||
.typewriter .paper:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
top: 7px;
|
||||
border-radius: 2px;
|
||||
height: 4px;
|
||||
transform: scaleY(0.8);
|
||||
background: var(--text);
|
||||
box-shadow: 0 12px 0 var(--text), 0 24px 0 var(--text), 0 36px 0 var(--text);
|
||||
}
|
||||
|
||||
.typewriter .keyboard {
|
||||
width: 120px;
|
||||
height: 56px;
|
||||
margin-top: -10px;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.typewriter .keyboard:before,
|
||||
.typewriter .keyboard:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.typewriter .keyboard:before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 7px;
|
||||
background: linear-gradient(135deg, var(--blue), var(--blue-dark));
|
||||
transform: perspective(10px) rotateX(2deg);
|
||||
transform-origin: 50% 100%;
|
||||
}
|
||||
|
||||
.typewriter .keyboard:after {
|
||||
left: 2px;
|
||||
top: 25px;
|
||||
width: 11px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 15px 0 0 var(--key), 30px 0 0 var(--key), 45px 0 0 var(--key), 60px 0 0 var(--key), 75px 0 0 var(--key), 90px 0 0 var(--key), 22px 10px 0 var(--key), 37px 10px 0 var(--key), 52px 10px 0 var(--key), 60px 10px 0 var(--key), 68px 10px 0 var(--key), 83px 10px 0 var(--key);
|
||||
-webkit-animation: keyboard05 var(--duration) linear infinite;
|
||||
animation: keyboard05 var(--duration) linear infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce05 {
|
||||
|
||||
85%,
|
||||
92%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
89% {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
95% {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide05 {
|
||||
5% {
|
||||
transform: translateX(14px);
|
||||
}
|
||||
|
||||
15%,
|
||||
30% {
|
||||
transform: translateX(6px);
|
||||
}
|
||||
|
||||
40%,
|
||||
55% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
65%,
|
||||
70% {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
80%,
|
||||
89% {
|
||||
transform: translateX(-12px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(14px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes paper05 {
|
||||
5% {
|
||||
transform: translateY(46px);
|
||||
}
|
||||
|
||||
20%,
|
||||
30% {
|
||||
transform: translateY(34px);
|
||||
}
|
||||
|
||||
40%,
|
||||
55% {
|
||||
transform: translateY(22px);
|
||||
}
|
||||
|
||||
65%,
|
||||
70% {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
80%,
|
||||
85% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
92%,
|
||||
100% {
|
||||
transform: translateY(46px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes keyboard05 {
|
||||
|
||||
5%,
|
||||
12%,
|
||||
21%,
|
||||
30%,
|
||||
39%,
|
||||
48%,
|
||||
57%,
|
||||
66%,
|
||||
75%,
|
||||
84% {
|
||||
box-shadow: 15px 0 0 var(--key), 30px 0 0 var(--key), 45px 0 0 var(--key), 60px 0 0 var(--key), 75px 0 0 var(--key), 90px 0 0 var(--key), 22px 10px 0 var(--key), 37px 10px 0 var(--key), 52px 10px 0 var(--key), 60px 10px 0 var(--key), 68px 10px 0 var(--key), 83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
9% {
|
||||
box-shadow: 15px 2px 0 var(--key), 30px 0 0 var(--key), 45px 0 0 var(--key), 60px 0 0 var(--key), 75px 0 0 var(--key), 90px 0 0 var(--key), 22px 10px 0 var(--key), 37px 10px 0 var(--key), 52px 10px 0 var(--key), 60px 10px 0 var(--key), 68px 10px 0 var(--key), 83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
18% {
|
||||
box-shadow: 15px 0 0 var(--key), 30px 0 0 var(--key), 45px 0 0 var(--key), 60px 2px 0 var(--key), 75px 0 0 var(--key), 90px 0 0 var(--key), 22px 10px 0 var(--key), 37px 10px 0 var(--key), 52px 10px 0 var(--key), 60px 10px 0 var(--key), 68px 10px 0 var(--key), 83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
27% {
|
||||
box-shadow: 15px 0 0 var(--key), 30px 0 0 var(--key), 45px 0 0 var(--key), 60px 0 0 var(--key), 75px 0 0 var(--key), 90px 0 0 var(--key), 22px 12px 0 var(--key), 37px 10px 0 var(--key), 52px 10px 0 var(--key), 60px 10px 0 var(--key), 68px 10px 0 var(--key), 83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
36% {
|
||||
box-shadow: 15px 0 0 var(--key), 30px 0 0 var(--key), 45px 0 0 var(--key), 60px 0 0 var(--key), 75px 0 0 var(--key), 90px 0 0 var(--key), 22px 10px 0 var(--key), 37px 10px 0 var(--key), 52px 12px 0 var(--key), 60px 12px 0 var(--key), 68px 12px 0 var(--key), 83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
45% {
|
||||
box-shadow: 15px 0 0 var(--key), 30px 0 0 var(--key), 45px 0 0 var(--key), 60px 0 0 var(--key), 75px 0 0 var(--key), 90px 2px 0 var(--key), 22px 10px 0 var(--key), 37px 10px 0 var(--key), 52px 10px 0 var(--key), 60px 10px 0 var(--key), 68px 10px 0 var(--key), 83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
54% {
|
||||
box-shadow: 15px 0 0 var(--key), 30px 2px 0 var(--key), 45px 0 0 var(--key), 60px 0 0 var(--key), 75px 0 0 var(--key), 90px 0 0 var(--key), 22px 10px 0 var(--key), 37px 10px 0 var(--key), 52px 10px 0 var(--key), 60px 10px 0 var(--key), 68px 10px 0 var(--key), 83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
63% {
|
||||
box-shadow: 15px 0 0 var(--key), 30px 0 0 var(--key), 45px 0 0 var(--key), 60px 0 0 var(--key), 75px 0 0 var(--key), 90px 0 0 var(--key), 22px 10px 0 var(--key), 37px 10px 0 var(--key), 52px 10px 0 var(--key), 60px 10px 0 var(--key), 68px 10px 0 var(--key), 83px 12px 0 var(--key);
|
||||
}
|
||||
|
||||
72% {
|
||||
box-shadow: 15px 0 0 var(--key), 30px 0 0 var(--key), 45px 2px 0 var(--key), 60px 0 0 var(--key), 75px 0 0 var(--key), 90px 0 0 var(--key), 22px 10px 0 var(--key), 37px 10px 0 var(--key), 52px 10px 0 var(--key), 60px 10px 0 var(--key), 68px 10px 0 var(--key), 83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
81% {
|
||||
box-shadow: 15px 0 0 var(--key), 30px 0 0 var(--key), 45px 0 0 var(--key), 60px 0 0 var(--key), 75px 0 0 var(--key), 90px 0 0 var(--key), 22px 10px 0 var(--key), 37px 12px 0 var(--key), 52px 10px 0 var(--key), 60px 10px 0 var(--key), 68px 10px 0 var(--key), 83px 10px 0 var(--key);
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Arial', sans-serif;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
height: 100vh;
|
||||
width: 250px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: -200px;
|
||||
background-color: #006699;
|
||||
overflow-x: hidden;
|
||||
transition: 0.5s;
|
||||
padding-top: 60px;
|
||||
color: white;
|
||||
z-index: 2; /* 保证遮罩在页面上方 */
|
||||
}
|
||||
|
||||
#sidebar a {
|
||||
padding: 15px 10px;
|
||||
text-decoration: none;
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
display: block;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
#sidebar a:hover {
|
||||
background-color: #3366CC;
|
||||
}
|
||||
|
||||
#main {
|
||||
padding: 30px;
|
||||
padding-left: 80px;
|
||||
}
|
||||
|
||||
#global-blur {
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(8px); /* 模糊度可以根据需要调整 */
|
||||
transition: display;
|
||||
z-index: 1; /* 保证遮罩在页面上方 */
|
||||
pointer-events: none; /* 确保遮罩不影响下方元素的交互 */
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
function openNav() {
|
||||
document.getElementById("sidebar").style.left = "0";
|
||||
document.getElementById("global-blur").style.opacity = 1;
|
||||
}
|
||||
|
||||
function closeNav() {
|
||||
document.getElementById("sidebar").style.left = "-200px";
|
||||
document.getElementById("global-blur").style.opacity = 0;
|
||||
}
|
171
static/popup.css
@ -1,171 +0,0 @@
|
||||
#global-blur {
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(8px);
|
||||
/* 模糊度可以根据需要调整 */
|
||||
transition: display;
|
||||
z-index: 1;
|
||||
/* 保证遮罩在页面上方 */
|
||||
pointer-events: none;
|
||||
/* 确保遮罩不影响下方元素的交互 */
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease
|
||||
}
|
||||
|
||||
#popup-container {
|
||||
background-color: #f1f1f1;
|
||||
height: 100%;
|
||||
width: 250px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
overflow-x: hidden;
|
||||
padding-top: 60px;
|
||||
z-index: 2;
|
||||
transition: transform 0.4s ease;
|
||||
transform: translateX(250px);
|
||||
}
|
||||
|
||||
#popup-container.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#close-popup {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
#dropdown {
|
||||
margin: auto;
|
||||
width: 80%;
|
||||
/* 让<select>元素宽度占满容器 */
|
||||
margin-bottom: 10px;
|
||||
/* 添加一些底部间距 */
|
||||
}
|
||||
|
||||
#buttons {
|
||||
margin: auto;
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* 设置按钮垂直排列 */
|
||||
}
|
||||
|
||||
#buttons button {
|
||||
margin: 10px 0;
|
||||
/* 添加按钮之间的垂直间距 */
|
||||
}
|
||||
|
||||
/* 弹窗消息框CSS */
|
||||
|
||||
/* 定义通知框的样式 */
|
||||
.notification {
|
||||
display: none;
|
||||
/* 初始状态下通知框不显示 */
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 300px;
|
||||
background-color: #f1f1f1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
opacity: 0;
|
||||
/* 初始透明度为 0,即不可见 */
|
||||
transform: translateY(-20px);
|
||||
/* 初始向上平移20px,用于动画效果 */
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
/* 添加过渡效果 */
|
||||
}
|
||||
|
||||
/* 定义显示状态下的通知框样式 */
|
||||
.notification.show {
|
||||
opacity: 1;
|
||||
/* 显示状态下透明度为 1,即完全可见 */
|
||||
transform: translateY(0);
|
||||
/* 平移恢复到原位,显示的过渡效果 */
|
||||
}
|
||||
|
||||
/* 定义通知框内的内容容器样式 */
|
||||
.notification-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 定义关闭按钮的样式 */
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 定义进度条样式 */
|
||||
.progress-bar {
|
||||
height: 10px;
|
||||
background-color: #ddd;
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 定义进度条内部样式 */
|
||||
.progress-bar-inner {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #4caf50;
|
||||
transition: width 0.3s linear;
|
||||
/* 添加进度条宽度变化的线性过渡效果 */
|
||||
}
|
||||
|
||||
/* --------------公告栏部分-------------- */
|
||||
|
||||
/* 模态框样式 */
|
||||
.modal {
|
||||
display: none;
|
||||
/* 默认隐藏 */
|
||||
position: fixed;
|
||||
/* 固定位置 */
|
||||
z-index: 1;
|
||||
/* 位于顶层 */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
/* 宽度为全屏 */
|
||||
height: 100%;
|
||||
/* 高度为全屏 */
|
||||
overflow: auto;
|
||||
/* 如果需要滚动条,则启用 */
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
/* 半透明背景 */
|
||||
}
|
||||
|
||||
/* 模态框内容样式 */
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 15% auto;
|
||||
/* 位于页面中心 */
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
/* 宽度 */
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.modal-content button {
|
||||
display: flex;
|
||||
left: 20px;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 10px 20px;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
// 消息框的js代码
|
||||
|
||||
// 获取页面元素
|
||||
var notification = document.getElementById('notification');
|
||||
var showNotificationBtn = document.getElementById('showNotificationBtn');
|
||||
var notificationText = document.getElementById('notificationText');
|
||||
var progressBarInner = document.getElementById('progressBarInner');
|
||||
|
||||
// 显示通知函数
|
||||
function showNotification(message) {
|
||||
// 设置通知文本
|
||||
notificationText.innerText = message;
|
||||
// 显示通知框
|
||||
notification.style.display = 'block';
|
||||
|
||||
// 触发回流(reflow)以启用动画效果
|
||||
notification.offsetHeight;
|
||||
|
||||
// 添加显示状态的类,触发过渡效果
|
||||
notification.classList.add('show');
|
||||
|
||||
// 启动倒计时
|
||||
startCountdown(5); // 延迟时间
|
||||
}
|
||||
|
||||
// 倒计时函数
|
||||
function startCountdown(duration) {
|
||||
var startTime = Date.now();
|
||||
var interval = setInterval(function () {
|
||||
var currentTime = Date.now();
|
||||
var elapsedTime = currentTime - startTime;
|
||||
var remainingTime = duration * 1000 - elapsedTime;
|
||||
|
||||
if (remainingTime <= 0) {
|
||||
clearInterval(interval);
|
||||
closeNotification();
|
||||
} else {
|
||||
var progressPercent = (remainingTime / (duration * 1000)) * 100;
|
||||
updateProgressBar(progressPercent);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 更新进度条函数
|
||||
function updateProgressBar(percent) {
|
||||
progressBarInner.style.width = percent + '%';
|
||||
}
|
||||
|
||||
// 关闭通知函数
|
||||
function closeNotification() {
|
||||
// 移除显示状态的类,触发过渡效果
|
||||
notification.classList.remove('show');
|
||||
// 延时等待动画完成后隐藏通知框
|
||||
setTimeout(function () {
|
||||
notification.style.display = 'none';
|
||||
// 重置进度条宽度为 100%
|
||||
progressBarInner.style.width = '100%';
|
||||
}, 300); // 等待动画完成再隐藏
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>创建密钥</title>
|
||||
<link rel="stylesheet" type="text/css" href="../static/menu.css">
|
||||
<script src="../static/echarts.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
background-color: #f7f7f7;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="global-blur"></div>
|
||||
<div id="sidebar" onmouseover="openNav()" onmouseout="closeNav()">
|
||||
<a href="/admin" class="nodecoration">仪表盘</a>
|
||||
<a href="/admin/list" class="nodecoration">列出所有Key</a>
|
||||
<a href="/admin/lookupkey" class="nodecoration">查询密钥</a>
|
||||
<a href="/admin/createkey" class="nodecoration">创建密钥</a>
|
||||
<a href="/admin/log?show=500" class="nodecoration">查看日志</a>
|
||||
<!-- 添加更多菜单项 -->
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<h2>创建单个密钥</h2>
|
||||
<form method="post">
|
||||
<span>密钥</span>
|
||||
<input name="key" required>
|
||||
<span>配额</span>
|
||||
<input name="quota" required>
|
||||
<button type="submit">创建</button>
|
||||
</form>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="container">
|
||||
<h2>批量创建密钥</h2>
|
||||
<form method="post">
|
||||
<input type="radio" id="type" name="type" value="uuid" checked="true">
|
||||
<label for="type">使用UUID</label><br>
|
||||
<span>个数</span>
|
||||
<input name="number" required>
|
||||
<span>配额</span>
|
||||
<input name="quota" required>
|
||||
<button type="submit">批量创建</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if resq != "null" %}
|
||||
<hr>
|
||||
<div class="container">
|
||||
<h2>执行结果</h2>
|
||||
{% for i in resq %}
|
||||
<h4>{{ i }}</h4>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script src="../static/menu.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
Before Width: | Height: | Size: 9.4 KiB |
@ -1,282 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KakuAI</title>
|
||||
<link rel="stylesheet" type="text/css" href="../static/popup.css">
|
||||
<link rel="stylesheet" type="text/css" href="../static/index.css">
|
||||
<link rel="stylesheet" type="text/css" href="../static/loading.css">
|
||||
</head>
|
||||
|
||||
<body onload="init()">
|
||||
<div id="loading" class="loading">
|
||||
<div class="typewriter">
|
||||
<div class="slide"><i></i></div>
|
||||
<div class="paper"></div>
|
||||
<div class="keyboard"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--全局遮罩-->
|
||||
<div id="global-blur"></div>
|
||||
<!-- 通知框容器 -->
|
||||
<div id="notification" class="notification">
|
||||
<!-- 通知框内容容器 -->
|
||||
<div class="notification-content">
|
||||
<!-- 关闭按钮 -->
|
||||
<span class="close" onclick="closeNotification()">×</span>
|
||||
<!-- 通知文本 -->
|
||||
<p id="notificationText">这是一条通知。</p>
|
||||
<!-- 进度条容器 -->
|
||||
<div class="progress-bar">
|
||||
<!-- 进度条内部 -->
|
||||
<div id="progressBarInner" class="progress-bar-inner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="chat-container">
|
||||
<div id="chat-messages"></div>
|
||||
<input type="text" id="user-input" placeholder="输入消息..."> <!-- 用户输入消息的输入框 -->
|
||||
<button id="user-input-button" disabled>发送</button> <!-- 发送消息按钮初始状态禁用 -->
|
||||
<!-- 新增复选框、文本和附加功能按钮 -->
|
||||
<img id="loadingico" alt="Loading..." src="../static/img/loading.gif"
|
||||
style="height: 50px;transform: translate(10px, 20px);display: none;" />
|
||||
<div id="additional-controls">
|
||||
<label>
|
||||
<input type="checkbox" id="additional-checkbox"> 联系上文 <a id="showtoken"><br>剩余Token将会被显示在这里</a>
|
||||
</label>
|
||||
<button id="additional-button">设置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="popup-container"> <!--菜单部分-->
|
||||
<div style="padding: 20px;">
|
||||
<button id="close-popup">关闭</button>
|
||||
<h1>设置</h1>
|
||||
<p>使用的AI模型</p>
|
||||
<select id="setUpDropdown" defaultValue="qwen-turbo"
|
||||
onchange="setCookie('modelSet', document.getElementById('setUpDropdown').value, 265)">
|
||||
<option value="qwen-turbo">qwen-turbo</option>
|
||||
<option value="chatglm-turbo">chatglmTurbo</option>
|
||||
<option value="gpt3.5-turbo">gpt3.5-turbo(X3 Token)</option>
|
||||
<option value="gpt4.0-turbo">gpt4.0-turbo(X45 Token)</option>
|
||||
</select>
|
||||
<hr>
|
||||
<h3>当前UserKey</h3>
|
||||
<a id="showUserKey"><br>当前UserKey将会被显示在这里</a>
|
||||
<hr>
|
||||
<div id="buttons">
|
||||
<button id="setUpButton1">设置UserKey</button>
|
||||
<button id="setUpButton2"
|
||||
onclick="window.location.href = 'https://afdian.net/a/kaku55'">获取更多Token</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 公告模态框 -->
|
||||
<div id="announcementModal" class="modal">
|
||||
<!-- 模态框内容 -->
|
||||
<div class="modal-content">
|
||||
<h2>公告标题</h2>
|
||||
<p id="modal-text">这里是公告内容。</p>
|
||||
<button id="closeModel">确定公告</button>
|
||||
<input id="dontShowNextTime" type="checkbox"><label>有新消息前不再显示</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../static/popupMessagesBox.js"></script>
|
||||
<script>
|
||||
const chatMessages = document.getElementById('chat-messages'); // 聊天消息容器
|
||||
const userInput = document.getElementById('user-input'); // 用户输入消息的输入框
|
||||
const sendButton = document.getElementById('user-input-button'); // 发送消息按钮
|
||||
const additionalButton = document.getElementById('additional-button'); // 附加功能按钮
|
||||
const additionalCheckbox = document.getElementById('additional-checkbox'); // 复选框
|
||||
const popupContainer = document.getElementById('popup-container'); // 菜单容器
|
||||
const closePopup = document.getElementById('close-popup'); // 关闭菜单按钮
|
||||
const setUpButton1 = document.getElementById('setUpButton1');// 菜单按钮1
|
||||
const globalBlur = document.getElementById('global-blur'); //全局遮罩
|
||||
|
||||
var userhs1 = "x"; // 历史记录的保存
|
||||
var userhs0 = "x";
|
||||
var boths1 = "x";
|
||||
var boths0 = "x";
|
||||
|
||||
// 关闭菜单
|
||||
closePopup.addEventListener('click', () => {
|
||||
popupContainer.classList.remove('show');
|
||||
globalBlur.style.opacity = 0;
|
||||
});
|
||||
|
||||
// 点击发送按钮后的处理函数
|
||||
sendButton.addEventListener('click', sendMessage);
|
||||
// 用户输入消息后的处理函数
|
||||
userInput.addEventListener('input', handleUserInput);
|
||||
// 点击附加功能按钮后的处理函数
|
||||
additionalButton.addEventListener('click', additionalFunction);
|
||||
// 菜单按钮的处理函数
|
||||
setUpButton1.addEventListener('click', resetCookie);
|
||||
|
||||
// 发送消息函数
|
||||
function sendMessage() {
|
||||
document.getElementById("loadingico").style.display = "";
|
||||
const userMessage = userInput.value; // 获取用户输入的消息
|
||||
appendMessage('你', userMessage); // 在聊天界面中添加用户消息
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight; //自动滚动
|
||||
userInput.value = ''; // 清空输入框
|
||||
|
||||
// 立即禁用发送按钮
|
||||
sendButton.disabled = true;
|
||||
|
||||
// 在实际应用中,你可以将用户消息发送到后端进行处理
|
||||
// 在这个示例中,我们模拟了来自助手的响应
|
||||
setTimeout(function () {
|
||||
requestAPI(userMessage);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 在聊天界面中添加消息
|
||||
function appendMessage(sender, message) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.innerHTML = `<strong>${sender}:</strong> ${message}`;
|
||||
chatMessages.appendChild(messageDiv);
|
||||
|
||||
// 在每条消息后添加分隔线
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'message-divider';
|
||||
chatMessages.appendChild(divider);
|
||||
}
|
||||
|
||||
// 附加功能按钮的处理函数
|
||||
function additionalFunction() {
|
||||
// 处理附加功能按钮的点击事件
|
||||
popupContainer.classList.add('show');
|
||||
globalBlur.style.opacity = 1; //开启毛玻璃效果
|
||||
}
|
||||
|
||||
// 用户输入框的输入事件处理函数
|
||||
function handleUserInput() {
|
||||
// 根据用户输入的内容启用或禁用发送按钮
|
||||
sendButton.disabled = userInput.value.trim() === '';
|
||||
}
|
||||
|
||||
// 使用Cookie保存用户的UserKey
|
||||
function setCookie(cname, cvalue, exdays) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
|
||||
var expires = "expires=" + d.toGMTString();
|
||||
document.cookie = cname + "=" + cvalue + "; " + expires;
|
||||
}
|
||||
|
||||
function init() {
|
||||
setTimeout(function () {
|
||||
checkCookie()
|
||||
document.getElementById("loading").style.opacity = 0;
|
||||
}, 1000);
|
||||
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var ca = document.cookie.split(';');
|
||||
for (var i = 0; i < ca.length; i++) {
|
||||
var c = ca[i].trim();
|
||||
if (c.indexOf(name) == 0) { return c.substring(name.length, c.length); }
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function checkCookie() {
|
||||
var user = getCookie("userkey");
|
||||
if (user != "") {
|
||||
showNotification("欢迎回来\nUserKey:" + user + "\n当前选择模型:" + getCookie("modelSet"));
|
||||
document.getElementById("showUserKey").innerHTML = user;
|
||||
document.getElementById('setUpDropdown').value = getCookie("modelSet");
|
||||
}
|
||||
else {
|
||||
user = prompt("请输入你的Userkey:", "");
|
||||
if (user != "" && user != null) {
|
||||
setCookie("userkey", user, 265);
|
||||
setCookie('modelSet', document.getElementById('setUpDropdown').value, 265);
|
||||
}
|
||||
}
|
||||
}
|
||||
function resetCookie() {
|
||||
user = prompt("请输入你的Userkey:", "");
|
||||
if (user != "" && user != null) {
|
||||
setCookie("userkey", user, 265);
|
||||
alert("UserKey已更新");
|
||||
document.getElementById("showUserKey").innerHTML = user;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function requestAPI(message) {
|
||||
// 创建包含JSON数据的对象
|
||||
var requestData = {
|
||||
"model": document.getElementById("setUpDropdown").value,
|
||||
"prompt": message,
|
||||
"userkey": getCookie("userkey"),
|
||||
"context": Number(additionalCheckbox.checked),
|
||||
"history": [
|
||||
{
|
||||
"user": userhs1,
|
||||
"bot": boths1
|
||||
},
|
||||
{
|
||||
"user": userhs0,
|
||||
"bot": boths0
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
fetch("api/user", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// 在这里处理返回的JSON数据
|
||||
if (data["code"] == 200) {
|
||||
appendMessage("KakuAI" + "(" + document.getElementById("setUpDropdown").value + ")", data["output"]);
|
||||
document.getElementById("showtoken").innerHTML = "<br>剩余Tokens:" + data["surplus"];
|
||||
}
|
||||
else {
|
||||
alert("ErrCode:" + data["code"] + " " + data["output"])
|
||||
}
|
||||
userhs1 = userhs0;
|
||||
boths1 = boths0;
|
||||
userhs0 = message;
|
||||
boths0 = data["output"];
|
||||
loading = false;
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight; //自动滚动
|
||||
document.getElementById("loadingico").style.display = "none";
|
||||
// 可以根据返回的数据执行相应的操作
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('请求出错:', error);
|
||||
alert('请求出错:', error);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () { //回车发送消息功能
|
||||
function handleKey(event) {
|
||||
if (event.keyCode === 13) {
|
||||
// 在这里执行你希望回车键触发的操作
|
||||
sendButton.click(); // 模拟按钮点击
|
||||
}
|
||||
}
|
||||
// 绑定事件到输入框
|
||||
userInput.addEventListener('keypress', handleKey);
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,81 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>列出密钥</title>
|
||||
<link rel="stylesheet" type="text/css" href="../static/menu.css">
|
||||
<script src="../static/echarts.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
background-color: #f7f7f7;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="global-blur"></div>
|
||||
<div id="sidebar" onmouseover="openNav()" onmouseout="closeNav()">
|
||||
<a href="/admin" class="nodecoration">仪表盘</a>
|
||||
<a href="/admin/list" class="nodecoration">列出所有Key</a>
|
||||
<a href="/admin/lookupkey" class="nodecoration">查询密钥</a>
|
||||
<a href="/admin/createkey" class="nodecoration">创建密钥</a>
|
||||
<a href="/admin/log?show=500" class="nodecoration">查看日志</a>
|
||||
<!-- 添加更多菜单项 -->
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<h2>列出密钥</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>剩余Tokens</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in data %}
|
||||
<tr>
|
||||
<td>{{item[0]}}</td>
|
||||
<td>{{item[1]}}</td>
|
||||
<td><a href="/admin/operate?type=del&target={{item[0]}}">删除</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
<script src="../static/menu.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,63 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录页面</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
padding: 20px;
|
||||
width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-container h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background-color: #5cb85c;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
background-color: #4cae4c;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h2>登录</h2>
|
||||
<form action="login" method="post">
|
||||
<input name="password" type="password" id="password" class="password-input" placeholder="输入密码" required />
|
||||
<button type="submit" class="login-button">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,84 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>列出日志</title>
|
||||
<link rel="stylesheet" type="text/css" href="../static/menu.css">
|
||||
<script src="../static/echarts.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
background-color: #f7f7f7;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="global-blur"></div>
|
||||
<div id="sidebar" onmouseover="openNav()" onmouseout="closeNav()">
|
||||
<a href="/admin" class="nodecoration">仪表盘</a>
|
||||
<a href="/admin/list" class="nodecoration">列出所有Key</a>
|
||||
<a href="/admin/lookupkey" class="nodecoration">查询密钥</a>
|
||||
<a href="/admin/createkey" class="nodecoration">创建密钥</a>
|
||||
<a href="/admin/log?show=500" class="nodecoration">查看日志</a>
|
||||
<!-- 添加更多菜单项 -->
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<h2>列出日志</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP</th>
|
||||
<th>时间</th>
|
||||
<th>token用量</th>
|
||||
<th>使用模型</th>
|
||||
<th>UserKey</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in data %}
|
||||
<tr>
|
||||
<td>{{item[0]}}</td>
|
||||
<td>{{item[1]}}</td>
|
||||
<td>{{item[2]}}</td>
|
||||
<td>{{item[3]}}</td>
|
||||
<td>{{item[4]}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<script src="../static/menu.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,61 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>查询密钥</title>
|
||||
<link rel="stylesheet" type="text/css" href="../static/menu.css">
|
||||
<script src="../static/echarts.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
background-color: #f7f7f7;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="global-blur"></div>
|
||||
<div id="sidebar" onmouseover="openNav()" onmouseout="closeNav()">
|
||||
<a href="/admin" class="nodecoration">仪表盘</a>
|
||||
<a href="/admin/list" class="nodecoration">列出所有Key</a>
|
||||
<a href="/admin/lookupkey" class="nodecoration">查询密钥</a>
|
||||
<a href="/admin/createkey" class="nodecoration">创建密钥</a>
|
||||
<a href="/admin/log?show=500" class="nodecoration">查看日志</a>
|
||||
<!-- 添加更多菜单项 -->
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<h2>查询密钥</h2>
|
||||
<form method="post">
|
||||
<span>UserKey</span>
|
||||
<input name="key" required>
|
||||
<button type="submit">查询</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if resq != "null" %}
|
||||
<hr>
|
||||
<div class="container">
|
||||
<h2>执行结果</h2>
|
||||
{% if resq == -99999 %}
|
||||
<h4>未找到UserKey</h4>
|
||||
{% else %}
|
||||
<h4>配额剩余 {{ resq }}</h4>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script src="../static/menu.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,152 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>后台管理</title>
|
||||
<link rel="stylesheet" type="text/css" href="../static/menu.css">
|
||||
<script src="../static/echarts.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
background-color: #f7f7f7;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.nodecoration {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="global-blur"></div>
|
||||
<div id="sidebar" onmouseover="openNav()" onmouseout="closeNav()">
|
||||
<a href="/admin" class="nodecoration">仪表盘</a>
|
||||
<a href="/admin/list" class="nodecoration">列出所有Key</a>
|
||||
<a href="/admin/lookupkey" class="nodecoration">查询密钥</a>
|
||||
<a href="/admin/createkey" class="nodecoration">创建密钥</a>
|
||||
<a href="/admin/log?show=500" class="nodecoration">查看日志</a>
|
||||
<!-- 添加更多菜单项 -->
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<h2>概况</h2>
|
||||
<div id="echart1" style="width: 600px;height:400px;"></div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="container">
|
||||
<h2>状态</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>项目</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>开发者心态</td>
|
||||
<td class="green">正常</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>数据库连接</td>
|
||||
{% if status["db"] == True %}
|
||||
<td class="green">正常</td>
|
||||
{% else %}
|
||||
<td class="red">连接异常</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>开发者大脑负载</td>
|
||||
<td class="red">较高</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../static/menu.js"></script>
|
||||
<script type="text/javascript">
|
||||
const apiURL = '/api/modelcount';
|
||||
datajson = null
|
||||
// 基于准备好的dom,初始化echarts实例
|
||||
var myChart = echarts.init(document.getElementById('echart1'));
|
||||
//使用fetch API发送GET请求
|
||||
fetch(apiURL)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json(); // 确保返回的是JSON对象
|
||||
})
|
||||
.then(data => {
|
||||
// 确保 data 是一个数组,并且每个元素都有 value 和 name 属性
|
||||
if (data && Array.isArray(data) && data.every(item => item.value && item.name)) {
|
||||
myChart.setOption({
|
||||
title: {
|
||||
text: '模型调用',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
fontSize: '25',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: '模型调用',
|
||||
type: 'pie',
|
||||
radius: '55%',
|
||||
data: data,
|
||||
radius: ['30%', '65%'],
|
||||
label: {
|
||||
show: true, //开启显示
|
||||
fontSize: '16',
|
||||
formatter: '{b}:{c}' + '\n\r' + '({d}%)',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}]
|
||||
});
|
||||
} else {
|
||||
console.error('Data received is not in the correct format for ECharts pie chart:', data);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('There has been a problem with your fetch operation:', error);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|