18 Commits

Author SHA1 Message Date
CakeCN
496c5be90a code-server-like ssh clash 2025-12-13 23:50:45 +08:00
CakeCN
ec6be8233c 增加code-serverlike cdnback 2025-12-13 22:52:16 +08:00
CakeCN
d0331d9361 navbar adjust 2025-12-13 21:47:10 +08:00
CakeCN
8e79380ba4 progress 检查 2025-12-13 18:56:21 +08:00
CakeCN
6acb1db093 direct no load history when no history 2025-12-13 18:36:33 +08:00
CakeCN
af0ebda419 try fix nextchapter bug 2025-12-13 18:16:34 +08:00
CakeCN
ac74c28f64 提示helpTip的标准化 2025-12-12 16:58:27 +08:00
CakeCN
41c17fcde1 help tips 标准化 2025-12-12 16:46:41 +08:00
CakeCN
5789974d2e 修复一些progressbar bug 2025-12-12 16:34:42 +08:00
CakeCN
221f3faace 美化processbar 2025-12-12 16:26:51 +08:00
CakeCN
560c590067 fix chatmanager 2025-12-12 16:15:48 +08:00
CakeCN
02cf3c0f78 优化前端js代码 2025-12-12 16:11:46 +08:00
CakeCN
9a7571f832 增加function approval button 2025-12-12 00:55:16 +08:00
CakeCN
14dd164545 function 提交 2025-12-12 00:47:09 +08:00
CakeCN
ff3d6e5bf9 fix chatmanager no scores bug 2025-12-12 00:29:03 +08:00
CakeCN
97df0f6407 logger /download 2025-12-12 00:25:39 +08:00
CakeCN
58a25b3e07 fix clear session 2025-12-11 22:40:43 +08:00
CakeCN
c4743bcadd restart with clear user_session 2025-12-11 22:37:58 +08:00
29 changed files with 9739 additions and 280 deletions

View File

@@ -0,0 +1,165 @@
# 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。

View File

@@ -49,7 +49,8 @@ class ChatManager:
self.chapter_chain = None
self.function_manager = FunctionManager()
self.bb = None
self.chat_historys = None
self.chat_historys = []
self.scores = []
self.vscode_ws = None
self._vscode_queue = deque()
self._lock = Semaphore(1)
@@ -119,6 +120,7 @@ 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()

View File

@@ -31,8 +31,6 @@ 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')
clear_user_session(init_data['asengine_url'], init_data['asengine_token'], init_data['room'])
restart = init_data.get("restart", False)
if restart: # 重启时,从-1章进入 0章

View File

@@ -36,6 +36,7 @@ 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()
}
@@ -92,6 +93,7 @@ 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}")
@@ -145,6 +147,7 @@ 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": "未找到学习记录,返回默认值"
@@ -163,6 +166,7 @@ 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

View File

