feat:完成简易的的前端和后端api

This commit is contained in:
Kakune55 2024-10-17 09:29:28 +08:00
parent aab5043fed
commit 848484682a
67 changed files with 3024 additions and 2053 deletions

1
.gitattributes vendored
View File

@ -1 +0,0 @@
*.html linguist-language=Python

40
.gitignore vendored
View File

@ -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

View File

@ -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
~~~

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"]

View File

@ -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
View 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
View 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
View File

@ -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]

View File

@ -1,7 +0,0 @@
# 配置文件说明
{"url": "<此处为api URL>", "userkey": "<此处为你的UserKey>", "context": <此处为1 开启上下位关联功能 默认0关闭>}
注意开启上下文关联会消耗大量的token
# 什么是Token
在大型语言模型中,"token"是指文本中的一个最小单位。 通常一个token可以是一个单词、一个标点符号、一个数字、一个符号等。 在自然语言处理中tokenization是将一个句子或文本分成tokens的过程。 在大型语言模型的训练和应用中模型接收一串tokens作为输入并尝试预测下一个最可能的token。
# 上下文关联
在使用上下文关联时模型除了处理你当前的问题时还需要一并将历史对话的一部分共同处理。所以需要消耗更多的token。在本应用中token消耗平均值大约是不开启上下文功能的300%。

View File

@ -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
View File

@ -0,0 +1,3 @@
/// <reference types="vite/client" />
declare module "*.vue"

13
frontend/index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

30
frontend/package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

34
frontend/src/App.vue Normal file
View 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>

View 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;
}

View 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
View 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');

View 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;

View 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>

View 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>

View 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
View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View 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
View 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
View File

@ -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
View File

@ -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
View 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
View 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

View File

@ -1,6 +1,3 @@
pymysql
requests
flask
zhipuai
openai
flask_cors
fastapi[standard]==0.115.0
zhipuai

21
router/chat.py Normal file
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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); // 等待动画完成再隐藏
}

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -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()">&times;</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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>