Version 0.1.1 rise to flask app factory
This commit is contained in:
34
Html/apps/__init__.py
Normal file
34
Html/apps/__init__.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import logging
|
||||
from flask import Flask
|
||||
from .config import Config
|
||||
from .extensions import init_extensions, register_namespaces
|
||||
from .views import register_blueprints
|
||||
|
||||
|
||||
|
||||
def create_app(config_object: type = Config) -> Flask:
|
||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||
app.config.from_object(config_object)
|
||||
|
||||
# secret_key 从 config 里来
|
||||
app.secret_key = app.config["SECRET_KEY"]
|
||||
|
||||
# logging
|
||||
app.logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
register_namespaces(app)
|
||||
|
||||
# 1) 初始化第三方扩展(db、cache、jwt、cors 等)
|
||||
init_extensions(app)
|
||||
|
||||
|
||||
# 2) 注册蓝图(把子文件暴露的蓝图统一挂到 app 上)
|
||||
register_blueprints(app)
|
||||
|
||||
# 3) 其他钩子/命令/错误处理
|
||||
@app.route("/healthz")
|
||||
def healthz():
|
||||
return {"status": "ok"}
|
||||
|
||||
return app
|
||||
98
Html/apps/auth/decorators.py
Normal file
98
Html/apps/auth/decorators.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# myapp/auth/decorators.py
|
||||
from functools import wraps
|
||||
from typing import Callable, Optional, Union, Iterable
|
||||
from flask import session, redirect, url_for, current_app
|
||||
|
||||
RoleType = Union[str, Iterable[str]]
|
||||
RoleChecker = Callable[[str, RoleType], bool] # (username, required_roles) -> bool
|
||||
|
||||
def _get_deps():
|
||||
"""从 app 注入点拿依赖,避免循环导入。"""
|
||||
app = current_app
|
||||
uuid2username = (
|
||||
app.config.get("UUID2USERNAME")
|
||||
or app.extensions.get("uuid2username")
|
||||
)
|
||||
users_list = (
|
||||
app.config.get("USERS_LIST")
|
||||
or app.extensions.get("users_list")
|
||||
)
|
||||
role_checker: Optional[RoleChecker] = (
|
||||
app.config.get("ROLE_CHECKER")
|
||||
or app.extensions.get("role_checker")
|
||||
)
|
||||
return uuid2username, users_list, role_checker
|
||||
|
||||
def require_role(
|
||||
view=None,
|
||||
*,
|
||||
# 未登录时跳转到哪个 endpoint
|
||||
login_endpoint: str = "auth.login",
|
||||
# 角色要求:None/空 -> 仅需登录;"teacher" 或 ["teacher", "admin"] -> 需具备其中之一
|
||||
roles: Optional[RoleType] = None,
|
||||
# 可选:自定义角色校验函数(优先级高于内置 users_list 适配)
|
||||
checker: Optional[RoleChecker] = None,
|
||||
):
|
||||
"""
|
||||
用法:
|
||||
1) 仅需登录(等价于 require_user):
|
||||
@require_role
|
||||
2) 需要 teacher 角色(等价于 require_teacher):
|
||||
@require_role(roles="teacher")
|
||||
3) 需要多个角色之一:
|
||||
@require_role(roles=["teacher", "admin"])
|
||||
4) 自定义校验器(例如 RBAC/权限码):
|
||||
@require_role(roles="teacher", checker=my_checker)
|
||||
|
||||
也支持指定登录端点:
|
||||
@require_role(login_endpoint="login")
|
||||
"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
uuid2username, users_list, injected_checker = _get_deps()
|
||||
|
||||
user_id = session.get("user_id")
|
||||
if not user_id or uuid2username is None or user_id not in uuid2username:
|
||||
return redirect(url_for(login_endpoint))
|
||||
|
||||
# 仅需登录
|
||||
if not roles:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
username = uuid2username[user_id]
|
||||
|
||||
# 选择校验器优先级:参数 checker > app 注入的 role_checker > 内置 users_list 适配
|
||||
effective_checker = checker or injected_checker
|
||||
|
||||
# 内置对 users_list 的向后兼容(你的现有接口)
|
||||
if effective_checker is None:
|
||||
def builtin_checker(u: str, required: RoleType) -> bool:
|
||||
# 支持 teacher 场景(原先的 get_user_is_teacher)
|
||||
def has_teacher(u_: str) -> bool:
|
||||
if users_list is None:
|
||||
return False
|
||||
getter = getattr(users_list, "get_user_is_teacher", None)
|
||||
return bool(getter and getter(u_) is True)
|
||||
|
||||
def match_one(required_role: str) -> bool:
|
||||
if required_role == "teacher":
|
||||
return has_teacher(u)
|
||||
# 你也可以在此扩展更多内置角色判断
|
||||
# 比如 get_user_is_admin / get_user_roles 等
|
||||
# 未知角色默认 False
|
||||
return False
|
||||
|
||||
if isinstance(required, str):
|
||||
return match_one(required)
|
||||
return any(match_one(r) for r in required)
|
||||
|
||||
effective_checker = builtin_checker
|
||||
|
||||
ok = effective_checker(username, roles)
|
||||
if not ok:
|
||||
return redirect(url_for(login_endpoint))
|
||||
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorator(view) if callable(view) else decorator
|
||||
1
Html/apps/bootstrap.py
Normal file
1
Html/apps/bootstrap.py
Normal file
@@ -0,0 +1 @@
|
||||
# Html/apps/bootstrap.py
|
||||
37
Html/apps/config.py
Normal file
37
Html/apps/config.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# app/config.py
|
||||
import configparser
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),'..'))
|
||||
|
||||
GLOBAL_CONFIG = configparser.ConfigParser()
|
||||
GLOBAL_CONFIG.read(os.getenv("APP_CONFIG_FILE", "config.ini"))
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret")
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///dev.db")
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "cakebaker") # 覆盖优先
|
||||
VSCODE_WEB_URL = GLOBAL_CONFIG['VSCODE_WEB']['url']
|
||||
USER_DATA_DIR = GLOBAL_CONFIG['USER_DATA']['dir']
|
||||
COURSE_DATA_DIR = GLOBAL_CONFIG['COURSE_DATA']['dir']
|
||||
|
||||
# socketio / cors 配置也可以放这里
|
||||
SOCKETIO_PING_TIMEOUT = 60
|
||||
SOCKETIO_PING_INTERVAL = 5
|
||||
MARKDOWN_DIR = os.path.join(BASE_DIR, "books", "markdown")
|
||||
STATIC_DIR = os.path.join(BASE_DIR, "static")
|
||||
IMAGE_DIR = "image"
|
||||
|
||||
# 你的学生工作区根目录(如无就放到项目 data 目录)
|
||||
STUDENT_WORKSPACE_ROOT = os.getenv(
|
||||
"STUDENT_WORKSPACE_ROOT",
|
||||
os.path.join(BASE_DIR, "data", "workspaces")
|
||||
)
|
||||
|
||||
# WSL 相关
|
||||
VSCODE_WEB_PATH = {
|
||||
"is_wsl": GLOBAL_CONFIG.getboolean("VSCODE_WEB_PATH", "is_wsl", fallback=False),
|
||||
"windows_path": GLOBAL_CONFIG.get("VSCODE_WEB_PATH", "windows_path", fallback=""),
|
||||
"wsl_path": GLOBAL_CONFIG.get("VSCODE_WEB_PATH", "wsl_path", fallback=""),
|
||||
}
|
||||
59
Html/apps/extensions.py
Normal file
59
Html/apps/extensions.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import os, sys
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_socketio import SocketIO
|
||||
from flask_cors import CORS
|
||||
from db.user_list import UserList
|
||||
from db.course_list import CourseList
|
||||
from .services.my_function import MyFunction
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
socketio = SocketIO(cors_allowed_origins="*") # 不直接传 app
|
||||
cors = CORS()
|
||||
# ===== 你的全局对象 =====
|
||||
# Backboard
|
||||
from backboardManager import BackBoardManager # 你已有的类
|
||||
backboard_manager = BackBoardManager()
|
||||
|
||||
# 用户映射(全局内存结构)
|
||||
userid_recorder = {} # 你代码里有使用
|
||||
users_list = UserList()
|
||||
uuid2username = {} # 你的映射,或换成数据库访问层
|
||||
username2uuid = {}
|
||||
course_list = CourseList()
|
||||
user_id2UserClass = {}
|
||||
|
||||
from AlgoriAgent.projects.algoriAgent.agent_manager import AgentManager
|
||||
agent_manager = AgentManager()
|
||||
def init_extensions(app):
|
||||
db.init_app(app)
|
||||
# 第三方
|
||||
socketio.init_app(
|
||||
app,
|
||||
cors_allowed_origins='*',#app.config["VSCODE_WEB_URL"],
|
||||
ping_timeout=app.config["SOCKETIO_PING_TIMEOUT"],
|
||||
ping_interval=app.config["SOCKETIO_PING_INTERVAL"],
|
||||
)
|
||||
cors.init_app(
|
||||
app,
|
||||
resources={r"/*": {"origins": app.config["VSCODE_WEB_URL"]}},
|
||||
supports_credentials=True,
|
||||
)
|
||||
|
||||
# 把自定义对象挂到 app.extensions,供各处通过 current_app 访问
|
||||
app.extensions["users_list"] = users_list
|
||||
app.extensions["course_list"] = course_list
|
||||
|
||||
app.extensions["backboard_manager"] = backboard_manager
|
||||
app.extensions["uuid2username"] = uuid2username
|
||||
app.extensions["username2uuid"] = username2uuid
|
||||
app.extensions["userid_recorder"] = userid_recorder
|
||||
app.extensions["user_id2UserClass"] = user_id2UserClass
|
||||
app.extensions["agent_manager"] = agent_manager
|
||||
app.extensions["my_function"] = MyFunction()
|
||||
def register_namespaces(app):
|
||||
"""把所有 Socket.IO namespaces 注册到 socketio"""
|
||||
# 延迟导入以避免循环
|
||||
from .sockets.namespaces import VSCodeNamespace, AgentNamespace
|
||||
socketio.on_namespace(VSCodeNamespace("/vscode"))
|
||||
socketio.on_namespace(AgentNamespace("/agent"))
|
||||
BIN
Html/apps/image/readme/1728381936172.png
Normal file
BIN
Html/apps/image/readme/1728381936172.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
46
Html/apps/services/auth_service.py
Normal file
46
Html/apps/services/auth_service.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# myapp/services/auth_service.py
|
||||
import os, uuid, json
|
||||
from flask import current_app, session
|
||||
|
||||
def create_user_json(username: str):
|
||||
user_dir = current_app.config["USER_DATA_DIR"]
|
||||
os.makedirs(user_dir, exist_ok=True)
|
||||
path = os.path.join(user_dir, f"{username}.json")
|
||||
if not os.path.exists(path):
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump({"username": username}, f, ensure_ascii=False)
|
||||
|
||||
def register_user(username: str, password: str, *, teacher: bool=False):
|
||||
users_list = current_app.extensions["users_list"]
|
||||
# 你原逻辑似乎把 has_user 的判断反了:直觉上「不存在才允许新增」
|
||||
exists = users_list.has_user(username)
|
||||
if exists:
|
||||
return False, "用户已存在,请更换用户名"
|
||||
users_list.add_user(username, password, teacher=teacher)
|
||||
create_user_json(username)
|
||||
return True, "注册成功"
|
||||
|
||||
def login_user(username: str, password: str, *, require_teacher: bool=False):
|
||||
users_list = current_app.extensions["users_list"]
|
||||
pswd = users_list.get_user_pswd(username)
|
||||
if pswd is None or pswd != password:
|
||||
return False, "用户名或密码错误"
|
||||
|
||||
if require_teacher:
|
||||
is_teacher = users_list.get_user_is_teacher(username)
|
||||
if not is_teacher:
|
||||
return False, "用户名不存在或非教师账号"
|
||||
|
||||
# 设置会话 + 关联映射
|
||||
user_uuid = "user_" + str(uuid.uuid4())
|
||||
session["user_id"] = user_uuid
|
||||
|
||||
username2uuid = current_app.extensions["username2uuid"]
|
||||
uuid2username = current_app.extensions["uuid2username"]
|
||||
username2uuid[username] = user_uuid
|
||||
uuid2username[user_uuid] = username
|
||||
|
||||
return True, "登录成功"
|
||||
|
||||
def logout_user():
|
||||
session.pop("user_id", None)
|
||||
59
Html/apps/services/backboard_service.py
Normal file
59
Html/apps/services/backboard_service.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# myapp/services/backboard_service.py
|
||||
import os
|
||||
from flask import current_app
|
||||
from backboardManager import Backboard # 你已有的类
|
||||
|
||||
def _to_wsl_path_if_needed(path: str) -> str:
|
||||
cfg = current_app.config["VSCODE_WEB_PATH"]
|
||||
if cfg.get("is_wsl") and cfg.get("windows_path") and cfg.get("wsl_path"):
|
||||
return path.replace("\\", "/").replace(cfg["windows_path"], cfg["wsl_path"])
|
||||
return path
|
||||
|
||||
def realtime_response(config: dict, realtime_action: dict) -> None:
|
||||
"""
|
||||
业务:根据 VSCode 上报的动作,更新 Backboard(黑板)状态。
|
||||
"""
|
||||
# 依赖从 app.extensions 取,避免循环导入
|
||||
username2uuid = current_app.extensions["username2uuid"]
|
||||
backboard_manager = current_app.extensions["backboard_manager"]
|
||||
|
||||
user_id = config["user_id"]
|
||||
folder_path = config["path"] # 如需 WSL 转换可在生成 config 时处理
|
||||
useruuid = username2uuid[user_id]
|
||||
bb = backboard_manager.get_backboard(useruuid)
|
||||
assert isinstance(bb, Backboard)
|
||||
|
||||
# 统一记历史
|
||||
bb.add_history(realtime_action)
|
||||
|
||||
rtype = realtime_action.get("type")
|
||||
|
||||
if rtype == "workspaceFolders":
|
||||
bb.file_tree = realtime_action.get("fileTree")
|
||||
|
||||
elif rtype == "activeFile":
|
||||
file_path = realtime_action.get("filePath")
|
||||
assert isinstance(file_path, str)
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
bb.active_file_content = f.read()
|
||||
bb.active_file_path = file_path
|
||||
|
||||
elif rtype == "paste":
|
||||
file_path = realtime_action.get("filePath")
|
||||
assert isinstance(file_path, str)
|
||||
bb.pasted_file_path = file_path
|
||||
bb.pasted_content = realtime_action.get("content")
|
||||
bb.active_file_path = file_path
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
bb.active_file_content = f.read()
|
||||
|
||||
elif rtype == "fileEdit":
|
||||
file_path = realtime_action.get("filePath")
|
||||
assert isinstance(file_path, str)
|
||||
bb.active_file_path = file_path
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
bb.active_file_content = f.read()
|
||||
|
||||
# 如需调试输出,可在这里统一记录日志
|
||||
current_app.logger.debug("vscode config: %s", config)
|
||||
current_app.logger.debug("vscode action: %s", realtime_action)
|
||||
20
Html/apps/services/course_service.py
Normal file
20
Html/apps/services/course_service.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# myapp/services/course_service.py
|
||||
from flask import current_app
|
||||
|
||||
# 假设你已有这两个 loader
|
||||
from db.course import load_course_from_json
|
||||
|
||||
def load_course(course_id: str):
|
||||
return load_course_from_json(
|
||||
course_id, course_data_dir=current_app.config["COURSE_DATA_DIR"]
|
||||
)
|
||||
|
||||
def user_selected_course_briefs(user_obj):
|
||||
"""根据用户对象的选课列表,拼装课程简要信息数组"""
|
||||
course_list = current_app.extensions["course_list"]
|
||||
briefs = []
|
||||
for cid in getattr(user_obj, "select_course", []):
|
||||
course_obj = load_course(cid)
|
||||
brief = course_list.get_course_brief_info(cid, course_obj)
|
||||
briefs.append(brief)
|
||||
return briefs
|
||||
40
Html/apps/services/markdown_service.py
Normal file
40
Html/apps/services/markdown_service.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import os, shutil, markdown
|
||||
|
||||
def convert_markdown_to_html(md_file_path: str) -> str:
|
||||
"""读取 markdown 并转成 HTML 片段"""
|
||||
with open(md_file_path, 'r', encoding='utf-8') as f:
|
||||
md_content = f.read()
|
||||
return markdown.markdown(md_content)
|
||||
|
||||
def wrap_with_styles(html_content: str) -> str:
|
||||
"""加上 CSS 样式和外层 HTML"""
|
||||
return f"""
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
img {{
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
|
||||
</head>
|
||||
<body class="markdown-body">
|
||||
{html_content}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def save_html(html_str: str, output_path: str) -> None:
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(html_str)
|
||||
|
||||
def copy_images(source_dir: str, target_dir: str) -> None:
|
||||
if not os.path.exists(source_dir):
|
||||
return
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
for filename in os.listdir(source_dir):
|
||||
src = os.path.join(source_dir, filename)
|
||||
if os.path.isfile(src):
|
||||
shutil.copy(src, target_dir)
|
||||
26
Html/apps/services/memory_service.py
Normal file
26
Html/apps/services/memory_service.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# myapp/services/memory_service.py
|
||||
import os, json
|
||||
from flask import current_app
|
||||
from db.user import load_user_from_json # 按你项目实际替换
|
||||
|
||||
def save_chapter_memory_by_uuid(
|
||||
user_uuid: str,
|
||||
course_id: str,
|
||||
lesson_id: str,
|
||||
subchapter_title: str,
|
||||
mem_list,
|
||||
score,
|
||||
is_rebuttal: bool,
|
||||
):
|
||||
uuid2username = current_app.extensions["uuid2username"]
|
||||
user_map = current_app.extensions["user_id2UserClass"] # 可复用缓存
|
||||
user_data_dir = current_app.config["USER_DATA_DIR"]
|
||||
|
||||
username = uuid2username[user_uuid]
|
||||
# 若内存里已有用户对象就直接用;否则从 JSON 载入
|
||||
user_obj = user_map.get(user_uuid) or load_user_from_json(username, user_data_dir=user_data_dir)
|
||||
user_map[user_uuid] = user_obj
|
||||
|
||||
user_obj.save_chapter_memory(
|
||||
course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal
|
||||
)
|
||||
10
Html/apps/services/my_function.py
Normal file
10
Html/apps/services/my_function.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# myapp/services/my_function.py
|
||||
from flask import current_app
|
||||
from .memory_service import save_chapter_memory_by_uuid
|
||||
|
||||
class MyFunction:
|
||||
def save_chapter_memory(self, user_uuid, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal):
|
||||
current_app.logger.debug("save_chapter_memory %s %s %s", user_uuid, course_id, lesson_id)
|
||||
save_chapter_memory_by_uuid(
|
||||
user_uuid, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal
|
||||
)
|
||||
42
Html/apps/services/user_service.py
Normal file
42
Html/apps/services/user_service.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# myapp/services/user_service.py
|
||||
import os
|
||||
from typing import Tuple
|
||||
from flask import current_app, session
|
||||
|
||||
# 假设你已有这两个 loader
|
||||
from db.user import load_user_from_json
|
||||
|
||||
def _username_from_session() -> str:
|
||||
"""通过 session['user_id'] -> username"""
|
||||
user_uuid = session.get("user_id")
|
||||
if not user_uuid:
|
||||
return None
|
||||
uuid2username = current_app.extensions["uuid2username"]
|
||||
return uuid2username.get(user_uuid)
|
||||
|
||||
def get_or_load_current_user():
|
||||
"""
|
||||
返回当前用户对象(如 user_id2UserClass 中没有则从 JSON 载入并缓存)
|
||||
"""
|
||||
user_uuid = session.get("user_id")
|
||||
if not user_uuid:
|
||||
return None
|
||||
|
||||
username = _username_from_session()
|
||||
if not username:
|
||||
return None
|
||||
|
||||
user_map = current_app.extensions["user_id2UserClass"]
|
||||
if user_uuid not in user_map:
|
||||
user_map[user_uuid] = load_user_from_json(
|
||||
username, user_data_dir=current_app.config["USER_DATA_DIR"]
|
||||
)
|
||||
return user_map[user_uuid]
|
||||
|
||||
def add_course_for_current_user(course_id: str, course_data):
|
||||
user_obj = get_or_load_current_user()
|
||||
if user_obj is None:
|
||||
return False
|
||||
# 你的 UserClass 应该有 select_new_course 接口
|
||||
user_obj.select_new_course(course_id, course_data)
|
||||
return True
|
||||
104
Html/apps/sockets/namespaces.py
Normal file
104
Html/apps/sockets/namespaces.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# myapp/sockets/namespaces.py
|
||||
import json
|
||||
from flask import current_app, request, session
|
||||
from flask_socketio import Namespace, join_room, leave_room, emit
|
||||
from ..services.memory_service import save_chapter_memory_by_uuid
|
||||
from ..services.backboard_service import realtime_response
|
||||
class VSCodeNamespace(Namespace):
|
||||
def on_login(self,data):
|
||||
ex = current_app.extensions
|
||||
username2uuid = ex["username2uuid"]
|
||||
backboard_manager = ex["backboard_manager"]
|
||||
print("VSCode client connected")
|
||||
print(data)
|
||||
dataconfig = data['config']
|
||||
user_id = dataconfig.get('user_id')
|
||||
path = dataconfig.get('path')
|
||||
course_id = dataconfig.get('course_id')
|
||||
lesson_id = dataconfig.get('chapter_id')
|
||||
print(f"User {user_id} connected with path: {path}")
|
||||
useruuid = username2uuid[user_id]
|
||||
join_room(useruuid, namespace='/vscode')
|
||||
if backboard_manager.get_backboard(useruuid) == None:
|
||||
backboard_manager.add_backboard(useruuid,user_id, course_id, lesson_id, path)
|
||||
|
||||
|
||||
def on_message(self, data):
|
||||
print(f"Received from VSCode client: {data}")
|
||||
dataconfig = data['config']
|
||||
realtime_response(dataconfig, data)
|
||||
# emit('response', {'message': 'Config data received and connection established'})
|
||||
def on_disconnect(self, data):
|
||||
print("VSCode client disconnected")
|
||||
print("Disconnect reason:"+str(data))
|
||||
|
||||
|
||||
class AgentNamespace(Namespace):
|
||||
def on_login(self, data):
|
||||
ex = current_app.extensions
|
||||
username2uuid = ex["username2uuid"]
|
||||
backboard_manager = ex["backboard_manager"]
|
||||
agent_manager = ex["agent_manager"]
|
||||
data = json.loads(data)
|
||||
user = data['username']
|
||||
course_id = data['course_id']
|
||||
chapter_id = data['chapter_id']
|
||||
user_uuid = username2uuid[user]
|
||||
session['user_id'] = user_uuid
|
||||
print(f'User connected with session user_id: {user_uuid}')
|
||||
# 从markdown、markdown_prompts、score_prompts中各读取数据folder.md并进行new_agent
|
||||
with open(f'books/markdown/{course_id}/{chapter_id}.md','r',encoding='UTF-8') as fmd,\
|
||||
open(f'books/markdown_prompts/{course_id}/{chapter_id}.md','r',encoding='UTF-8')as fmdp,\
|
||||
open(f'books/score_prompts/{course_id}/{chapter_id}.md','r',encoding='UTF-8') as fsp:
|
||||
markdown = fmd.read()
|
||||
markdown_prompts = fmdp.read()
|
||||
score_prompts = fsp.read()
|
||||
user_uuid, agent = agent_manager.new_agent(course_id, chapter_id, markdown, markdown_prompts, score_prompts, id=user_uuid,
|
||||
root_path=f'../../study/{user}/{course_id}/{chapter_id}')
|
||||
print(user_uuid)
|
||||
join_room(user_uuid, namespace='/agent') # 将该用户加入以user_id为名的room
|
||||
backboard_manager.add_backboard(user_uuid, user, course_id, chapter_id, root_path=f'../../study/{user}/{course_id}/{chapter_id}')
|
||||
|
||||
def on_language(self, language):
|
||||
ex = current_app.extensions
|
||||
agent_manager = ex["agent_manager"]
|
||||
id = session.get('user_id')
|
||||
agent_manager.change_language(id, language)
|
||||
|
||||
def on_message(self, data):
|
||||
print(f"Message from client: {data}")
|
||||
ex = current_app.extensions
|
||||
agent_manager = ex["agent_manager"]
|
||||
backboard_manager = ex["backboard_manager"]
|
||||
id = session.get('user_id')
|
||||
if (type(data)==str):
|
||||
data = json.loads(data)
|
||||
print(id)
|
||||
if data['type'] == 'text':
|
||||
res = agent_manager.invoke(id, data['data'], backboard_manager.get_backboard(id).get_info_prompt())
|
||||
print('=*='*20)
|
||||
print(res.content)
|
||||
reply = f"{res.content['speak']}"
|
||||
with current_app.app_context():
|
||||
emit('message',reply, room=id, namespace='/agent')
|
||||
emit('request_function',res.content['function'], room=id, namespace='/agent')
|
||||
|
||||
if data['type'] == 'function':
|
||||
agent_manager.function_call(id, data['data'])
|
||||
|
||||
def on_initiative(self,data):
|
||||
print("User active function call")
|
||||
ex = current_app.extensions
|
||||
agent_manager = ex["agent_manager"]
|
||||
backboard_manager = ex["backboard_manager"]
|
||||
user_id = session['user_id']
|
||||
if data['name'] == 'sample_judge':
|
||||
agent_manager.sample_judge(user_id, backboard_manager.get_backboard(user_id))
|
||||
if data['name'] == 'judge':
|
||||
agent_manager.judge(user_id, backboard_manager.get_backboard(user_id))
|
||||
|
||||
|
||||
|
||||
def on_disconnect(self,data):
|
||||
print("VSCode client disconnected")
|
||||
print("Disconnect reason:"+str(data))
|
||||
53
Html/apps/static/binary_search.html
Normal file
53
Html/apps/static/binary_search.html
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>二分查找与二分答案</h1>
|
||||
<h2>二分查找</h2>
|
||||
<h3>引入</h3>
|
||||
<p>二分是一个很简单基础,但很重要的知识点,为以后许多高级的数据结构与算法铺垫。</p>
|
||||
<p>下面是一个用二分的简单场景:</p>
|
||||
<p>假设小明从0到1000之间选择了一个数字但不告诉你,你可以不断猜测这个数,每次猜测小明会告知你的猜测得过大还是过小,问最多几次就一定能猜中?</p>
|
||||
<p>答案是利用二分查找的原理,猜测11次即可。</p>
|
||||
<ol>
|
||||
<li>对于0到1000的答案备选区,猜测中位数500,假设过小,</li>
|
||||
<li>则对于501到1000的答案备选区,猜测750,假设过大</li>
|
||||
<li>则对于501到749的答案备选区,猜测625,假设过小,</li>
|
||||
<li>则对于626到749区间......</li>
|
||||
<li>(688-749)</li>
|
||||
<li>(718-749)</li>
|
||||
<li>(734-749)</li>
|
||||
<li>(742-749)</li>
|
||||
<li>(746-749)</li>
|
||||
<li>(748-749)</li>
|
||||
<li>(749-749)</li>
|
||||
</ol>
|
||||
<p>在最差的情况下,第11次的答案备选区就一定长度为1了,也就是必然是答案。</p>
|
||||
<p>因此如果序列是有序的,就可以通过二分查找快速定位所需要的数据。</p>
|
||||
<h4>思考题(询问Agent以学习计算方法,或验证你的答案)</h4>
|
||||
<p>对于上面那个题目,如果问题区间是1到4000,最差情况下需要猜测几次?</p>
|
||||
<h3>练习:二分查找</h3>
|
||||
<p>试试对于下面的题目,用代码实现一下二分查找。</p>
|
||||
<h4>题目:有序数组寻址</h4>
|
||||
<p>给出一个长度为n的有序数组(从小到大),有q次询问,对于每次询问,输出指定数在数组中的下标。如果不存在则输出-1。</p>
|
||||
<h5>输入</h5>
|
||||
<p>第一行一个整数n。(1<=n<=10^5)</p>
|
||||
<p>第二行n个用空格分开的整数ai。(0<=ai<=10^8)</p>
|
||||
<p>第三行一个整数q,表示询问的次数。(1<=q<=10^4)</p>
|
||||
<p>后q行,每行一个整数b,表示询问的数。(0<=b<=10^8)</p>
|
||||
<h5>输出</h5>
|
||||
<p>q行,每行一个整数,对应每次询问的返回结果。</p>
|
||||
<h5>提示:</h5>
|
||||
<p>完成代码后,通知Agent进行评测。</p>
|
||||
<p>如果你还不完全会这个算法,询问Agent获取提示并进行学习。</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
158
Html/apps/static/css/course.css
Normal file
158
Html/apps/static/css/course.css
Normal file
@@ -0,0 +1,158 @@
|
||||
/* Reset some default styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Body & Page Background */
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 页眉导航栏 */
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #2575fc;
|
||||
padding: 10px 30px;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.navbar .logo h1 {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.navbar-links a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
margin: 0 15px;
|
||||
font-size: 16px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.navbar-links a:hover {
|
||||
color: #f1c40f;
|
||||
}
|
||||
|
||||
.navbar .avatar img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
/* 课程详情页面主体部分 */
|
||||
.course-details {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
}
|
||||
|
||||
/* 上半部分:课程封面和课程详情 */
|
||||
.course-overview {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30px;
|
||||
max-height: 200px;
|
||||
height: 30%;
|
||||
}
|
||||
|
||||
.course-cover {
|
||||
width: 40%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
|
||||
height: 100%;
|
||||
width: auto;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.course-info {
|
||||
width: 55%;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.course-author {
|
||||
font-size: 16px;
|
||||
color: #777;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.course-description {
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* 下半部分:课程教案列表 */
|
||||
.lesson-plans {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.lesson-plans h3 {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 课程教案卡片容器 */
|
||||
.lesson-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
/* 课程教案卡片 */
|
||||
.lesson-card {
|
||||
width: 200px;
|
||||
height: 300px;
|
||||
background-color: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.lesson-card:hover {
|
||||
transform: translateY(-10px); /* 提升效果 */
|
||||
}
|
||||
|
||||
.lesson-image {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.lesson-info {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.lesson-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.lesson-description {
|
||||
font-size: 14px;
|
||||
color: #777;
|
||||
}
|
||||
196
Html/apps/static/css/dashboard.css
Normal file
196
Html/apps/static/css/dashboard.css
Normal file
@@ -0,0 +1,196 @@
|
||||
/* Reset some default styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Body & Page Background */
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 页眉导航栏 */
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #2575fc;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.navbar .logo h1 {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.navbar-links a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
margin: 0 15px;
|
||||
font-size: 16px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.navbar-links a:hover {
|
||||
color: #f1c40f;
|
||||
}
|
||||
|
||||
.navbar .avatar img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
/* Dashboard 主体部分 */
|
||||
.dashboard {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
/* 课程卡片容器 */
|
||||
.course-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 课程卡片 */
|
||||
.course-card {
|
||||
width: 300px;
|
||||
height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.select-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #2575fc;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-top: auto; /* 将按钮推到父元素的底部 */
|
||||
}
|
||||
|
||||
.course-card:hover {
|
||||
transform: translateY(-10px); /* 提升效果 */
|
||||
}
|
||||
|
||||
.course-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.course-name {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.course-description {
|
||||
font-size: 14px;
|
||||
color: #777;
|
||||
margin-top: 10px;
|
||||
}
|
||||
/* 右侧滑出进度卡片样式 */
|
||||
.course-progress {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -400px; /* 初始时在屏幕外 */
|
||||
width: 400px;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
|
||||
padding: 30px;
|
||||
transition: right 0.3s ease;
|
||||
z-index: 9999;
|
||||
border-radius: 15px; /* 添加圆角 */
|
||||
}
|
||||
|
||||
.course-progress.open {
|
||||
right: 0; /* 打开时显示 */
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
/* 章节和子章节样式 */
|
||||
.chapter-list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.chapter-item {
|
||||
margin: 5px 0;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chapter-item .chapter-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chapter-item .sub-chapter-list {
|
||||
display: none; /* 初始状态下子章节隐藏 */
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.chapter-item.open .sub-chapter-list {
|
||||
display: block; /* 展开时显示子章节 */
|
||||
}
|
||||
|
||||
.chapter-item:hover {
|
||||
background-color: #f1f1f1;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* 子章节的样式 */
|
||||
.sub-chapter-item {
|
||||
margin: 3px 0;
|
||||
padding: 3px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sub-chapter-item:hover {
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
298
Html/apps/static/css/desktop.css
Normal file
298
Html/apps/static/css/desktop.css
Normal file
@@ -0,0 +1,298 @@
|
||||
|
||||
/* 左右两侧的页面内容 */
|
||||
.sidebar {
|
||||
background-color: #f0f0f0;
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.chatbox {
|
||||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
height: 75%;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
max-width: 70%;
|
||||
padding: 10px 15px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.user-message {
|
||||
background-color: #d1e7dd;
|
||||
text-align: right;
|
||||
margin-left: auto;
|
||||
}
|
||||
.server-message {
|
||||
background-color: #f1f1f1;
|
||||
text-align: left;
|
||||
margin-right: auto;
|
||||
}
|
||||
textarea {
|
||||
flex-grow: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
resize: none;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
margin-left: 10px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
pre {
|
||||
background-color: #eee;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
code {
|
||||
font-family: monospace;
|
||||
}
|
||||
/* 让父容器为flex布局 */
|
||||
.maxcontainer {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
/* 中间的拖拽条 */
|
||||
.resizer {
|
||||
width: 5px;
|
||||
background-color: #ccc;
|
||||
cursor: ew-resize;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chatbox-header {
|
||||
padding: 10px;
|
||||
background-color: #f1f1f1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background-color: #f1f1f1;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.slider-container, .language-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slider-container input[type="range"] {
|
||||
width: 70%;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn-group-toggle .btn {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
#leftSidebar, .vscode-web, #rightSidebar {
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#dragbar, #dragbar2 {
|
||||
background-color: #ccc;
|
||||
cursor: col-resize;
|
||||
}
|
||||
/* 主容器,使用 grid 布局 */
|
||||
#maxcontainer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 5px 3fr 5px 1fr; /* 默认宽度比例 */
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* 各区域基础样式 */
|
||||
.sidebar {
|
||||
overflow: auto; /* 确保内容可以滚动 */
|
||||
}
|
||||
|
||||
.vscode-web {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.resizer {
|
||||
background-color: #ccc;
|
||||
cursor: col-resize;
|
||||
width: 5px; /* 设置拖动条宽度 */
|
||||
}
|
||||
|
||||
.gutter {
|
||||
background-color: #ccc; /* 分隔条颜色 */
|
||||
cursor: col-resize;
|
||||
}#container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 5px 3fr 5px 1fr; /* 初始比例 */
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#leftSidebar, #vscodeWeb, #rightSidebar {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#dragbar, #dragbar2 {
|
||||
background-color: #ccc;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* 设置每个子框的基本样式 */
|
||||
.progress-box {
|
||||
height: 100%; /* 高度为 100%,撑满容器 */
|
||||
display: flex;
|
||||
flex-direction: row; /* 使子框横向排列 */
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.progress-title {
|
||||
height: 60px; /* 每个进度节点的高度 */
|
||||
text-align: center;
|
||||
line-height: 60px; /* 垂直居中 */
|
||||
margin: 2px; /* 水平方向上的间隔 */
|
||||
border: 1px solid #ddd;
|
||||
transition: background-color 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.green {
|
||||
background-color: green;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.white {
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.progress-box > .progress-title:last-child {
|
||||
margin-bottom: 0; /* 防止最后一个子框出现多余的间距 */
|
||||
}
|
||||
|
||||
/* 进度条详细信息的样式 */
|
||||
#progress-detail {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
padding: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.button:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.close-button {
|
||||
width: auto;
|
||||
height: auto;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
padding-bottom: 2px;
|
||||
padding-top: 2px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
/* 新增的侧边工具栏样式 */
|
||||
.sidebar-tools {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: -250px; /* 初始隐藏 */
|
||||
width: 250px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px 0 0 10px;
|
||||
transition: right 0.3s ease-in-out;
|
||||
z-index: 1000;
|
||||
}
|
||||
.sidebar-tools.open {
|
||||
right: 0;
|
||||
}
|
||||
.sidebar-tools .tool-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.sidebar-tools .tool-button:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.sidebar-tools .tool-button .icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
background-color: #007bff;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
}
|
||||
.sidebar-tools .tool-button .description {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.toggle-button {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: -10px; /* 初始隐藏 */
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: #007bff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: right 0.3s ease-in-out;
|
||||
z-index: 1001;
|
||||
}
|
||||
.toggle-button.open {
|
||||
right: 250px;
|
||||
}
|
||||
.toggle-button .icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
153
Html/apps/static/css/index.css
Normal file
153
Html/apps/static/css/index.css
Normal file
@@ -0,0 +1,153 @@
|
||||
|
||||
/* General reset */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
/* Body & Page Background */
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
font-family: 'Arial', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
footer {
|
||||
background-color: #f8f9fa;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
/* Header styles */
|
||||
header {
|
||||
background-color: #2575fc;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.site-name h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.nav ul {
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.nav ul li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.nav ul li a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.user-avatar img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Main content styles */
|
||||
main {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.course-selection h2 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.course-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.course-card {
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.select-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #2575fc;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-top: auto; /* 将按钮推到父元素的底部 */
|
||||
}
|
||||
.course-card:hover {
|
||||
transform: translateY(-10px); /* 提升效果 */
|
||||
}
|
||||
.course-card img {
|
||||
width: 100%;
|
||||
height: 261px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.course-card h3 {
|
||||
padding: 15px;
|
||||
font-size: 20px;
|
||||
color: #2575fc;
|
||||
}
|
||||
|
||||
.course-card p {
|
||||
padding: 0 15px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
|
||||
.select-button:hover {
|
||||
background-color: #113c86;
|
||||
}
|
||||
|
||||
.selected-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #8a8b8c;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
cursor: not-allowed;
|
||||
margin-top: auto; /* 将按钮推到父元素的底部 */
|
||||
}
|
||||
|
||||
/* Footer styles */
|
||||
footer {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
188
Html/apps/static/css/login.css
Normal file
188
Html/apps/static/css/login.css
Normal file
@@ -0,0 +1,188 @@
|
||||
/* Reset some default styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Body & Page background */
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background: linear-gradient(135deg, #70ff88, #e6d05f); /* 美丽的渐变背景 */
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background-color: white;
|
||||
padding: 40px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-switch {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
background-color: #2575fc;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.switch-btn:hover {
|
||||
background-color: #6a11cb;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ddd;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
border-color: #2575fc;
|
||||
outline: none;
|
||||
}
|
||||
/* 基本按钮样式 */
|
||||
.role-btn {
|
||||
background-color: #2575fc; /* 默认的蓝色背景(学生登录) */
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, transform 0.3s; /* 背景色平滑过渡,按钮缩放 */
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 按钮悬停效果 */
|
||||
.role-btn:hover {
|
||||
background-color: #6a11cb; /* 悬停时变为深蓝色 */
|
||||
transform: scale(1.05); /* 按钮放大效果 */
|
||||
}
|
||||
|
||||
/* 切换到教师登录时的背景色 */
|
||||
.teacher-login .role-btn {
|
||||
background-color: #005848; /* 深蓝色背景(教师登录) */
|
||||
}
|
||||
|
||||
/* 切换到教师登录时的悬停效果 */
|
||||
.teacher-login .role-btn:hover {
|
||||
background-color: #017c6e;
|
||||
}
|
||||
.login-btn {
|
||||
background-color: #2575fc;
|
||||
color: white;
|
||||
padding: 14px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background-color: #6a11cb;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.forgot-password a {
|
||||
color: #2575fc;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.forgot-password a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.social-login {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.social-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin: 5px 0;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.social-btn.google {
|
||||
background-color: #db4437;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social-btn.google:hover {
|
||||
background-color: #c1351d;
|
||||
}
|
||||
|
||||
.social-btn.facebook {
|
||||
background-color: #3b5998;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social-btn.facebook:hover {
|
||||
background-color: #2d4373;
|
||||
}
|
||||
199
Html/apps/static/css/teacherboard.css
Normal file
199
Html/apps/static/css/teacherboard.css
Normal file
@@ -0,0 +1,199 @@
|
||||
/* Teacherboard 样式 */
|
||||
.teacherboard {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.teacherboard-title {
|
||||
text-align: center;
|
||||
font-size: 32px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.course-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.course-card {
|
||||
width: 250px;
|
||||
background-color: #f9f9f9;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.course-image {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.course-name {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.course-description {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* 课程目录弹出框 */
|
||||
|
||||
.course-details {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -400px;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
transition: right 0.3s ease;
|
||||
z-index: 9999;
|
||||
border-radius: 15px; /* 添加圆角 */
|
||||
}
|
||||
|
||||
.course-details.open {
|
||||
right: 0; /* 打开时显示 */
|
||||
}
|
||||
|
||||
.details-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.details-header button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.details-content {
|
||||
padding-right: 20px;
|
||||
}
|
||||
.lesson-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between; /* 让垃圾桶靠右 */
|
||||
padding: 5px 0;
|
||||
}
|
||||
.lesson-text {
|
||||
cursor: pointer; /* 鼠标悬停时变成手指 */
|
||||
transition: color 0.3s;
|
||||
flex: 1; /* 占据左侧空间 */
|
||||
}
|
||||
|
||||
.lesson-text:hover {
|
||||
color: #2575fc; /* 这里你可以设置悬停时的颜色 */
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
color: #2575fc; /* hover 高亮 */
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
color: #c0392b; /* 红色垃圾桶 */
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
color: #e74c3c;
|
||||
}
|
||||
.new-chapter-input {
|
||||
width: 200px;
|
||||
padding: 8px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
color: #2575fc; /* hover 高亮 */
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
color: #c0392b; /* 红色垃圾桶 */
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
|
||||
.add-chapter-btn {
|
||||
background-color: #2575fc;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 编辑弹窗 */
|
||||
.edit-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 400px;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.edit-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edit-modal-content input,
|
||||
.edit-modal-content textarea {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.edit-modal-content button {
|
||||
background-color: #2575fc;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
20
Html/apps/static/example.html
Normal file
20
Html/apps/static/example.html
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>这是一个Makedown文件</h2>
|
||||
<h3>教学目标</h3>
|
||||
<h3>第一章:什么是计算机</h3>
|
||||
<h4>例1-1:计算机的组成部分是</h4>
|
||||
<h3>第二章:什么是算法</h3>
|
||||
<p><img alt="图片" src="./image/example/p1.png" /></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
Html/apps/static/image/CS101/CS101.png
Normal file
BIN
Html/apps/static/image/CS101/CS101.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
Html/apps/static/image/algorithm/book_cover.png
Normal file
BIN
Html/apps/static/image/algorithm/book_cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 912 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
BIN
Html/apps/static/image/example/p1.png
Normal file
BIN
Html/apps/static/image/example/p1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
258
Html/apps/static/js/chatbox.js
Normal file
258
Html/apps/static/js/chatbox.js
Normal file
@@ -0,0 +1,258 @@
|
||||
|
||||
var socket;
|
||||
let system_message_idx=0
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
data = window.appData;
|
||||
console.log(data);
|
||||
socket = io('ws://localhost:5551/agent',{
|
||||
query:{
|
||||
username:data.username,
|
||||
folder:data.folder
|
||||
}
|
||||
});
|
||||
socket.on('connect', function() {
|
||||
console.log('Connected to server');
|
||||
socket.emit('login',JSON.stringify(data));
|
||||
});
|
||||
// 监听来自服务器的消息
|
||||
socket.on('message', function(data) {
|
||||
// 显示服务器的回复消息
|
||||
const serverMessage = document.createElement('div');
|
||||
serverMessage.innerHTML = `<div class="chat-message server-message"><b>华实君:</b> ${marked.parse(data)}</div>`;
|
||||
document.getElementById('chatbox').appendChild(serverMessage);
|
||||
|
||||
// 滚动到最新消息
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
});
|
||||
socket.on('request_function', function(data){
|
||||
try {
|
||||
console.log("request_function", data);
|
||||
if (typeof data === 'string') {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const element = data[i]
|
||||
const requestMessage = document.createElement('div');
|
||||
requestMessage.className = 'chat-message server-message';
|
||||
const approveButton = document.createElement('button');
|
||||
approveButton.innerText = '批准';
|
||||
approveButton.data = element;
|
||||
approveButton.style.width = '100%';
|
||||
approveButton.style.borderRadius = '4px';
|
||||
approveButton.onclick = function(event) {
|
||||
console.log(element)
|
||||
socket.emit('message', JSON.stringify({type:'function', data:element}));
|
||||
event.currentTarget.disabled = true;
|
||||
event.currentTarget.style.backgroundColor = 'gray';
|
||||
event.currentTarget.style.color = 'white'; // 可选:设置文字颜色为白色,以便更清晰地显示
|
||||
event.currentTarget.style.border = 'none'; // 可选:去掉边框
|
||||
};
|
||||
// 将按钮添加到消息气泡中
|
||||
requestMessage.innerHTML = `<b>Function:</b> ${element.name}(${JSON.stringify(element.arguments)})<br>`;
|
||||
requestMessage.appendChild(approveButton);
|
||||
document.getElementById('chatbox').appendChild(requestMessage);
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
});
|
||||
socket.on('next_chapter', function(data){
|
||||
next_chapter(data);
|
||||
});
|
||||
socket.on('chapter_score',function(data){
|
||||
if (typeof data === 'string'){
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
console.log(data)
|
||||
const scoreMessage = document.createElement('div');
|
||||
scoreMessage.className = 'chat-message server-message';
|
||||
scoreMessage.innerHTML = `<b>Chapter ${data.chapter_id} Score:</b> ${JSON.stringify(data.data.content.speak)}`;
|
||||
document.getElementById('chatbox').appendChild(scoreMessage);
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
})
|
||||
socket.on('system_message',function(data){
|
||||
console.log(data)
|
||||
let systemMessage = document.createElement('div');
|
||||
systemMessage.className = 'chat-message server-message';
|
||||
let closeButton = document.createElement('button');
|
||||
closeButton.className = 'close-button'; // 可以根据需要添加样式类
|
||||
closeButton.innerHTML = '×'; // 关闭按钮的文本内容
|
||||
closeButton.style.marginRight = '10px'; // 可选:设置关闭按钮的右边距
|
||||
|
||||
systemMessage.appendChild(closeButton);
|
||||
systemMessage.innerHTML += `<b>系统提示: </b> ${data}`;
|
||||
systemMessage.id = 'system_message_'+system_message_idx;
|
||||
system_message_idx+=1
|
||||
document.getElementById('chatbox').appendChild(systemMessage);
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
$('.close-button').on('click', function(event) {
|
||||
$(this).css({
|
||||
'background-color': 'gray',
|
||||
'color': 'white', // 可选:设置文字颜色为白色,以便更清晰地显示
|
||||
'border': 'none' // 可选:去掉边框
|
||||
});
|
||||
$(this).parent().remove();
|
||||
});
|
||||
setTimeout(function() {
|
||||
$(systemMessage.id).remove();
|
||||
}, 5000); // 5000 毫秒 = 5 秒
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
// 控制文字大小的滑块
|
||||
const fontSizeSlider = document.getElementById('fontSizeSlider');
|
||||
const chatbox = document.getElementById('chatbox');
|
||||
|
||||
fontSizeSlider.addEventListener('input', function() {
|
||||
console.log(fontSizeSlider.value);
|
||||
chatbox.style.fontSize = fontSizeSlider.value + 'px';
|
||||
});
|
||||
|
||||
// 语言切换按钮
|
||||
const englishRadio = document.getElementById('english');
|
||||
const chineseRadio = document.getElementById('chinese');
|
||||
|
||||
const englishLabel = document.getElementById('label_for_en');
|
||||
const chineseLabel = document.getElementById('label_for_zh');
|
||||
let language = 'zh'; // 默认语言
|
||||
|
||||
// 切换语言事件
|
||||
englishRadio.addEventListener('change', function() {
|
||||
if (englishRadio.checked) {
|
||||
language = 'en';
|
||||
englishLabel.classList.add('active');
|
||||
chineseLabel.classList.remove('active');
|
||||
}
|
||||
socket.emit('language',language);
|
||||
});
|
||||
|
||||
chineseRadio.addEventListener('change', function() {
|
||||
if (chineseRadio.checked) {
|
||||
language = 'zh';
|
||||
chineseLabel.classList.add('active');
|
||||
englishLabel.classList.remove('active');
|
||||
}
|
||||
socket.emit('language',language);
|
||||
});
|
||||
|
||||
|
||||
function sendInitiativeFunctionCall(function_name){
|
||||
socket.emit('initiative', {'name': function_name});
|
||||
}
|
||||
// 发送消息
|
||||
function sendMessage() {
|
||||
const input = document.getElementById('messageInput').value;
|
||||
if (input.trim() === '') return;
|
||||
|
||||
// 使用 marked.js 解析 markdown
|
||||
const markdownHtml = marked.parse(input);
|
||||
|
||||
// 显示用户发送的消息
|
||||
const userMessage = document.createElement('div');
|
||||
userMessage.innerHTML = `<div class="chat-message user-message"><b>你:</b> ${markdownHtml}</div>`;
|
||||
document.getElementById('chatbox').appendChild(userMessage);
|
||||
|
||||
// 发送消息到服务器
|
||||
socket.emit('message', JSON.stringify({data: input, type:'text'}));
|
||||
|
||||
// 清空输入框
|
||||
document.getElementById('messageInput').value = '';
|
||||
|
||||
// 滚动到最新消息
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
}
|
||||
|
||||
let currentChapterIndex = -1;
|
||||
function next_chapter(data) {
|
||||
// 获取所有的 h3 元素
|
||||
var iframe = document.getElementById('markdown-content-iframe');
|
||||
|
||||
// 访问 iframe 的文档对象
|
||||
var iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
|
||||
var titles = []
|
||||
var h3Elements = iframeDocument.querySelectorAll('h3');
|
||||
console.log("h3Elements next")
|
||||
if (currentChapterIndex >= h3Elements.length) {
|
||||
return; // 如果已经到了最后一个章节,则不进行任何操作
|
||||
}
|
||||
|
||||
// 隐藏当前章节及其后的内容
|
||||
for (let i = 0; i < h3Elements.length; i++) {
|
||||
h3Elements[i].style.display = 'none';
|
||||
titles.push(h3Elements[i].innerText)
|
||||
let nextSibling = h3Elements[i].nextElementSibling;
|
||||
while (nextSibling && nextSibling.tagName !== 'H3') {
|
||||
nextSibling.style.display = 'none';
|
||||
nextSibling = nextSibling.nextElementSibling;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示下一个章节及其后的内容,直到再下一个h3元素或结束
|
||||
if (currentChapterIndex + 1 < h3Elements.length) {
|
||||
h3Elements[currentChapterIndex + 1].style.display = 'block';
|
||||
let nextSibling = h3Elements[currentChapterIndex + 1].nextElementSibling;
|
||||
while (nextSibling && nextSibling.tagName !== 'H3') {
|
||||
nextSibling.style.display = 'block';
|
||||
nextSibling = nextSibling.nextElementSibling;
|
||||
}
|
||||
}
|
||||
currentChapterIndex++;
|
||||
|
||||
generateProgressBar(h3Elements.length, currentChapterIndex, titles);
|
||||
}
|
||||
|
||||
function generateProgressBar(N, idx, titles) {
|
||||
const container = document.getElementById('markdown-content-process');
|
||||
container.innerHTML = ''; // 清空内容
|
||||
|
||||
// 创建进度条的外框
|
||||
const progressBox = document.createElement('div');
|
||||
progressBox.className = 'progress-box';
|
||||
|
||||
// 获取详细信息容器
|
||||
const detailBox = document.getElementById('progress-detail');
|
||||
|
||||
// 根据N值动态调整每个进度节点的宽度
|
||||
const nodeWidth = 100 / N; // 每个节点的宽度为 100% / N
|
||||
|
||||
// 根据N值生成子框并设置颜色
|
||||
for (let i = 0; i < N; i++) {
|
||||
const titleBox = document.createElement('div');
|
||||
titleBox.className = 'progress-title';
|
||||
|
||||
// 设置进度条的颜色
|
||||
if (i < idx) {
|
||||
titleBox.classList.add('green'); // 已完成部分
|
||||
} else {
|
||||
titleBox.classList.add('white'); // 未完成部分
|
||||
}
|
||||
|
||||
// 设置每个子框的标题
|
||||
titleBox.innerHTML = titles[i] || `Step ${i + 1}`;
|
||||
|
||||
// 设置每个进度节点的宽度
|
||||
titleBox.style.width = `${nodeWidth}%`;
|
||||
|
||||
// 监听鼠标移入事件来显示详情
|
||||
titleBox.addEventListener('mouseenter', function() {
|
||||
detailBox.innerHTML = `Step ${i + 1}: ${titles[i] || "No title"}`;// - Additional details here...`;
|
||||
detailBox.style.display = 'block';
|
||||
// 根据进度条位置显示详情
|
||||
const rect = titleBox.getBoundingClientRect();
|
||||
detailBox.style.top = `${rect.top + window.scrollY - 100}px`; // 微调位置
|
||||
detailBox.style.left = `${rect.left + window.scrollX + rect.width / 2 - detailBox.offsetWidth / 2}px`;
|
||||
});
|
||||
|
||||
// 监听鼠标移出事件来隐藏详情
|
||||
titleBox.addEventListener('mouseleave', function() {
|
||||
detailBox.style.display = 'none';
|
||||
});
|
||||
|
||||
progressBox.appendChild(titleBox);
|
||||
}
|
||||
|
||||
// 将生成的进度条添加到容器中
|
||||
container.appendChild(progressBox);
|
||||
}
|
||||
125
Html/apps/static/js/dashboard.js
Normal file
125
Html/apps/static/js/dashboard.js
Normal file
@@ -0,0 +1,125 @@
|
||||
// dashboard.js
|
||||
let courseData;
|
||||
function show_user_data(user_data, course_brief_data_list){
|
||||
courseData = user_data.course_process_dict
|
||||
for (let i=0;i<course_brief_data_list.length;i++){
|
||||
course_brief_data = course_brief_data_list[i]
|
||||
console.log(course_brief_data)
|
||||
courseData[course_brief_data.course_id].course_id = course_brief_data.course_id;
|
||||
courseData[course_brief_data.course_id].title = course_brief_data.course_name;
|
||||
courseData[course_brief_data.course_id].lessons = course_brief_data.lessons;
|
||||
courseData[course_brief_data.course_id].course_img_path = course_brief_data.course_img_path;
|
||||
courseData[course_brief_data.course_id].course_description = course_brief_data.course_description;
|
||||
courseData[course_brief_data.course_id].course_create_date = course_brief_data.course_create_date;
|
||||
courseData[course_brief_data.course_id].course_update_data = course_brief_data.course_update_data;
|
||||
}
|
||||
for (var course in courseData){
|
||||
courseData[course].chapters = courseData[course].lessons
|
||||
console.log(courseData[course])
|
||||
}
|
||||
|
||||
let courseCardsContainer = document.getElementById('course-cards');
|
||||
courseCardsContainer.innerHTML = "";
|
||||
for (var course in courseData){
|
||||
course_id = course;
|
||||
course = courseData[course];
|
||||
let courseCard = document.createElement('div');
|
||||
|
||||
courseCard.className = 'course-card';
|
||||
courseCard.innerHTML = `
|
||||
<img src="${course.course_img_path}" alt="课程封面", class="course-image">
|
||||
<div class="course-info">
|
||||
<h3>${course.title}</h3>
|
||||
<p>${course.course_description}</p>
|
||||
</div>
|
||||
<button class="select-button" >查看目录</button>
|
||||
`;
|
||||
courseCardsContainer.appendChild(courseCard);
|
||||
courseCard.data = course_id
|
||||
courseCard.addEventListener('click',() => {
|
||||
openCourseProgress(courseCard.data);
|
||||
});
|
||||
}
|
||||
}
|
||||
function openCourseProgress(courseId) {
|
||||
console.log(courseId)
|
||||
const course = courseData[courseId];
|
||||
if (!course) return;
|
||||
|
||||
// 更新右侧滑出卡片的内容
|
||||
document.getElementById('course-title').textContent = course.title;
|
||||
///////////////////////// course.progress;还没有计算
|
||||
document.getElementById('progress').textContent = course.progress;
|
||||
|
||||
// 更新章节列表
|
||||
const chapterList = document.getElementById('chapter-list');
|
||||
chapterList.innerHTML = ''; // 清空之前的章节
|
||||
|
||||
course.chapters.forEach(chapter => {
|
||||
const chapterItem = document.createElement('li');
|
||||
chapterItem.classList.add('chapter-item');
|
||||
|
||||
const chapterTitle = document.createElement('div');
|
||||
chapterTitle.classList.add('chapter-title');
|
||||
chapterTitle.textContent = chapter.lesson_id;
|
||||
chapterItem.appendChild(chapterTitle);
|
||||
|
||||
// 创建子章节
|
||||
if (chapter.subChapters && chapter.subChapters.length > 0) {
|
||||
const subChapterList = document.createElement('ul');
|
||||
subChapterList.classList.add('sub-chapter-list');
|
||||
|
||||
chapter.subChapters.forEach(subChapter => {
|
||||
const subChapterItem = document.createElement('li');
|
||||
subChapterItem.classList.add('sub-chapter-item');
|
||||
subChapterItem.textContent = subChapter;
|
||||
console.log('-------------------')
|
||||
console.log(course)
|
||||
// 查找学生进度
|
||||
for(let i = 0; i < course.lesson_processs.length; i++) {
|
||||
if (course.lesson_processs[i][1] == chapter.lesson_id) {//确定lesson_id对应正确
|
||||
console.log('-------------------')
|
||||
console.log(course.lesson_processs[i][0])
|
||||
lesson_chapters_progress = course.lesson_processs[i][0];//寻找对应的每一个章节的进度
|
||||
for (let j=0; j<lesson_chapters_progress.length;j++){
|
||||
chapter_progress = lesson_chapters_progress[j]
|
||||
if(chapter_progress.title == subChapter){
|
||||
subChapterItem.textContent += ("(已"+(chapter_progress.is_rebuttal?"申辩" : "完成")+" 得分"+ (chapter_progress.is_rebuttal?chapter_progress.rebuttal_score : chapter_progress.score) +")");
|
||||
}
|
||||
}
|
||||
if (course.lessons[i].progress === 1) {
|
||||
subChapterItem.classList.add('completed');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加点击事件跳转到学习页面
|
||||
subChapterItem.addEventListener('click', () => {
|
||||
const courseId = encodeURIComponent(course.course_id); // 对课程名称进行编码
|
||||
const url = `/desktop_nouser/${courseId}/${chapter.lesson_id}`;
|
||||
window.location.href = url; // 跳转到该学习页面
|
||||
});
|
||||
|
||||
subChapterList.appendChild(subChapterItem);
|
||||
});
|
||||
|
||||
chapterItem.appendChild(subChapterList);
|
||||
|
||||
// 添加点击事件切换子章节显示
|
||||
chapterItem.addEventListener('click', () => {
|
||||
chapterItem.classList.toggle('open');
|
||||
});
|
||||
}
|
||||
|
||||
chapterList.appendChild(chapterItem);
|
||||
});
|
||||
|
||||
// 打开滑出卡片
|
||||
document.getElementById('courseProgress').classList.add('open');
|
||||
}
|
||||
|
||||
function closeCourseProgress() {
|
||||
// 关闭滑出卡片
|
||||
document.getElementById('courseProgress').classList.remove('open');
|
||||
}
|
||||
42
Html/apps/static/js/index.js
Normal file
42
Html/apps/static/js/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
function show_books(courses_data, user_selected_courses){
|
||||
console.log(courses_data)
|
||||
console.log(user_selected_courses)
|
||||
}
|
||||
function show_course_details(course_id){
|
||||
window.location.href = '/course/' + course_id
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const selectButtons = document.querySelectorAll('.select-button');
|
||||
|
||||
selectButtons.forEach(button => {
|
||||
button.addEventListener('click', function(event) {
|
||||
event.stopPropagation(); // 阻止事件冒泡
|
||||
const courseId = this.getAttribute('data-course-id');
|
||||
fetch(`/select_course`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}' // 如果使用 CSRF 保护
|
||||
},
|
||||
body: JSON.stringify({ course_id: courseId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// 更新按钮样式或显示消息
|
||||
this.classList.remove('select-button');
|
||||
this.classList.add('selected-button');
|
||||
this.textContent = '已选择';
|
||||
alert('选择课程成功');
|
||||
} else {
|
||||
alert('选择课程失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('选择课程失败');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
87
Html/apps/static/js/login.js
Normal file
87
Html/apps/static/js/login.js
Normal file
@@ -0,0 +1,87 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const switchRoleButton = document.getElementById('switch-role');
|
||||
const loginForm = document.getElementById('login-form');
|
||||
const usernameLabel = document.getElementById('username-label');
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.getElementById('password');
|
||||
const registerLinkHref = document.getElementById('register-link-href');
|
||||
|
||||
let isTeacherLogin = false;
|
||||
|
||||
// 切换角色按钮点击事件
|
||||
switchRoleButton.addEventListener('click', function() {
|
||||
isTeacherLogin = !isTeacherLogin; // 切换状态
|
||||
toggleButtonColor(isTeacherLogin);
|
||||
toggleLoginFields(isTeacherLogin);
|
||||
});
|
||||
// 根据角色切换按钮背景色
|
||||
function toggleButtonColor(isTeacher) {
|
||||
if (isTeacher) {
|
||||
loginForm.classList.add('teacher-login');
|
||||
} else {
|
||||
loginForm.classList.remove('teacher-login');
|
||||
}
|
||||
}
|
||||
// 切换显示不同的表单字段
|
||||
function toggleLoginFields(isTeacher) {
|
||||
if (isTeacher) {
|
||||
// 显示教师登录字段
|
||||
usernameLabel.textContent = '教师用户名';
|
||||
switchRoleButton.textContent = '切换到学生登录'; // 更新按钮文本
|
||||
registerLinkHref.href = '/register_teacher';
|
||||
registerLinkHref.textContent = '注册新教师账号';
|
||||
} else {
|
||||
// 显示学生登录字段
|
||||
usernameLabel.textContent = '学生用户名';
|
||||
switchRoleButton.textContent = '切换到教师登录'; // 更新按钮文本
|
||||
registerLinkHref.href = '/register';
|
||||
registerLinkHref.textContent = '注册新学生账号';
|
||||
}
|
||||
}
|
||||
|
||||
// 处理登录提交
|
||||
loginForm.addEventListener('submit', function(event) {
|
||||
event.preventDefault(); // 防止默认提交
|
||||
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value.trim();
|
||||
|
||||
if (!username || !password) {
|
||||
alert('请填写所有必需的字段');
|
||||
return;
|
||||
}
|
||||
|
||||
// 登录请求的 URL 依据角色不同而不同
|
||||
const loginUrl = isTeacherLogin ? '/login_teacher_post' : '/login_post';
|
||||
|
||||
const data = isTeacherLogin ? { username, password } : { username, password };
|
||||
|
||||
// 发送登录请求
|
||||
fetch(loginUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data) // 将数据发送到后端
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// 登录成功,跳转到主页
|
||||
if (isTeacherLogin) {
|
||||
window.location.href = '/teacherboard';
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
} else {
|
||||
// 登录失败,提示错误信息
|
||||
alert('登录失败: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('登录请求失败,请稍后重试');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
39
Html/apps/static/js/register.js
Normal file
39
Html/apps/static/js/register.js
Normal file
@@ -0,0 +1,39 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const registerForm = document.getElementById('register-form');
|
||||
|
||||
registerForm.addEventListener('submit', function(event) {
|
||||
event.preventDefault(); // 阻止表单默认提交行为
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const email = document.getElementById('email').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const confirmPassword = document.getElementById('confirm-password').value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
alert('密码和确认密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/register_post', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}' // 如果使用 CSRF 保护
|
||||
},
|
||||
body: JSON.stringify({ username, email, password })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('注册成功');
|
||||
window.location.href = '/login'; // 注册成功后跳转到登录页面
|
||||
} else {
|
||||
alert('注册失败: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('注册失败');
|
||||
});
|
||||
});
|
||||
});
|
||||
41
Html/apps/static/js/register_teacher.js
Normal file
41
Html/apps/static/js/register_teacher.js
Normal file
@@ -0,0 +1,41 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const registerTeacherForm = document.getElementById('register-teacher-form');
|
||||
|
||||
registerTeacherForm.addEventListener('submit', function(event) {
|
||||
event.preventDefault(); // 阻止表单默认提交行为
|
||||
|
||||
const username = document.getElementById('teacher-username').value;
|
||||
const email = document.getElementById('teacher-email').value;
|
||||
const password = document.getElementById('teacher-password').value;
|
||||
const confirmPassword = document.getElementById('teacher-confirm-password').value;
|
||||
|
||||
// 检查密码和确认密码是否一致
|
||||
if (password !== confirmPassword) {
|
||||
alert('密码和确认密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 Fetch API 发送注册请求
|
||||
fetch('/register_teacher_post', { // 教师注册路由
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}' // 如果使用 CSRF 保护
|
||||
},
|
||||
body: JSON.stringify({ username, email, password }) // 发送数据
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('教师注册成功');
|
||||
window.location.href = '/login'; // 注册成功后跳转到登录页面
|
||||
} else {
|
||||
alert('注册失败: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('注册失败');
|
||||
});
|
||||
});
|
||||
});
|
||||
189
Html/apps/static/js/teacherboard.js
Normal file
189
Html/apps/static/js/teacherboard.js
Normal file
@@ -0,0 +1,189 @@
|
||||
function lessonTemplate(lesson) {
|
||||
return `
|
||||
<li class="lesson-item">
|
||||
<span class="lesson-text" onclick="editLesson('${lesson}')">${lesson}
|
||||
<button class="icon-btn edit-btn" onclick="editLesson('${lesson}')">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
</span>
|
||||
<button class="icon-btn delete-btn" onclick="deleteLesson('${lesson}')">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
function show_teacher_data(user_data, user_course_data) {
|
||||
// 这里可以根据user_course_data生成课程卡片
|
||||
const courseCardsContainer = document.getElementById('course-cards');
|
||||
user_course_data.forEach(course => {
|
||||
const courseCard = document.createElement('div');
|
||||
courseCard.classList.add('course-card');
|
||||
courseCard.onclick = function() { openCourseDetails(course.id); };
|
||||
courseCard.innerHTML = `
|
||||
<img src="${course.cover_image}" alt="课程封面" class="course-image">
|
||||
<div class="course-info">
|
||||
<h3 class="course-name">${course.name}</h3>
|
||||
<p class="course-description">${course.description}</p>
|
||||
</div>
|
||||
`;
|
||||
courseCardsContainer.appendChild(courseCard);
|
||||
});
|
||||
}
|
||||
let isAddingChapter = false;
|
||||
|
||||
function showInputForNewChapter() {
|
||||
if (isAddingChapter) return; // 防止重复点击
|
||||
|
||||
isAddingChapter = true; // 标记正在添加章节
|
||||
|
||||
// 找到新增章节按钮并隐藏
|
||||
const addButton = document.querySelector('.add-chapter-btn');
|
||||
addButton.style.display = 'none';
|
||||
|
||||
// 创建一个输入框
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.placeholder = '请输入章节名称...';
|
||||
input.className = 'new-chapter-input';
|
||||
input.onblur = () => checkAndAddChapter(input);
|
||||
|
||||
// 将输入框插入到页面
|
||||
const chapterList = document.getElementById('chapter-list');
|
||||
chapterList.appendChild(input);
|
||||
input.focus(); // 聚焦到输入框
|
||||
}
|
||||
|
||||
function checkAndAddChapter(input) {
|
||||
const chapterName = input.value.trim();
|
||||
|
||||
// 如果章节名称为空,显示提示并不添加
|
||||
if (!chapterName) {
|
||||
input.remove(); // 删除输入框
|
||||
document.querySelector('.add-chapter-btn').style.display = 'block'; // 重新显示新增章节按钮
|
||||
isAddingChapter = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果章节名称合法,添加到章节列表
|
||||
const chapterList = document.getElementById('chapter-list');
|
||||
const chapterItem = document.createElement('li');
|
||||
chapterItem.innerHTML = `
|
||||
<li class="lesson-item">
|
||||
<strong>${chapterName}</strong> <button class="icon-btn delete-btn" onclick="deleteChapter(this)">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</li>
|
||||
<ul>
|
||||
</ul>
|
||||
<button class="add-lesson-btn" onclick="showInputForNewLesson('${chapterName}', this)">新增课时</button>
|
||||
|
||||
`;
|
||||
chapterList.appendChild(chapterItem);
|
||||
// 恢复新增按钮的显示
|
||||
document.querySelector('.add-chapter-btn').style.display = 'block';
|
||||
input.remove();
|
||||
isAddingChapter = false;
|
||||
}
|
||||
function openCourseDetails(courseId) {
|
||||
// 打开课程目录
|
||||
const courseDetails = document.getElementById('courseDetails');
|
||||
const courseTitle = document.getElementById('course-title');
|
||||
courseTitle.textContent = "课程名称: " + courseId; // 假设这里显示课程ID,实际情况应该是课程名称
|
||||
|
||||
courseDetails.classList.add('open');
|
||||
|
||||
// 获取章节数据并渲染
|
||||
renderChapters(courseId);
|
||||
}
|
||||
function deleteChapter(deleteButton) {
|
||||
// 删除章节
|
||||
const chapterItem = deleteButton.closest('li');
|
||||
chapterItem.remove();
|
||||
}
|
||||
function renderChapters(courseId) {
|
||||
const chapterList = document.getElementById('chapter-list');
|
||||
chapterList.innerHTML = ""; // 清空章节列表
|
||||
|
||||
// 假设从服务器获取课程章节数据
|
||||
const chapters = [
|
||||
{ title: "第一章:算法基础", lessons: ["算法简介", "算法设计方法"] },
|
||||
{ title: "第二章:数据结构", lessons: ["数组", "链表"] }
|
||||
];
|
||||
|
||||
chapters.forEach(chapter => {
|
||||
const chapterItem = document.createElement('li');
|
||||
|
||||
// 渲染章节标题
|
||||
let lessonsHtml = chapter.lessons.map(lesson => lessonTemplate(lesson)).join('');
|
||||
|
||||
chapterItem.innerHTML = `
|
||||
<strong>${chapter.title}</strong>
|
||||
<ul>
|
||||
${lessonsHtml}
|
||||
</ul>
|
||||
<button class="add-lesson-btn" onclick="showInputForNewLesson('${chapter.title}', this)">新增课时</button>
|
||||
`;
|
||||
|
||||
|
||||
chapterList.appendChild(chapterItem);
|
||||
});
|
||||
}
|
||||
|
||||
function showInputForNewLesson(chapterTitle, button) {
|
||||
// 防止重复点击
|
||||
const addButton = button;
|
||||
addButton.style.display = 'none'; // 隐藏按钮
|
||||
|
||||
// 创建一个输入框
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.placeholder = '请输入课时名称...';
|
||||
input.className = 'new-lesson-input';
|
||||
input.onblur = () => checkAndAddLesson(input, chapterTitle, addButton); // 失去焦点时检查输入
|
||||
|
||||
// 将输入框插入到章节列表中
|
||||
const chapterItem = addButton.closest('li'); // 获取到点击按钮的父元素
|
||||
chapterItem.appendChild(input);
|
||||
input.focus(); // 聚焦到输入框
|
||||
}
|
||||
function checkAndAddLesson(input, chapterTitle, addButton) {
|
||||
const lessonName = input.value.trim();
|
||||
|
||||
// 如果课时名称为空,显示提示并不添加
|
||||
if (!lessonName) {
|
||||
input.remove(); // 删除输入框
|
||||
addButton.style.display = 'block'; // 重新显示新增课时按钮
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果课时名称合法,添加到课时列表
|
||||
const chapterList = document.getElementById('chapter-list');
|
||||
const chapterItem = Array.from(chapterList.children).find(item => item.querySelector('strong').textContent === chapterTitle);
|
||||
const lessonsList = chapterItem.querySelector('ul');
|
||||
|
||||
const lessonItem = document.createElement('span');
|
||||
lessonItem.innerHTML = lessonTemplate(lessonName);
|
||||
|
||||
lessonsList.appendChild(lessonItem); // 将新课时添加到章节下
|
||||
input.remove(); // 删除输入框
|
||||
addButton.style.display = 'block'; // 重新显示新增课时按钮
|
||||
}
|
||||
function deleteLesson(lesson) {
|
||||
const lessonItems = document.querySelectorAll('.lesson-item');
|
||||
lessonItems.forEach(item => {
|
||||
if (item.querySelector('.lesson-text').textContent === lesson) {
|
||||
item.remove(); // 删除对应的课时项
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function editLesson(lesson) {
|
||||
// 编辑课时的逻辑
|
||||
alert("编辑课时: " + lesson);
|
||||
}
|
||||
|
||||
function closeCourseDetails() {
|
||||
document.getElementById('courseDetails').classList.remove('open');
|
||||
}
|
||||
52
Html/apps/static/twice_split.html
Normal file
52
Html/apps/static/twice_split.html
Normal file
@@ -0,0 +1,52 @@
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>二分查找与二分答案</h1>
|
||||
<h2>二分查找</h2>
|
||||
<h3>引入</h3>
|
||||
<p>二分是一个很简单基础,但很重要的知识点,为以后许多高级的数据结构与算法铺垫。</p>
|
||||
<p>下面是一个用二分的简单场景:</p>
|
||||
<p>假设小明从0到1000之间选择了一个数字但不告诉你,你可以不断猜测这个数,每次猜测小明会告知你的猜测得过大还是过小,问最多几次就一定能猜中?</p>
|
||||
<p>答案是利用二分查找的原理,猜测11次即可。</p>
|
||||
<ol>
|
||||
<li>对于0到1000的答案备选区,猜测中位数500,假设过小,</li>
|
||||
<li>则对于501到1000的答案备选区,猜测750,假设过大</li>
|
||||
<li>则对于501到749的答案备选区,猜测625,假设过小,</li>
|
||||
<li>则对于626到749区间......</li>
|
||||
<li>(688-749)</li>
|
||||
<li>(718-749)</li>
|
||||
<li>(734-749)</li>
|
||||
<li>(742-749)</li>
|
||||
<li>(746-749)</li>
|
||||
<li>(748-749)</li>
|
||||
<li>(749-749)</li>
|
||||
</ol>
|
||||
<p>在最差的情况下,第11次的答案备选区就一定长度为1了,也就是必然是答案。</p>
|
||||
<p>因此如果序列是有序的,就可以通过二分查找快速定位所需要的数据。</p>
|
||||
<h4>思考题(询问Agent以学习计算方法,或验证你的答案)</h4>
|
||||
<p>对于上面那个题目,如果问题区间是1到4000,最差情况下需要猜测几次?</p>
|
||||
<h3>练习:二分查找</h3>
|
||||
<p>试试对于下面的题目,用代码实现一下二分查找。</p>
|
||||
<h4>题目:有序数组寻址</h4>
|
||||
<p>给出一个长度为n的有序数组(从小到大),有q次询问,对于每次询问,输出指定数在数组中的下标。如果不存在则输出-1。</p>
|
||||
<h5>输入</h5>
|
||||
<p>第一行一个整数n。(1<=n<=10^5)</p>
|
||||
<p>第二行n个用空格分开的整数ai。(0<=ai<=10^8)</p>
|
||||
<p>第三行一个整数q,表示询问的次数。(1<=q<=10^4)</p>
|
||||
<p>后q行,每行一个整数b,表示询问的数。(0<=b<=10^8)</p>
|
||||
<h5>输出</h5>
|
||||
<p>q行,每行一个整数,对应每次询问的返回结果。</p>
|
||||
<h5>提示:</h5>
|
||||
<p>完成代码后,通知Agent进行评测。</p>
|
||||
<p>如果你还不完全会这个算法,询问Agent获取提示并进行学习。</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
54
Html/apps/templates/course.html
Normal file
54
Html/apps/templates/course.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>课程详情</title>
|
||||
<link rel="stylesheet" href="/static/css/course.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% include 'navbar.html' %} <!-- 引入navbar.html -->
|
||||
|
||||
<!-- 课程详情主体部分 -->
|
||||
<div class="course-details">
|
||||
<!-- 上半部分:课程封面和详情 -->
|
||||
<div class="course-overview">
|
||||
<div class="course-cover">
|
||||
<img src="{{course_data.course_img_path}}" alt="课程封面" class="cover-image">
|
||||
</div>
|
||||
<div class="course-info">
|
||||
<h2 class="course-title">{{course_data.course_name}}</h2>
|
||||
<p class="course-author">作者:{{course_data.course_auther}}</p>
|
||||
<p class="course-description">
|
||||
{{course_data.description}}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<br/>
|
||||
<!-- 下半部分:课程教案列表 -->
|
||||
<div class="lesson-plans">
|
||||
<!-- <h3>课程教案</h3> -->
|
||||
<div class="lesson-cards">
|
||||
<!-- 每个教案的卡片 -->
|
||||
{% for lesson in course_data.lessons %}
|
||||
<div class="lesson-card">
|
||||
<img src="{{lesson.lesson_img_path}}" alt="教案封面" class="lesson-image">
|
||||
<div class="lesson-info">
|
||||
<h4 class="lesson-title">{{lesson.lesson_name}}</h4>
|
||||
<p class="lesson-description">{{lesson.markdown}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
var course_id = "{{course_id}}";
|
||||
|
||||
</script>
|
||||
</html>
|
||||
64
Html/apps/templates/dashboard.html
Normal file
64
Html/apps/templates/dashboard.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>学生个人主页</title>
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% include 'navbar.html' %} <!-- 引入navbar.html -->
|
||||
|
||||
<!-- 主页主体部分 -->
|
||||
<div class="dashboard">
|
||||
<h2 class="dashboard-title">—— 我的选课 ——</h2>
|
||||
<div class="course-cards" id="course-cards">
|
||||
<!-- 每个课程的卡片 -->
|
||||
<div class="course-card"onclick="openCourseProgress('algorithm')">
|
||||
<img src="/static/image/algorithm/book_cover.png" alt="课程封面" class="course-image">
|
||||
<div class="course-info">
|
||||
<h3 class="course-name">算法分析与设计</h3>
|
||||
<p class="course-description">学习基本的算法知识概念,与算法实现。</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 继续添加其他课程卡片 -->
|
||||
<div class="course-card"onclick="openCourseProgress('data-structures')">
|
||||
<img src="/static/image/algorithm/book_cover.png" alt="课程封面" class="course-image">
|
||||
<div class="course-info">
|
||||
<h3 class="course-name">数据结构与算法</h3>
|
||||
<p class="course-description">深入学习数据结构和算法设计。</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 可以继续添加课程卡片 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 右侧滑出的学习进度卡片 -->
|
||||
<div id="courseProgress" class="course-progress">
|
||||
<div class="progress-header">
|
||||
<span id="course-title"></span>
|
||||
<button class="close-btn" onclick="closeCourseProgress()">×</button>
|
||||
</div>
|
||||
<div class="progress-content">
|
||||
<p>当前学习进度: <span id="progress"></span></p>
|
||||
<hr/>
|
||||
<h3>章节列表:</h3>
|
||||
<ul id="chapter-list">
|
||||
<!-- 章节列表会在点击课程时动态生成 -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/dashboard.js"></script>
|
||||
<script>
|
||||
window.appData = {
|
||||
user_data: JSON.parse(`{{user_data}}`.replace(/"/g, "\"")),
|
||||
user_course_data: JSON.parse(`{{user_course_data}}`.replace(/"/g, "\""))
|
||||
}
|
||||
console.log(window.appData.user_data)
|
||||
console.log(window.appData.user_course_data)
|
||||
show_user_data(window.appData.user_data, window.appData.user_course_data)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
217
Html/apps/templates/desktop.html
Normal file
217
Html/apps/templates/desktop.html
Normal file
@@ -0,0 +1,217 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Code Development Platform</title>
|
||||
<style>
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/css/desktop.css">
|
||||
<script>
|
||||
document.cookie = "vscode-tkn=44edc269-6f88-46ab-9790-dc253f66ac36; path=/; SameSite=None; Secure";
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/1.1.1/marked.min.js"></script>
|
||||
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://unpkg.com/split.js/dist/split.min.js"></script>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="maxcontainer" id="maxcontainer">
|
||||
<!-- 左侧内容 -->
|
||||
<div class="sidebar" id="leftSidebar" style="width: 100%;height: 100%;">
|
||||
<div id="markdown-content"style="width: 100%;height: 95%;">
|
||||
<!-- 动态加载的 HTML 会显示在这里 -->
|
||||
</div>
|
||||
<div id="markdown-content-process"style="width: 100%;height: 5%;">
|
||||
|
||||
</div>
|
||||
<div id="progress-detail" style="position: absolute; top: 0; left: 0; display: none; padding: 10px; background-color: rgba(0, 0, 0, 0.7); color: white; border-radius: 5px;">
|
||||
<!-- 这里将显示鼠标悬停的详情 -->
|
||||
</div>
|
||||
</div>
|
||||
<div id="dragbar"></div>
|
||||
|
||||
<!-- 中间嵌入的 Vscode-web 窗口,带有 tkn 参数 -->
|
||||
<div class="vscode-web"id="vscodeWeb">
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dragbar2"></div>
|
||||
<!-- 右侧内容 -->
|
||||
<div class="sidebar" id="rightSidebar"style="width: 100%;height: 100%;">
|
||||
<div class="chatbox-header">
|
||||
<div class="slider-container">
|
||||
<label for="fontSizeSlider">Aa</label>
|
||||
<input type="range" id="fontSizeSlider" min="12" max="24" value="16" />
|
||||
</div>
|
||||
<div class="language-toggle btn-group btn-group-toggle" data-toggle="buttons">
|
||||
<label class="btn btn-outline-secondary " id="label_for_en">
|
||||
<input type="radio" name="language" id="english" autocomplete="off" > En
|
||||
</label>
|
||||
<label class="btn btn-outline-secondary active" id="label_for_zh">
|
||||
<input type="radio" name="language" id="chinese" autocomplete="off" checked> Zn
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chatbox" id="chatbox" style="width: 100%;height: 80%;">
|
||||
<!-- 消息会在这里显示 -->
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<textarea id="messageInput" rows="2" class="form-control" placeholder="输入你的消息 (支持 Markdown 语法)"></textarea>
|
||||
<button class="btn btn-primary" onclick="sendMessage()">发送</button>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.appData = {
|
||||
username:"{{user_id}}",
|
||||
course_id:"{{course_id}}",
|
||||
chapter_id:"{{chapter_id}}",
|
||||
folder:"{{workspace_path}}"
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 侧边工具栏 -->
|
||||
<div class="sidebar-tools" id="sidebarTools">
|
||||
<div class="tool-button" onclick="sendInitiativeFunctionCall('sample_judge')">
|
||||
<div class="icon">+</div>
|
||||
<div class="description">代码试运行</div>
|
||||
</div>
|
||||
<div class="tool-button" onclick="sendInitiativeFunctionCall('judge')">
|
||||
<div class="icon">√</div>
|
||||
<div class="description">代码提交</div>
|
||||
</div>
|
||||
<!-- <div class="tool-button">
|
||||
<div class="icon">-</div>
|
||||
<div class="description">工具2</div>
|
||||
</div>
|
||||
<div class="tool-button">
|
||||
<div class="icon">?</div>
|
||||
<div class="description">工具3</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 切换按钮 -->
|
||||
<div class="toggle-button" id="toggleButton">
|
||||
<div class="icon" id="toggleButton_icon">❮</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/chatbox.js"></script>
|
||||
<script>
|
||||
// 实现左右边栏可拖动
|
||||
|
||||
const dragbar = document.getElementById('dragbar');
|
||||
const dragbar2 = document.getElementById('dragbar2');
|
||||
const container = document.getElementById('maxcontainer');
|
||||
let leftWidth = 300; // 设置左侧栏的初始宽度
|
||||
let rightWidth = 300; // 设置右侧栏的初始宽度
|
||||
|
||||
let isDraggingLeft = false;
|
||||
let isDraggingRight = false;
|
||||
|
||||
dragbar.addEventListener('mousedown', function() {
|
||||
isDraggingLeft = true;
|
||||
});
|
||||
|
||||
dragbar2.addEventListener('mousedown', function() {
|
||||
isDraggingRight = true;
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', function(e) {
|
||||
if (isDraggingLeft) {
|
||||
leftWidth = e.clientX; // 更新左侧栏宽度
|
||||
container.style.gridTemplateColumns = `${leftWidth}px 5px 1fr 5px ${rightWidth}px`;
|
||||
} else if (isDraggingRight) {
|
||||
rightWidth = window.innerWidth - e.clientX; // 更新右侧栏宽度
|
||||
container.style.gridTemplateColumns = `${leftWidth}px 5px 1fr 5px ${rightWidth}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', function() {
|
||||
isDraggingLeft = false;
|
||||
isDraggingRight = false;
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 使用 fetch 发送请求到 Flask 服务器,获取 session 信息
|
||||
fetch('/get_session', {
|
||||
method: 'GET',
|
||||
credentials: 'include' // 包含 Cookie,确保 Flask 可以识别 session
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) return response.json();
|
||||
throw new Error('Failed to fetch session');
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Session data:', data);
|
||||
|
||||
// 在成功获取 session 后创建并插入 iframe
|
||||
const iframe = document.createElement('iframe');
|
||||
const sessionInfo = data
|
||||
iframe.src = '{{vscode_web_url}}/?workspace={{workspace_path}}&folder={{workspace_path}}';
|
||||
iframe.style.width = '100%';
|
||||
iframe.style.height = '100%';
|
||||
|
||||
// 将 iframe 插入到指定的 div 中
|
||||
document.getElementById('vscodeWeb').appendChild(iframe);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching session:', error);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 动态加载 Markdown 文件
|
||||
function loadMarkdown(course_id, filename) {
|
||||
fetch(`/${course_id}-${filename}-markdown`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.html_url) {
|
||||
document.getElementById('markdown-content').innerHTML = `<iframe id="markdown-content-iframe" src="${data.html_url}"style="width: 100%;height: 100%;"></iframe>`;
|
||||
document.getElementById('markdown-content-iframe').addEventListener('load', function() {
|
||||
// 在这里执行你想要的操作
|
||||
console.log('Iframe has finished loading');
|
||||
next_chapter();
|
||||
});
|
||||
} else {
|
||||
console.error('Markdown file not found');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading markdown:', error);
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadMarkdown('{{course_id}}','{{chapter_id}}');
|
||||
});
|
||||
|
||||
|
||||
// 侧边工具栏切换
|
||||
const sidebarTools = document.getElementById('sidebarTools');
|
||||
const toggleButton = document.getElementById('toggleButton');
|
||||
|
||||
toggleButton.addEventListener('click', function() {
|
||||
sidebarTools.classList.toggle('open');
|
||||
toggleButton.classList.toggle('open');
|
||||
if (document.getElementById('toggleButton_icon').innerText === '❮'){
|
||||
document.getElementById('toggleButton_icon').innerText = '❯';
|
||||
}else{
|
||||
document.getElementById('toggleButton_icon').innerText = '❮';
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
44
Html/apps/templates/index.html
Normal file
44
Html/apps/templates/index.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>课程选择主页</title>
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
</head>
|
||||
<body onload="init_index()" >
|
||||
{% include 'navbar.html' %} <!-- 引入navbar.html -->
|
||||
<main>
|
||||
<div class="course-selection">
|
||||
<h2>课程列表</h2>
|
||||
<div class="course-list">
|
||||
{% for course_id, course in courses_data.items() %}
|
||||
<div class="course-card" onclick="show_course_details('{{ course_id }}')">
|
||||
<img src="{{ course.course_img_path }}" alt="{{ course.course_name }}">
|
||||
<h3>{{ course.course_name }}</h3>
|
||||
<p>{{ course.course_description }}</p>
|
||||
{% if course_id in selected_courses %}
|
||||
<button class="selected-button">已选择</button>
|
||||
{% else %}
|
||||
<button class="select-button" data-course-id="{{course_id}}">选择课程</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>版权所有 © 2024 “华实伴学君”——教学练评一体的虚拟编码助教</p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
<script>
|
||||
window.appData = {
|
||||
user_selected_courses: JSON.parse(`{{selected_courses}}`.replace(/"/g, "\""))
|
||||
}
|
||||
|
||||
</script>
|
||||
<script src="/static/js/index.js"></script>
|
||||
</html>
|
||||
47
Html/apps/templates/login.html
Normal file
47
Html/apps/templates/login.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录</title>
|
||||
<link rel="stylesheet" href="/static/css/login.css">
|
||||
<script src="/static/js/login.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<h2 class="login-title">欢迎回来!</h2>
|
||||
<form class="login-form" id="login-form">
|
||||
|
||||
<!-- 用户名字段 -->
|
||||
<div class="input-group">
|
||||
<label for="username" id="username-label">用户名</label>
|
||||
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 密码字段 -->
|
||||
<div class="input-group">
|
||||
<label for="password" id="password-label">密码</label>
|
||||
<input type="password" id="password" name="password" placeholder="请输入密码" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-btn">登录</button>
|
||||
|
||||
<!-- 切换角色按钮 -->
|
||||
<div class="role-switch">
|
||||
<button type="button" id="switch-role" class="role-btn">切换到教师登录</button>
|
||||
</div>
|
||||
<div class="register-link">
|
||||
<a href="/register" id="register-link-href">注册新账号</a>
|
||||
</div>
|
||||
<div class="forgot-password">
|
||||
<a href="#">忘记密码?</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
99
Html/apps/templates/navbar.html
Normal file
99
Html/apps/templates/navbar.html
Normal file
@@ -0,0 +1,99 @@
|
||||
<!-- 页眉导航栏 -->
|
||||
<header class="navbar">
|
||||
<style>
|
||||
/* 页眉导航栏 */
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #2575fc;
|
||||
padding: 10px 30px;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
margin: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.navbar .logo h1 {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.navbar-links a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
margin: 0 20px;
|
||||
font-size: 16px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.navbar-links a:hover {
|
||||
color: #f1c40f;
|
||||
}
|
||||
|
||||
.navbar .avatar img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-left: 200px;
|
||||
}
|
||||
|
||||
/* 控制浮框 */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
color: black;
|
||||
min-width: 200px;
|
||||
box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 10px;
|
||||
z-index: 1;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
padding: 10px;
|
||||
display: block;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.dropdown-content a:hover {
|
||||
background-color: #f1c40f;
|
||||
}
|
||||
|
||||
/* 显示浮框 */
|
||||
.dropdown:hover .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
</style>
|
||||
<div class="logo">
|
||||
<h1>华实君:教学练评一体的虚拟助教</h1>
|
||||
</div>
|
||||
<nav class="navbar-links">
|
||||
<a href="/">首页</a>
|
||||
<a href="/dashboard">课程</a>
|
||||
<a href="/">成绩</a>
|
||||
</nav>
|
||||
<div class="avatar dropdown">
|
||||
<img src="/static/image/algorithm/book_cover.png" alt="个人头像" class="avatar-img">
|
||||
<div class="dropdown-content">
|
||||
<a href="/switch-role" id="switch-role">切换到学生/教师</a>
|
||||
<a href="/logout" id="logout">注销</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
36
Html/apps/templates/register.html
Normal file
36
Html/apps/templates/register.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>学生注册</title>
|
||||
<link rel="stylesheet" href="/static/css/login.css">
|
||||
<script src="/static/js/register.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<h2 class="login-title">欢迎注册!</h2>
|
||||
<form class="login-form" id="register-form">
|
||||
<div class="input-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="email">邮箱</label>
|
||||
<input type="email" id="email" name="email" placeholder="请输入邮箱" required>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" name="password" placeholder="请输入密码" required>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="confirm-password">确认密码</label>
|
||||
<input type="password" id="confirm-password" name="confirm-password" placeholder="请确认密码" required>
|
||||
</div>
|
||||
<button type="submit" class="login-btn">注册</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
36
Html/apps/templates/register_teacher.html
Normal file
36
Html/apps/templates/register_teacher.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>教师注册</title>
|
||||
<link rel="stylesheet" href="/static/css/login.css">
|
||||
<script src="/static/js/register_teacher.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<h2 class="login-title">欢迎注册教师账号!</h2>
|
||||
<form class="login-form" id="register-teacher-form">
|
||||
<div class="input-group">
|
||||
<label for="teacher-username">用户名</label>
|
||||
<input type="text" id="teacher-username" name="teacher-username" placeholder="请输入用户名" required>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="teacher-email">邮箱</label>
|
||||
<input type="email" id="teacher-email" name="teacher-email" placeholder="请输入邮箱" required>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="teacher-password">密码</label>
|
||||
<input type="password" id="teacher-password" name="teacher-password" placeholder="请输入密码" required>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="teacher-confirm-password">确认密码</label>
|
||||
<input type="password" id="teacher-confirm-password" name="teacher-confirm-password" placeholder="请确认密码" required>
|
||||
</div>
|
||||
<button type="submit" class="login-btn">注册</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
137
Html/apps/templates/saveindex.html
Normal file
137
Html/apps/templates/saveindex.html
Normal file
@@ -0,0 +1,137 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Code Development Platform</title>
|
||||
<style>
|
||||
</style>
|
||||
<link rel="stylesheet" href="static/css/index.css">
|
||||
<script>
|
||||
document.cookie = "vscode-tkn=44edc269-6f88-46ab-9790-dc253f66ac36; path=/; SameSite=None; Secure";
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.min.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="maxcontainer">
|
||||
<!-- 左侧内容 -->
|
||||
<div class="sidebar" id="leftSidebar" style="width: 20%;">
|
||||
<h2>教材或题目</h2>
|
||||
<p>这里是提供给学生的教材或题目,后台提供给Agent针对不同教材题目的Prompt。</p>
|
||||
<div id="markdown-content">
|
||||
<!-- 动态加载的 HTML 会显示在这里 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 拖动条 -->
|
||||
<div class="resizer" id="dragbar"></div>
|
||||
|
||||
<!-- 中间嵌入的Vscode-web窗口,带有tkn参数 -->
|
||||
<iframe src="http://127.0.0.1:9888/?workspace=/mnt/c/CAKE/vscode/example_folder&folder=/mnt/c/CAKE/vscode/example_folder" class="vscode-web" id="vscodeWeb" style="flex-grow: 1;"></iframe>
|
||||
|
||||
<!-- 拖动条 -->
|
||||
<div class="resizer" id="dragbar2"></div>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="sidebar" id="rightSidebar" >
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="chatbox-header">
|
||||
<!-- 字体大小调节滑块 -->
|
||||
<div class="slider-container">
|
||||
<label for="fontSizeSlider">Aa</label>
|
||||
<input type="range" id="fontSizeSlider" min="12" max="24" value="16" />
|
||||
</div>
|
||||
|
||||
<!-- 语言切换按钮 -->
|
||||
<div class="language-toggle btn-group btn-group-toggle" data-toggle="buttons">
|
||||
<label class="btn btn-outline-secondary active">
|
||||
<input type="radio" name="language" id="english" autocomplete="off" checked> En
|
||||
</label>
|
||||
<label class="btn btn-outline-secondary">
|
||||
<input type="radio" name="language" id="chinese" autocomplete="off"> Zn
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chatbox" id="chatbox">
|
||||
<!-- 消息会在这里显示 -->
|
||||
</div>
|
||||
|
||||
<div class="input-area">
|
||||
<textarea id="messageInput" rows="2" class="form-control" placeholder="输入你的消息 (支持 Markdown 语法)"></textarea>
|
||||
<button class="btn btn-primary" onclick="sendMessage()">发送</button>
|
||||
</div>
|
||||
<!-- 聊天室js获取 -->
|
||||
<script src="static/js/chatbox.js"></script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 实现左右边栏可拖动
|
||||
const leftSidebar = document.getElementById('leftSidebar');
|
||||
const rightSidebar = document.getElementById('rightSidebar');
|
||||
const dragbar = document.getElementById('dragbar');
|
||||
const dragbar2 = document.getElementById('dragbar2');
|
||||
const vscodeWeb = document.getElementById('vscodeWeb');
|
||||
|
||||
let isDragging = false;
|
||||
|
||||
dragbar.addEventListener('mousedown', function(e) {
|
||||
isDragging = true;
|
||||
document.addEventListener('mousemove', resizeLeftSidebar);
|
||||
});
|
||||
|
||||
dragbar2.addEventListener('mousedown', function(e) {
|
||||
isDragging = true;
|
||||
document.addEventListener('mousemove', resizeRightSidebar);
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', function() {
|
||||
isDragging = false;
|
||||
document.removeEventListener('mousemove', resizeLeftSidebar);
|
||||
document.removeEventListener('mousemove', resizeRightSidebar);
|
||||
});
|
||||
|
||||
function resizeLeftSidebar(e) {
|
||||
if (isDragging) {
|
||||
let newWidth = e.clientX / window.innerWidth * 100;
|
||||
leftSidebar.style.width = `${newWidth}%`;
|
||||
vscodeWeb.style.flexGrow = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function resizeRightSidebar(e) {
|
||||
if (isDragging) {
|
||||
let newWidth = (window.innerWidth - e.clientX) / window.innerWidth * 100;
|
||||
rightSidebar.style.width = `${newWidth}%`;
|
||||
vscodeWeb.style.flexGrow = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 动态加载 Markdown 文件
|
||||
function loadMarkdown(filename) {
|
||||
fetch(`/${filename}-markdown`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.html_url) {
|
||||
document.getElementById('markdown-content').innerHTML = `<iframe src="${data.html_url}" width="100%" height="600px"></iframe>`;
|
||||
} else {
|
||||
console.error('Markdown file not found');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading markdown:', error);
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
loadMarkdown('example');
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
81
Html/apps/templates/teacherboard.html
Normal file
81
Html/apps/templates/teacherboard.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>教师个人主页</title>
|
||||
<link rel="stylesheet" href="/static/css/teacherboard.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% include 'navbar.html' %} <!-- 引入navbar.html -->
|
||||
|
||||
<!-- 主页主体部分 -->
|
||||
<div class="teacherboard">
|
||||
<h2 class="teacherboard-title">—— 我的课程 ——</h2>
|
||||
<div class="course-cards" id="course-cards">
|
||||
<!-- 每个课程的卡片 -->
|
||||
<div class="course-card" onclick="openCourseDetails('algorithm')">
|
||||
<img src="/static/image/algorithm/book_cover.png" alt="课程封面" class="course-image">
|
||||
<div class="course-info">
|
||||
<h3 class="course-name">算法分析与设计</h3>
|
||||
<p class="course-description">学习基本的算法知识概念,与算法实现。</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 继续添加其他课程卡片 -->
|
||||
<div class="course-card" onclick="openCourseDetails('data-structures')">
|
||||
<img src="/static/image/algorithm/book_cover.png" alt="课程封面" class="course-image">
|
||||
<div class="course-info">
|
||||
<h3 class="course-name">数据结构与算法</h3>
|
||||
<p class="course-description">深入学习数据结构和算法设计。</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 可以继续添加课程卡片 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧滑出的课程目录 -->
|
||||
<div id="courseDetails" class="course-details">
|
||||
<div class="details-header">
|
||||
<span id="course-title"></span>
|
||||
<button class="close-btn" onclick="closeCourseDetails()">×</button>
|
||||
</div>
|
||||
<div class="details-content">
|
||||
<h3>课程章节列表:</h3>
|
||||
<ul id="chapter-list">
|
||||
<!-- 章节列表会在点击课程时动态生成 -->
|
||||
</ul>
|
||||
<button class="add-chapter-btn" onclick="showInputForNewChapter()">新增章节</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑章节或课时弹窗 -->
|
||||
<div id="editModal" class="edit-modal">
|
||||
<div class="edit-modal-content">
|
||||
<h3>编辑章节/课时</h3>
|
||||
<form id="edit-form">
|
||||
<label for="edit-title">标题:</label>
|
||||
<input type="text" id="edit-title" name="title" required><br>
|
||||
<label for="edit-description">描述:</label>
|
||||
<textarea id="edit-description" name="description"></textarea><br>
|
||||
<button type="submit">保存</button>
|
||||
<button type="button" onclick="closeEditModal()">取消</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/teacherboard.js"></script>
|
||||
<script>
|
||||
window.appData = {
|
||||
user_data: JSON.parse(`{{user_data}}`.replace(/"/g, "\"")),
|
||||
user_course_data: JSON.parse(`{{user_course_data}}`.replace(/"/g, "\""))
|
||||
}
|
||||
console.log(window.appData.user_data);
|
||||
console.log(window.appData.user_course_data);
|
||||
show_teacher_data(window.appData.user_data, window.appData.user_course_data);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
10
Html/apps/views/__init__.py
Normal file
10
Html/apps/views/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from flask import Flask
|
||||
from .markdown import bp as markdown_bp
|
||||
from .vscode import bp as vscode_bp
|
||||
from .auth import bp as auth_bp
|
||||
from .dashboard import bp as main_bp
|
||||
def register_blueprints(app: Flask):
|
||||
app.register_blueprint(markdown_bp) # 默认就是挂在根路径
|
||||
app.register_blueprint(vscode_bp)
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(main_bp)
|
||||
57
Html/apps/views/auth.py
Normal file
57
Html/apps/views/auth.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# myapp/views/auth.py
|
||||
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, session, current_app
|
||||
from ..services.auth_service import register_user, login_user, logout_user
|
||||
from ..auth.decorators import require_role
|
||||
|
||||
bp = Blueprint("auth", __name__)
|
||||
|
||||
@bp.get("/register")
|
||||
def register():
|
||||
return render_template("register.html")
|
||||
|
||||
@bp.get("/register_teacher")
|
||||
def register_teacher():
|
||||
return render_template("register_teacher.html")
|
||||
|
||||
@bp.post("/register_post")
|
||||
def register_post():
|
||||
data = request.get_json(force=True) or {}
|
||||
ok, msg = register_user(data.get("username"), data.get("password"), teacher=False)
|
||||
return jsonify({"success": ok, "message": msg})
|
||||
|
||||
@bp.post("/register_teacher_post")
|
||||
def register_teacher_post():
|
||||
data = request.get_json(force=True) or {}
|
||||
ok, msg = register_user(data.get("username"), data.get("password"), teacher=True)
|
||||
return jsonify({"success": ok, "message": msg})
|
||||
|
||||
@bp.get("/login")
|
||||
def login():
|
||||
# 你原来 return 后还有逻辑,已被覆盖;这里简化
|
||||
return render_template("login.html")
|
||||
|
||||
@bp.post("/login_post")
|
||||
def login_post():
|
||||
data = request.get_json(force=True) or {}
|
||||
ok, msg = login_user(data.get("username"), data.get("password"), require_teacher=False)
|
||||
return jsonify({"success": ok, "message": msg})
|
||||
|
||||
@bp.post("/login_teacher_post")
|
||||
def login_teacher_post():
|
||||
data = request.get_json(force=True) or {}
|
||||
ok, msg = login_user(data.get("username"), data.get("password"), require_teacher=True)
|
||||
return jsonify({"success": ok, "message": msg})
|
||||
|
||||
@bp.get("/teacherboard")
|
||||
@require_role(roles="teacher")
|
||||
def teacherboard():
|
||||
return render_template("teacherboard.html")
|
||||
|
||||
@bp.get("/logout")
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
@bp.get("/get_session")
|
||||
def get_session():
|
||||
return jsonify({"session": session.get("user_id", "default_session")})
|
||||
60
Html/apps/views/dashboard.py
Normal file
60
Html/apps/views/dashboard.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# myapp/views/dashboard.py
|
||||
import json
|
||||
from flask import Blueprint, render_template, request, jsonify, redirect, url_for
|
||||
from ..auth.decorators import require_role
|
||||
from ..services.user_service import get_or_load_current_user, add_course_for_current_user
|
||||
from ..services.course_service import load_course, user_selected_course_briefs
|
||||
|
||||
bp = Blueprint("main", __name__) # 根路径蓝图
|
||||
|
||||
@bp.get("/dashboard")
|
||||
@require_role
|
||||
def dashboard():
|
||||
user_obj = get_or_load_current_user()
|
||||
if user_obj is None:
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
user_course_data = user_selected_course_briefs(user_obj)
|
||||
return render_template(
|
||||
"dashboard.html",
|
||||
user_data=user_obj.to_json_without_dialog(),
|
||||
user_course_data=json.dumps(user_course_data, ensure_ascii=False),
|
||||
)
|
||||
|
||||
@bp.get("/course/<course_id>")
|
||||
@require_role
|
||||
def course(course_id):
|
||||
c = load_course(course_id)
|
||||
return render_template("course.html", course_id=course_id, course_data=c)
|
||||
|
||||
@bp.post("/select_course")
|
||||
@require_role
|
||||
def select_course():
|
||||
data = request.get_json(force=True) or {}
|
||||
course_id = data.get("course_id")
|
||||
if not course_id:
|
||||
return jsonify({"success": False, "message": "缺少 course_id"}), 400
|
||||
|
||||
course_data = load_course(course_id)
|
||||
ok = add_course_for_current_user(course_id, course_data)
|
||||
if not ok:
|
||||
return jsonify({"success": False, "message": "未登录或会话失效"}), 401
|
||||
return jsonify({"success": True, "message": "课程选择成功"})
|
||||
|
||||
@bp.get("/")
|
||||
@require_role
|
||||
def home_index():
|
||||
user_obj = get_or_load_current_user()
|
||||
if user_obj is None:
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
selected_courses = list(getattr(user_obj, "select_course", []))
|
||||
# 课程目录:依据你的 CourseList 暴露的接口进行传递(这里直接传对象,模板里用)
|
||||
from flask import current_app
|
||||
course_list = current_app.extensions["course_list"]
|
||||
|
||||
return render_template(
|
||||
"index.html",
|
||||
courses_data=course_list,
|
||||
selected_courses=selected_courses
|
||||
)
|
||||
35
Html/apps/views/markdown.py
Normal file
35
Html/apps/views/markdown.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import os
|
||||
from flask import Blueprint, jsonify, current_app, send_from_directory
|
||||
from ..services.markdown_service import (
|
||||
convert_markdown_to_html, wrap_with_styles,
|
||||
save_html, copy_images
|
||||
)
|
||||
|
||||
bp = Blueprint("markdown", __name__)
|
||||
|
||||
@bp.route("/<course_id>-<filename>-markdown", methods=["GET"])
|
||||
def convert_md(course_id, filename):
|
||||
md_file_path = os.path.join(current_app.config["MARKDOWN_DIR"], course_id, f"{filename}.md")
|
||||
|
||||
if not os.path.exists(md_file_path):
|
||||
return jsonify({"error": "Markdown file not found"}), 404
|
||||
|
||||
# 转换 markdown -> html
|
||||
html = convert_markdown_to_html(md_file_path)
|
||||
html_with_styles = wrap_with_styles(html)
|
||||
|
||||
# 保存 HTML 文件到 static
|
||||
html_output_path = os.path.join(current_app.config["STATIC_DIR"], f"{filename}.html")
|
||||
save_html(html_with_styles, html_output_path)
|
||||
|
||||
# 拷贝图片资源
|
||||
image_source = os.path.join(current_app.config["MARKDOWN_DIR"], course_id, current_app.config["IMAGE_DIR"], filename)
|
||||
image_target = os.path.join(current_app.config["STATIC_DIR"], current_app.config["IMAGE_DIR"], filename)
|
||||
copy_images(image_source, image_target)
|
||||
|
||||
return jsonify({"html_url": f"/static/{filename}.html"})
|
||||
|
||||
# 提供静态文件访问
|
||||
@bp.route("/static/<path:filename>")
|
||||
def serve_static(filename):
|
||||
return send_from_directory(current_app.config["STATIC_DIR"], filename)
|
||||
69
Html/apps/views/vscode.py
Normal file
69
Html/apps/views/vscode.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# myapp/views/vscode.py
|
||||
import os, uuid, json
|
||||
from flask import Blueprint, current_app, session, redirect, url_for, render_template, request, jsonify
|
||||
from ..services.backboard_service import realtime_response
|
||||
|
||||
bp = Blueprint("vscode", __name__)
|
||||
|
||||
@bp.route("/desktop/<user_id>/<course_id>/<chapter_id>")
|
||||
def desktop(user_id, course_id, chapter_id):
|
||||
# session 中放 uuid(访客也能进则给一个)
|
||||
if "user_id" not in session:
|
||||
session["user_id"] = "user_" + str(uuid.uuid4())
|
||||
|
||||
current_app.logger.debug("user %s uuid is %s", user_id, session["user_id"])
|
||||
|
||||
# 全局映射
|
||||
username2uuid = current_app.extensions["username2uuid"]
|
||||
uuid2username = current_app.extensions["uuid2username"]
|
||||
userid_recorder = current_app.extensions["userid_recorder"]
|
||||
|
||||
username2uuid[user_id] = session["user_id"]
|
||||
uuid2username[session["user_id"]] = user_id
|
||||
userid_recorder[f"{user_id}&{course_id}"] = session["user_id"]
|
||||
|
||||
# 按课程/章节创建工作目录
|
||||
base_root = current_app.config["STUDENT_WORKSPACE_ROOT"]
|
||||
path_dir = os.path.join(base_root, user_id, course_id, chapter_id)
|
||||
os.makedirs(path_dir, exist_ok=True)
|
||||
|
||||
# 写 .config(并按需要转换为 WSL 路径)
|
||||
cfg = current_app.config["VSCODE_WEB_PATH"]
|
||||
path_for_vscode = path_dir
|
||||
if cfg.get("is_wsl"):
|
||||
path_for_vscode = path_for_vscode.replace("\\", "/") \
|
||||
.replace(cfg.get("windows_path", ""), cfg.get("wsl_path", ""))
|
||||
|
||||
config_path = os.path.join(path_dir, ".config")
|
||||
tmpd = {
|
||||
"user_id": user_id,
|
||||
"course_id": course_id,
|
||||
"chapter_id": chapter_id,
|
||||
"path": path_for_vscode
|
||||
}
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(tmpd, f, ensure_ascii=False)
|
||||
|
||||
current_app.logger.debug("config file path: %s", config_path)
|
||||
|
||||
return render_template(
|
||||
"desktop.html",
|
||||
vscode_web_url=current_app.config["VSCODE_WEB_URL"],
|
||||
user_id=user_id, course_id=course_id, chapter_id=chapter_id,
|
||||
workspace_path=path_for_vscode
|
||||
)
|
||||
|
||||
@bp.route("/desktop_nouser/<course_id>/<chapter_id>")
|
||||
def desktop_nouser(course_id, chapter_id):
|
||||
if "user_id" not in session:
|
||||
return redirect(url_for("auth.login")) # 如果你有 auth 蓝图
|
||||
user_uuid = session["user_id"]
|
||||
username = current_app.extensions["uuid2username"].get(user_uuid)
|
||||
return redirect(url_for("vscode.desktop", user_id=username, course_id=course_id, chapter_id=chapter_id))
|
||||
|
||||
@bp.route("/vscode_data", methods=["POST"])
|
||||
def vscode_data():
|
||||
data = request.get_json(force=True)
|
||||
config = data.get("config") or {}
|
||||
realtime_response(config, data) # 调用服务层逻辑
|
||||
return jsonify({"status": "success", "received": data})
|
||||
Reference in New Issue
Block a user