Version 0.1.1 rise to flask app factory
513
Html/app.py
@@ -1,497 +1,20 @@
|
|||||||
from functools import wraps
|
# app.py 0.1.1 rise to factory mode
|
||||||
from flask import Flask, redirect, session, request, jsonify, render_template, send_from_directory, url_for
|
from bootstrap import bootstrap_paths
|
||||||
from flask_cors import CORS
|
bootstrap_paths()
|
||||||
from flask_socketio import SocketIO, join_room, emit, Namespace
|
from apps import create_app
|
||||||
import markdown
|
from apps.extensions import socketio
|
||||||
import os
|
|
||||||
import uuid
|
# 工厂创建 app,工厂里已经做了:加载配置/注册蓝图/注册 namespaces
|
||||||
import shutil
|
app = create_app()
|
||||||
import sys
|
|
||||||
import json
|
if __name__ == "__main__":
|
||||||
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
# 仅本地开发用;生产不要用 werkzeug 自带的开发服务器
|
||||||
sys.path.insert(0, parent_dir)
|
socketio.run(
|
||||||
student_workspace_root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../study'))
|
app,
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=5551,
|
||||||
|
debug=True, # 开发期打开
|
||||||
|
allow_unsafe_werkzeug=True # 避免 dev server 的安全限制提示
|
||||||
|
)
|
||||||
|
|
||||||
current_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '.'))
|
|
||||||
sys.path.insert(0, current_dir)
|
|
||||||
from AlgoriAgent.projects.algoriAgent.agent_manager import AgentManager
|
|
||||||
|
|
||||||
|
|
||||||
import configparser
|
|
||||||
|
|
||||||
from db.user_list import UserList
|
|
||||||
from db.user import User, load_user_from_json, create_user_json
|
|
||||||
from db.course_list import CourseList
|
|
||||||
from db.course import Course, load_course_from_json
|
|
||||||
GLOBAL_CONFIG = configparser.ConfigParser()
|
|
||||||
GLOBAL_CONFIG.read('config.ini')
|
|
||||||
VSCODE_WEB_URL = GLOBAL_CONFIG['VSCODE_WEB']['url']
|
|
||||||
USER_DATA_DIR = GLOBAL_CONFIG['USER_DATA']['dir']
|
|
||||||
COURSE_DATA_DIR = GLOBAL_CONFIG['COURSE_DATA']['dir']
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
socketio = SocketIO(app, cors_allowed_origins="*",ping_timeout=60, ping_interval=5)
|
|
||||||
# socketio = SocketIO(app, cors_allowed_origins="http://localhost:9888") # 设置跨域支持
|
|
||||||
import logging
|
|
||||||
app.secret_key = 'cakebaker'
|
|
||||||
app.logger.setLevel(logging.DEBUG)
|
|
||||||
# 配置 CORS
|
|
||||||
CORS(app, resources={r"/*": {"origins": VSCODE_WEB_URL},}, supports_credentials=True)
|
|
||||||
|
|
||||||
|
|
||||||
userid_recorder = {} # user_id&path -> session['user_id']
|
|
||||||
|
|
||||||
'''
|
|
||||||
Backboard
|
|
||||||
'''
|
|
||||||
from backboardManager import BackBoardManager, Backboard
|
|
||||||
|
|
||||||
backboard_manager = BackBoardManager()
|
|
||||||
uuid2username = {}
|
|
||||||
username2uuid = {}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 定义命名空间:用于和 VSCode 插件交流
|
|
||||||
class VSCodeNamespace(Namespace):
|
|
||||||
def on_login(self,data):
|
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
def realtime_response(config, realtime_action):
|
|
||||||
user_id = config['user_id']
|
|
||||||
folder_path = config['path']
|
|
||||||
useruuid = username2uuid[user_id]
|
|
||||||
bb = backboard_manager.get_backboard(useruuid)
|
|
||||||
assert type(bb) == Backboard
|
|
||||||
|
|
||||||
bb.add_history(realtime_action)
|
|
||||||
|
|
||||||
if realtime_action['type'] == 'workspaceFolders':
|
|
||||||
bb.file_tree = realtime_action['fileTree']
|
|
||||||
|
|
||||||
if realtime_action['type'] == 'activeFile':
|
|
||||||
file_path = realtime_action['filePath']
|
|
||||||
assert type(file_path) == str
|
|
||||||
with open(f"{file_path}") as f:
|
|
||||||
bb.active_file_content = f.read()
|
|
||||||
|
|
||||||
if realtime_action['type'] == 'paste':
|
|
||||||
file_path = realtime_action['filePath']
|
|
||||||
assert type(file_path) == str
|
|
||||||
bb.pasted_file_path = file_path
|
|
||||||
bb.pasted_content = realtime_action['content']
|
|
||||||
bb.active_file_path = file_path
|
|
||||||
with open(f"{file_path}") as f:
|
|
||||||
bb.active_file_content = f.read()
|
|
||||||
|
|
||||||
|
|
||||||
if realtime_action['type'] == 'fileEdit':
|
|
||||||
file_path = realtime_action['filePath']
|
|
||||||
assert type(file_path) == str
|
|
||||||
bb.active_file_path = file_path
|
|
||||||
with open(f"{file_path}") as f:
|
|
||||||
bb.active_file_content = f.read()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
print("config"+str(config))
|
|
||||||
print("realtime_action"+str(realtime_action))
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Agent and Chat
|
|
||||||
'''
|
|
||||||
agent_manager = AgentManager(app = app, socketio = socketio)
|
|
||||||
user_threads = {}
|
|
||||||
import threading
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
||||||
|
|
||||||
# 初始化线程池
|
|
||||||
executor = ThreadPoolExecutor(max_workers=10)
|
|
||||||
class AgentNamespace(Namespace):
|
|
||||||
def on_login(self, data):
|
|
||||||
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):
|
|
||||||
id = session.get('user_id')
|
|
||||||
agent_manager.change_language(id, language)
|
|
||||||
|
|
||||||
def on_message(self, data):
|
|
||||||
print(f"Message from client: {data}")
|
|
||||||
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 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")
|
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Markdown to HTML
|
|
||||||
'''
|
|
||||||
# 配置文件路径
|
|
||||||
MARKDOWN_DIR = 'books/markdown' # 存放 markdown 文件的文件夹
|
|
||||||
STATIC_DIR = 'static' # 存放生成的 HTML 和图片资源的文件夹
|
|
||||||
IMAGE_DIR = 'image' # 图片资源相对于 markdown 的位置
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/<course_id>-<filename>-markdown', methods=['GET'])
|
|
||||||
def convert_md(course_id, filename):
|
|
||||||
md_file_path = os.path.join(MARKDOWN_DIR,course_id, f'{filename}.md')
|
|
||||||
|
|
||||||
if not os.path.exists(md_file_path):
|
|
||||||
return jsonify({'error': 'Markdown file not found'}), 404
|
|
||||||
|
|
||||||
# 读取 markdown 文件内容
|
|
||||||
with open(md_file_path, 'r', encoding='utf-8') as file:
|
|
||||||
md_content = file.read()
|
|
||||||
|
|
||||||
# 将 markdown 转换为 HTML
|
|
||||||
html_content = markdown.markdown(md_content)
|
|
||||||
# 插入 CSS 样式,限制图片最大宽度为 100%,高度自动调整
|
|
||||||
html_with_styles = 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>
|
|
||||||
{html_content}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
# 保存 HTML 文件到 static 文件夹
|
|
||||||
html_output_path = os.path.join(STATIC_DIR, f'{filename}.html')
|
|
||||||
with open(html_output_path, 'w', encoding='utf-8') as html_file:
|
|
||||||
html_file.write(html_with_styles)
|
|
||||||
|
|
||||||
# 处理图片资源,将 image/xxx 文件夹拷贝到 static/image/xxx
|
|
||||||
image_source_dir = os.path.join(MARKDOWN_DIR,course_id, IMAGE_DIR, filename)
|
|
||||||
image_target_dir = os.path.join(STATIC_DIR, IMAGE_DIR, filename)
|
|
||||||
print(f"Copying image resources from {image_source_dir} to {image_target_dir}")
|
|
||||||
if os.path.exists(image_source_dir):
|
|
||||||
# 确保 static/image/xxx 目录存在
|
|
||||||
if not os.path.exists(image_target_dir):
|
|
||||||
os.makedirs(image_target_dir)
|
|
||||||
|
|
||||||
# 拷贝图片资源
|
|
||||||
for image_file in os.listdir(image_source_dir):
|
|
||||||
full_image_file_path = os.path.join(image_source_dir, image_file)
|
|
||||||
if os.path.isfile(full_image_file_path):
|
|
||||||
shutil.copy(full_image_file_path, image_target_dir)
|
|
||||||
|
|
||||||
return jsonify({'html_url': f'/static/{filename}.html'})
|
|
||||||
|
|
||||||
# 静态文件(HTML 和图片资源)的访问
|
|
||||||
@app.route('/static/<path:filename>')
|
|
||||||
def serve_static(filename):
|
|
||||||
return send_from_directory(STATIC_DIR, filename)
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Vscode
|
|
||||||
'''
|
|
||||||
@app.route('/desktop/<user_id>/<course_id>/<chapter_id>')
|
|
||||||
def desktop(user_id, course_id, chapter_id):
|
|
||||||
if 'user_id' not in session:
|
|
||||||
# return redirect(url_for('login'))
|
|
||||||
session['user_id'] = 'user_' + str(uuid.uuid4())
|
|
||||||
print("user "+ user_id + "uuid is"+ session['user_id'])
|
|
||||||
username2uuid[user_id] = session['user_id']
|
|
||||||
uuid2username[session['user_id']] = user_id
|
|
||||||
userid_recorder[user_id+'&'+course_id] = session['user_id']
|
|
||||||
|
|
||||||
# 在学习目录下创建一个名为 user_id_path 的文件夹
|
|
||||||
path_dir = os.path.join(student_workspace_root_path, user_id, course_id,chapter_id)
|
|
||||||
os.makedirs(path_dir, exist_ok=True)
|
|
||||||
# 在此文件夹内部创建一个.config文件,并写入 user_id=user_id\n path=path
|
|
||||||
config_path = os.path.join(path_dir, '.config')
|
|
||||||
if GLOBAL_CONFIG['VSCODE_WEB_PATH']['is_wsl']: # 如果是在 WSL 中运行,则将路径转换为 WSL 路径
|
|
||||||
path_dir = path_dir.replace('\\', '/')
|
|
||||||
path_dir = path_dir.replace(GLOBAL_CONFIG['VSCODE_WEB_PATH']['windows_path'] , GLOBAL_CONFIG['VSCODE_WEB_PATH']['wsl_path'])
|
|
||||||
with open(config_path, 'w', encoding='utf-8') as f:
|
|
||||||
tmpd = {'user_id': user_id, 'course_id': course_id, 'chapter_id': chapter_id, 'path': path_dir}
|
|
||||||
json.dump(tmpd, f)
|
|
||||||
print('---------------------')
|
|
||||||
print(config_path)
|
|
||||||
return render_template('desktop.html', vscode_web_url=VSCODE_WEB_URL, user_id=user_id, course_id=course_id, chapter_id=chapter_id, workspace_path=path_dir)
|
|
||||||
|
|
||||||
@app.route('/desktop_nouser/<course_id>/<chapter_id>')
|
|
||||||
def desktop_nouser(course_id, chapter_id):
|
|
||||||
if 'user_id' not in session:
|
|
||||||
return redirect(url_for('login'))
|
|
||||||
user_id = session['user_id']
|
|
||||||
username = uuid2username[user_id]
|
|
||||||
return redirect(url_for('desktop', user_id=username, course_id=course_id, chapter_id=chapter_id))
|
|
||||||
|
|
||||||
@app.route('/vscode_data', methods=['POST'])
|
|
||||||
def vscode_data():
|
|
||||||
data = request.json
|
|
||||||
config = data['config']
|
|
||||||
realtime_response(config,data)
|
|
||||||
print(f"Received data from VSCode: {data}")
|
|
||||||
return jsonify({"status": "success", "received": data})
|
|
||||||
|
|
||||||
@app.route('/logout')
|
|
||||||
def logout():
|
|
||||||
session.pop('user_id', None)
|
|
||||||
return redirect(url_for('login'))
|
|
||||||
|
|
||||||
@app.route('/get_session')
|
|
||||||
def get_session():
|
|
||||||
user_session = session.get('user_id', 'default_session')
|
|
||||||
return jsonify({"session": user_session})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Login
|
|
||||||
|
|
||||||
'''
|
|
||||||
users_list = UserList()
|
|
||||||
course_list = CourseList()
|
|
||||||
|
|
||||||
def require_teacher(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
if 'user_id' not in session or session['user_id'] not in uuid2username:
|
|
||||||
return redirect(url_for('login'))
|
|
||||||
user_id = session['user_id']
|
|
||||||
username = uuid2username[user_id]
|
|
||||||
if users_list.get_user_is_teacher(username) is None or users_list.get_user_is_teacher(username) == False:
|
|
||||||
return redirect(url_for('login'))
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
def require_user(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
if 'user_id' not in session or session['user_id'] not in uuid2username:
|
|
||||||
return redirect(url_for('login'))
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
@app.route('/register')
|
|
||||||
def register():
|
|
||||||
return render_template('register.html')
|
|
||||||
|
|
||||||
@app.route('/register_teacher')
|
|
||||||
def register_teacher():
|
|
||||||
return render_template('register_teacher.html')
|
|
||||||
|
|
||||||
@app.route('/register_teacher_post', methods=['POST'])
|
|
||||||
def register_teacher_post():
|
|
||||||
data = request.get_json()
|
|
||||||
username = data.get('username')
|
|
||||||
password = data.get('password')
|
|
||||||
if users_list.has_user(username) is not None:
|
|
||||||
users_list.add_user(username, password, teacher=True)
|
|
||||||
create_user_json(username, USER_DATA_DIR)
|
|
||||||
return jsonify({'success': True, 'message': '注册成功'})
|
|
||||||
else: return jsonify({'success': False, 'message': '用户已存在,请更换用户名'})
|
|
||||||
|
|
||||||
@app.route('/register_post', methods=['POST'])
|
|
||||||
def register_post():
|
|
||||||
data = request.get_json()
|
|
||||||
username = data.get('username')
|
|
||||||
password = data.get('password')
|
|
||||||
if users_list.has_user(username) is not None:
|
|
||||||
users_list.add_user(username, password)
|
|
||||||
create_user_json(username, USER_DATA_DIR)
|
|
||||||
return jsonify({'success': True, 'message': '注册成功'})
|
|
||||||
else: return jsonify({'success': False, 'message': '用户已存在,请更换用户名'})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/login')
|
|
||||||
def login():
|
|
||||||
return render_template('login.html')
|
|
||||||
if 'user_id' not in session:
|
|
||||||
return render_template('login.html')
|
|
||||||
else:
|
|
||||||
return redirect(url_for('/'))
|
|
||||||
|
|
||||||
@app.route('/login_post', methods=['POST'])
|
|
||||||
def login_post():
|
|
||||||
# 获取请求的 JSON 数据
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
username = data.get('username')
|
|
||||||
password = data.get('password')
|
|
||||||
|
|
||||||
pswd = users_list.get_user_pswd(username)
|
|
||||||
|
|
||||||
if pswd is None:
|
|
||||||
return jsonify({'success': False, 'message': '用户名或密码错误'})
|
|
||||||
if pswd != password:
|
|
||||||
return jsonify({'success': False, 'message': '用户名或密码错误'})
|
|
||||||
|
|
||||||
session['user_id'] = 'user_' + str(uuid.uuid4())
|
|
||||||
print("user "+ username + "uuid is"+ session['user_id'])
|
|
||||||
username2uuid[username] = session['user_id']
|
|
||||||
uuid2username[session['user_id']] = username
|
|
||||||
return jsonify({'success': True, 'message': '登录成功'})
|
|
||||||
|
|
||||||
@app.route('/login_teacher_post', methods=['POST'])
|
|
||||||
def login_teacher_post():
|
|
||||||
data = request.get_json()
|
|
||||||
username = data.get('username')
|
|
||||||
password = data.get('password')
|
|
||||||
pswd = users_list.get_user_pswd(username)
|
|
||||||
is_teacher = users_list.get_user_is_teacher(username)
|
|
||||||
if is_teacher is None or is_teacher == False:
|
|
||||||
return jsonify({'success': False, 'message': '用户名不存在或非教师账号'})
|
|
||||||
if pswd is None:
|
|
||||||
return jsonify({'success': False, 'message': '用户名或密码错误'})
|
|
||||||
if pswd != password:
|
|
||||||
return jsonify({'success': False, 'message': '用户名或密码错误'})
|
|
||||||
session['user_id'] = 'user_' + str(uuid.uuid4())
|
|
||||||
print("user "+ username + "uuid is"+ session['user_id'])
|
|
||||||
username2uuid[username] = session['user_id']
|
|
||||||
uuid2username[session['user_id']] = username
|
|
||||||
return jsonify({'success': True, 'message': '登录成功'})
|
|
||||||
|
|
||||||
@app.route('/teacherboard')
|
|
||||||
@require_teacher
|
|
||||||
def teacherboard():
|
|
||||||
return render_template('teacherboard.html')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
user_id2UserClass = {}
|
|
||||||
'''
|
|
||||||
DashBoard
|
|
||||||
'''
|
|
||||||
@app.route('/dashboard')
|
|
||||||
@require_user
|
|
||||||
def dashboard():
|
|
||||||
user_id = session['user_id']
|
|
||||||
username = uuid2username[user_id]
|
|
||||||
if (user_id not in user_id2UserClass):
|
|
||||||
user_id2UserClass[user_id] = load_user_from_json(username, user_data_dir = USER_DATA_DIR)
|
|
||||||
user_course_data = []
|
|
||||||
for course_id in user_id2UserClass[user_id].select_course:
|
|
||||||
course_brief_info = course_list.get_course_brief_info(course_id, load_course_from_json(course_id, course_data_dir = COURSE_DATA_DIR))
|
|
||||||
user_course_data.append(course_brief_info)
|
|
||||||
return render_template('dashboard.html',user_data = user_id2UserClass[user_id].to_json_without_dialog(), user_course_data = json.dumps(user_course_data))
|
|
||||||
|
|
||||||
'''
|
|
||||||
Course
|
|
||||||
'''
|
|
||||||
|
|
||||||
@app.route('/course/<course_id>')
|
|
||||||
@require_user
|
|
||||||
def course(course_id):
|
|
||||||
c = load_course_from_json(course_id, course_data_dir = COURSE_DATA_DIR)
|
|
||||||
return render_template('course.html', course_id=course_id, course_data=c)
|
|
||||||
|
|
||||||
@app.route('/select_course', methods=['POST'])
|
|
||||||
@require_user
|
|
||||||
def select_course():
|
|
||||||
user = user_id2UserClass[session['user_id']]
|
|
||||||
data = request.get_json()
|
|
||||||
course_id = data.get('course_id')
|
|
||||||
if course_id:
|
|
||||||
user.select_new_course(course_id, load_course_from_json(course_id, COURSE_DATA_DIR))
|
|
||||||
return jsonify({'success': True, 'message': '课程选择成功'})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
@require_user
|
|
||||||
def home_index():
|
|
||||||
selected_courses=[]
|
|
||||||
if ('user_id' in session):
|
|
||||||
if (session['user_id'] not in user_id2UserClass): return redirect(url_for('login'))
|
|
||||||
user = user_id2UserClass[session['user_id']]
|
|
||||||
for course_id in user.select_course:
|
|
||||||
selected_courses.append(course_id)
|
|
||||||
|
|
||||||
return render_template('index.html', courses_data = course_list, selected_courses =selected_courses)
|
|
||||||
|
|
||||||
# 一些app辅助函数,主要提供给Agent与数据库的交互能力
|
|
||||||
|
|
||||||
class MyFunction:
|
|
||||||
def save_chapter_memory(self, id, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal):
|
|
||||||
print("-=-=-=-=-=-=-=-")
|
|
||||||
print(id, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal)
|
|
||||||
username = uuid2username[id]
|
|
||||||
u = load_user_from_json(username, user_data_dir = USER_DATA_DIR)
|
|
||||||
u.save_chapter_memory(course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal)
|
|
||||||
|
|
||||||
app.my_function = MyFunction()
|
|
||||||
if __name__ == '__main__':
|
|
||||||
socketio.on_namespace(VSCodeNamespace('/vscode'))
|
|
||||||
socketio.on_namespace(AgentNamespace('/agent'))
|
|
||||||
socketio.run(app, host='0.0.0.0', port=5551, debug=False,allow_unsafe_werkzeug=True)
|
|
||||||
|
|
||||||
|
|||||||
480
Html/app_back.py
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
from bootstrap import bootstrap_paths
|
||||||
|
bootstrap_paths()
|
||||||
|
from functools import wraps
|
||||||
|
from flask import Flask, redirect, session, request, jsonify, render_template, send_from_directory, url_for
|
||||||
|
from flask_cors import CORS
|
||||||
|
from flask_socketio import SocketIO, join_room, emit, Namespace
|
||||||
|
import markdown
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from apps.auth.decorators import require_role
|
||||||
|
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
sys.path.insert(0, parent_dir)
|
||||||
|
student_workspace_root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../study'))
|
||||||
|
current_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '.'))
|
||||||
|
sys.path.insert(0, current_dir)
|
||||||
|
from AlgoriAgent.projects.algoriAgent.agent_manager import AgentManager
|
||||||
|
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
from db.user_list import UserList
|
||||||
|
from db.user import User, load_user_from_json, create_user_json
|
||||||
|
from db.course_list import CourseList
|
||||||
|
from db.course import Course, load_course_from_json
|
||||||
|
GLOBAL_CONFIG = configparser.ConfigParser()
|
||||||
|
GLOBAL_CONFIG.read('config.ini')
|
||||||
|
VSCODE_WEB_URL = GLOBAL_CONFIG['VSCODE_WEB']['url']
|
||||||
|
USER_DATA_DIR = GLOBAL_CONFIG['USER_DATA']['dir']
|
||||||
|
COURSE_DATA_DIR = GLOBAL_CONFIG['COURSE_DATA']['dir']
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
socketio = SocketIO(app, cors_allowed_origins="*",ping_timeout=60, ping_interval=5)
|
||||||
|
# socketio = SocketIO(app, cors_allowed_origins="http://localhost:9888") # 设置跨域支持
|
||||||
|
import logging
|
||||||
|
app.secret_key = 'cakebaker'
|
||||||
|
app.logger.setLevel(logging.DEBUG)
|
||||||
|
# 配置 CORS
|
||||||
|
CORS(app, resources={r"/*": {"origins": VSCODE_WEB_URL},}, supports_credentials=True)
|
||||||
|
|
||||||
|
|
||||||
|
userid_recorder = {} # user_id&path -> session['user_id']
|
||||||
|
|
||||||
|
'''
|
||||||
|
Backboard
|
||||||
|
'''
|
||||||
|
from backboardManager import BackBoardManager, Backboard
|
||||||
|
|
||||||
|
backboard_manager = BackBoardManager()
|
||||||
|
uuid2username = {}
|
||||||
|
username2uuid = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 定义命名空间:用于和 VSCode 插件交流
|
||||||
|
class VSCodeNamespace(Namespace):
|
||||||
|
def on_login(self,data):
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
def realtime_response(config, realtime_action):
|
||||||
|
user_id = config['user_id']
|
||||||
|
folder_path = config['path']
|
||||||
|
useruuid = username2uuid[user_id]
|
||||||
|
bb = backboard_manager.get_backboard(useruuid)
|
||||||
|
assert type(bb) == Backboard
|
||||||
|
|
||||||
|
bb.add_history(realtime_action)
|
||||||
|
|
||||||
|
if realtime_action['type'] == 'workspaceFolders':
|
||||||
|
bb.file_tree = realtime_action['fileTree']
|
||||||
|
|
||||||
|
if realtime_action['type'] == 'activeFile':
|
||||||
|
file_path = realtime_action['filePath']
|
||||||
|
assert type(file_path) == str
|
||||||
|
with open(f"{file_path}") as f:
|
||||||
|
bb.active_file_content = f.read()
|
||||||
|
|
||||||
|
if realtime_action['type'] == 'paste':
|
||||||
|
file_path = realtime_action['filePath']
|
||||||
|
assert type(file_path) == str
|
||||||
|
bb.pasted_file_path = file_path
|
||||||
|
bb.pasted_content = realtime_action['content']
|
||||||
|
bb.active_file_path = file_path
|
||||||
|
with open(f"{file_path}") as f:
|
||||||
|
bb.active_file_content = f.read()
|
||||||
|
|
||||||
|
|
||||||
|
if realtime_action['type'] == 'fileEdit':
|
||||||
|
file_path = realtime_action['filePath']
|
||||||
|
assert type(file_path) == str
|
||||||
|
bb.active_file_path = file_path
|
||||||
|
with open(f"{file_path}") as f:
|
||||||
|
bb.active_file_content = f.read()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print("config"+str(config))
|
||||||
|
print("realtime_action"+str(realtime_action))
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
Agent and Chat
|
||||||
|
'''
|
||||||
|
agent_manager = AgentManager(app = app, socketio = socketio)
|
||||||
|
user_threads = {}
|
||||||
|
import threading
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
|
# 初始化线程池
|
||||||
|
executor = ThreadPoolExecutor(max_workers=10)
|
||||||
|
class AgentNamespace(Namespace):
|
||||||
|
def on_login(self, data):
|
||||||
|
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):
|
||||||
|
id = session.get('user_id')
|
||||||
|
agent_manager.change_language(id, language)
|
||||||
|
|
||||||
|
def on_message(self, data):
|
||||||
|
print(f"Message from client: {data}")
|
||||||
|
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 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")
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
Markdown to HTML
|
||||||
|
'''
|
||||||
|
# 配置文件路径
|
||||||
|
MARKDOWN_DIR = 'books/markdown' # 存放 markdown 文件的文件夹
|
||||||
|
STATIC_DIR = 'static' # 存放生成的 HTML 和图片资源的文件夹
|
||||||
|
IMAGE_DIR = 'image' # 图片资源相对于 markdown 的位置
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/<course_id>-<filename>-markdown', methods=['GET'])
|
||||||
|
def convert_md(course_id, filename):
|
||||||
|
md_file_path = os.path.join(MARKDOWN_DIR,course_id, f'{filename}.md')
|
||||||
|
|
||||||
|
if not os.path.exists(md_file_path):
|
||||||
|
return jsonify({'error': 'Markdown file not found'}), 404
|
||||||
|
|
||||||
|
# 读取 markdown 文件内容
|
||||||
|
with open(md_file_path, 'r', encoding='utf-8') as file:
|
||||||
|
md_content = file.read()
|
||||||
|
|
||||||
|
# 将 markdown 转换为 HTML
|
||||||
|
html_content = markdown.markdown(md_content)
|
||||||
|
# 插入 CSS 样式,限制图片最大宽度为 100%,高度自动调整
|
||||||
|
html_with_styles = 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>
|
||||||
|
{html_content}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
# 保存 HTML 文件到 static 文件夹
|
||||||
|
html_output_path = os.path.join(STATIC_DIR, f'{filename}.html')
|
||||||
|
with open(html_output_path, 'w', encoding='utf-8') as html_file:
|
||||||
|
html_file.write(html_with_styles)
|
||||||
|
|
||||||
|
# 处理图片资源,将 image/xxx 文件夹拷贝到 static/image/xxx
|
||||||
|
image_source_dir = os.path.join(MARKDOWN_DIR,course_id, IMAGE_DIR, filename)
|
||||||
|
image_target_dir = os.path.join(STATIC_DIR, IMAGE_DIR, filename)
|
||||||
|
print(f"Copying image resources from {image_source_dir} to {image_target_dir}")
|
||||||
|
if os.path.exists(image_source_dir):
|
||||||
|
# 确保 static/image/xxx 目录存在
|
||||||
|
if not os.path.exists(image_target_dir):
|
||||||
|
os.makedirs(image_target_dir)
|
||||||
|
|
||||||
|
# 拷贝图片资源
|
||||||
|
for image_file in os.listdir(image_source_dir):
|
||||||
|
full_image_file_path = os.path.join(image_source_dir, image_file)
|
||||||
|
if os.path.isfile(full_image_file_path):
|
||||||
|
shutil.copy(full_image_file_path, image_target_dir)
|
||||||
|
|
||||||
|
return jsonify({'html_url': f'/static/{filename}.html'})
|
||||||
|
|
||||||
|
# 静态文件(HTML 和图片资源)的访问
|
||||||
|
@app.route('/static/<path:filename>')
|
||||||
|
def serve_static(filename):
|
||||||
|
return send_from_directory(STATIC_DIR, filename)
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
Vscode
|
||||||
|
'''
|
||||||
|
@app.route('/desktop/<user_id>/<course_id>/<chapter_id>')
|
||||||
|
def desktop(user_id, course_id, chapter_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
# return redirect(url_for('login'))
|
||||||
|
session['user_id'] = 'user_' + str(uuid.uuid4())
|
||||||
|
print("user "+ user_id + "uuid is"+ session['user_id'])
|
||||||
|
username2uuid[user_id] = session['user_id']
|
||||||
|
uuid2username[session['user_id']] = user_id
|
||||||
|
userid_recorder[user_id+'&'+course_id] = session['user_id']
|
||||||
|
|
||||||
|
# 在学习目录下创建一个名为 user_id_path 的文件夹
|
||||||
|
path_dir = os.path.join(student_workspace_root_path, user_id, course_id,chapter_id)
|
||||||
|
os.makedirs(path_dir, exist_ok=True)
|
||||||
|
# 在此文件夹内部创建一个.config文件,并写入 user_id=user_id\n path=path
|
||||||
|
config_path = os.path.join(path_dir, '.config')
|
||||||
|
if GLOBAL_CONFIG['VSCODE_WEB_PATH']['is_wsl']: # 如果是在 WSL 中运行,则将路径转换为 WSL 路径
|
||||||
|
path_dir = path_dir.replace('\\', '/')
|
||||||
|
path_dir = path_dir.replace(GLOBAL_CONFIG['VSCODE_WEB_PATH']['windows_path'] , GLOBAL_CONFIG['VSCODE_WEB_PATH']['wsl_path'])
|
||||||
|
with open(config_path, 'w', encoding='utf-8') as f:
|
||||||
|
tmpd = {'user_id': user_id, 'course_id': course_id, 'chapter_id': chapter_id, 'path': path_dir}
|
||||||
|
json.dump(tmpd, f)
|
||||||
|
print('---------------------')
|
||||||
|
print(config_path)
|
||||||
|
return render_template('desktop.html', vscode_web_url=VSCODE_WEB_URL, user_id=user_id, course_id=course_id, chapter_id=chapter_id, workspace_path=path_dir)
|
||||||
|
|
||||||
|
@app.route('/desktop_nouser/<course_id>/<chapter_id>')
|
||||||
|
def desktop_nouser(course_id, chapter_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
user_id = session['user_id']
|
||||||
|
username = uuid2username[user_id]
|
||||||
|
return redirect(url_for('desktop', user_id=username, course_id=course_id, chapter_id=chapter_id))
|
||||||
|
|
||||||
|
@app.route('/vscode_data', methods=['POST'])
|
||||||
|
def vscode_data():
|
||||||
|
data = request.json
|
||||||
|
config = data['config']
|
||||||
|
realtime_response(config,data)
|
||||||
|
print(f"Received data from VSCode: {data}")
|
||||||
|
return jsonify({"status": "success", "received": data})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
Login
|
||||||
|
|
||||||
|
'''
|
||||||
|
users_list = UserList()
|
||||||
|
course_list = CourseList()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/register')
|
||||||
|
def register():
|
||||||
|
return render_template('register.html')
|
||||||
|
|
||||||
|
@app.route('/register_teacher')
|
||||||
|
def register_teacher():
|
||||||
|
return render_template('register_teacher.html')
|
||||||
|
|
||||||
|
@app.route('/register_teacher_post', methods=['POST'])
|
||||||
|
def register_teacher_post():
|
||||||
|
data = request.get_json()
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
if users_list.has_user(username) is not None:
|
||||||
|
users_list.add_user(username, password, teacher=True)
|
||||||
|
create_user_json(username, USER_DATA_DIR)
|
||||||
|
return jsonify({'success': True, 'message': '注册成功'})
|
||||||
|
else: return jsonify({'success': False, 'message': '用户已存在,请更换用户名'})
|
||||||
|
|
||||||
|
@app.route('/register_post', methods=['POST'])
|
||||||
|
def register_post():
|
||||||
|
data = request.get_json()
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
if users_list.has_user(username) is not None:
|
||||||
|
users_list.add_user(username, password)
|
||||||
|
create_user_json(username, USER_DATA_DIR)
|
||||||
|
return jsonify({'success': True, 'message': '注册成功'})
|
||||||
|
else: return jsonify({'success': False, 'message': '用户已存在,请更换用户名'})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/login')
|
||||||
|
def login():
|
||||||
|
return render_template('login.html')
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return render_template('login.html')
|
||||||
|
else:
|
||||||
|
return redirect(url_for('/'))
|
||||||
|
|
||||||
|
@app.route('/login_post', methods=['POST'])
|
||||||
|
def login_post():
|
||||||
|
# 获取请求的 JSON 数据
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
|
||||||
|
pswd = users_list.get_user_pswd(username)
|
||||||
|
|
||||||
|
if pswd is None:
|
||||||
|
return jsonify({'success': False, 'message': '用户名或密码错误'})
|
||||||
|
if pswd != password:
|
||||||
|
return jsonify({'success': False, 'message': '用户名或密码错误'})
|
||||||
|
|
||||||
|
session['user_id'] = 'user_' + str(uuid.uuid4())
|
||||||
|
print("user "+ username + "uuid is"+ session['user_id'])
|
||||||
|
username2uuid[username] = session['user_id']
|
||||||
|
uuid2username[session['user_id']] = username
|
||||||
|
return jsonify({'success': True, 'message': '登录成功'})
|
||||||
|
|
||||||
|
@app.route('/login_teacher_post', methods=['POST'])
|
||||||
|
def login_teacher_post():
|
||||||
|
data = request.get_json()
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
pswd = users_list.get_user_pswd(username)
|
||||||
|
is_teacher = users_list.get_user_is_teacher(username)
|
||||||
|
if is_teacher is None or is_teacher == False:
|
||||||
|
return jsonify({'success': False, 'message': '用户名不存在或非教师账号'})
|
||||||
|
if pswd is None:
|
||||||
|
return jsonify({'success': False, 'message': '用户名或密码错误'})
|
||||||
|
if pswd != password:
|
||||||
|
return jsonify({'success': False, 'message': '用户名或密码错误'})
|
||||||
|
session['user_id'] = 'user_' + str(uuid.uuid4())
|
||||||
|
print("user "+ username + "uuid is"+ session['user_id'])
|
||||||
|
username2uuid[username] = session['user_id']
|
||||||
|
uuid2username[session['user_id']] = username
|
||||||
|
return jsonify({'success': True, 'message': '登录成功'})
|
||||||
|
|
||||||
|
@app.route('/teacherboard')
|
||||||
|
@require_role(roles="teacher")
|
||||||
|
def teacherboard():
|
||||||
|
return render_template('teacherboard.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
def logout():
|
||||||
|
session.pop('user_id', None)
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
@app.route('/get_session')
|
||||||
|
def get_session():
|
||||||
|
user_session = session.get('user_id', 'default_session')
|
||||||
|
return jsonify({"session": user_session})
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
DashBoard
|
||||||
|
'''
|
||||||
|
user_id2UserClass = {}
|
||||||
|
@app.route('/dashboard')
|
||||||
|
@require_role
|
||||||
|
def dashboard():
|
||||||
|
user_id = session['user_id']
|
||||||
|
username = uuid2username[user_id]
|
||||||
|
if (user_id not in user_id2UserClass):
|
||||||
|
user_id2UserClass[user_id] = load_user_from_json(username, user_data_dir = USER_DATA_DIR)
|
||||||
|
user_course_data = []
|
||||||
|
for course_id in user_id2UserClass[user_id].select_course:
|
||||||
|
course_brief_info = course_list.get_course_brief_info(course_id, load_course_from_json(course_id, course_data_dir = COURSE_DATA_DIR))
|
||||||
|
user_course_data.append(course_brief_info)
|
||||||
|
return render_template('dashboard.html',user_data = user_id2UserClass[user_id].to_json_without_dialog(), user_course_data = json.dumps(user_course_data))
|
||||||
|
|
||||||
|
'''
|
||||||
|
Course
|
||||||
|
'''
|
||||||
|
|
||||||
|
@app.route('/course/<course_id>')
|
||||||
|
@require_role
|
||||||
|
def course(course_id):
|
||||||
|
c = load_course_from_json(course_id, course_data_dir = COURSE_DATA_DIR)
|
||||||
|
return render_template('course.html', course_id=course_id, course_data=c)
|
||||||
|
|
||||||
|
@app.route('/select_course', methods=['POST'])
|
||||||
|
@require_role
|
||||||
|
def select_course():
|
||||||
|
user = user_id2UserClass[session['user_id']]
|
||||||
|
data = request.get_json()
|
||||||
|
course_id = data.get('course_id')
|
||||||
|
if course_id:
|
||||||
|
user.select_new_course(course_id, load_course_from_json(course_id, COURSE_DATA_DIR))
|
||||||
|
return jsonify({'success': True, 'message': '课程选择成功'})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
@require_role
|
||||||
|
def home_index():
|
||||||
|
selected_courses=[]
|
||||||
|
if ('user_id' in session):
|
||||||
|
if (session['user_id'] not in user_id2UserClass): return redirect(url_for('login'))
|
||||||
|
user = user_id2UserClass[session['user_id']]
|
||||||
|
for course_id in user.select_course:
|
||||||
|
selected_courses.append(course_id)
|
||||||
|
|
||||||
|
return render_template('index.html', courses_data = course_list, selected_courses =selected_courses)
|
||||||
|
|
||||||
|
# 一些app辅助函数,主要提供给Agent与数据库的交互能力
|
||||||
|
|
||||||
|
class MyFunction:
|
||||||
|
def save_chapter_memory(self, id, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal):
|
||||||
|
print("-=-=-=-=-=-=-=-")
|
||||||
|
print(id, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal)
|
||||||
|
username = uuid2username[id]
|
||||||
|
u = load_user_from_json(username, user_data_dir = USER_DATA_DIR)
|
||||||
|
u.save_chapter_memory(course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal)
|
||||||
|
|
||||||
|
app.my_function = MyFunction()
|
||||||
|
if __name__ == '__main__':
|
||||||
|
socketio.on_namespace(VSCodeNamespace('/vscode'))
|
||||||
|
socketio.on_namespace(AgentNamespace('/agent'))
|
||||||
|
socketio.run(app, host='0.0.0.0', port=5551, debug=False,allow_unsafe_werkzeug=True)
|
||||||
|
|
||||||
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
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
# Html/apps/bootstrap.py
|
||||||
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
@@ -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"))
|
||||||
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
|
|
||||||
@@ -84,6 +84,75 @@
|
|||||||
.details-content {
|
.details-content {
|
||||||
padding-right: 20px;
|
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 {
|
.add-chapter-btn {
|
||||||
background-color: #2575fc;
|
background-color: #2575fc;
|
||||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 912 KiB After Width: | Height: | Size: 912 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
@@ -4,7 +4,12 @@ let system_message_idx=0
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
data = window.appData;
|
data = window.appData;
|
||||||
console.log(data);
|
console.log(data);
|
||||||
socket = io('/agent?username='+data.username+'&folder='+data.folder);
|
socket = io('ws://localhost:5551/agent',{
|
||||||
|
query:{
|
||||||
|
username:data.username,
|
||||||
|
folder:data.folder
|
||||||
|
}
|
||||||
|
});
|
||||||
socket.on('connect', function() {
|
socket.on('connect', function() {
|
||||||
console.log('Connected to server');
|
console.log('Connected to server');
|
||||||
socket.emit('login',JSON.stringify(data));
|
socket.emit('login',JSON.stringify(data));
|
||||||
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');
|
||||||
|
}
|
||||||
@@ -66,7 +66,8 @@
|
|||||||
window.appData = {
|
window.appData = {
|
||||||
username:"{{user_id}}",
|
username:"{{user_id}}",
|
||||||
course_id:"{{course_id}}",
|
course_id:"{{course_id}}",
|
||||||
chapter_id:"{{chapter_id}}"
|
chapter_id:"{{chapter_id}}",
|
||||||
|
folder:"{{workspace_path}}"
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,44 +1,44 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh">
|
<html lang="zh">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>课程选择主页</title>
|
<title>课程选择主页</title>
|
||||||
<link rel="stylesheet" href="/static/css/index.css">
|
<link rel="stylesheet" href="/static/css/index.css">
|
||||||
</head>
|
</head>
|
||||||
<body onload="init_index()" >
|
<body onload="init_index()" >
|
||||||
{% include 'navbar.html' %} <!-- 引入navbar.html -->
|
{% include 'navbar.html' %} <!-- 引入navbar.html -->
|
||||||
<main>
|
<main>
|
||||||
<div class="course-selection">
|
<div class="course-selection">
|
||||||
<h2>课程列表</h2>
|
<h2>课程列表</h2>
|
||||||
<div class="course-list">
|
<div class="course-list">
|
||||||
{% for course_id, course in courses_data.items() %}
|
{% for course_id, course in courses_data.items() %}
|
||||||
<div class="course-card" onclick="show_course_details('{{ course_id }}')">
|
<div class="course-card" onclick="show_course_details('{{ course_id }}')">
|
||||||
<img src="{{ course.course_img_path }}" alt="{{ course.course_name }}">
|
<img src="{{ course.course_img_path }}" alt="{{ course.course_name }}">
|
||||||
<h3>{{ course.course_name }}</h3>
|
<h3>{{ course.course_name }}</h3>
|
||||||
<p>{{ course.course_description }}</p>
|
<p>{{ course.course_description }}</p>
|
||||||
{% if course_id in selected_courses %}
|
{% if course_id in selected_courses %}
|
||||||
<button class="selected-button">已选择</button>
|
<button class="selected-button">已选择</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="select-button" data-course-id="{{course_id}}">选择课程</button>
|
<button class="select-button" data-course-id="{{course_id}}">选择课程</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>版权所有 © 2024 “华实伴学君”——教学练评一体的虚拟编码助教</p>
|
<p>版权所有 © 2024 “华实伴学君”——教学练评一体的虚拟编码助教</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
<script>
|
<script>
|
||||||
window.appData = {
|
window.appData = {
|
||||||
user_selected_courses: JSON.parse(`{{selected_courses}}`.replace(/"/g, "\""))
|
user_selected_courses: JSON.parse(`{{selected_courses}}`.replace(/"/g, "\""))
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/js/index.js"></script>
|
<script src="/static/js/index.js"></script>
|
||||||
</html>
|
</html>
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<link rel="stylesheet" href="/static/css/teacherboard.css">
|
<link rel="stylesheet" href="/static/css/teacherboard.css">
|
||||||
<link rel="stylesheet" href="/static/css/dashboard.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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
<ul id="chapter-list">
|
<ul id="chapter-list">
|
||||||
<!-- 章节列表会在点击课程时动态生成 -->
|
<!-- 章节列表会在点击课程时动态生成 -->
|
||||||
</ul>
|
</ul>
|
||||||
<button class="add-chapter-btn" onclick="addChapter()">新增章节</button>
|
<button class="add-chapter-btn" onclick="showInputForNewChapter()">新增章节</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
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
@@ -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
@@ -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
@@ -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
@@ -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})
|
||||||
31
Html/bootstrap.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import sys, os
|
||||||
|
|
||||||
|
def bootstrap_paths():
|
||||||
|
"""
|
||||||
|
让 'AlgoriAgent'(位于 repo_root/AlgoriAgent)可被 import;
|
||||||
|
并提供 STUDENT_WORKSPACE_ROOT 的默认值(repo_root/study)。
|
||||||
|
适配当前工厂位置:repo_root/Html/apps/__init__.py
|
||||||
|
"""
|
||||||
|
here = Path(__file__).resolve()
|
||||||
|
# repo_root = .../Html/apps/../../ => parents[2]
|
||||||
|
repo_root = here.parents[2]
|
||||||
|
|
||||||
|
# 兜底:如果目录结构变化,尝试向上搜索包含 AlgoriAgent 的目录作为 repo_root
|
||||||
|
if not (repo_root / "AlgoriAgent").exists():
|
||||||
|
for p in here.parents:
|
||||||
|
if (p / "AlgoriAgent").is_dir():
|
||||||
|
repo_root = p
|
||||||
|
break
|
||||||
|
|
||||||
|
# 确保仓库根在 sys.path(这样 'AlgoriAgent' 顶层包可被发现)
|
||||||
|
repo_root_str = str(repo_root)
|
||||||
|
if repo_root_str not in sys.path:
|
||||||
|
sys.path.insert(0, repo_root_str)
|
||||||
|
|
||||||
|
# 可选:如果你还有其它顶层包,也可在此处继续插入
|
||||||
|
|
||||||
|
# 默认学习工作区目录(也可放到 config.ini / 环境变量覆盖)
|
||||||
|
os.environ.setdefault("STUDENT_WORKSPACE_ROOT", str(repo_root / "study"))
|
||||||
|
|
||||||
|
return repo_root # 方便调试/日志
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="markdown-body">
|
||||||
<h1>二分查找与二分答案</h1>
|
<h1>二分查找与二分答案</h1>
|
||||||
<h2>二分查找</h2>
|
<h2>二分查找</h2>
|
||||||
<h3>引入</h3>
|
<h3>引入</h3>
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 closeCourseDetails() {
|
|
||||||
document.getElementById('courseDetails').classList.remove('open');
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
chapterItem.innerHTML = `
|
|
||||||
<strong>${chapter.title}</strong>
|
|
||||||
<ul>
|
|
||||||
${chapter.lessons.map(lesson => `<li>${lesson} <button onclick="editLesson('${lesson}')">编辑</button></li>`).join('')}
|
|
||||||
</ul>
|
|
||||||
<button onclick="addLesson('${chapter.title}')">新增课时</button>
|
|
||||||
`;
|
|
||||||
chapterList.appendChild(chapterItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addChapter() {
|
|
||||||
// 新增章节的逻辑
|
|
||||||
alert("新增章节功能");
|
|
||||||
}
|
|
||||||
|
|
||||||
function addLesson(chapterTitle) {
|
|
||||||
// 新增课时的逻辑
|
|
||||||
alert("新增课时功能: " + chapterTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
function editLesson(lesson) {
|
|
||||||
// 编辑课时的逻辑
|
|
||||||
alert("编辑课时: " + lesson);
|
|
||||||
}
|
|
||||||
6
Html/wsgi.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# wsgi.py
|
||||||
|
from bootstrap import bootstrap_paths
|
||||||
|
bootstrap_paths()
|
||||||
|
from apps import create_app
|
||||||
|
# 提供一个可被 WSGI/进程管理器导入的 app 对象
|
||||||
|
app = create_app()
|
||||||