Version 0.1.1 rise to flask app factory

This commit is contained in:
CakeCN
2025-08-27 19:39:38 +08:00
parent 8dba8688a7
commit 5c0187bc4b
55 changed files with 1708 additions and 613 deletions

34
Html/apps/__init__.py Normal file
View 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

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

@@ -0,0 +1 @@
# Html/apps/bootstrap.py

37
Html/apps/config.py Normal file
View 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
View 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"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

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

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

View 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

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

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

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

View 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

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

View 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&lt;=n&lt;=10^5)</p>
<p>第二行n个用空格分开的整数ai。(0&lt;=ai&lt;=10^8)</p>
<p>第三行一个整数q表示询问的次数。(1&lt;=q&lt;=10^4)</p>
<p>后q行每行一个整数b表示询问的数。(0&lt;=b&lt;=10^8)</p>
<h5>输出</h5>
<p>q行每行一个整数对应每次询问的返回结果。</p>
<h5>提示:</h5>
<p>完成代码后通知Agent进行评测。</p>
<p>如果你还不完全会这个算法询问Agent获取提示并进行学习。</p>
</body>
</html>

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

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

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

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

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

View 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('选择课程失败');
});
});
});
});

View 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('登录请求失败,请稍后重试');
});
});
});

View 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('注册失败');
});
});
});

View 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('注册失败');
});
});
});

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

View 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&lt;=n&lt;=10^5)</p>
<p>第二行n个用空格分开的整数ai。(0&lt;=ai&lt;=10^8)</p>
<p>第三行一个整数q表示询问的次数。(1&lt;=q&lt;=10^4)</p>
<p>后q行每行一个整数b表示询问的数。(0&lt;=b&lt;=10^8)</p>
<h5>输出</h5>
<p>q行每行一个整数对应每次询问的返回结果。</p>
<h5>提示:</h5>
<p>完成代码后通知Agent进行评测。</p>
<p>如果你还不完全会这个算法询问Agent获取提示并进行学习。</p>
</body>
</html>

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

View 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(/&#34;/g, "\"")),
user_course_data: JSON.parse(`{{user_course_data}}`.replace(/&#34;/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>

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

View 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>版权所有 &copy; 2024 “华实伴学君”——教学练评一体的虚拟编码助教</p>
</footer>
</body>
<script>
window.appData = {
user_selected_courses: JSON.parse(`{{selected_courses}}`.replace(/&#34;/g, "\""))
}
</script>
<script src="/static/js/index.js"></script>
</html>

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

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

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

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

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

View 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(/&#34;/g, "\"")),
user_course_data: JSON.parse(`{{user_course_data}}`.replace(/&#34;/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>

View 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
View 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")})

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

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