Compare commits
3 Commits
code-serve
...
fc7c6708b4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc7c6708b4 | ||
|
|
a041650a54 | ||
|
|
ad7fd04f19 |
1
Html/.gitattributes
vendored
Normal file
1
Html/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Html/config.ini merge=ours
|
||||
480
Html/app_back.py
480
Html/app_back.py
@@ -1,480 +0,0 @@
|
||||
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)
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
# HelpTip 组件文档
|
||||
|
||||
## 功能介绍
|
||||
|
||||
HelpTip 是一个通用的提示框组件,用于在鼠标悬停时显示提示信息。它具有以下特点:
|
||||
|
||||
- 提示框跟随鼠标移动
|
||||
- 支持多行文本
|
||||
- 自动防出界
|
||||
- 平滑的显示/隐藏动画
|
||||
- 支持在不同页面复用
|
||||
- 代码标准化,易于维护
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
/static/
|
||||
├── css/
|
||||
│ └── helpTip.css # 样式文件
|
||||
└── js/
|
||||
└── helpTip.js # 核心功能文件
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 引入文件
|
||||
|
||||
在需要使用 HelpTip 的 HTML 页面中引入以下文件:
|
||||
|
||||
```html
|
||||
<!-- 样式文件 -->
|
||||
<link rel="stylesheet" href="/static/css/helpTip.css">
|
||||
|
||||
<!-- 功能文件 -->
|
||||
<script src="/static/js/helpTip.js"></script>
|
||||
```
|
||||
|
||||
### 2. 基本用法
|
||||
|
||||
```javascript
|
||||
// 获取需要绑定提示框的元素
|
||||
const element = document.getElementById('target-element');
|
||||
|
||||
// 绑定提示框
|
||||
attachImmediateTooltip(element, '这是一条提示信息');
|
||||
```
|
||||
|
||||
### 3. 支持换行的提示信息
|
||||
|
||||
```javascript
|
||||
attachImmediateTooltip(element, `这是一条\n多行提示信息\n\n支持换行符`);
|
||||
```
|
||||
|
||||
### 4. 与问号图标配合使用
|
||||
|
||||
```html
|
||||
<!-- HTML -->
|
||||
<span>这是一个元素<span class="help-icon">?</span></span>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// JavaScript
|
||||
const helpIcon = document.querySelector('.help-icon');
|
||||
attachImmediateTooltip(helpIcon, '这是一条详细的提示信息');
|
||||
```
|
||||
|
||||
## API 说明
|
||||
|
||||
### attachImmediateTooltip(anchorEl, text)
|
||||
|
||||
绑定提示框到指定元素。
|
||||
|
||||
#### 参数
|
||||
|
||||
- `anchorEl` (HTMLElement): 触发提示框的元素
|
||||
- `text` (string): 提示框显示的文本内容,支持换行符 `\n`
|
||||
|
||||
#### 返回值
|
||||
|
||||
- `Function`: 清理函数,用于移除事件监听器
|
||||
|
||||
## 样式说明
|
||||
|
||||
### 问号图标样式
|
||||
|
||||
| 类名 | 说明 |
|
||||
|------|------|
|
||||
| `.help-icon` | 问号图标基础样式 |
|
||||
| `.help-icon:hover` | 问号图标悬停样式 |
|
||||
|
||||
### 提示框样式
|
||||
|
||||
| 类名 | 说明 |
|
||||
|------|------|
|
||||
| `.tooltip-portal` | 提示框基础样式 |
|
||||
| `.tooltip-portal.show` | 提示框显示状态 |
|
||||
| `.tooltip-portal.fade` | 提示框动画效果 |
|
||||
|
||||
## 示例场景
|
||||
|
||||
### 1. 进度条提示
|
||||
|
||||
```javascript
|
||||
// 为进度条的每个阶段绑定提示框
|
||||
for (let i = 0; i < progressItems.length; i++) {
|
||||
const item = progressItems[i];
|
||||
attachImmediateTooltip(item, `阶段 ${i + 1}: ${item.title}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 表单字段提示
|
||||
|
||||
```javascript
|
||||
// 为表单字段绑定提示框
|
||||
const formFields = document.querySelectorAll('.form-field');
|
||||
formFields.forEach(field => {
|
||||
const tip = field.getAttribute('data-tip');
|
||||
if (tip) {
|
||||
attachImmediateTooltip(field, tip);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 工具栏图标提示
|
||||
|
||||
```javascript
|
||||
// 为工具栏图标绑定提示框
|
||||
const toolbarIcons = document.querySelectorAll('.toolbar-icon');
|
||||
toolbarIcons.forEach(icon => {
|
||||
const title = icon.getAttribute('title');
|
||||
if (title) {
|
||||
attachImmediateTooltip(icon, title);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保在 DOM 加载完成后再调用 `attachImmediateTooltip` 函数
|
||||
2. 如果元素是动态生成的,需要在元素生成后再绑定提示框
|
||||
3. 提示框会自动添加到 `document.body` 中,不会被父元素的 `overflow` 属性裁切
|
||||
4. 提示框的 `z-index` 为 999999,确保它能覆盖所有元素
|
||||
5. 提示框默认支持多行文本,使用 `white-space: pre-wrap` 样式
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
- Chrome/Edge (最新版)
|
||||
- Firefox (最新版)
|
||||
- Safari (最新版)
|
||||
|
||||
## 代码优化建议
|
||||
|
||||
1. 对于大量元素需要绑定提示框的情况,建议使用事件委托
|
||||
2. 如果页面中不再需要使用提示框,可以调用返回的清理函数移除事件监听器
|
||||
3. 提示文本应简洁明了,避免过长
|
||||
|
||||
## 更新日志
|
||||
|
||||
- v1.0.0: 初始版本,实现了基本的提示框功能
|
||||
- v1.0.1: 优化了样式,增加了动画效果
|
||||
- v1.0.2: 支持多行文本,自动防出界
|
||||
|
||||
## 贡献指南
|
||||
|
||||
如果您发现问题或有改进建议,欢迎提出 Issue 或 Pull Request。
|
||||
@@ -70,8 +70,10 @@ class ASEngineClient:
|
||||
# ACK事件处理
|
||||
@self.sio.on('ack', namespace=self.namespace)
|
||||
def on_ack(data):
|
||||
print(f"Received ACK: {data}")
|
||||
ack_id = data.get('ack_id')
|
||||
if ack_id and ack_id in self.ack_events:
|
||||
print(f"Received ACK for message: {ack_id}")
|
||||
event = self.ack_events[ack_id]
|
||||
event.set() # 触发事件,通知等待的线程
|
||||
|
||||
|
||||
@@ -49,8 +49,7 @@ class ChatManager:
|
||||
self.chapter_chain = None
|
||||
self.function_manager = FunctionManager()
|
||||
self.bb = None
|
||||
self.chat_historys = []
|
||||
self.scores = []
|
||||
self.chat_historys = None
|
||||
self.vscode_ws = None
|
||||
self._vscode_queue = deque()
|
||||
self._lock = Semaphore(1)
|
||||
@@ -120,7 +119,6 @@ class ChatManager:
|
||||
|
||||
|
||||
def next_chapter(self):
|
||||
print(f"now next chapter {self.chapter_chain_now} and len {len(self.chapter_chain)}")
|
||||
assert self.chapter_chain_now + 1 < len(self.chapter_chain), "chapter_chain_now out of range"
|
||||
self.chapter_chain_now += 1
|
||||
self.bb.next_chapter()
|
||||
@@ -131,11 +129,10 @@ class ChatManager:
|
||||
self.chapter_chain_now = chapter_index
|
||||
self.chapter_chain[chapter_index].Focus()
|
||||
|
||||
def load_now_chapter(self, load_code = True):
|
||||
def load_now_chapter(self):
|
||||
print(f"now load now chapter markdown {self.chapter_chain_now}")
|
||||
self.ase_client.loadmarkdown(self.chapter_chain[self.chapter_chain_now].chapter, self.chapter_chain[self.chapter_chain_now].chapter_prompt, self.chapter_chain[self.chapter_chain_now].score_prompt)
|
||||
if load_code:
|
||||
self.load_code_in_markdown()
|
||||
self.load_code_in_markdown()
|
||||
|
||||
|
||||
def upload_bb(self):
|
||||
|
||||
@@ -2,28 +2,6 @@ import requests
|
||||
from flask_socketio import Namespace
|
||||
|
||||
|
||||
def clear_user_session(asengine_url, asengine_token, user_id):
|
||||
'''
|
||||
快速清理用户会话的函数
|
||||
'''
|
||||
try:
|
||||
base_url = asengine_url
|
||||
clear_url = f"{base_url}/api/sync/clear_user_session"
|
||||
payload = {
|
||||
'namespace_url': asengine_token, # WebSocket命名空间URL
|
||||
'user_id': user_id, # 用户ID,对应session_info.id
|
||||
'clear_type': 'all' # 清理类型,默认为全部
|
||||
}
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
response = requests.post(clear_url, json=payload, headers=headers)
|
||||
response.raise_for_status() # 检查请求是否成功
|
||||
print(f"User session {user_id} cleared successfully. Response: {response.json()}")
|
||||
except Exception as e:
|
||||
print(f"Error clearing user session: {e}")
|
||||
|
||||
|
||||
def on_connect_to_ase(client, entry, init_data, **kwargs):
|
||||
'''
|
||||
此时刚刚建立好aseclient连接,并向前端发送打开code-server-like指令。
|
||||
@@ -31,14 +9,25 @@ def on_connect_to_ase(client, entry, init_data, **kwargs):
|
||||
namespace_entry, ase_client = entry
|
||||
namespace_entry.emit('open-iframe',"", room=init_data['room'], namespace='/agent')
|
||||
namespace_entry.emit('message', "Agents服务已连接, 加载教案中", room=init_data['room'], namespace='/agent')
|
||||
|
||||
restart = init_data.get("restart", False)
|
||||
if restart: # 重启时,从-1章进入 0章
|
||||
ase_client.chatmanager.next_chapter()
|
||||
ase_client.chatmanager.load_now_chapter(load_code=restart)
|
||||
print("load_now_chapter with restart:", restart)
|
||||
if restart:
|
||||
ase_client.send_text("chapter-start", "")
|
||||
|
||||
if init_data.get("restart", False):
|
||||
try:
|
||||
base_url = namespace_entry.app.config['ASE_ENGINE_URL']
|
||||
clear_url = f"{base_url}/clear_user_session"
|
||||
payload = {
|
||||
'namespace_url': namespace_entry.app.config['ASE_ENGINE_URL_TOKEN'], # WebSocket命名空间URL
|
||||
'user_id': init_data['room'], # 用户ID,对应session_info.id
|
||||
'clear_type': 'all' # 清理类型,默认为全部
|
||||
}
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
response = requests.post(clear_url, json=payload, headers=headers)
|
||||
response.raise_for_status() # 检查请求是否成功
|
||||
except Exception as e:
|
||||
print(f"Error clearing user session: {e}")
|
||||
ase_client.chatmanager.load_now_chapter()
|
||||
ase_client.send_text("chapter-start", "")
|
||||
namespace_entry.emit('message', "教案加载完毕", room=init_data['room'], namespace='/agent')
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ def save_learning_progress(chatmanager, user_id: str) -> bool:
|
||||
"chat_historys": chatmanager.chat_historys,
|
||||
"scores": chatmanager.scores,
|
||||
"multiagents_dialogs": his_data,
|
||||
"chapter_chain_length": len(chatmanager.chapter_chain),
|
||||
"updated_at": datetime.now()
|
||||
}
|
||||
|
||||
@@ -93,7 +92,6 @@ def get_multiagents_dialog_list(user_id: str) -> List[Dict]:
|
||||
|
||||
# 检查响应
|
||||
if response.status_code == 200:
|
||||
current_app.logger.info(f"获取MultiAgents对话列表成功: user_id={user_id}, json={response.json()}")
|
||||
return response.json()
|
||||
else:
|
||||
current_app.logger.error(f"获取MultiAgents对话列表失败: HTTP {response.status_code}")
|
||||
@@ -147,7 +145,6 @@ def load_learning_progress(user_id: str, material_id: str, chapter_name: str, le
|
||||
"chat_historys": [],
|
||||
"scores": [],
|
||||
"chapter_chain_now": None,
|
||||
"chapter_chain_length": 0,
|
||||
"updated_at": datetime.now().isoformat()
|
||||
},
|
||||
"message": "未找到学习记录,返回默认值"
|
||||
@@ -166,7 +163,6 @@ def load_learning_progress(user_id: str, material_id: str, chapter_name: str, le
|
||||
"chat_historys": [],
|
||||
"scores": [],
|
||||
"chapter_chain_now": None,
|
||||
"chapter_chain_length": 0,
|
||||
"updated_at": datetime.now().isoformat()
|
||||
},
|
||||
"message": error_msg
|
||||
|
||||
@@ -24,6 +24,7 @@ def load_markdown_file(course_id, chapter_name, lesson_name):
|
||||
break
|
||||
if file_link is None:
|
||||
return None
|
||||
print(f"load_markdown_file: {file_link}")
|
||||
return get_text_file(file_link)
|
||||
|
||||
def convert_markdown_to_html(md_file: str) -> str:
|
||||
|
||||
@@ -23,8 +23,7 @@ from ..services.asectrl_service import (
|
||||
receive_ase_process,
|
||||
receive_ase_judge,
|
||||
receive_ase_next_chapter,
|
||||
receive_ase_chapter_score,
|
||||
clear_user_session
|
||||
receive_ase_chapter_score
|
||||
)
|
||||
|
||||
|
||||
@@ -63,24 +62,32 @@ class VSCodeNamespace(Namespace):
|
||||
|
||||
|
||||
class AgentNamespace(Namespace):
|
||||
def _load_learning_progress(self, chatmanager, user_id, course_id, chapter_name, lesson_name, continue_learn):
|
||||
"""加载并恢复用户学习进度
|
||||
def on_login(self, data):
|
||||
self.app = current_app
|
||||
ex = current_app.extensions
|
||||
uuid2username = ex["uuid2username"]
|
||||
backboard_manager = ex["backboard_manager"]
|
||||
data = json.loads(data)
|
||||
user_uuid = data['user_uuid']
|
||||
course_id = data['course_id']
|
||||
lesson_name = data['lesson_name']
|
||||
chapter_name = data['chapter_name']
|
||||
user_id = uuid2username[user_uuid]
|
||||
session['user_uuid'] = user_uuid
|
||||
join_room(user_uuid, namespace='/agent') # 将该用户加入以user_id为名的room
|
||||
print(f'User connected with session user_uuid: {user_uuid}')
|
||||
|
||||
user_uuid2chatmanager = ex["user_uuid2chatmanager"]
|
||||
user_uuid2ase_client = ex["user_uuid2ase_client"]
|
||||
if (user_uuid2ase_client.get(user_uuid,None) is not None and user_uuid2ase_client[user_uuid].connected):
|
||||
user_uuid2ase_client[user_uuid].disconnect()
|
||||
chatmanager = user_uuid2chatmanager[user_uuid]
|
||||
chatmanager.function_manager.add_function(judge, {'course_id': course_id, 'root_path': f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}'})
|
||||
chatmanager.function_manager.add_function(next_chapter, {'user_uuid': user_uuid})
|
||||
|
||||
Args:
|
||||
chatmanager: 聊天管理器实例
|
||||
user_id: 用户ID
|
||||
course_id: 课程ID
|
||||
chapter_name: 章节名称
|
||||
lesson_name: 课时名称
|
||||
continue_learn: 是否继续学习
|
||||
|
||||
Returns:
|
||||
int: 需要跳过的章节数
|
||||
"""
|
||||
need_skip_chapters = 0
|
||||
|
||||
if continue_learn: # 尝试加载用户学习进度
|
||||
if not chatmanager.restart:# 尝试加载用户学习进度
|
||||
progress_result = load_learning_progress(user_id, course_id, chapter_name, lesson_name)
|
||||
need_skip_chapters = 0
|
||||
|
||||
if progress_result['exists']:
|
||||
progress = progress_result['data']
|
||||
@@ -104,52 +111,18 @@ class AgentNamespace(Namespace):
|
||||
# 使用默认值,确保有完整的结构
|
||||
chatmanager.chat_historys = progress_result['data']['chat_historys']
|
||||
chatmanager.scores = progress_result['data']['scores']
|
||||
|
||||
return need_skip_chapters
|
||||
|
||||
def on_login(self, data):
|
||||
self.app = current_app
|
||||
ex = current_app.extensions
|
||||
uuid2username = ex["uuid2username"]
|
||||
backboard_manager = ex["backboard_manager"]
|
||||
data = json.loads(data)
|
||||
user_uuid = data['user_uuid']
|
||||
course_id = data['course_id']
|
||||
lesson_name = data['lesson_name']
|
||||
chapter_name = data['chapter_name']
|
||||
user_id = uuid2username[user_uuid]
|
||||
session['user_uuid'] = user_uuid
|
||||
join_room(user_uuid, namespace='/agent') # 将该用户加入以user_id为名的room
|
||||
print(f'User connected with session user_uuid: {user_uuid}')
|
||||
|
||||
user_uuid2chatmanager = ex["user_uuid2chatmanager"]
|
||||
user_uuid2ase_client = ex["user_uuid2ase_client"]
|
||||
if (user_uuid2ase_client.get(user_uuid,None) is not None and user_uuid2ase_client[user_uuid].connected):
|
||||
user_uuid2ase_client[user_uuid].disconnect()
|
||||
chatmanager = user_uuid2chatmanager[user_uuid]
|
||||
chatmanager.function_manager.add_function(judge, {'course_id': course_id, 'root_path': f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}'})
|
||||
chatmanager.function_manager.add_function(next_chapter, {'user_uuid': user_uuid})
|
||||
|
||||
clear_user_session(current_app.config["ASE_ENGINE_URL"], current_app.config["ASE_ENGINE_URL_TOKEN"], user_id)
|
||||
continue_learn = not chatmanager.restart
|
||||
need_skip_chapters = self._load_learning_progress(chatmanager, user_id, course_id, chapter_name, lesson_name, continue_learn)
|
||||
|
||||
fmd,fmdp,fsp = load_full_markdown_file(course_id, chapter_name, lesson_name)
|
||||
user_uuid2ase_client[user_uuid] = HSAEngineClient(current_app.config["ASE_ENGINE_URL"], current_app.config["ASE_ENGINE_NAMESPACE"],id=user_id, chatmanager = chatmanager)
|
||||
chatmanager.init(user_uuid, user_uuid2ase_client[user_uuid],
|
||||
fmd, fmdp, fsp, course_id, chapter_name, lesson_name, max_iter=5, app=current_app, socketio=self)
|
||||
backboard_manager.add_backboard(user_uuid, user_id, course_id,
|
||||
lesson_name, root_path=f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}')
|
||||
chatmanager.init(user_uuid, user_uuid2ase_client[user_uuid], fmd, fmdp, fsp, course_id, chapter_name, lesson_name, max_iter=5, app=current_app, socketio=self)
|
||||
backboard_manager.add_backboard(user_uuid, user_id, course_id, lesson_name, root_path=f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}')
|
||||
chatmanager.bb = backboard_manager.get_backboard(user_uuid)
|
||||
# 如果有保存的进度,需要跳过已经完成的章节、无需清空代码、对话历史恢复
|
||||
if continue_learn:
|
||||
for _ in range(need_skip_chapters):
|
||||
chatmanager.next_chapter()
|
||||
emit('next_chapter', room=user_uuid, namespace='/agent')
|
||||
# 恢复对话历史到前端 - 使用批量发送消息列表
|
||||
if chatmanager.chat_historys:
|
||||
emit('messages', chatmanager.chat_historys, room=user_uuid, namespace='/agent')
|
||||
if not progress:
|
||||
chatmanager.next_chapter()
|
||||
|
||||
# 如果有保存的进度,需要跳过已经完成的章节
|
||||
for _ in range(need_skip_chapters):
|
||||
chatmanager.next_chapter()
|
||||
self.chatmanager = chatmanager
|
||||
self.ase_client = user_uuid2ase_client[user_uuid]
|
||||
user_uuid2ase_client[user_uuid].register_on_connect_entry(
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,218 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
* https://github.com/chjj/term.js
|
||||
* @license MIT
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* Originally forked from (with the author's permission):
|
||||
* Fabrice Bellard's javascript vt100 for jslinux:
|
||||
* http://bellard.org/jslinux/
|
||||
* Copyright (c) 2011 Fabrice Bellard
|
||||
* The original design remains. The terminal itself
|
||||
* has been extended to include xterm CSI codes, among
|
||||
* other features.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default styles for xterm.js
|
||||
*/
|
||||
|
||||
.xterm {
|
||||
cursor: text;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.xterm.focus,
|
||||
.xterm:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-helpers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
/**
|
||||
* The z-index of the helpers must be higher than the canvases in order for
|
||||
* IMEs to appear on top.
|
||||
*/
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.xterm .xterm-helper-textarea {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
left: -9999em;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
z-index: -5;
|
||||
/** Prevent wrapping so the IME appears against the textarea at the correct position */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.xterm .composition-view {
|
||||
/* TODO: Composition position got messed up somewhere */
|
||||
background: #000;
|
||||
color: #FFF;
|
||||
display: none;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.xterm .composition-view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
/* On OS X this is required in order for the scroll bar to appear fully opaque */
|
||||
background-color: #000;
|
||||
overflow-y: scroll;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-scroll-area {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.xterm-char-measure-element {
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -9999em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.xterm.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.xterm.xterm-cursor-pointer,
|
||||
.xterm .xterm-cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.xterm.column-select.focus {
|
||||
/* Column selection mode */
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility:not(.debug),
|
||||
.xterm .xterm-message {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility-tree {
|
||||
user-select: text;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xterm-dim {
|
||||
/* Dim should not apply to background, so the opacity of the foreground color is applied
|
||||
* explicitly in the generated class and reset to 1 here */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xterm-underline-1 { text-decoration: underline; }
|
||||
.xterm-underline-2 { text-decoration: double underline; }
|
||||
.xterm-underline-3 { text-decoration: wavy underline; }
|
||||
.xterm-underline-4 { text-decoration: dotted underline; }
|
||||
.xterm-underline-5 { text-decoration: dashed underline; }
|
||||
|
||||
.xterm-overline {
|
||||
text-decoration: overline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
|
||||
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
|
||||
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
|
||||
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
|
||||
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
|
||||
|
||||
.xterm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
||||
z-index: 6;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.xterm-decoration-overview-ruler {
|
||||
z-index: 8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm-decoration-top {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fit = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function proposeGeometry(term) {
|
||||
if (!term.element.parentElement) {
|
||||
return null;
|
||||
}
|
||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement);
|
||||
var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
var elementStyle = window.getComputedStyle(term.element);
|
||||
var elementPadding = {
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
||||
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
||||
};
|
||||
var elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
var elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
var availableHeight = parentElementHeight - elementPaddingVer;
|
||||
var availableWidth = parentElementWidth - elementPaddingHor - term._core.viewport.scrollBarWidth;
|
||||
var geometry = {
|
||||
cols: Math.floor(availableWidth / term._core.renderer.dimensions.actualCellWidth),
|
||||
rows: Math.floor(availableHeight / term._core.renderer.dimensions.actualCellHeight)
|
||||
};
|
||||
return geometry;
|
||||
}
|
||||
exports.proposeGeometry = proposeGeometry;
|
||||
function fit(term) {
|
||||
var geometry = proposeGeometry(term);
|
||||
if (geometry) {
|
||||
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
|
||||
term._core.renderer.clear();
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.fit = fit;
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.proposeGeometry = function () {
|
||||
return proposeGeometry(this);
|
||||
};
|
||||
terminalConstructor.prototype.fit = function () {
|
||||
fit(this);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=fit.js.map
|
||||
@@ -1,27 +0,0 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fullscreen = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function toggleFullScreen(term, fullscreen) {
|
||||
var fn;
|
||||
if (typeof fullscreen === 'undefined') {
|
||||
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
|
||||
}
|
||||
else if (!fullscreen) {
|
||||
fn = 'remove';
|
||||
}
|
||||
else {
|
||||
fn = 'add';
|
||||
}
|
||||
term.element.classList[fn]('fullscreen');
|
||||
}
|
||||
exports.toggleFullScreen = toggleFullScreen;
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.toggleFullScreen = function (fullscreen) {
|
||||
toggleFullScreen(this, fullscreen);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=fullscreen.js.map
|
||||
@@ -1,126 +0,0 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.search = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var SearchHelper = (function () {
|
||||
function SearchHelper(_terminal) {
|
||||
this._terminal = _terminal;
|
||||
}
|
||||
SearchHelper.prototype.findNext = function (term) {
|
||||
if (!term || term.length === 0) {
|
||||
return false;
|
||||
}
|
||||
var result;
|
||||
var startRow = this._terminal._core.buffer.ydisp;
|
||||
if (this._terminal._core.selectionManager.selectionEnd) {
|
||||
startRow = this._terminal._core.selectionManager.selectionEnd[1];
|
||||
}
|
||||
for (var y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!result) {
|
||||
for (var y = 0; y < startRow; y++) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._selectResult(result);
|
||||
};
|
||||
SearchHelper.prototype.findPrevious = function (term) {
|
||||
if (!term || term.length === 0) {
|
||||
return false;
|
||||
}
|
||||
var result;
|
||||
var startRow = this._terminal._core.buffer.ydisp;
|
||||
if (this._terminal._core.selectionManager.selectionStart) {
|
||||
startRow = this._terminal._core.selectionManager.selectionStart[1];
|
||||
}
|
||||
for (var y = startRow - 1; y >= 0; y--) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!result) {
|
||||
for (var y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._selectResult(result);
|
||||
};
|
||||
SearchHelper.prototype._findInLine = function (term, y) {
|
||||
var lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();
|
||||
var lowerTerm = term.toLowerCase();
|
||||
var searchIndex = lowerStringLine.indexOf(lowerTerm);
|
||||
if (searchIndex >= 0) {
|
||||
var line = this._terminal._core.buffer.lines.get(y);
|
||||
for (var i = 0; i < searchIndex; i++) {
|
||||
var charData = line[i];
|
||||
var char = charData[1];
|
||||
if (char.length > 1) {
|
||||
searchIndex -= char.length - 1;
|
||||
}
|
||||
var charWidth = charData[2];
|
||||
if (charWidth === 0) {
|
||||
searchIndex++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
term: term,
|
||||
col: searchIndex,
|
||||
row: y
|
||||
};
|
||||
}
|
||||
};
|
||||
SearchHelper.prototype._selectResult = function (result) {
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
|
||||
this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp);
|
||||
return true;
|
||||
};
|
||||
return SearchHelper;
|
||||
}());
|
||||
exports.SearchHelper = SearchHelper;
|
||||
|
||||
},{}],2:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var SearchHelper_1 = require("./SearchHelper");
|
||||
function findNext(terminal, term) {
|
||||
var addonTerminal = terminal;
|
||||
if (!addonTerminal.__searchHelper) {
|
||||
addonTerminal.__searchHelper = new SearchHelper_1.SearchHelper(addonTerminal);
|
||||
}
|
||||
return addonTerminal.__searchHelper.findNext(term);
|
||||
}
|
||||
exports.findNext = findNext;
|
||||
function findPrevious(terminal, term) {
|
||||
var addonTerminal = terminal;
|
||||
if (!addonTerminal.__searchHelper) {
|
||||
addonTerminal.__searchHelper = new SearchHelper_1.SearchHelper(addonTerminal);
|
||||
}
|
||||
return addonTerminal.__searchHelper.findPrevious(term);
|
||||
}
|
||||
exports.findPrevious = findPrevious;
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.findNext = function (term) {
|
||||
return findNext(this, term);
|
||||
};
|
||||
terminalConstructor.prototype.findPrevious = function (term) {
|
||||
return findPrevious(this, term);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
},{"./SearchHelper":1}]},{},[2])(2)
|
||||
});
|
||||
//# sourceMappingURL=search.js.map
|
||||
@@ -1,41 +0,0 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.webLinks = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var protocolClause = '(https?:\\/\\/)';
|
||||
var domainCharacterSet = '[\\da-z\\.-]+';
|
||||
var negatedDomainCharacterSet = '[^\\da-z\\.-]+';
|
||||
var domainBodyClause = '(' + domainCharacterSet + ')';
|
||||
var tldClause = '([a-z\\.]{2,6})';
|
||||
var ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||
var localHostClause = '(localhost)';
|
||||
var portClause = '(:\\d{1,5})';
|
||||
var hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||
var pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
|
||||
var queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||
var queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||
var hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
|
||||
var negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
|
||||
var bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
|
||||
var start = '(?:^|' + negatedDomainCharacterSet + ')(';
|
||||
var end = ')($|' + negatedPathCharacterSet + ')';
|
||||
var strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
|
||||
function handleLink(event, uri) {
|
||||
window.open(uri, '_blank');
|
||||
}
|
||||
function webLinksInit(term, handler, options) {
|
||||
if (handler === void 0) { handler = handleLink; }
|
||||
if (options === void 0) { options = {}; }
|
||||
options.matchIndex = 1;
|
||||
term.registerLinkMatcher(strictUrlRegex, handler, options);
|
||||
}
|
||||
exports.webLinksInit = webLinksInit;
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.webLinksInit = function (handler, options) {
|
||||
webLinksInit(this, handler, options);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=webLinks.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -107,7 +107,13 @@
|
||||
.btn-group-toggle .btn {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
/* 主容器,使用 grid 布局 */
|
||||
#leftSidebar, .vscode-web, #rightSidebar {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
/* 主容器,使用 grid 布局 */
|
||||
#maxcontainer {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 5px 3fr 5px 2fr; /* 默认宽度比例 */
|
||||
@@ -116,20 +122,7 @@
|
||||
|
||||
/* 各区域基础样式 */
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* 隐藏溢出内容,让内部元素单独滚动 */
|
||||
}
|
||||
|
||||
/* 左侧边栏样式 */
|
||||
#leftSidebar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 中间和右侧边栏样式 */
|
||||
.vscode-web, #rightSidebar {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow: auto; /* 确保内容可以滚动 */
|
||||
}
|
||||
|
||||
.vscode-web {
|
||||
@@ -169,75 +162,55 @@
|
||||
|
||||
|
||||
|
||||
/* Markdown内容区域样式 */
|
||||
#markdown-content {
|
||||
flex: 1; /* 占满剩余空间 */
|
||||
overflow-y: auto; /* 只有内容区域可以垂直滚动 */
|
||||
overflow-x: hidden; /* 隐藏水平滚动条 */
|
||||
}
|
||||
|
||||
/* 设置进度条容器样式 */
|
||||
#markdown-content-process {
|
||||
display: flex;
|
||||
align-items: flex-end; /* 让进度条靠底部对齐 */
|
||||
padding: 0 10px 10px 10px; /* 添加底部内边距,调整与底部的距离 */
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0; /* 防止进度条被压缩 */
|
||||
}
|
||||
|
||||
/* 设置每个子框的基本样式 */
|
||||
.progress-box {
|
||||
width: 100%; /* 宽度为 100%,撑满容器 */
|
||||
height: 100%; /* 高度为 100%,撑满容器 */
|
||||
display: flex;
|
||||
flex-direction: row; /* 使子框横向排列 */
|
||||
justify-content: flex-start;
|
||||
gap: 2px; /* 进度条之间的间距 */
|
||||
margin-bottom: 0; /* 移除底部边距 */
|
||||
}
|
||||
|
||||
.progress-title {
|
||||
height: 8px; /* 更细的进度条 */
|
||||
flex: 1; /* 均匀分配宽度 */
|
||||
border-radius: 4px; /* 圆角设计,更扁平化 */
|
||||
transition: all 0.3s ease; /* 平滑过渡效果 */
|
||||
height: 60px;
|
||||
text-align: center;
|
||||
line-height: 60px;
|
||||
margin-right: 2px;
|
||||
border: 1px solid #ddd;
|
||||
transition: background-color 0.3s ease;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1); /* 轻微阴影,增加层次感 */
|
||||
overflow: visible; /* 允许文字溢出 */
|
||||
|
||||
/* 新增以下属性实现文字溢出省略 */
|
||||
white-space: nowrap; /* 防止文字换行 */
|
||||
overflow: hidden; /* 隐藏溢出内容 */
|
||||
text-overflow: ellipsis; /* 溢出部分显示为省略号 */
|
||||
}
|
||||
|
||||
/* 颜色方案:已学习为蓝色,当前学习为绿色,未学习为白色 */
|
||||
.blue {
|
||||
background-color: #007bff; /* 蓝色 - 已学习 */
|
||||
}
|
||||
|
||||
.green {
|
||||
background-color: #28a745; /* 绿色 - 当前学习 */
|
||||
background-color: green;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.white {
|
||||
background-color: #e9ecef; /* 浅灰色 - 未学习,比纯白色更美观 */
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* 进度条悬停效果 */
|
||||
.progress-title:hover {
|
||||
transform: translateY(-1px); /* 轻微上浮效果 */
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* 增强阴影 */
|
||||
.progress-box > .progress-title:last-child {
|
||||
margin-bottom: 0; /* 防止最后一个子框出现多余的间距 */
|
||||
}
|
||||
|
||||
/* 进度条详细信息的样式 */
|
||||
#progress-detail {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
padding: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
border-radius: 5px;
|
||||
max-width: 200px;
|
||||
font-size: 14px;
|
||||
z-index: 1000;
|
||||
pointer-events: none; /* 防止干扰鼠标事件 */
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.3); /* 增强阴影 */
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/* HelpTip 组件样式 */
|
||||
|
||||
/* 问号图标样式 */
|
||||
.help-icon {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
cursor: help;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.help-icon:hover {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
/* 全局提示框样式(挂在 body 下) */
|
||||
.tooltip-portal {
|
||||
position: fixed; /* 不受父级 overflow 影响 */
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 999999; /* 足够高,覆盖所有元素 */
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.08s linear;
|
||||
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: #fff;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
pointer-events: none; /* 鼠标穿透,避免闪烁 */
|
||||
white-space: pre-wrap; /* 支持换行符 */
|
||||
max-width: 300px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 提示框显示状态 */
|
||||
.tooltip-portal.show {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 提示框动画效果 */
|
||||
.tooltip-portal.fade {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
@@ -1,3 +1,35 @@
|
||||
/* 问号图标 */
|
||||
.help-icon {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
cursor: help;
|
||||
}
|
||||
.help-icon:hover { color: #4CAF50; }
|
||||
|
||||
/* Portal Tooltip(挂在 body 下) */
|
||||
.tooltip-portal {
|
||||
position: fixed; /* 不受父级 overflow 影响 */
|
||||
left: 0; top: 0;
|
||||
z-index: 999999; /* 足够高,覆盖侧栏等 */
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: opacity .08s linear;
|
||||
|
||||
background: rgba(0,0,0,.9);
|
||||
color: #fff;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
pointer-events: none; /* 鼠标穿透,避免闪烁 */
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,.25);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.add-step-btn, .add-phase-btn {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
|
||||
@@ -3,28 +3,13 @@
|
||||
background: linear-gradient(135deg, var(--primary-blue), var(--accent-blue));
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 0.8rem 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.navbar .logo h1 {
|
||||
color: rgb(245, 245, 245);
|
||||
font-size: 18px;
|
||||
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.navbar-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.navbar-links a {
|
||||
@@ -84,41 +69,4 @@
|
||||
/* 下拉浮框 hover 显示 */
|
||||
.dropdown:hover .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.navbar {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.navbar-links {
|
||||
margin: 0;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.navbar-links a {
|
||||
margin: 0 0.75rem;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.navbar .logo h1 {
|
||||
font-size: 16px;
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.navbar .logo h1 {
|
||||
font-size: 14px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.navbar-links a {
|
||||
margin: 0 0.5rem;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -2,57 +2,6 @@
|
||||
var socket;
|
||||
let system_message_idx = 0
|
||||
const activeApprovals = {};
|
||||
let chatManager; // 全局聊天管理器实例
|
||||
|
||||
// 聊天消息管理器 - 更现代的数据结构
|
||||
class ChatMessageManager {
|
||||
constructor() {
|
||||
this.chatbox = document.getElementById('chatbox');
|
||||
this.messageTemplate = {
|
||||
user: `<div class="chat-message user-message"><b>你:</b> {{content}}</div>`,
|
||||
assistant: `<div><b>华实君:</b></div><div class="chat-message server-message"> {{content}}</div>`
|
||||
};
|
||||
}
|
||||
|
||||
// 渲染单个消息
|
||||
renderMessage(message) {
|
||||
let html;
|
||||
let content;
|
||||
|
||||
// 处理不同的数据格式
|
||||
if (typeof message === 'string') {
|
||||
// 旧格式:直接字符串
|
||||
content = marked.parse(message);
|
||||
html = this.messageTemplate.assistant.replace('{{content}}', content);
|
||||
} else if (typeof message === 'object' && message !== null) {
|
||||
// 新格式:{role, content} 对象
|
||||
content = marked.parse(message.content);
|
||||
if (message.role === 'user') {
|
||||
html = this.messageTemplate.user.replace('{{content}}', content);
|
||||
} else {
|
||||
html = this.messageTemplate.assistant.replace('{{content}}', content);
|
||||
}
|
||||
}
|
||||
|
||||
if (html) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.innerHTML = html;
|
||||
this.chatbox.appendChild(messageDiv);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染多条消息(用于恢复历史)
|
||||
renderMessages(messages) {
|
||||
messages.forEach(message => this.renderMessage(message));
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
scrollToBottom() {
|
||||
this.chatbox.scrollTop = this.chatbox.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理过期的批准记录
|
||||
function cleanupExpiredApprovals() {
|
||||
const now = Date.now();
|
||||
@@ -75,10 +24,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
folder: data.folder
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化聊天消息管理器
|
||||
chatManager = new ChatMessageManager();
|
||||
|
||||
socket.on('connect', function () {
|
||||
console.log('Connected to server');
|
||||
socket.emit('login',JSON.stringify(data));
|
||||
@@ -93,13 +38,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
input.value += data;
|
||||
});
|
||||
socket.on('message', function (data) {
|
||||
// 显示服务器的回复消息 - 单个消息
|
||||
chatManager.renderMessage(data);
|
||||
});
|
||||
|
||||
socket.on('messages', function (data) {
|
||||
// 显示服务器发送的消息列表 - 用于恢复对话历史
|
||||
chatManager.renderMessages(data);
|
||||
// 显示服务器的回复消息
|
||||
const serverMessage = document.createElement('div');
|
||||
serverMessage.innerHTML = `<div><b>华实君:</b></div><div class="chat-message server-message"> ${marked.parse(data)}</div>`;
|
||||
document.getElementById('chatbox').appendChild(serverMessage);
|
||||
// 滚动到最新消息
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
});
|
||||
socket.on('message-hint', function(data){
|
||||
console.log('get message-hint');
|
||||
@@ -146,7 +90,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}
|
||||
|
||||
const approveButton = document.createElement('button');
|
||||
approveButton.innerHTML = '<i class="bi bi-check-circle-fill"></i>';
|
||||
approveButton.innerHTML = '<i class="fas fa-check-circle-check"></i>';
|
||||
approveButton.data = element;
|
||||
approveButton.className = 'approve-button';
|
||||
approveButton.onclick = function (event) {
|
||||
@@ -266,14 +210,22 @@ function sendMessage() {
|
||||
const input = document.getElementById('messageInput').value;
|
||||
if (input.trim() === '') return;
|
||||
|
||||
// 使用 marked.js 解析 markdown
|
||||
const markdownHtml = marked.parse(input);
|
||||
|
||||
// 显示用户发送的消息
|
||||
const userMessage = document.createElement('div');
|
||||
userMessage.innerHTML = `<div class="chat-message user-message"><b>你:</b> ${markdownHtml}</div>`;
|
||||
document.getElementById('chatbox').appendChild(userMessage);
|
||||
|
||||
// 发送消息到服务器
|
||||
socket.emit('message', JSON.stringify({ data: input, type: 'text' }));
|
||||
|
||||
// 使用聊天管理器显示用户消息
|
||||
chatManager.renderMessage({ role: 'user', content: input });
|
||||
|
||||
// 清空输入框
|
||||
document.getElementById('messageInput').value = '';
|
||||
|
||||
// 滚动到最新消息
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
}
|
||||
|
||||
|
||||
@@ -424,11 +376,14 @@ window.addEventListener('beforeunload', () => {
|
||||
|
||||
let currentChapterIndex = -1;
|
||||
function next_chapter(data) {
|
||||
var container = document.getElementById('markdown-content-container');
|
||||
var titles = [];
|
||||
var h3Elements = container.querySelectorAll('h3');
|
||||
console.log("h3Elements next");
|
||||
|
||||
// 获取所有的 h3 元素
|
||||
var iframe = document.getElementById('markdown-content-iframe');
|
||||
|
||||
// 访问 iframe 的文档对象
|
||||
var iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
|
||||
var titles = []
|
||||
var h3Elements = iframeDocument.querySelectorAll('h3');
|
||||
console.log("h3Elements next")
|
||||
if (currentChapterIndex >= h3Elements.length) {
|
||||
return; // 如果已经到了最后一个章节,则不进行任何操作
|
||||
}
|
||||
@@ -436,7 +391,7 @@ function next_chapter(data) {
|
||||
// 隐藏当前章节及其后的内容
|
||||
for (let i = 0; i < h3Elements.length; i++) {
|
||||
h3Elements[i].style.display = 'none';
|
||||
titles.push(h3Elements[i].innerText);
|
||||
titles.push(h3Elements[i].innerText)
|
||||
let nextSibling = h3Elements[i].nextElementSibling;
|
||||
while (nextSibling && nextSibling.tagName !== 'H3') {
|
||||
nextSibling.style.display = 'none';
|
||||
@@ -466,31 +421,48 @@ function generateProgressBar(N, idx, titles) {
|
||||
const progressBox = document.createElement('div');
|
||||
progressBox.className = 'progress-box';
|
||||
|
||||
// 获取详细信息容器
|
||||
const detailBox = document.getElementById('progress-detail');
|
||||
|
||||
// 根据N值动态调整每个进度节点的宽度
|
||||
const nodeWidth = 100 / N; // 每个节点的宽度为 100% / N
|
||||
|
||||
// 根据N值生成子框并设置颜色
|
||||
for (let i = 0; i < N; i++) {
|
||||
const titleBox = document.createElement('div');
|
||||
titleBox.className = 'progress-title';
|
||||
|
||||
// 设置进度条的颜色:已学习为蓝色,当前学习为绿色,未学习为白色
|
||||
// 设置进度条的颜色
|
||||
if (i < idx) {
|
||||
titleBox.classList.add('blue'); // 已完成部分 - 蓝色
|
||||
} else if (i === idx) {
|
||||
titleBox.classList.add('green'); // 当前正在学习 - 绿色
|
||||
titleBox.classList.add('green'); // 已完成部分
|
||||
} else {
|
||||
titleBox.classList.add('white'); // 未完成部分 - 白色
|
||||
titleBox.classList.add('white'); // 未完成部分
|
||||
}
|
||||
|
||||
// 移除进度条上的文字,因为现在进度条很细
|
||||
titleBox.innerHTML = '';
|
||||
// 设置每个子框的标题
|
||||
titleBox.innerHTML = titles[i] || `Step ${i + 1}`;
|
||||
|
||||
// 使用attachImmediateTooltip函数,使提示框跟随鼠标移动
|
||||
const tooltipText = `Step ${i + 1}: ${titles[i] || "No title"}`;
|
||||
attachImmediateTooltip(titleBox, tooltipText);
|
||||
// 设置每个进度节点的宽度
|
||||
titleBox.style.width = `${nodeWidth}%`;
|
||||
|
||||
// 监听鼠标移入事件来显示详情
|
||||
titleBox.addEventListener('mouseenter', function () {
|
||||
detailBox.innerHTML = `Step ${i + 1}: ${titles[i] || "No title"}`;// - Additional details here...`;
|
||||
detailBox.style.display = 'block';
|
||||
// 根据进度条位置显示详情
|
||||
const rect = titleBox.getBoundingClientRect();
|
||||
detailBox.style.top = `${rect.top + window.scrollY - 100}px`; // 微调位置
|
||||
detailBox.style.left = `${rect.left + window.scrollX + rect.width / 2 - detailBox.offsetWidth / 2}px`;
|
||||
});
|
||||
|
||||
// 监听鼠标移出事件来隐藏详情
|
||||
titleBox.addEventListener('mouseleave', function () {
|
||||
detailBox.style.display = 'none';
|
||||
});
|
||||
|
||||
progressBox.appendChild(titleBox);
|
||||
}
|
||||
|
||||
// 将生成的进度条添加到容器中
|
||||
container.appendChild(progressBox);
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// helpTip.js - 全局tooltip功能,可在不同页面复用
|
||||
|
||||
// 在页面里复用一个全局 tooltip 节点
|
||||
let __globalTooltip;
|
||||
|
||||
// 确保tooltip节点存在
|
||||
function ensureTooltip() {
|
||||
if (!__globalTooltip) {
|
||||
__globalTooltip = document.createElement('div');
|
||||
__globalTooltip.className = 'tooltip-portal';
|
||||
document.body.appendChild(__globalTooltip);
|
||||
}
|
||||
return __globalTooltip;
|
||||
}
|
||||
|
||||
// 绑定到任意图标:移入→显示;移动→跟随;移出→隐藏
|
||||
function attachImmediateTooltip(anchorEl, text) {
|
||||
const tip = ensureTooltip();
|
||||
let shown = false;
|
||||
|
||||
function show(e) {
|
||||
tip.textContent = text;
|
||||
tip.style.display = 'block';
|
||||
tip.style.opacity = '1';
|
||||
position(e);
|
||||
shown = true;
|
||||
}
|
||||
|
||||
function position(e) {
|
||||
// 以鼠标位置为准
|
||||
const padding = 8;
|
||||
const tipRect = tip.getBoundingClientRect();
|
||||
let x = e.clientX + 12;
|
||||
let y = e.clientY - tipRect.height - 12;
|
||||
|
||||
// 简易防出界
|
||||
const vw = window.innerWidth, vh = window.innerHeight;
|
||||
if (x + tipRect.width + padding > vw) x = vw - tipRect.width - padding;
|
||||
if (y < padding) y = e.clientY + 16; // 放到下面
|
||||
|
||||
tip.style.left = `${x}px`;
|
||||
tip.style.top = `${y}px`;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
tip.style.opacity = '0';
|
||||
// 用小延迟避免闪烁
|
||||
setTimeout(() => { if (!shown) return; tip.style.display = 'none'; }, 100);
|
||||
shown = false;
|
||||
}
|
||||
|
||||
anchorEl.addEventListener('mouseenter', show);
|
||||
anchorEl.addEventListener('mousemove', position);
|
||||
anchorEl.addEventListener('mouseleave', hide);
|
||||
}
|
||||
|
||||
// 导出函数,供其他文件使用
|
||||
window.attachImmediateTooltip = attachImmediateTooltip;
|
||||
@@ -4,6 +4,60 @@ window.addEventListener('load', ()=>{
|
||||
editLesson(window.material.material_id, window.chapter.chapter_name, window.lesson.lesson_name);
|
||||
});
|
||||
|
||||
// 在页面里复用一个全局 tooltip 节点
|
||||
let __globalTooltip;
|
||||
function ensureTooltip() {
|
||||
if (!__globalTooltip) {
|
||||
__globalTooltip = document.createElement('div');
|
||||
__globalTooltip.className = 'tooltip-portal';
|
||||
document.body.appendChild(__globalTooltip);
|
||||
}
|
||||
return __globalTooltip;
|
||||
}
|
||||
|
||||
// 绑定到任意图标:移入→显示;移动→跟随;移出→隐藏
|
||||
function attachImmediateTooltip(anchorEl, text) {
|
||||
const tip = ensureTooltip();
|
||||
let shown = false;
|
||||
|
||||
function show(e) {
|
||||
tip.textContent = text;
|
||||
tip.style.display = 'block';
|
||||
tip.style.opacity = '1';
|
||||
position(e);
|
||||
shown = true;
|
||||
}
|
||||
function position(e) {
|
||||
// 以鼠标位置为准(也可用 anchorEl.getBoundingClientRect())
|
||||
const padding = 8;
|
||||
const tipRect = tip.getBoundingClientRect();
|
||||
let x = e.clientX + 12;
|
||||
let y = e.clientY - tipRect.height - 12;
|
||||
|
||||
// 简易防出界
|
||||
const vw = window.innerWidth, vh = window.innerHeight;
|
||||
if (x + tipRect.width + padding > vw) x = vw - tipRect.width - padding;
|
||||
if (y < padding) y = e.clientY + 16; // 放到下面
|
||||
|
||||
tip.style.left = `${x}px`;
|
||||
tip.style.top = `${y}px`;
|
||||
}
|
||||
function hide() {
|
||||
tip.style.opacity = '0';
|
||||
// 用小延迟避免闪烁
|
||||
setTimeout(() => { if (!shown) return; tip.style.display = 'none'; }, 100);
|
||||
shown = false;
|
||||
}
|
||||
|
||||
anchorEl.addEventListener('mouseenter', show);
|
||||
anchorEl.addEventListener('mousemove', position);
|
||||
anchorEl.addEventListener('mouseleave', hide);
|
||||
}
|
||||
|
||||
// 你的 renderOutline 里调用:
|
||||
// if (outline.theme) addThemeWithHelp(tree, outline.theme);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -142,7 +196,6 @@ function parseHeadings(mdText) {
|
||||
help.className = 'help-icon';
|
||||
help.textContent = ' ?';
|
||||
li.appendChild(help);
|
||||
|
||||
// 新增:铅笔编辑按钮
|
||||
const editBtn = createEditButton(() => {
|
||||
openTitleEditor({
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
// 动态加载 Markdown 文件
|
||||
function loadMarkdown(course_id, chapter_name, lesson_name) {
|
||||
fetch(`/${course_id}-${chapter_name}-${lesson_name}-markdown`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.markdown_content) {
|
||||
// 使用 marked 解析 Markdown 内容
|
||||
const html = marked.parse(data.markdown_content);
|
||||
|
||||
// 创建包含 MathJax 支持的 HTML 结构
|
||||
const markdownContainer = document.createElement('div');
|
||||
markdownContainer.id = 'markdown-content-container';
|
||||
markdownContainer.innerHTML = html;
|
||||
|
||||
// 处理代码块,实现缩略显示
|
||||
const codeBlocks = markdownContainer.querySelectorAll('pre code');
|
||||
codeBlocks.forEach(codeBlock => {
|
||||
const preBlock = codeBlock.parentElement;
|
||||
|
||||
// 保存原始代码内容
|
||||
const originalContent = preBlock.innerHTML;
|
||||
|
||||
// 创建缩略提示元素
|
||||
const codeHint = document.createElement('div');
|
||||
codeHint.className = 'code-hint';
|
||||
codeHint.innerHTML = '<div style="padding: 10px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; color: #666; font-size: 14px; text-align: center;">💡 代码已放置在代码区中,点击展开查看</div>';
|
||||
|
||||
// 创建展开/折叠按钮
|
||||
const toggleButton = document.createElement('button');
|
||||
toggleButton.className = 'toggle-code';
|
||||
toggleButton.innerHTML = '展开代码';
|
||||
toggleButton.style.cssText = 'margin: 5px; padding: 5px 10px; background-color: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;';
|
||||
|
||||
// 添加到提示元素中
|
||||
codeHint.appendChild(toggleButton);
|
||||
|
||||
// 替换原始代码块为提示
|
||||
preBlock.innerHTML = '';
|
||||
preBlock.appendChild(codeHint);
|
||||
|
||||
// 保存原始代码,用于切换
|
||||
preBlock.dataset.originalContent = originalContent;
|
||||
preBlock.dataset.isExpanded = 'false';
|
||||
|
||||
// 添加切换事件
|
||||
toggleButton.addEventListener('click', () => {
|
||||
if (preBlock.dataset.isExpanded === 'false') {
|
||||
// 展开代码
|
||||
preBlock.innerHTML = preBlock.dataset.originalContent;
|
||||
toggleButton.innerHTML = '折叠代码';
|
||||
preBlock.appendChild(toggleButton);
|
||||
preBlock.dataset.isExpanded = 'true';
|
||||
// 如果MathJax已加载,重新排版
|
||||
if (typeof MathJax !== 'undefined') {
|
||||
MathJax.typesetPromise([preBlock]);
|
||||
}
|
||||
} else {
|
||||
// 折叠代码
|
||||
preBlock.innerHTML = '';
|
||||
preBlock.appendChild(codeHint.cloneNode(true));
|
||||
preBlock.appendChild(toggleButton.cloneNode(true));
|
||||
preBlock.dataset.isExpanded = 'false';
|
||||
// 重新添加事件监听
|
||||
const newToggleButton = preBlock.querySelector('.toggle-code');
|
||||
newToggleButton.addEventListener('click', arguments.callee);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 清空内容容器并添加渲染后的内容
|
||||
const contentElement = document.getElementById('markdown-content');
|
||||
contentElement.innerHTML = '';
|
||||
contentElement.appendChild(markdownContainer);
|
||||
|
||||
// 配置并加载 MathJax
|
||||
if (typeof MathJax === 'undefined') {
|
||||
// 动态加载 MathJax
|
||||
const script = document.createElement('script');
|
||||
script.src = '/static/cdnback/js/tex-mml-chtml.js';
|
||||
script.async = true;
|
||||
script.onload = function() {
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\\(', '\\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
MathJax.typesetPromise([markdownContainer]);
|
||||
// 初始化课程进度
|
||||
next_chapter();
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
// MathJax 已加载,直接排版
|
||||
MathJax.typesetPromise([markdownContainer]);
|
||||
// 初始化课程进度
|
||||
next_chapter();
|
||||
}
|
||||
} else {
|
||||
console.error('Markdown file not found');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading markdown:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 文档加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 从全局变量获取课程信息
|
||||
if (window.appData) {
|
||||
loadMarkdown(window.appData.course_id, window.appData.chapter_name, window.appData.lesson_name);
|
||||
}
|
||||
});
|
||||
@@ -8,7 +8,6 @@
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/css/desktop.css">
|
||||
<link rel="stylesheet" href="/static/css/chatbox_approve_icon.css">
|
||||
<link rel="stylesheet" href="/static/css/helpTip.css">
|
||||
<script>
|
||||
document.cookie = "vscode-tkn=44edc269-6f88-46ab-9790-dc253f66ac36; path=/; SameSite=None; Secure";
|
||||
</script>
|
||||
@@ -141,7 +140,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/process_show_floating_window.js"></script>
|
||||
<script src="/static/js/helpTip.js"></script>
|
||||
|
||||
|
||||
|
||||
<script src="/static/js/chatbox.js"></script>
|
||||
|
||||
<script>
|
||||
@@ -192,6 +193,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
// 使用 fetch 发送请求到 Flask 服务器,获取 session 信息
|
||||
fetch('/get_session', {
|
||||
method: 'GET',
|
||||
@@ -228,6 +230,31 @@
|
||||
}
|
||||
|
||||
|
||||
// 动态加载 Markdown 文件
|
||||
function loadMarkdown(course_id, chapter_name, lesson_name) {
|
||||
fetch(`/${course_id}-${chapter_name}-${lesson_name}-markdown`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.html_url) {
|
||||
document.getElementById('markdown-content').innerHTML = `<iframe id="markdown-content-iframe" src="${data.html_url}"style="width: 100%;height: 100%;"></iframe>`;
|
||||
document.getElementById('markdown-content-iframe').addEventListener('load', function() {
|
||||
// 在这里执行你想要的操作
|
||||
console.log('Iframe has finished loading');
|
||||
next_chapter();
|
||||
});
|
||||
} else {
|
||||
console.error('Markdown file not found');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading markdown:', error);
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadMarkdown('{{course_id}}','{{chapter_name}}','{{lesson_name}}');
|
||||
});
|
||||
|
||||
|
||||
// 侧边工具栏切换
|
||||
const sidebarTools = document.getElementById('sidebarTools');
|
||||
const toggleButton = document.getElementById('toggleButton');
|
||||
@@ -243,7 +270,5 @@
|
||||
});
|
||||
|
||||
</script>
|
||||
<!-- 引入 Markdown 渲染脚本 -->
|
||||
<script src="/static/js/render-desktop-markdown.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -61,26 +61,16 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="confirm-container">
|
||||
{% if is_course_finished %}
|
||||
<h1>重新学习确认</h1>
|
||||
<p>您已经完成了该课程的学习,是否要重新开始学习?</p>
|
||||
<div class="btn-group">
|
||||
<a href="{{ url_for('vscode.desktop', user_uuid=user_uuid, course_id=course_id, chapter_name=chapter_name, lesson_name=lesson_name, load_history='false') }}" class="btn btn-primary">
|
||||
重新开始学习
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<h1>加载历史数据确认</h1>
|
||||
<p>您即将进入学习环境,是否需要加载您之前的对话历史记录和学习进度数据?</p>
|
||||
<div class="btn-group">
|
||||
<a href="{{ url_for('vscode.desktop', user_uuid=user_uuid, course_id=course_id, chapter_name=chapter_name, lesson_name=lesson_name, load_history='true') }}" class="btn btn-primary">
|
||||
是,加载历史数据
|
||||
</a>
|
||||
<a href="{{ url_for('vscode.desktop', user_uuid=user_uuid, course_id=course_id, chapter_name=chapter_name, lesson_name=lesson_name, load_history='false') }}" class="btn btn-secondary">
|
||||
否,重新开始
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h1>加载历史数据确认</h1>
|
||||
<p>您即将进入学习环境,是否需要加载您之前的对话历史记录和学习进度数据?</p>
|
||||
<div class="btn-group">
|
||||
<a href="{{ url_for('vscode.desktop', user_uuid=user_uuid, course_id=course_id, chapter_name=chapter_name, lesson_name=lesson_name, load_history='true') }}" class="btn btn-primary">
|
||||
是,加载历史数据
|
||||
</a>
|
||||
<a href="{{ url_for('vscode.desktop', user_uuid=user_uuid, course_id=course_id, chapter_name=chapter_name, lesson_name=lesson_name, load_history='false') }}" class="btn btn-secondary">
|
||||
否,重新开始
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,7 +8,6 @@
|
||||
<link rel="stylesheet" href="/static/css/teacherboard.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
<link rel="stylesheet" href="/static/css/lesson.css">
|
||||
<link rel="stylesheet" href="/static/css/helpTip.css">
|
||||
<link rel="stylesheet" href="/static/css/teacher_course_setting.css">
|
||||
<link rel="stylesheet" href="/static/css/lesson_setting.css">
|
||||
<link rel="stylesheet" href="/static/css/modelinput.css">
|
||||
@@ -101,7 +100,6 @@
|
||||
<script src="/static/js/teacherboard.js"></script>
|
||||
<script src="/static/js/teacher_course_setting.js"></script>
|
||||
<script src="/static/js/lesson_markdown.js"></script>
|
||||
<script src="/static/js/helpTip.js"></script>
|
||||
<script src="/static/js/lesson.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<link rel="stylesheet" href="/static/css/navbar.css">
|
||||
|
||||
<link rel="stylesheet" href="/static/cdnback/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
<link rel="stylesheet" href="/static/css/navbar.css">
|
||||
<header class="navbar">
|
||||
<div class="logo">
|
||||
<h1>华实学伴:教学练评一体的虚拟助教</h1>
|
||||
|
||||
@@ -14,8 +14,17 @@ bp = Blueprint("markdown", __name__)
|
||||
def convert_md(course_id, chapter_name, lesson_name):
|
||||
print(f"convert_md: {course_id}, {chapter_name}, {lesson_name}")
|
||||
md_file = load_markdown_file(course_id, chapter_name, lesson_name)
|
||||
# 直接返回Markdown源文件内容,不进行服务器端渲染
|
||||
return jsonify({"markdown_content": md_file})
|
||||
# 配置Markdown解析器,启用数学公式支持
|
||||
md = MarkdownIt()
|
||||
md.use(texmath.texmath_plugin) # 添加数学公式插件
|
||||
# 转换markdown为html(包含公式标记)
|
||||
html = md.render(md_file)
|
||||
# 包装样式时添加MathJax支持,用于在浏览器中渲染公式
|
||||
html_with_styles = wrap_with_styles(html)
|
||||
# 保存HTML文件到static
|
||||
html_output_path = os.path.join(current_app.config["STATIC_DIR"], f"{course_id}-{chapter_name}-{lesson_name}.html")
|
||||
save_html(html_with_styles, html_output_path)
|
||||
return jsonify({"html_url": f"/static/{course_id}-{chapter_name}-{lesson_name}.html"})
|
||||
|
||||
# 提供静态文件访问
|
||||
@bp.route("/static/<path:filename>")
|
||||
|
||||
@@ -6,7 +6,6 @@ from ..services.backboard_service import realtime_response
|
||||
from ..extension_ase.ase_client.manager import ChatManager
|
||||
import subprocess
|
||||
from ..auth.decorators import require_role
|
||||
from ..services.course_service import load_learning_progress
|
||||
|
||||
bp = Blueprint("vscode", __name__)
|
||||
|
||||
@@ -20,14 +19,11 @@ def desktop(user_uuid, course_id, chapter_name, lesson_name):
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
# 检查是否有 load_history 参数
|
||||
load_history_str = request.args.get('load_history')
|
||||
if load_history_str is None:
|
||||
load_history = request.args.get('load_history')
|
||||
if load_history is None:
|
||||
# 如果没有参数,跳转到确认页面
|
||||
return redirect(url_for("vscode.desktop_nouser", course_id=course_id, chapter_name=chapter_name, lesson_name=lesson_name))
|
||||
|
||||
# 将字符串转换为布尔值
|
||||
load_history = load_history_str.lower() == 'true'
|
||||
|
||||
# 全局映射
|
||||
user_id = uuid2username[user_uuid]
|
||||
userid_recorder[f"{user_id}&{course_id}"] = session["user_uuid"]
|
||||
@@ -70,6 +66,7 @@ def desktop(user_uuid, course_id, chapter_name, lesson_name):
|
||||
|
||||
|
||||
chatmanager = ChatManager(restart=not load_history)
|
||||
print(f"now user uuid {user_uuid}")
|
||||
current_app.extensions["user_uuid2chatmanager"][user_uuid] = chatmanager
|
||||
code_server_port = 10000 + int(uuid.uuid4().int % 10000)
|
||||
# chatmanager.start_code_server(user_uuid, user_id, path_dir, code_server_port)
|
||||
@@ -150,33 +147,13 @@ def desktop(user_uuid, course_id, chapter_name, lesson_name):
|
||||
def desktop_nouser(course_id, chapter_name, lesson_name):
|
||||
if "user_uuid" not in session:
|
||||
return redirect(url_for("auth.login")) # 如果你有 auth 蓝图
|
||||
|
||||
user_uuid = session["user_uuid"]
|
||||
uuid2username = current_app.extensions["uuid2username"]
|
||||
user_id = uuid2username[user_uuid]
|
||||
|
||||
# 检查是否有学习记录
|
||||
progress_result = load_learning_progress(user_id, course_id, chapter_name, lesson_name)
|
||||
|
||||
if not progress_result["exists"]:
|
||||
# 没有学习记录,直接跳转到学习页面,不显示确认加载历史的页面
|
||||
return redirect(url_for("vscode.desktop", user_uuid=user_uuid, course_id=course_id, chapter_name=chapter_name, lesson_name=lesson_name, load_history="false"))
|
||||
|
||||
# 获取学习记录数据
|
||||
progress_data = progress_result["data"]
|
||||
chapter_chain_now = progress_data.get("chapter_chain_now", -1)
|
||||
chapter_chain_length = progress_data.get("chapter_chain_length", 0)
|
||||
|
||||
# 检查是否已经学完所有章节
|
||||
is_course_finished = chapter_chain_now >= chapter_chain_length - 1
|
||||
|
||||
return render_template(
|
||||
"confirm_load_history.html",
|
||||
user_uuid=user_uuid,
|
||||
course_id=course_id,
|
||||
chapter_name=chapter_name,
|
||||
lesson_name=lesson_name,
|
||||
is_course_finished=is_course_finished
|
||||
lesson_name=lesson_name
|
||||
)
|
||||
|
||||
@bp.route("/vscode_data", methods=["POST"])
|
||||
|
||||
@@ -39,56 +39,35 @@ def read_and_forward_pty_output(fd=None, pid=None, room_id=None,namespace=None):
|
||||
"""
|
||||
max_read_bytes = 1024 * 20
|
||||
timeout=0.1
|
||||
try:
|
||||
while True:
|
||||
socketio.sleep(timeout)
|
||||
timeout=min(timeout*2, 0.4)
|
||||
# using flask default web server, or uwsgi production web server
|
||||
# when the child process is terminated, it will not disappear from linux process list
|
||||
# and keep staying as a zombie process until the parent exits.
|
||||
try:
|
||||
child_process = psutil.Process(pid)
|
||||
except psutil.NoSuchProcess as err:
|
||||
# Process already terminated, clean up any zombie
|
||||
try:
|
||||
os.waitpid(pid, os.WNOHANG)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
if child_process.status() not in ('running', 'sleeping'):
|
||||
# Process is terminated or in other state, clean up
|
||||
try:
|
||||
child_process.wait(timeout=1)
|
||||
except Exception:
|
||||
try:
|
||||
os.waitpid(pid, os.WNOHANG)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
if fd:
|
||||
timeout_sec = 0
|
||||
(data_ready, _, _) = select.select([fd], [], [], timeout_sec)
|
||||
if data_ready:
|
||||
# output = os.read(fd, max_read_bytes).decode('ascii')
|
||||
timeout=0.1
|
||||
try:
|
||||
output = os.read(fd, max_read_bytes).decode()
|
||||
except Exception as err:
|
||||
output = """
|
||||
***AQUI WEB TERM ERR***
|
||||
{}
|
||||
***********************
|
||||
""".format(err)
|
||||
# the key for different visitor to get different terminal (instead of mixing up)
|
||||
# is to let the background task push pty response to each one's own (default) ROOM!
|
||||
namespace.emit("pty_output", {"output": output}, room=room_id)
|
||||
finally:
|
||||
# Clean up file descriptor if it's open
|
||||
while True:
|
||||
socketio.sleep(timeout)
|
||||
timeout=min(timeout*2, 0.4)
|
||||
# using flask default web server, or uwsgi production web server
|
||||
# when the child process is terminated, it will not disappear from linux process list
|
||||
# and keep staying as a zombie process until the parent exits.
|
||||
try:
|
||||
child_process = psutil.Process(pid)
|
||||
except psutil.NoSuchProcess as err:
|
||||
return
|
||||
if child_process.status() not in ('running', 'sleeping'):
|
||||
return
|
||||
if fd:
|
||||
try:
|
||||
os.close(fd)
|
||||
except Exception:
|
||||
pass
|
||||
timeout_sec = 0
|
||||
(data_ready, _, _) = select.select([fd], [], [], timeout_sec)
|
||||
if data_ready:
|
||||
# output = os.read(fd, max_read_bytes).decode('ascii')
|
||||
timeout=0.1
|
||||
try:
|
||||
output = os.read(fd, max_read_bytes).decode()
|
||||
except Exception as err:
|
||||
output = """
|
||||
***AQUI WEB TERM ERR***
|
||||
{}
|
||||
***********************
|
||||
""".format(err)
|
||||
# the key for different visitor to get different terminal (instead of mixing up)
|
||||
# is to let the background task push pty response to each one's own (default) ROOM!
|
||||
namespace.emit("pty_output", {"output": output}, room=room_id)
|
||||
|
||||
class VSCLikeNameSpace(Namespace):
|
||||
def on_connect(self):
|
||||
@@ -177,40 +156,15 @@ class VSCLikeNameSpace(Namespace):
|
||||
set_winsize(fd, data["rows"], data["cols"])
|
||||
|
||||
def on_disconnect(self):
|
||||
child_pid = session.get('terminal_config', {}).get('child_pid')
|
||||
if child_pid:
|
||||
try:
|
||||
child_process = psutil.Process(child_pid)
|
||||
if child_process.status() in ('running', 'sleeping'):
|
||||
# if visitor just close the browser tab then left alone the pty here
|
||||
# it should be terminated by the parent process after
|
||||
child_process.terminate()
|
||||
# Wait for the process to terminate and collect its exit status
|
||||
child_process.wait(timeout=2)
|
||||
current_app.logger.debug('user left the pty alone, terminated and waited')
|
||||
except psutil.NoSuchProcess as err:
|
||||
# Process already terminated, try to wait anyway to clean up any zombie
|
||||
try:
|
||||
os.waitpid(child_pid, os.WNOHANG)
|
||||
except Exception:
|
||||
pass
|
||||
except psutil.TimeoutExpired:
|
||||
# If process didn't terminate in time, kill it forcefully
|
||||
child_process.kill()
|
||||
try:
|
||||
child_process.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as err:
|
||||
current_app.logger.error(f'Error terminating process: {err}')
|
||||
finally:
|
||||
# Close the file descriptor if it's open
|
||||
fd = session.get('terminal_config', {}).get('fd')
|
||||
if fd:
|
||||
try:
|
||||
os.close(fd)
|
||||
except Exception:
|
||||
pass
|
||||
# Reset session config
|
||||
session['terminal_config'] = TERM_INIT_CONFIG
|
||||
try:
|
||||
child_process = psutil.Process(session.get('terminal_config', {}).get('child_pid'))
|
||||
except psutil.NoSuchProcess as err:
|
||||
disconnect()
|
||||
session['terminal_config'] = TERM_INIT_CONFIG
|
||||
return
|
||||
if child_process.status() in ('running', 'sleeping'):
|
||||
# if visitor just close the browser tab then left alone the pty here
|
||||
# it should be terminated by the parent process after
|
||||
child_process.terminate()
|
||||
current_app.logger.debug('user left the pty alone, terminated')
|
||||
current_app.logger.debug('Client disconnected')
|
||||
@@ -4,17 +4,17 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cloud IDE</title>
|
||||
<link rel="stylesheet" href="static/cdnback/css/editor/editor.main.css">
|
||||
<script src="static/cdnback/socket.io.min.js"></script>
|
||||
<link rel="stylesheet" href="static/cdnback/all.min.css">
|
||||
<link rel="stylesheet" href="static/cdnback/css/xterm.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.5.0/xterm.css" />
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.5.0/xterm.js"></script> -->
|
||||
<script src="static/cdnback/js/xterm.js"></script>
|
||||
<script src="static/cdnback/js/addons/fit/fit.js"></script>
|
||||
<script src="static/cdnback/js/addons/webLinks/webLinks.js"></script>
|
||||
<script src="static/cdnback/js/addons/fullscreen/fullscreen.js"></script>
|
||||
<script src="static/cdnback/js/addons/search/search.js"></script>
|
||||
<link rel="stylesheet" href="static/cdnback/all.min.css">
|
||||
<script src="https://unpkg.com/xterm@3.6.0/dist/xterm.js"></script>
|
||||
<script src="https://unpkg.com/xterm@3.6.0/dist/addons/fit/fit.js"></script>
|
||||
<script src="https://unpkg.com/xterm@3.6.0/dist/addons/webLinks/webLinks.js"></script>
|
||||
<script src="https://unpkg.com/xterm@3.6.0/dist/addons/fullscreen/fullscreen.js"></script>
|
||||
<script src="https://unpkg.com/xterm@3.6.0/dist/addons/search/search.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||
|
||||
<link rel="stylesheet" href="/vsc-like/static/css/index.css">
|
||||
<link rel="stylesheet" href="/vsc-like/static/css/notification.css">
|
||||
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
<div id="notificationsContainer"></div>
|
||||
|
||||
<script src="static/cdnback/js/monaco-editor/0.34.0/min/vs/loader.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/loader.js"></script>
|
||||
<script src="/vsc-like/static/js/code-like-extension.js"></script>
|
||||
<script src="/vsc-like/static/js/file.js"></script>
|
||||
<script src="/vsc-like/static/js/terminal.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user