@@ -23,7 +23,8 @@ from ..services.asectrl_service import (
receive_ase_process,
receive_ase_judge,
receive_ase_next_chapter,
receive_ase_chapter_score
receive_ase_chapter_score,
clear_user_session
)
@@ -129,7 +130,7 @@ class AgentNamespace(Namespace):
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)
@@ -154,7 +155,7 @@ class AgentNamespace(Namespace):
user_uuid2ase_client[user_uuid].register_on_connect_entry(
callback=on_connect_to_ase,
entry=(self, user_uuid2ase_client[user_uuid]),
init_data={'room':user_uuid, 'restart':chatmanager.restart, 'asengine_url':current_app.config["ASE_ENGINE_URL"], 'asengine_token':current_app.config["ASE_ENGINE_URL_TOKEN"]}
init_data={'room':user_uuid, 'restart':chatmanager.restart}
)
user_uuid2ase_client[user_uuid].register_route_with_entry(
route='dialog',

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,218 @@
/**
* 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;
}

View File

@@ -0,0 +1,51 @@
(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

View File

@@ -0,0 +1,27 @@
(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

View File

@@ -0,0 +1,126 @@
(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

View File

@@ -0,0 +1,41 @@
(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 it is too large Load Diff

View File

@@ -107,13 +107,7 @@
.btn-group-toggle .btn {
padding: 5px 10px;
}
#leftSidebar, .vscode-web, #rightSidebar {
height: 100%;
overflow: auto;
}
/* 主容器,使用 grid 布局 */
/* 主容器,使用 grid 布局 */
#maxcontainer {
display: grid;
grid-template-columns: 2fr 5px 3fr 5px 2fr; /* 默认宽度比例 */
@@ -122,7 +116,20 @@
/* 各区域基础样式 */
.sidebar {
overflow: auto; /* 确保内容可以滚动 */
display: flex;
flex-direction: column;
overflow: hidden; /* 隐藏溢出内容,让内部元素单独滚动 */
}
/* 左侧边栏样式 */
#leftSidebar {
height: 100%;
}
/* 中间和右侧边栏样式 */
.vscode-web, #rightSidebar {
height: 100%;
overflow: auto;
}
.vscode-web {
@@ -162,55 +169,75 @@
/* 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 {
height: 100%; /* 度为 100%,撑满容器 */
width: 100%; /* 度为 100%,撑满容器 */
display: flex;
flex-direction: row; /* 使子框横向排列 */
justify-content: flex-start;
gap: 2px; /* 进度条之间的间距 */
margin-bottom: 0; /* 移除底部边距 */
}
.progress-title {
height: 60px;
text-align: center;
line-height: 60px;
margin-right: 2px;
border: 1px solid #ddd;
transition: background-color 0.3s ease;
height: 8px; /* 更细的进度条 */
flex: 1; /* 均匀分配宽度 */
border-radius: 4px; /* 圆角设计,更扁平化 */
transition: all 0.3s ease; /* 平滑过渡效果 */
position: relative;
/* 新增以下属性实现文字溢出省略 */
white-space: nowrap; /* 防止文字换行 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 溢出部分显示为省略号 */
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.1); /* 轻微阴影,增加层次感 */
overflow: visible; /* 允许文字溢出 */
}
/* 颜色方案:已学习为蓝色,当前学习为绿色,未学习为白色 */
.blue {
background-color: #007bff; /* 蓝色 - 已学习 */
}
.green {
background-color: green;
color: white;
background-color: #28a745; /* 绿色 - 当前学习 */
}
.white {
background-color: white;
color: black;
background-color: #e9ecef; /* 浅灰色 - 未学习,比纯白色更美观 */
}
.progress-box > .progress-title:last-child {
margin-bottom: 0; /* 防止最后一个子框出现多余的间距 */
/* 进度条悬停效果 */
.progress-title:hover {
transform: translateY(-1px); /* 轻微上浮效果 */
box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* 增强阴影 */
}
/* 进度条详细信息的样式 */
#progress-detail {
position: absolute;
top: 0;
left: 0;
display: none;
padding: 10px;
background-color: rgba(0, 0, 0, 0.7);
background-color: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 5px;
border-radius: 6px;
max-width: 200px;
font-size: 14px;
z-index: 1000;
pointer-events: none; /* 防止干扰鼠标事件 */
box-shadow: 0 2px 10px rgba(0,0,0,0.3); /* 增强阴影 */
}
.button {

View File

@@ -0,0 +1,51 @@
/* 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;
}

View File

@@ -1,35 +1,3 @@
/* 问号图标 */
.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;

View File

@@ -3,13 +3,28 @@
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: 24px;
font-size: 18px;
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 {
@@ -69,4 +84,41 @@
/* 下拉浮框 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;
}
}

View File

@@ -77,7 +77,7 @@ document.addEventListener('DOMContentLoaded', function () {
});
// 初始化聊天消息管理器
const chatManager = new ChatMessageManager();
chatManager = new ChatMessageManager();
socket.on('connect', function () {
console.log('Connected to server');
@@ -146,7 +146,7 @@ document.addEventListener('DOMContentLoaded', function () {
}
const approveButton = document.createElement('button');
approveButton.innerHTML = '<i class="fas fa-check-circle-check"></i>';
approveButton.innerHTML = '<i class="bi bi-check-circle-fill"></i>';
approveButton.data = element;
approveButton.className = 'approve-button';
approveButton.onclick = function (event) {
@@ -424,7 +424,6 @@ window.addEventListener('beforeunload', () => {
let currentChapterIndex = -1;
function next_chapter(data) {
// 直接获取页面中的 h3 元素(不再使用 iframe
var container = document.getElementById('markdown-content-container');
var titles = [];
var h3Elements = container.querySelectorAll('h3');
@@ -467,48 +466,31 @@ 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('green'); // 已完成部分
titleBox.classList.add('blue'); // 已完成部分 - 蓝色
} else if (i === idx) {
titleBox.classList.add('green'); // 当前正在学习 - 绿色
} else {
titleBox.classList.add('white'); // 未完成部分
titleBox.classList.add('white'); // 未完成部分 - 白色
}
// 设置每个子框的标题
titleBox.innerHTML = titles[i] || `Step ${i + 1}`;
// 移除进度条上的文字,因为现在进度条很细
titleBox.innerHTML = '';
// 设置每个进度节点的宽度
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';
});
// 使用attachImmediateTooltip函数使提示框跟随鼠标移动
const tooltipText = `Step ${i + 1}: ${titles[i] || "No title"}`;
attachImmediateTooltip(titleBox, tooltipText);
progressBox.appendChild(titleBox);
}
// 将生成的进度条添加到容器中
container.appendChild(progressBox);
}

View File

@@ -0,0 +1,58 @@
// 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;

View File

@@ -4,60 +4,6 @@ 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);
@@ -196,6 +142,7 @@ function parseHeadings(mdText) {
help.className = 'help-icon';
help.textContent = ' ?';
li.appendChild(help);
// 新增:铅笔编辑按钮
const editBtn = createEditButton(() => {
openTitleEditor({

View File

@@ -0,0 +1,116 @@
// 动态加载 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);
}
});

View File

@@ -8,6 +8,7 @@
</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>
@@ -140,9 +141,7 @@
</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>
@@ -193,7 +192,6 @@
// 使用 fetch 发送请求到 Flask 服务器,获取 session 信息
fetch('/get_session', {
method: 'GET',
@@ -230,64 +228,6 @@
}
// 动态加载 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 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() {
loadMarkdown('{{course_id}}','{{chapter_name}}','{{lesson_name}}');
});
// 侧边工具栏切换
const sidebarTools = document.getElementById('sidebarTools');
const toggleButton = document.getElementById('toggleButton');
@@ -303,5 +243,7 @@
});
</script>
<!-- 引入 Markdown 渲染脚本 -->
<script src="/static/js/render-desktop-markdown.js"></script>
</body>
</html>

View File

@@ -61,16 +61,26 @@
</head>
<body>
<div class="confirm-container">
<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>
{% 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 %}
</div>
</body>
</html>

View File

@@ -8,6 +8,7 @@
<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">
@@ -100,6 +101,7 @@
<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>

View File

@@ -1,7 +1,6 @@
<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>

View File

@@ -6,6 +6,7 @@ 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__)
@@ -149,13 +150,33 @@ 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
lesson_name=lesson_name,
is_course_finished=is_course_finished
)
@bp.route("/vscode_data", methods=["POST"])

View File

@@ -39,35 +39,56 @@ def read_and_forward_pty_output(fd=None, pid=None, room_id=None,namespace=None):
"""
max_read_bytes = 1024 * 20
timeout=0.1
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:
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:
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:
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)
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
if fd:
try:
os.close(fd)
except Exception:
pass
class VSCLikeNameSpace(Namespace):
def on_connect(self):
@@ -156,15 +177,40 @@ class VSCLikeNameSpace(Namespace):
set_winsize(fd, data["rows"], data["cols"])
def on_disconnect(self):
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')
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
current_app.logger.debug('Client disconnected')

View File

@@ -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="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" />
<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" />
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.5.0/xterm.js"></script> -->
<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">
<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">
<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="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/loader.js"></script>
<script src="static/cdnback/js/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>