Compare commits

...

5 Commits

Author SHA1 Message Date
e65db69fc6 feat: 开发 unifront 前端会话模块 2026-04-21 16:52:04 +08:00
9dd6733063 refactor: 开始翻新状态机 2026-04-21 12:52:30 +08:00
bcdfddce10 fix: 修复部分问题 2026-04-21 02:06:28 +08:00
6f183e4d9d style: 格式化代码 2026-04-21 00:17:03 +08:00
4246061c29 fix: 优化 CSS 结构 2026-04-21 00:15:57 +08:00
47 changed files with 481 additions and 760 deletions

View File

@@ -1,42 +1,93 @@
# 贡献指南 # 贡献指南与二次开发
欢迎为此项目做出贡献!\ 欢迎为此项目做出贡献!
本项目是一个开源项目, 我们鼓励社区成员参与改进.
## 开发规范 ## 开发规范
1. 分支划分: 分支划分:
- `main` 分支: 稳定版本
- `dev` 分支: 开发版本 - `main` 分支: 稳定版本, 仅当稳定版本释出或修补版本时将 `dev` 合并到 `main`
- 功能分支: 从 `dev` 分支创建, 命名格式为 `feature/描述``fix/描述``refactor/描述` - `dev` 分支: 主线开发版本, 自身仅用于非重构的问题修复和整合功能分支
1. 代码风格: - 功能与重构分支: 从 `dev` 分支创建, 命名格式为 `feature/描述``fix/描述``refactor/v版本号`()
- 请使用 Black 格式化代码 - 不要将功能与重构分支先应被合并至 `dev` 后在 `dev` 完成文档开发后再释出至 `main`
- 遵循 PEP 8 规范 代码格式化:
- 添加适当的文档字符串 - 对于 Python, 使用 `black` 格式化
1. 提交消息: - 对于 Markdown, 使用 `mdformat` 格式化
- 使用简体中文或英文撰写清晰的提交消息 - 对于 Textual CSS, 使用 `prettier` 格式化
- 格式: 遵循 Conventional Commits 规范 - 格式化不是必需的, 可以整合入一次 `style` 提交, 但 `main``dev` 分支上的代码应尽量整洁, 以便合并时审查
1. 合并方式: 提交消息:
- 使用 Fast-forward 合并 - 使用简体中文或英文撰写清晰的提交消息
- 可以设置 `git config merge.ff false` - 提交消息格式: 遵循 [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 规范, 建议使用 `koji` 工具
合并方式:
- 为了一致性和可追溯性, 项目自 v0.4.0 重构后重新初始化仓库起就禁止使用 Fast-forward 合并
- 可以设置 `git config merge.ff false`
## 设置开发环境 ## 设置开发环境
```bash ```bash
# 克隆仓库 # 克隆仓库
git clone https://gitea.imwangzhiyu.xyz/ajax/HeurAMS git clone https://git.pluv27.top/pluv/HeurAMS
cd HeurAMS cd HeurAMS
# 可能需要切换分支 # 可能需要切换到 dev 分支
git checkout dev
# 安装依赖 # 如果决定使用 uv (推荐)
pip install -r requirements.txt
# 安装开发版本 ## 首先要安装uv, 例如通过 pip 或者其他包管理器
pip install -e . python3 -m pip install uv
uv sync # 同步开发运行环境
uv run heurams # 验证包安装
uv run tui # 启动 TUI
# 如果决定使用原生 python 环境 (不推荐, 但我们保留了这种方式以便在不支持 uv 的环境运行 HeurAMS)
## 安装依赖并将 HeurAMS 安装为本地包
python3 -m pip install -r requirements.txt
python3 -m pip install -e .
python3 -m heurams # 验证安装
python3 -m heurams.__interface__ # 启动 TUI
``` ```
## 许可证 ## 许可证与外部引用
贡献者同意其贡献将在 AGPL-3.0 许可证下发布. 贡献者拥有其贡献部分的版权同意其贡献将在 AGPL-3.0 许可证下发布.
如果您认为有必要引入其他开源的 vendor, 请在 PR 中注明或手动联系以便我们审查 vendor 许可证并更改此处和网站上的关于与版权声明
如果您认为有必要引入其他专有的网络服务(就像现在项目中的 edgetts), 请也在 PR 中注明
如果您认为有必要升级某个依赖或运行环境的版本, 请也在 PR 中注明
## 新的用户界面前端与其他语言移植
HeurAMS 被设计为一个可独立于前端的程序库, 这意味着:
- 我们的内置 Textual TUI 前端不是唯一可用的前端
- 您可以在自己的项目中以独立进程/服务调用 HeurAMS (但不能在代码中链接), 而免于受 AGPL-3.0 "污染"
- 如果您有一个自己开发的且可用的 HeurAMS 前端 (例如我们暂未实现的 flutter 前端), 并且以 AGPL-3.0/GPL-3.0 开放源代码, 可以联系我们将它转移到 HeurAMS 的官方仓库中以便共同维护, 您将保留您的版权并可主导该仓库下的开发工作 :)
- 如果您通过独立进程/服务调用方式开发了另外的软件, 开源但不愿使用 AGPL-3.0/GPL-3.0 许可证, 也可以联系我们, 我们乐于将您的项目链接添加到友链中
- 如果您想创建程序库的其他语言 (例如 dart) 版本以协助此语言下的方便集成, 并且同样以 AGPL-3.0/GPL-3.0 开放源代码, 也可以联系我们将它转移到 HeurAMS 的官方仓库中以便共同维护, 您将保留您的版权并可主导该仓库下的开发工作 :)
## 软件开发之外的贡献
即使您不是软件开发人员, 我们也欢迎您加入贡献!
您可以:
- 协助创建各种语言的翻译来翻译软件的界面 (但我们目前还没有 i18n 平台, 所以如果您想贡献翻译, 可能需要手动联系我们)
- 制作图像、主题、音效乃至制作开放的记忆单元集给其他用户使用
- 改进软件配套的文档
- 维护软件的开发/交流群组
- 给其他用户答疑解惑或分享自己的经验
- 在讨论区提出新想法或反馈问题
您的角色您来定!

View File

@@ -2,7 +2,7 @@ zmq_debug = true
_zmq_debug_desc = "[调试] ZeroMQ 调试服务器, 这会在 zmq_debug_port 上打开一个服务器\n调试工具可远程在 HeurAMS 内执行任意 python 代码, 无必要请关闭" _zmq_debug_desc = "[调试] ZeroMQ 调试服务器, 这会在 zmq_debug_port 上打开一个服务器\n调试工具可远程在 HeurAMS 内执行任意 python 代码, 无必要请关闭"
zmq_debug_port = 5555 zmq_debug_port = 5555
_zmq_debug_port_desc = "[调试] ZeroMQ 调试服务器端口" _zmq_debug_port_desc = "[调试] ZeroMQ 调试服务器端口"
enable_built_in_interface = false enable_built_in_interface = true
_enable_built_in_interface_desc = "启用内置基本用户界面\n(当且仅当 HeurAMS 作为程序库时禁用, 以跳过用户界面逻辑)" _enable_built_in_interface_desc = "启用内置基本用户界面\n(当且仅当 HeurAMS 作为程序库时禁用, 以跳过用户界面逻辑)"
_paths_desc = "用户数据路径定义" _paths_desc = "用户数据路径定义"

View File

@@ -1,4 +1,4 @@
_global_desc = "用户界面通用设置" _global_desc = "用户界面通用设置"
_widgets_desc = "组件设置" _widgets_desc = "组件设置"
_screens_desc = "界面设置" _screens_desc = "界面设置"
_puzzles_desc = "谜题生成器设置" _puzzles_desc = "谜题生成器设置"

View File

@@ -1,3 +1,9 @@
show_header = true
_show_header_desc = "展示界面顶部的标题栏\n如果您想节省这一行空间, 可以禁用它"
clock_on_header = true
_clock_on_header_desc = "在界面顶部的标题栏显示时间"
change_window_title = true
_change_window_title_desc = "更改终端模拟器窗口的标题\n如果禁用了 header, 则建议启用"
persist_to_file = true persist_to_file = true
_persist_to_file_desc = "[调试] 将记忆更改保存到文件" _persist_to_file_desc = "[调试] 将记忆更改保存到文件"
quick_pass = true quick_pass = true
@@ -5,13 +11,15 @@ _quick_pass_desc = "[调试] 启用快速应答功能(跳过测验)"
auto_pass = false auto_pass = false
_auto_pass_desc = "[调试] 自动通过测试模式" _auto_pass_desc = "[调试] 自动通过测试模式"
scheduled_num = 420 scheduled_num = 420
_scheduled_num_desc = "默认记忆单元数量(可被单元集设置覆盖)" _scheduled_num_desc = "默认记忆单元数量\n可被单元集设置覆盖"
refresh_on_resume = true
_refresh_on_resume_desc = "[调试] 每当 Screen 激活后都刷新状态"
algorithm = "SM-2" algorithm = "SM-2"
_algorithm_desc = "默认记忆调度算法(可被单元集设置覆盖)" _algorithm_desc = "默认记忆调度算法\n可被单元集设置覆盖"
[_algorithm_candidate] [_algorithm_candidate]
NSP-0 = "筛选用非间隔重复调度器" NSP-0 = "筛选用非间隔重复调度器"
none = "不设置默认调度器" none = "不设置默认调度器"
SM-2 = "第二代 SuperMemo 简单间隔重复调度器" SM-2 = "第二代 SuperMemo 简单间隔重复调度器\nWozniak 于 1987 年提出, Anki 的默认算法"
SM-15M = "第15代 SuperMemo 复杂间隔重复调度器 (不稳定且逆向工程)" SM-15M = "第15代 SuperMemo 复杂间隔重复调度器\n不稳定且逆向工程"
FSRS = "先进开放间隔重复调度器" FSRS = "先进开放间隔重复调度器"

View File

@@ -1,6 +1,6 @@
schedule = ["quick_review", "recognition", "final_review"] schedule = ["quick_review", "recognition", "final_review"]
[phases] [routes]
quick_review = [["FillBlank", "1.0"], ["Recognition", "1.0"]] quick_review = [["FillBlank", "1.0"], ["Recognition", "1.0"]]
recognition = [["FillBlank", "1.0"]] recognition = [["FillBlank", "1.0"]]
final_review = [["FillBlank", "1.0"], ["Recognition", "1.0"]] final_review = [["FillBlank", "1.0"], ["Recognition", "1.0"]]

View File

@@ -1,14 +0,0 @@
# encoding=utf-8
import jieba
# jieba.enable_paddle()# 启动paddle模式。 0.40版之后开始支持,早期版本不支持
strs = ["我来到北京清华大学", "乒乓球拍卖完了", "中国科学技术大学"]
# for str in strs:
# seg_list = jieba.cut(str,use_paddle=True) # 使用paddle模式
# print("Paddle Mode: " + '/'.join(list(seg_list)))
seg_list = jieba.cut("秦孝公据崤函之固, 拥雍州之地", cut_all=False)
print("Default Mode: " + "/ ".join(seg_list)) # 精确模式
seg_list = jieba.cut("他来到了网易杭研大厦") # 默认是精确模式
print(", ".join(seg_list))

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,3 +0,0 @@
title = "测试单元: 过秦论"
author = "__heurams__"
desc = "高考古诗文: 过秦论"

View File

@@ -1,11 +0,0 @@
["秦孝公据崤函之固, 拥雍州之地,"]
note = []
content = "秦孝公/据/崤函/之固/, 拥/雍州/之地,/"
translation = "秦孝公占据着崤山和函谷关的险固地势,拥有雍州的土地,"
keyword_note = {"据"="占据", "崤函"="崤山和函谷关", "雍州"="古代九州之一"}
["君臣固守以窥周室,"]
note = []
content = "君臣/固守/以窥/周室,/"
translation = "君臣牢固地守卫着,借以窥视周王室的权力,"
keyword_note = {"窥"="窥视"}

View File

@@ -1,5 +0,0 @@
schedule = ["quick_review", "recognition", "final_review"]
[phases]
quick_review = [["FillBlank", "1.0"], ["SelectMeaning", "0.5"], ["Recognition", "1.0"]]
recognition = [["Recognition", "1.0"]]
final_review = [["FillBlank", "0.7"], ["SelectMeaning", "0.7"], ["Recognition", "1.0"]]

View File

@@ -1,17 +0,0 @@
[annotation]
note = "笔记"
keyword_note = "关键词翻译"
translation = "语句翻译"
delimiter = "分隔符"
content = "内容"
tts_text = "文本转语音文本"
[common]
delimiter = "/"
tts_text = "eval:payload['content'].replace('/', '')"
[common.puzzles] # 谜题定义
# 我们称 "Recognition" 为 recognition 谜题的 alia
"Recognition" = { __origin__ = "recognition", __hint__ = "", primary = "eval:payload['content']", secondary = ["eval:payload['keyword_note']", "eval:payload['note']"], top_dim = ["eval:payload['translation']"] }
"SelectMeaning" = { __origin__ = "mcq", __hint__ = "eval:payload['content']", primary = "eval:payload['content']", mapping = "eval:payload['keyword_note']", jammer = "eval:list(payload['keyword_note'].values())", max_riddles_num = "eval:default['mcq']['max_riddles_num']", prefix = "选择正确项: " }
"FillBlank" = { __origin__ = "cloze", __hint__ = "", text = "eval:payload['content']", delimiter = "eval:nucleon['delimiter']", min_denominator = "eval:default['cloze']['min_denominator']"}

View File

@@ -1,15 +0,0 @@
# 运行时对象
Atom: 原子, 由核子, 电子, 轨道对象一并构成, 用于处理记忆所需一系列对象
Nucleon: 核子, 负责解析文件动态内容, 并储存记忆材料内容与谜题定义, 是静态只读但可临时覆盖内容的\
Electron: 电子, 负责处理记忆算法数据\
Orbital: 轨道, 储存记忆阶段信息与谜题阶段内出现配置
# 状态机对象
Transitions: 一种状态机框架库
Reactor: 状态机库
Phaser...
rating: 用户评估生成的值
quality: 用于单元反馈的值

View File

@@ -25,6 +25,7 @@ from .screens.navigator import NavigatorScreen
from .screens.precache import PrecachingScreen from .screens.precache import PrecachingScreen
from .screens.setting import SettingScreen from .screens.setting import SettingScreen
from .screens.synctool import SyncScreen from .screens.synctool import SyncScreen
from . import shim
_end = perf_counter() _end = perf_counter()
print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)") print(f"已完成! (耗时: {round(1000 * (_end - _start))}ms)")
@@ -56,6 +57,9 @@ class HeurAMSApp(App):
"setting": SettingScreen, "setting": SettingScreen,
} }
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def on_mount(self) -> None: def on_mount(self) -> None:
self.push_screen("dashboard") self.push_screen("dashboard")

View File

@@ -2,22 +2,6 @@ NavigatorScreen {
align: center middle; align: center middle;
} }
.infview {
width: 5fr
}
.dataview {
width: 3fr
}
.repo_listitem {
layout: grid;
grid-size: 2;
}
.repo_listitem_btn {
dock: right;
offset: -5% 0
}
#dialog { #dialog {
grid-size: 2; grid-size: 2;
@@ -29,33 +13,3 @@ NavigatorScreen {
border: thick $background 80%; border: thick $background 80%;
background: $surface; background: $surface;
} }
#dashboardtop {
height: 4
}
#input-container {
height: 3;
margin-top: 1;
align: center middle;
}
.container {
height: auto;
padding: 0 0 1 0;
}
#message-input {
width: 1fr;
margin-right: 1;
}
#status-bar {
height: 1;
margin-top: 1;
text-style: italic;
color: $text-muted;
}
.session-label {
color: $primary;
text-style: bold;
}

View File

@@ -3,12 +3,12 @@
} }
#header { #header {
height: 4; height: 3;
} }
.repo-list-item { .repo-list-item {
layout: grid; layout: grid;
grid-size: 2; grid-size: 1;
height: 3; height: 3;
} }

View File

@@ -0,0 +1,4 @@
#puzzle_container > * {
height: auto;
width: auto;
}

View File

@@ -0,0 +1,24 @@
#operations {
height: auto;
}
.btn {
margin: 0 1 0 0;
padding: 0 1 0 1;
}
#main_container {
}
#previewer_container {
}
.unit-statline {
}
#title {
padding: 0;
}

View File

@@ -0,0 +1,27 @@
.foot {
align-vertical: bottom;
}
Switch {
dock: right;
}
Label {
width: auto
}
Select {
width: 55%;
dock: right;
}
Input {
width: 55%;
dock: right;
}
Horizontal {
width: 100%;
height: 4;
padding: 0 0 1 0;
}

View File

@@ -5,6 +5,8 @@ from textual.containers import ScrollableContainer
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, Markdown, Static from textual.widgets import Button, Footer, Header, Label, Markdown, Static
from textual import events, on
import heurams.services.version as version import heurams.services.version as version
from heurams.context import * from heurams.context import *
import platform import platform
@@ -19,8 +21,19 @@ class AboutScreen(Screen):
("q", "go_back", "返回"), ("q", "go_back", "返回"),
] ]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header(show_clock=True)
if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer(id="about_container"): with ScrollableContainer(id="about_container"):
yield Label("[b]关于与版本信息[/b]") yield Label("[b]关于与版本信息[/b]")
@@ -41,30 +54,27 @@ class AboutScreen(Screen):
API 版本代号: `{version.codename.capitalize()}` API 版本代号: `{version.codename.capitalize()}`
一个基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划. 一个基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划.
一个开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的研究.
以 AGPL-3.0 开放源代码, 这直接意味着任何个体直接基于此代码对外或内部提供的应用和服务, 无论本地或网络, 必须向所有用户公开完整修改后的源代码, 且继续沿用 AGPL-3.0 协议. 以 AGPL-3.0 开放源代码, 这直接意味着任何个体直接基于此代码对外或内部提供的应用和服务, 无论本地或网络, 必须向所有用户公开完整修改后的源代码, 且继续沿用 AGPL-3.0 协议.
您正使用的 TUI 用户界面是 python 版本程序库自带的基本用户界面, 作为基本的全功能前端实现与程序库测试, 如果您想去除它, 请移除程序库根目录中的 interface 文件夹. 您正使用的 TUI 用户界面是 python 版本程序库自带的基本用户界面, 作为基本的全功能前端实现与程序库测试, 位于程序库根目录中的 interface 文件夹.
您可在项目主页 https://ams.pluv27.top 获取用户指南, 开发文档与软件更新. 您可在项目主页 https://ams.pluv27.top 获取用户指南, 开发文档与软件更新.
如果您觉得这个软件有用, 可以在它的源代码仓库给它添加一个星标 :) 如果您觉得这个软件有用, 可以考虑参与贡献, 或在它的源代码仓库给它添加一个星标 :)
> 潜进(HeurAMS), 以及它作为一个"程序库"是自由且免费的, 但是开发工作必须投入大量精力. 您的慷慨支持, 我们必当涌泉相报.
> 您可以加入各种语言的翻译团队来翻译软件的界面, 您还可以制作图像、主题、音效, 或者改进软件配套的文档……
> 不管您来自何方, 我们都欢迎您加入社区并做出贡献.
> 我们的共同目标是为人人带来高品质的辅助记忆 & 学习软件.
> 您的慷慨支持, 我们必当涌泉相报.
开发人员列表: 开发人员列表:
- Wang Zhiyu([@pluvium27](https://github.com/pluvium27)): 发起项目与主要维护者 - Wang Zhiyu([@pluvium27](https://github.com/pluvium27)): 发起项目与主要维护者
特别感谢以下人士, 他们的算法与理论构成了此软件算法的基石: 特别感谢以下人士, 他们的算法与理论构成了此软件现有算法的基石:
- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 算法与 SM-15 算法理论 - [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 算法与 SM-15 算法理论
- [Kazuaki Tanida](https://github.com/slaypni): SM-15 算法的 CoffeeScript 实现 - [Kazuaki Tanida](https://github.com/slaypni): SM-15 算法的 CoffeeScript 实现
- [Thoughts Memo](https://www.zhihu.com/people/L.M.Sherlock): 文献参考 - [Thoughts Memo](https://www.zhihu.com/people/L.M.Sherlock): 中文文献参考
# 运行环境信息 # 运行环境信息
@@ -85,6 +95,7 @@ Textual 框架版本: {textual_version}
"返回主界面", "返回主界面",
id="back_button", id="back_button",
variant="primary", variant="primary",
flat=True,
classes="back-button", classes="back-button",
) )
yield Footer() yield Footer()

View File

@@ -10,6 +10,8 @@ from textual.containers import ScrollableContainer, Container, Horizontal, Verti
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, ListItem, ListView, Static from textual.widgets import Button, Footer, Header, Label, ListItem, ListView, Static
from textual.layouts import horizontal from textual.layouts import horizontal
from textual import events, on
from textual.reactive import reactive
import heurams.kernel.particles as pt import heurams.kernel.particles as pt
import heurams.services.timer as timer import heurams.services.timer as timer
@@ -35,7 +37,9 @@ class DashboardScreen(Screen):
("q", "go_back", "返回"), ("q", "go_back", "返回"),
] ]
CSS_PATH = Path(__file__).parent.parent / "css" / "screens" / "dashboard.tcss" CSS_PATH = rootdir / "interface" / "css" / "screens" / "dashboard.tcss"
repolink = reactive({})
def __init__( def __init__(
self, self,
@@ -44,22 +48,21 @@ class DashboardScreen(Screen):
classes: str | None = None, classes: str | None = None,
) -> None: ) -> None:
super().__init__(name, id, classes) super().__init__(name, id, classes)
self.repolink = {} self._load_data()
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
"""组合界面组件""" """组合界面组件"""
self._load_data() if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=True) yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer(): with ScrollableContainer():
yield Horizontal( # 顶部的状态 yield Horizontal( # 顶部的状态
Vertical( Vertical(
Label(f'欢迎使用 "潜进" 版本 {version.ver}'), Label(f"当前日时间戳: {timer.get_daystamp()}"),
Label(f"当前 UNIX 日时间戳: {timer.get_daystamp()}"),
Label( Label(
f"应用时区修正: UTC+{config_var.get()['services']['timer']['timezone_offset'] / 3600}" f"应用时区修正: UTC+{str(config_var.get()['services']['timer']['timezone_offset'] / 3600).rstrip('.0')}"
), ),
Label( Label(
f"默认算法设置: {config_var.get()['interface']['global']['algorithm']}" f"默认算法设置: {config_var.get()['interface']['global']['algorithm']}",
), ),
classes="left", classes="left",
), ),
@@ -79,11 +82,16 @@ class DashboardScreen(Screen):
yield ListView(id="repo_list", classes="repo-list") # 单元集选择 yield ListView(id="repo_list", classes="repo-list") # 单元集选择
yield Label( yield Label(f"版本 {version.ver} {version.stage.capitalize()}") # 版本信息
f'"潜进" 启发式辅助记忆调度器 版本 {version.ver} {version.stage.capitalize()}'
) # 版本信息
yield Footer() yield Footer()
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
# https://github.com/Textualize/textual/discussions/4268
# self.refresh(recompose=True) 此函数有问题且官方不管 而且性能低
def _load_data(self): def _load_data(self):
repo_dirs = Repo.probe_valid_repos_in_dir( repo_dirs = Repo.probe_valid_repos_in_dir(
Path(config_var.get()["global"]["paths"]["repo"]) Path(config_var.get()["global"]["paths"]["repo"])
@@ -96,7 +104,6 @@ class DashboardScreen(Screen):
# need_review: 需要/不需要学习 # need_review: 需要/不需要学习
# nearest_review_time: 最近下次学习时间 # nearest_review_time: 最近下次学习时间
# progress: 进度 # progress: 进度
# algotype: 算法类型
## initial_time: 起始时间 ## initial_time: 起始时间
# package: 包名 # package: 包名
# prompt: 最终呈现信息 # prompt: 最终呈现信息
@@ -107,6 +114,10 @@ class DashboardScreen(Screen):
"touched": 0, "touched": 0,
"have_activated_ever": 0, "have_activated_ever": 0,
} }
repo.preview = {
"review": 0,
"new": repo.config["scheduled_num"],
}
initial_time = float("inf") initial_time = float("inf")
for i in range(repo.data_length): for i in range(repo.data_length):
e = pt.Electron.from_data( e = pt.Electron.from_data(
@@ -118,11 +129,13 @@ class DashboardScreen(Screen):
repo.progress["have_activated_ever"] = 1 repo.progress["have_activated_ever"] = 1
repo.progress["touched"] += 1 repo.progress["touched"] += 1
repo.nearest_review_time = min(repo.nearest_review_time, e.nextdate()) repo.nearest_review_time = min(repo.nearest_review_time, e.nextdate())
if timer.get_daystamp() >= e.nextdate():
repo.preview["review"] += 1
# initial_time = min(initial_time, e.) # initial_time = min(initial_time, e.)
repo.need_review = timer.get_daystamp() >= repo.nearest_review_time repo.need_review = timer.get_daystamp() >= repo.nearest_review_time
repo.prompt = f"""{repo.manifest['title']} ({repo.config['algorithm']}) repo.prompt = f"""{repo.manifest['title']} \\[{repo.config['algorithm']}]
[d]进度: {repo.progress['touched']}/{repo.progress['total']} ({round(repo.progress['touched']/repo.progress['total']*100, 1)}%)[/d] [d]进度: {repo.progress['touched']}/{repo.progress['total']} ({round(repo.progress['touched']/repo.progress['total']*100, 1)}%)[/d]
[d]{'需要复习' if repo.need_review else ("暂未开始" if not repo.progress['have_activated_ever'] else '无需操作')}[/d]""" [d]{f'需要学习: {repo.preview['review']}R + {repo.preview['new']}U' if repo.need_review else (f"暂未开始: 0R + {repo.preview['new']}U" if not repo.progress['have_activated_ever'] else '无需操作')}[/d]"""
def on_mount(self) -> None: def on_mount(self) -> None:
"""挂载组件时初始化""" """挂载组件时初始化"""
@@ -152,7 +165,7 @@ class DashboardScreen(Screen):
for r in self.repos: for r in self.repos:
self.repolink[str(id(r))] = r # 用于规避 ctype id 对象还原 self.repolink[str(id(r))] = r # 用于规避 ctype id 对象还原
list_item = ListItem( list_item = ListItem(
Label(r.prompt), *[Label(line) for line in r.prompt.splitlines()],
Button( Button(
f"开始学习", f"开始学习",
flat=True, flat=True,

View File

@@ -4,6 +4,8 @@ import base64
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import List, Optional
from textual import events, on
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import ScrollableContainer from textual.containers import ScrollableContainer
from textual.screen import Screen from textual.screen import Screen
@@ -18,6 +20,7 @@ from textual.widgets import (
Static, Static,
) )
from textual import events, on
from heurams.context import config_var from heurams.context import config_var
from heurams.kernel.repolib import Repo from heurams.kernel.repolib import Repo
from heurams.services.favorite_service import FavoriteItem, favorite_manager from heurams.services.favorite_service import FavoriteItem, favorite_manager
@@ -53,7 +56,9 @@ class FavoriteManagerScreen(Screen):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
"""组合界面组件""" """组合界面组件"""
yield Header(show_clock=True)
if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer(id="favorites-container"): with ScrollableContainer(id="favorites-container"):
if not self.favorites: if not self.favorites:
yield Label("暂无收藏", classes="empty-label") yield Label("暂无收藏", classes="empty-label")
@@ -63,6 +68,12 @@ class FavoriteManagerScreen(Screen):
yield ListView(id="favorites-list") yield ListView(id="favorites-list")
yield Footer() yield Footer()
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def on_mount(self) -> None: def on_mount(self) -> None:
"""挂载后填充列表""" """挂载后填充列表"""
if self.favorites: if self.favorites:

View File

@@ -5,26 +5,24 @@ from pathlib import Path
from typing import Callable from typing import Callable
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import Center, ScrollableContainer from textual.containers import Center, ScrollableContainer, Container
from textual.reactive import reactive from textual.reactive import reactive
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, Static from textual.widgets import Button, Footer, Header, Label, Static
from textual import events, on
import heurams.kernel.particles as pt import heurams.kernel.particles as pt
import heurams.kernel.puzzles as pz import heurams.kernel.puzzles as pz
from heurams.context import config_var from heurams.context import config_var, rootdir
from heurams.kernel.reactor import * from heurams.kernel.reactor import *
from heurams.services.favorite_service import favorite_manager from heurams.services.favorite_service import favorite_manager
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
import pickle
from .. import shim from .. import shim
class AtomState(Enum):
FAILED = auto()
NORMAL = auto()
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -35,50 +33,61 @@ class MemScreen(Screen):
("d", "toggle_dark", ""), ("d", "toggle_dark", ""),
("v", "play_voice", "朗读"), ("v", "play_voice", "朗读"),
("*", "toggle_favorite", "收藏"), ("*", "toggle_favorite", "收藏"),
("0,1,2,3", "app.push_screen('about')", ""), ("n", "block_prompt"),
("s", "block_prompt"),
("z", "block_prompt"),
] ]
SUB_TITLE = "学习中"
CSS_PATH = rootdir / "interface" / "css" / "screens" / "memoqueue.tcss"
if config_var.get()["interface"]["global"]["quick_pass"]: if config_var.get()["interface"]["global"]["quick_pass"]:
BINDINGS.append(("k", "quick_pass", "正确应答")) BINDINGS.append(("k", "quick_pass", "正确应答"))
BINDINGS.append(("f", "quick_fail", "错误应答")) BINDINGS.append(("f", "quick_fail", "错误应答"))
rating = reactive(-1) rating = reactive(-1)
def __init__( def __init__(
self, self,
phaser: Phaser, router: Router,
save_func: Callable,
repo=None, repo=None,
name=None, name=None,
id=None, id=None,
classes=None, classes=None,
) -> None: ) -> None:
super().__init__(name, id, classes) super().__init__(name, id, classes)
self.phaser = phaser self.router = router
self.save_func = save_func
self.repo = repo self.repo = repo
self.update_state() self.update_state()
self.fission: Fission self.expander: Expander
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header(show_clock=True) if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer(): with ScrollableContainer():
yield Label(self._get_progress_text(), id="progress") yield Label(self._get_progress_text(), id="head_stat")
yield ScrollableContainer(id="puzzle-container") yield ScrollableContainer(id="puzzle_container")
yield Footer() yield Footer()
def update_state(self): def update_state(self):
"""更新状态机""" """更新状态机"""
self.procession: Procession = self.phaser.current_procession() # type: ignore self.procession: Procession = self.router.current_procession() # type: ignore
self.atom: pt.Atom = self.procession.current_atom # type: ignore self.atom: pt.Atom = self.procession.current_atom # type: ignore
def on_mount(self): def on_mount(self):
self.fission = self.procession.get_fission() self.expander = self.procession.get_expander()
self.mount_puzzle() self.mount_puzzle()
self.update_display() self.update_display()
def puzzle_widget(self): def puzzle_widget(self):
try: try:
puzzle = self.fission.get_current_puzzle_inf() puzzle = self.expander.get_current_puzzle_inf()
return shim.puzzle2widget[puzzle["puzzle"]]( # type: ignore return shim.puzzle2widget[puzzle["puzzle"]]( # type: ignore
atom=self.atom, alia=puzzle["alia"] # type: ignore atom=self.atom, alia=puzzle["alia"] # type: ignore
) )
@@ -87,53 +96,32 @@ class MemScreen(Screen):
return Static(f"无法生成谜题 {e}") return Static(f"无法生成谜题 {e}")
def _get_progress_text(self): def _get_progress_text(self):
s = f"阶段: {self.procession.phase.name}\n" s = f"阶段: {self.procession.route.name}\n"
# 收藏状态 # 收藏状态
if self.repo is not None: if self.repo is not None:
fav_status = "已收藏" if self._is_current_atom_favorited() else "未收藏" fav_status = "已收藏" if self._is_current_atom_favorited() else "未收藏"
s += f"收藏: {fav_status}\n" s += f"收藏: {fav_status}\n"
"""if config_var.get().get("debug_topline", 0):
try:
alia = self.fission.get_current_puzzle_inf()["alia"] # type: ignore
s += f"谜题: {alia}\n"
except:
pass
try:
stat = self.phaser.__repr__("simple", "")
s += f"{stat}\n"
except:
pass
try:
stat = self.procession.__repr__("simple", "")
s += f"{stat}\n"
except:
pass
try:
stat = self.fission.__repr__("simple", "")
s += f"{stat}\n"
except Exception as e:
s = str(e)"""
s += f"进度: {self.procession.process() + 1}/{self.procession.total_length()}" s += f"进度: {self.procession.process() + 1}/{self.procession.total_length()}"
return s return s
def update_display(self): def update_display(self):
"""更新进度显示""" """更新进度显示"""
progress_widget = self.query_one("#progress") progress_widget = self.query_one("#head_stat")
progress_widget.update(self._get_progress_text()) # type: ignore progress_widget.update(self._get_progress_text()) # type: ignore
def mount_puzzle(self): def mount_puzzle(self):
"""挂载当前谜题组件""" """挂载当前谜题组件"""
if self.procession.phase == PhaserState.FINISHED: if self.procession.route == RouterState.FINISHED:
self.mount_finished_widget() self.mount_finished_widget()
return return
container = self.query_one("#puzzle-container") container = self.query_one("#puzzle_container")
for i in container.children: for i in container.children:
i.remove() i.remove()
container.mount(self.puzzle_widget()) container.mount(self.puzzle_widget())
def mount_finished_widget(self): def mount_finished_widget(self):
"""挂载已完成组件""" """挂载已完成组件"""
container = self.query_one("#puzzle-container") container = self.query_one("#puzzle_container")
for i in container.children: for i in container.children:
i.remove() i.remove()
from heurams.interface.widgets.finished import Finished from heurams.interface.widgets.finished import Finished
@@ -169,10 +157,10 @@ class MemScreen(Screen):
if new_rating == -1: # 安全值 if new_rating == -1: # 安全值
return return
self.update_state() self.update_state()
if self.procession.phase == PhaserState.FINISHED: if self.procession.route == RouterState.FINISHED:
rating = -1 rating = -1
return return
self.fission.report(new_rating) self.expander.report(new_rating)
self.forward(new_rating) self.forward(new_rating)
self.rating = -1 self.rating = -1
@@ -180,9 +168,9 @@ class MemScreen(Screen):
self.update_state() self.update_state()
allow_forward = 1 if rating >= 4 else 0 allow_forward = 1 if rating >= 4 else 0
if allow_forward: if allow_forward:
self.fission.forward() self.expander.forward()
if self.fission.state == "retronly": if self.expander.state == "retronly":
self.forward_atom(self.fission.get_quality()) self.forward_atom(self.expander.get_quality())
self.update_state() self.update_state()
self.mount_puzzle() self.mount_puzzle()
self.update_display() self.update_display()
@@ -207,7 +195,7 @@ class MemScreen(Screen):
self.update_state() # 刷新状态 self.update_state() # 刷新状态
self.procession.forward(1) self.procession.forward(1)
self.update_state() # 刷新状态 self.update_state() # 刷新状态
self.fission = self.procession.get_fission() self.expander = self.procession.get_expander()
def action_go_back(self): def action_go_back(self):
self.app.pop_screen() self.app.pop_screen()
@@ -254,3 +242,6 @@ class MemScreen(Screen):
self.app.notify(f"已收藏:{ident}", severity="information") self.app.notify(f"已收藏:{ident}", severity="information")
# 更新显示(如果需要) # 更新显示(如果需要)
self.update_display() self.update_display()
def action_block_prompt(self):
self.app.notify("功能在记忆界面中不可用, 完成或返回后再试", severity="error")

View File

@@ -5,6 +5,8 @@ from textual.containers import Grid, ScrollableContainer
from textual.screen import ModalScreen from textual.screen import ModalScreen
from textual.widgets import Button, Footer, Header, Label, ListItem, ListView, Static from textual.widgets import Button, Footer, Header, Label, ListItem, ListView, Static
from textual import events, on
from heurams.context import * from heurams.context import *
from heurams.services.logger import get_logger from heurams.services.logger import get_logger

View File

@@ -8,6 +8,8 @@ from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, ProgressBar, Static from textual.widgets import Button, Footer, Header, Label, ProgressBar, Static
from textual.worker import get_current_worker from textual.worker import get_current_worker
from textual import events, on
import heurams.kernel.particles as pt import heurams.kernel.particles as pt
import heurams.services.hasher as hasher import heurams.services.hasher as hasher
from heurams.context import * from heurams.context import *
@@ -17,7 +19,7 @@ paths = config_var.get()["global"]["paths"]
cache_dir = pathlib.Path(paths.get("cache", paths["data"] + "/cache")) / "voice" cache_dir = pathlib.Path(paths.get("cache", paths["data"] + "/cache")) / "voice"
def format_size(bytes_num: int) -> str: def human_size(bytes_num: int) -> str:
"""将字节数格式化为人类可读的字符串""" """将字节数格式化为人类可读的字符串"""
for unit in ["B", "KB", "MB", "GB", "TB"]: for unit in ["B", "KB", "MB", "GB", "TB"]:
if bytes_num < 1024.0: if bytes_num < 1024.0:
@@ -80,6 +82,12 @@ class PrecachingScreen(Screen):
continue continue
return total return total
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def _update_cache_stats(self) -> None: def _update_cache_stats(self) -> None:
"""更新缓存统计信息""" """更新缓存统计信息"""
total_size = 0 total_size = 0
@@ -97,13 +105,15 @@ class PrecachingScreen(Screen):
self.cache_stats["total_size"] = total_size self.cache_stats["total_size"] = total_size
self.cache_stats["file_count"] = file_count self.cache_stats["file_count"] = file_count
self.cache_stats["human_size"] = format_size(total_size) self.cache_stats["human_size"] = human_size(total_size)
self.cache_stats["cached_units"] = cached_units self.cache_stats["cached_units"] = cached_units
self.cache_stats["total_units"] = total_units self.cache_stats["total_units"] = total_units
self.cache_stats["cache_rate"] = cache_rate self.cache_stats["cache_rate"] = cache_rate
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header(show_clock=True)
if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer(id="precache_container"): with ScrollableContainer(id="precache_container"):
yield Label("[b]音频预缓存[/b]", classes="title-label") yield Label("[b]音频预缓存[/b]", classes="title-label")
with Container(): with Container():

View File

@@ -1,7 +1,7 @@
"""记忆准备界面""" """记忆准备界面"""
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import ScrollableContainer from textual.containers import ScrollableContainer, Container, Horizontal
from textual.reactive import reactive from textual.reactive import reactive
from textual.screen import Screen from textual.screen import Screen
from textual.widget import Widget from textual.widget import Widget
@@ -15,12 +15,16 @@ from textual.widgets import (
Rule, Rule,
Sparkline, Sparkline,
) )
from textual.lazy import Reveal, Lazy
from textual import events, on
import heurams.kernel.particles as pt import heurams.kernel.particles as pt
import heurams.services.hasher as hasher import heurams.services.hasher as hasher
from heurams.context import * from heurams.context import *
from heurams.context import config_var from heurams.context import config_var
from heurams.kernel.repolib import * from heurams.kernel.repolib import *
from heurams.kernel.algorithms import algorithms
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -32,46 +36,66 @@ class PreparationScreen(Screen):
BINDINGS = [ BINDINGS = [
("q", "go_back", "返回"), ("q", "go_back", "返回"),
("p", "precache", "缓存音频"), ("p", "precache", "缓存"),
("d", "toggle_dark", ""), ("d", "toggle_dark", ""),
("0,1,2,3", "app.push_screen('about')", ""), ("0,1,2,3", "app.push_screen('about')", ""),
] ]
scheduled_num = reactive(config_var.get()["interface"]["global"]["scheduled_num"]) CSS_PATH = rootdir / "interface" / "css" / "screens" / "preparation.tcss"
def __init__(self, repo: Repo) -> None: def __init__(self, repo: Repo) -> None:
super().__init__(name=None, id=None, classes=None) super().__init__(name=None, id=None, classes=None)
self.repo = repo self.repo = repo
self.load_data() self.load_data()
def compose(self) -> ComposeResult: @on(events.ScreenResume)
yield Header(show_clock=True) def post_active(self, event):
with ScrollableContainer(id="vice_container"): from heurams.interface import shim
yield Label(f"准备就绪: [b]{self.repo.manifest['title']}[/b]\n")
yield Label(f"[b]仓库路径: {self.repo.source}[/b]")
yield Label(f"\n单元数量: {len(self.repo)}\n")
yield Label(f"最小记忆分组: {self.scheduled_num}\n", id="schnum_label")
yield Button( shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
"开始记忆",
id="start_memorizing_button", def compose(self) -> ComposeResult:
variant="primary",
classes="start-button", if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer(id="main_container"):
yield Markdown(
f"**准备就绪**: `{self.repo.manifest['title']}`\n", id="title"
) )
yield Button( yield Label(f"单元集路径: {self.repo.source}")
"预缓存音频", yield Label(
id="precache_button", f"学习完成度: {self.repo.progress['touched']}/{len(self.repo)} [d]\\[{round(self.repo.progress['touched']/self.repo.progress['total']*100, 1)}%][/d]"
variant="success", )
classes="precache-button", yield Label(
f"调度算法: {self.repo.config["algorithm"]} {algorithms[self.repo.config["algorithm"]].desc}"
)
yield Label(
f"学习数量: {self.repo.preview['review'] + self.scheduled_num} = {self.repo.preview['review']} [d][复习][/d] + {self.scheduled_num} [d][新识记][/d]\n",
id="schnum_label",
)
yield Horizontal(
Button(
"开始记忆",
id="start_memorizing_button",
variant="primary",
classes="btn",
),
Button(
"管理缓存",
id="precache_button",
variant="success",
classes="btn",
),
id="operations",
) )
yield Static() yield Static()
yield Sparkline(self.spark_line_arr, summary_function=max) yield Sparkline(self.spark_line_arr, summary_function=max)
yield Rule()
# yield Static(str(self.spark_line_arr)) # yield Static(str(self.spark_line_arr))
yield Static(f"单元状态预览:\n") with Reveal(ScrollableContainer(id="previewer_container")):
for i in self.content.splitlines(): for i in self.content.splitlines():
yield Static(i, classes="full") yield Static(i, classes="unit-statline")
yield Footer() yield Footer()
# def watch_scheduled_num(self, old_scheduled_num, new_scheduled_num): # def watch_scheduled_num(self, old_scheduled_num, new_scheduled_num):
@@ -83,6 +107,7 @@ class PreparationScreen(Screen):
# pass # pass
def load_data(self): def load_data(self):
self.scheduled_num = self.repo.config["scheduled_num"]
content = "" content = ""
spark_line_arr = [] spark_line_arr = []
for i in self.repo.ident_index: for i in self.repo.ident_index:
@@ -166,7 +191,6 @@ def launch(repo, app, scheduled_num):
from .memoqueue import MemScreen from .memoqueue import MemScreen
pheser = rt.Phaser(atoms_to_provide) router = rt.Router(atoms_to_provide)
save_func = repo.persist_to_repodir memscreen = MemScreen(router=router, repo=repo)
memscreen = MemScreen(pheser, save_func, repo=repo)
app.push_screen(memscreen) app.push_screen(memscreen)

View File

@@ -23,6 +23,8 @@ from textual.widgets import (
) )
from textual.layouts import horizontal from textual.layouts import horizontal
from textual import events, on
import heurams.kernel.particles as pt import heurams.kernel.particles as pt
import heurams.services.timer as timer import heurams.services.timer as timer
import heurams.services.version as version import heurams.services.version as version
@@ -44,6 +46,7 @@ class SettingScreen(Screen):
BINDINGS = [ BINDINGS = [
("q", "go_back", "返回"), ("q", "go_back", "返回"),
] ]
CSS_PATH = rootdir / "interface" / "css" / "screens" / "setting.tcss"
def __init__( def __init__(
self, self,
@@ -53,9 +56,16 @@ class SettingScreen(Screen):
) -> None: ) -> None:
super().__init__(name, id, classes) super().__init__(name, id, classes)
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
"""组合界面组件""" """组合界面组件"""
yield Header(show_clock=True) if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer(): with ScrollableContainer():
yield Label("[b]设置页面[/b]") yield Label("[b]设置页面[/b]")
for i in config_var.get(): for i in config_var.get():
@@ -64,8 +74,12 @@ class SettingScreen(Screen):
a = self._get_subcfg(f"{i}") a = self._get_subcfg(f"{i}")
if a: if a:
yield Collapsible( yield Collapsible(
*a, title=i + f'\n{config_var.get().get(f"_{i}_desc", "")}' *a, title=i + f'\n[d]{config_var.get().get(f"_{i}_desc", "")}[/d]'
) )
yield Label(
"退出页面时, 所作的更改会立即保存, 但仍建议重启软件以确保新的配置得到应用",
classes="foot",
)
yield Footer() yield Footer()
def _get_subcfg(self, parent_epath: str): def _get_subcfg(self, parent_epath: str):
@@ -80,7 +94,7 @@ class SettingScreen(Screen):
if a: if a:
lst.append( lst.append(
Collapsible( Collapsible(
*a, title=i + f'\n{parent.get(f"_{i}_desc", "")}' *a, title=i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'
) )
) )
return lst return lst
@@ -96,85 +110,79 @@ class SettingScreen(Screen):
if a: if a:
lst.append( lst.append(
Collapsible( Collapsible(
*a, title=i + f'\n{parent.get(f"_{i}_desc", "")}' *a, title=i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'
) )
) )
elif f"_{i}_candidate" in parent: # 选择框模式 elif f"_{i}_candidate" in parent: # 选择框模式
if isinstance(parent[f"_{i}_candidate"], dict): if isinstance(parent[f"_{i}_candidate"], dict):
lst.append( lst.append(
Horizontal( Horizontal(
Label(i + f'\n{parent.get(f"_{i}_desc", "")}'), Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Select( Select(
( (
(f"{j} ({k})", j) (f"{j}\n[d]{k}[/d]", j)
for j, k in parent[f"_{i}_candidate"].items() for j, k in parent[f"_{i}_candidate"].items()
), ),
prompt=f'{parent.get(f"{i}", "")}', prompt=f'{parent.get(f"{i}", "")}',
id=domize(f"{parent_epath}.{i}"), id=domize(f"{parent_epath}.{i}"),
), ),
classes="container",
) )
) )
elif isinstance(parent[f"_{i}_candidate"], list): elif isinstance(parent[f"_{i}_candidate"], list):
lst.append( lst.append(
Horizontal( Horizontal(
Label(i + f'\n{parent.get(f"_{i}_desc", "")}'), Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Select( Select(
((j, j) for j in parent[f"_{i}_candidate"]), ((j, j) for j in parent[f"_{i}_candidate"]),
prompt=f'{parent.get(f"{i}", "")}', prompt=f'{parent.get(f"{i}", "")}',
id=domize(f"{parent_epath}.{i}"), id=domize(f"{parent_epath}.{i}"),
), ),
classes="container",
) )
) )
else: else:
if isinstance(parent[i], float): if isinstance(parent[i], float):
lst.append( lst.append(
Horizontal( Horizontal(
Label(i + f'\n{parent.get(f"_{i}_desc", "")}'), Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Input( Input(
value=str(parent[i]), value=str(parent[i]),
placeholder="要求一个浮点数", placeholder="要求一个浮点数",
type="number", type="number",
id=domize(f"{parent_epath}.{i}"), id=domize(f"{parent_epath}.{i}"),
), ),
classes="container",
) )
) )
elif isinstance(parent[i], str): elif isinstance(parent[i], str):
lst.append( lst.append(
Horizontal( Horizontal(
Label(i + f'\n{parent.get(f"_{i}_desc", "")}'), Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Input( Input(
value=parent[i], value=parent[i],
placeholder="要求一个字符串", placeholder="要求一个字符串",
type="text", type="text",
id=domize(f"{parent_epath}.{i}"), id=domize(f"{parent_epath}.{i}"),
), ),
classes="container",
) )
) )
elif isinstance(parent[i], bool): elif isinstance(parent[i], bool):
lst.append( lst.append(
Horizontal( Horizontal(
Label(i + f'\n{parent.get(f"_{i}_desc", "")}'), Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Switch( Switch(
value=parent[i], id=domize(f"{parent_epath}.{i}") value=parent[i], id=domize(f"{parent_epath}.{i}")
), ),
classes="container",
) )
) )
elif isinstance(parent[i], int): elif isinstance(parent[i], int):
lst.append( lst.append(
Horizontal( Horizontal(
Label(i + f'\n{parent.get(f"_{i}_desc", "")}'), Label(i + f'\n[d]{parent.get(f"_{i}_desc", "")}[/d]'),
Input( Input(
value=str(parent[i]), value=str(parent[i]),
placeholder="要求一个整数", placeholder="要求一个整数",
type="integer", type="integer",
id=domize(f"{parent_epath}.{i}"), id=domize(f"{parent_epath}.{i}"),
), ),
classes="container",
) )
) )
elif isinstance(parent[i], list): elif isinstance(parent[i], list):

View File

@@ -9,6 +9,8 @@ from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label, ProgressBar, Static from textual.widgets import Button, Footer, Header, Label, ProgressBar, Static
from textual.worker import get_current_worker from textual.worker import get_current_worker
from textual import events, on
import heurams.kernel.particles as pt import heurams.kernel.particles as pt
import heurams.services.hasher as hasher import heurams.services.hasher as hasher
from heurams.context import * from heurams.context import *
@@ -26,8 +28,16 @@ class SyncScreen(Screen):
self.log_messages = [] self.log_messages = []
self.max_log_lines = 50 self.max_log_lines = 50
@on(events.ScreenResume)
def post_active(self, event):
from heurams.interface import shim
shim.set_term_title(f"{self.app.TITLE} - {self.SUB_TITLE}")
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header(show_clock=True)
if config_var.get()['interface']['global']['show_header']:
yield Header(show_clock=config_var.get()['interface']['global']['clock_on_header'])
with ScrollableContainer(id="sync_container"): with ScrollableContainer(id="sync_container"):
# 标题和连接状态 # 标题和连接状态
yield Static("同步工具", classes="title") yield Static("同步工具", classes="title")

View File

@@ -2,6 +2,8 @@
import heurams.interface.widgets as pzw import heurams.interface.widgets as pzw
import heurams.kernel.puzzles as pz import heurams.kernel.puzzles as pz
import platform, os, sys
from heurams.context import config_var
puzzle2widget = { puzzle2widget = {
pz.RecognitionPuzzle: pzw.Recognition, pz.RecognitionPuzzle: pzw.Recognition,
@@ -9,3 +11,13 @@ puzzle2widget = {
pz.MCQPuzzle: pzw.MCQPuzzle, pz.MCQPuzzle: pzw.MCQPuzzle,
pz.BasePuzzle: pzw.BasePuzzleWidget, pz.BasePuzzle: pzw.BasePuzzleWidget,
} }
def set_term_title(title):
if not config_var.get()['interface']['global']['change_window_title']:
return
system = platform.system()
if system == "Windows":
os.system(f"title {title}")
else: # Linux, Mac, etc.
os.write(2, f"\033]2;{title}\007".encode("utf-8"))

View File

@@ -2,7 +2,7 @@ import copy
import random import random
from typing import TypedDict from typing import TypedDict
from textual.containers import Container from textual.containers import Container, ScrollableContainer
from textual.message import Message from textual.message import Message
from textual.widget import Widget from textual.widget import Widget
from textual.widgets import Button, Label, Markdown from textual.widgets import Button, Label, Markdown
@@ -68,7 +68,7 @@ class ClozePuzzle(BasePuzzleWidget):
yield Label(self.puzzle.wording, id="sentence") yield Label(self.puzzle.wording, id="sentence")
yield Markdown(f"> {self.listprint(self.inputlist)}", id="inputpreview") yield Markdown(f"> {self.listprint(self.inputlist)}", id="inputpreview")
# 渲染当前问题的选项 # 渲染当前问题的选项
with Container(id="btn-container"): with ScrollableContainer(id="btn-container"):
for i in self.ans: for i in self.ans:
h = str(hash(i)) h = str(hash(i))
self.hashmap[h] = i self.hashmap[h] = i

View File

@@ -28,7 +28,7 @@ class Finished(Widget):
def compose(self): def compose(self):
yield Label("本次记忆进程结束", id="finished_msg") yield Label("本次记忆进程结束", id="finished_msg")
yield Label(f"算法数据{'已保存' if self.is_saved else "未能保存"}") yield Label(f"算法数据{'已保存' if self.is_saved else "未能保存"}")
yield Button("返回上一级", id="back-to-menu") yield Button("返回上一级", flat=True, id="back-to-menu")
def on_button_pressed(self, event): def on_button_pressed(self, event):
button_id = event.button.id button_id = event.button.id

View File

@@ -75,7 +75,7 @@ class MCQPuzzle(BasePuzzleWidget):
yield Label(f"当前输入: {self.inputlist}", id="inputpreview") yield Label(f"当前输入: {self.inputlist}", id="inputpreview")
# 渲染当前问题的选项 # 渲染当前问题的选项
with Container(id="btn-container"): with ScrollableContainer(id="btn-container"):
for i in current_options: for i in current_options:
self.hashmap[str(hash(i))] = i self.hashmap[str(hash(i))] = i
btnid = f"sel{str(self.cursor).zfill(3)}-{hash(i)}" btnid = f"sel{str(self.cursor).zfill(3)}-{hash(i)}"

View File

@@ -1271,7 +1271,7 @@ class SM:
self.ofm = OFM(self) self.ofm = OFM(self)
def _find_index_to_insert(self, item, r=None): def _find_index_to_insert(self, item, r=None):
"""Binary search to find insertion index for sorted queue.""" """Binary search to find insertion index for sorted procession."""
if r is None: if r is None:
r = list(range(len(self.q))) r = list(range(len(self.q)))
@@ -1290,7 +1290,7 @@ class SM:
return self._find_index_to_insert(item, r[i:]) return self._find_index_to_insert(item, r[i:])
def add_item(self, value): def add_item(self, value):
"""Add a new item to the queue.""" """Add a new item to the procession."""
item = Item(self, value) item = Item(self, value)
index = self._find_index_to_insert(item) index = self._find_index_to_insert(item)
self.q.insert(index, item) self.q.insert(index, item)
@@ -1330,7 +1330,7 @@ class SM:
item.answer(grade, now) item.answer(grade, now)
def discard(self, item): def discard(self, item):
"""Remove item from queue.""" """Remove item from procession."""
if item in self.q: if item in self.q:
self.q.remove(item) self.q.remove(item)

View File

@@ -1,5 +1,4 @@
from .evalizor import Evalizer from .evalizor import Evalizer
from .lict import Lict from .lict import Lict
from .refvar import RefVar
__all__ = ["Evalizer", "Lict", "RefVar"] __all__ = ["Evalizer", "Lict"]

View File

@@ -1,141 +0,0 @@
# 已弃用
from collections import UserList
from typing import Any, Iterator
class Lict(UserList): # TODO: 优化同步(惰性同步), 当前性能为 O(n)
""" "列典" 对象
同时兼容字典和列表大多数 API, 两边数据同步的容器
列表数据是 dictobj.items() 的格式
支持根据字典或列表初始化
限制要求:
- 键名一定唯一, 且仅能为字符串
- 值一定是引用对象
- 不使用并发
- 不在乎列表顺序语义(严格按键名字符序排列)和列表索引查找, 因此外部的 sort, index 等功能不可用
- append 的元组中, 表示键名的元素不能重复, 否则会导致覆盖行为
"""
def __init__(
self,
initlist: list | None = None,
initdict: dict | None = None,
):
self.dicted_data = {}
if initdict != None:
initlist = list(initdict.items())
super().__init__(initlist=initlist)
self._sync_based_on_list()
self.data.sort()
def _sync_based_on_dict(self):
self.data = list(self.dicted_data.items())
self.data.sort()
def _sync_based_on_list(self):
self.dicted_data = {}
for i in self.data:
self.dicted_data[i[0]] = i[1]
def __iter__(self) -> Iterator:
return self.data.__iter__()
def __getitem__(self, i):
if isinstance(i, str):
return self.dicted_data[i]
else:
return super().__getitem__(i)
def get_itemic_unit(self, ident):
return (ident, self.dicted_data[ident])
def __setitem__(self, i, item):
if isinstance(i, str):
self.dicted_data[i] = item
self._sync_based_on_dict()
else:
if item != (item[0], item[1]):
raise NotImplementedError
super().__setitem__(i, item)
self._sync_based_on_list()
def __delitem__(self, i):
if isinstance(i, str):
del self.dicted_data[i]
self._sync_based_on_dict()
else:
super().__delitem__(i)
self._sync_based_on_list()
def __contains__(self, item):
return item in self.data or item in self.keys() or item in self.values()
def append(self, item: Any) -> None:
if item != (item[0], item[1]):
raise NotImplementedError
super().append(item)
self._sync_based_on_list()
self.data.sort()
def append_new(self, item: Any):
if item != (item[0], item[1]):
raise NotImplementedError
if item[0] not in self:
super().append(item)
self._sync_based_on_list()
self.data.sort()
def insert(self, i: int, item: Any) -> None:
if item != (item[0], item[1]): # 确保 item 是遵从限制的元组
raise NotImplementedError
super().insert(i, item)
self._sync_based_on_list()
self.data.sort()
def pop(self, i: int = -1) -> Any:
res = super().pop(i)
self._sync_based_on_list()
return res
def remove(self, item: Any) -> None:
if isinstance(item, str):
item = (item, self.dicted_data[item])
if item != (item[0], item[1]):
raise NotImplementedError
super().remove(item)
self._sync_based_on_list()
self.data.sort()
def clear(self) -> None:
super().clear()
self._sync_based_on_list()
def index(self):
raise NotImplementedError
def extend(self):
raise NotImplementedError
def sort(self):
raise NotImplementedError
def reverse(self):
raise NotImplementedError
def keys(self):
return self.dicted_data.keys()
def values(self):
return self.dicted_data.values()
def items(self):
return self.data
def keys_equal_with(self, other):
return self.key_equality(self, other)
@classmethod
def key_equality(cls, a, b):
return a.keys() == b.keys()

View File

@@ -1,6 +1,7 @@
from collections.abc import MutableSequence from collections.abc import MutableSequence
from typing import Any, Iterator, Optional from typing import Any, Iterator, Optional
class Lict(MutableSequence): class Lict(MutableSequence):
""" "列典" 对象 """ "列典" 对象

View File

@@ -1,242 +0,0 @@
# 已弃用
class RefVar:
def __init__(self, initvalue) -> None:
self.data = initvalue
def __repr__(self) -> str:
return f"RefVar({repr(self.data)})"
def __str__(self) -> str:
return str(self.data)
def __add__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data + other.data)
return RefVar(self.data + other)
def __radd__(self, other):
return RefVar(other + self.data)
def __sub__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data - other.data)
return RefVar(self.data - other)
def __rsub__(self, other):
return RefVar(other - self.data)
def __mul__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data * other.data)
return RefVar(self.data * other)
def __rmul__(self, other):
return RefVar(other * self.data)
def __truediv__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data / other.data)
return RefVar(self.data / other)
def __rtruediv__(self, other):
return RefVar(other / self.data)
def __floordiv__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data // other.data)
return RefVar(self.data // other)
def __rfloordiv__(self, other):
return RefVar(other // self.data)
def __mod__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data % other.data)
return RefVar(self.data % other)
def __rmod__(self, other):
return RefVar(other % self.data)
def __pow__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data**other.data)
return RefVar(self.data**other)
def __rpow__(self, other):
return RefVar(other**self.data)
def __neg__(self):
return RefVar(-self.data)
def __pos__(self):
return RefVar(+self.data)
def __abs__(self):
return RefVar(abs(self.data))
def __eq__(self, other):
if isinstance(other, RefVar):
return self.data == other.data
return self.data == other
def __ne__(self, other):
return not self.__eq__(other)
def __lt__(self, other):
if isinstance(other, RefVar):
return self.data < other.data
return self.data < other
def __le__(self, other):
if isinstance(other, RefVar):
return self.data <= other.data
return self.data <= other
def __gt__(self, other):
if isinstance(other, RefVar):
return self.data > other.data
return self.data > other
def __ge__(self, other):
if isinstance(other, RefVar):
return self.data >= other.data
return self.data >= other
# 位运算
def __and__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data & other.data)
return RefVar(self.data & other)
def __rand__(self, other):
return RefVar(other & self.data)
def __or__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data | other.data)
return RefVar(self.data | other)
def __ror__(self, other):
return RefVar(other | self.data)
def __xor__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data ^ other.data)
return RefVar(self.data ^ other)
def __rxor__(self, other):
return RefVar(other ^ self.data)
def __lshift__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data << other.data)
return RefVar(self.data << other)
def __rlshift__(self, other):
return RefVar(other << self.data)
def __rshift__(self, other):
if isinstance(other, RefVar):
return RefVar(self.data >> other.data)
return RefVar(self.data >> other)
def __rrshift__(self, other):
return RefVar(other >> self.data)
def __invert__(self):
return RefVar(~self.data)
# 类型转换
def __int__(self):
return int(self.data)
def __float__(self):
return float(self.data)
def __bool__(self):
return bool(self.data)
def __complex__(self):
return complex(self.data)
def __bytes__(self):
return bytes(self.data)
def __hash__(self):
return hash(self.data)
# 容器操作(如果底层数据支持)
def __len__(self):
return len(self.data)
def __getitem__(self, key):
return self.data[key]
def __setitem__(self, key, value):
self.data[key] = value
def __delitem__(self, key):
del self.data[key]
def __contains__(self, item):
return item in self.data
def __iter__(self):
return iter(self.data)
def __iadd__(self, other):
if isinstance(other, RefVar):
self.data += other.data
else:
self.data += other
return self
def __isub__(self, other):
if isinstance(other, RefVar):
self.data -= other.data
else:
self.data -= other
return self
def __imul__(self, other):
if isinstance(other, RefVar):
self.data *= other.data
else:
self.data *= other
return self
def __itruediv__(self, other):
if isinstance(other, RefVar):
self.data /= other.data
else:
self.data /= other
return self
def __ifloordiv__(self, other):
if isinstance(other, RefVar):
self.data //= other.data
else:
self.data //= other
return self
def __imod__(self, other):
if isinstance(other, RefVar):
self.data %= other.data
else:
self.data %= other
return self
def __ipow__(self, other):
if isinstance(other, RefVar):
self.data **= other.data
else:
self.data **= other
return self
def __call__(self, *args, **kwargs):
if callable(self.data):
return self.data(*args, **kwargs)
raise TypeError(f"'{type(self.data).__name__}' object is not callable")
def __getattr__(self, name):
return getattr(self.data, name)

View File

@@ -7,8 +7,8 @@
orbital, 即轨道, 是定义队列式复习阶段流程的数据结构, 其实就是个字典, 至于为何不用typeddict, 因为懒. orbital, 即轨道, 是定义队列式复习阶段流程的数据结构, 其实就是个字典, 至于为何不用typeddict, 因为懒.
orbital_example = { orbital_example = {
"schedule": [列表 存储阶段(phases)名称] "schedule": [列表 存储阶段(routes)名称]
"phases":{ "routes":{
阶段名称 = [["谜题(puzzle 现称 Puzzles 评估器)名称", "概率系数 可大于1(整数部分为重复次数) 注意使用字符串包裹(toml 规范)"], ...], 阶段名称 = [["谜题(puzzle 现称 Puzzles 评估器)名称", "概率系数 可大于1(整数部分为重复次数) 注意使用字符串包裹(toml 规范)"], ...],
... ...
} }

View File

@@ -6,7 +6,7 @@ from .nucleon import Nucleon
orbital_placeholder = { orbital_placeholder = {
"schedule": ["quick_review", "recognition", "final_review"], "schedule": ["quick_review", "recognition", "final_review"],
"phases": { "routes": {
"quick_review": [ "quick_review": [
["FillBlank", 1.0], ["FillBlank", 1.0],
["SelectMeaning", 0.5], ["SelectMeaning", 0.5],

View File

@@ -3,9 +3,9 @@
Reactor 是 HeurAMS 的记忆流程状态机模块, 和界面 (interface) 的实现是解耦的, 以便后期与其他框架的适配.\ Reactor 是 HeurAMS 的记忆流程状态机模块, 和界面 (interface) 的实现是解耦的, 以便后期与其他框架的适配.\
得益于 Pickle, 状态机模块支持快照! 得益于 Pickle, 状态机模块支持快照!
## Phaser - 全局阶段控制器 ## Router - 全局阶段控制器
在一次队列记忆流程中, Phaser 代表记忆流程本身. 在一次队列记忆流程中, Router 代表记忆流程本身.
### 属性 ### 属性
@@ -25,7 +25,7 @@ Reactor 是 HeurAMS 的记忆流程状态机模块, 和界面 (interface) 的实
在初始化 Procession 时, 每个 Procession 被赋予一个不重复的状态属性 作为"阶段状态"属性, 以此标识 Procession 的阶段属性, 因为每个 Procession 管理一个阶段下的复习进程. 在初始化 Procession 时, 每个 Procession 被赋予一个不重复的状态属性 作为"阶段状态"属性, 以此标识 Procession 的阶段属性, 因为每个 Procession 管理一个阶段下的复习进程.
你可以用 state 属性获取 Phaser 的当前状态. 你可以用 state 属性获取 Router 的当前状态.
#### Procession 属性 #### Procession 属性
@@ -34,26 +34,26 @@ Reactor 是 HeurAMS 的记忆流程状态机模块, 和界面 (interface) 的实
### 初始化 ### 初始化
Phaser 接受一个存储 Atom 对象的列表, 作为组织记忆流程的材料\ Router 接受一个存储 Atom 对象的列表, 作为组织记忆流程的材料\
在内部, 根据是否激活将其分为 new_atoms 与 old_atoms.\ 在内部, 根据是否激活将其分为 new_atoms 与 old_atoms.\
因此, 如果你传入的列表中有算法上"无所事事"的 Atom, 流程会对其进行"加强复习" 因此, 如果你传入的列表中有算法上"无所事事"的 Atom, 流程会对其进行"加强复习"
由此创建 Procession. 由此创建 Procession.
### 直接输出呈现形式 ### 直接输出呈现形式
Phaser 的 __repr__ 定义了此对象"官方的显示"用作直观的调试.\ Router 的 __repr__ 定义了此对象"官方的显示"用作直观的调试.\
其以 ascii 表格形式输出, 格式也符合 markdown 表格规范, 你可以直接复制到 markdown.\ 其以 ascii 表格形式输出, 格式也符合 markdown 表格规范, 你可以直接复制到 markdown.\
示例: 示例:
```text ```text
| Type | State | Processions | Current Procession | | Type | State | Processions | Current Procession |
|:-------|:--------|:-----------------------|:---------------------| | :----- | :----- | :--------------------- | :----------------- |
| Phaser | unsure | ['新记忆', '总体复习'] | 新记忆 | | Router | unsure | ['新记忆', '总体复习'] | 新记忆 |
``` ```
| Type | State | Processions | Current Procession | | Type | State | Processions | Current Procession |
|:-------|:--------|:-----------------------|:---------------------| | :----- | :----- | :--------------------- | :----------------- |
| Phaser | unsure | ['新记忆', '总体复习'] | 新记忆 | | Router | unsure | ['新记忆', '总体复习'] | 新记忆 |
### 方法 ### 方法
@@ -83,31 +83,31 @@ Phaser 的 __repr__ 定义了此对象"官方的显示"用作直观的调试.\
- current_atom: 当前记忆原子的引用 - current_atom: 当前记忆原子的引用
- atoms: 队列中所有原子列表 - atoms: 队列中所有原子列表
- cursor: 指针, 是当前原子在 atoms 列表中的索引 - cursor: 指针, 是当前原子在 atoms 列表中的索引
- phase: "阶段属性" - route: "阶段属性"
> 注意区分 "Phaser" 和 "Phase", 其中 "Phase" 表示 "Phaser State". > 注意区分 "Router" 和 "Route", 其中 "Route" 表示 "Router State".
- name\_: 阶段的命名 - name\_: 阶段的命名
- state: 当前状态属性 - state: 当前状态属性
### 初始化 ### 初始化
接受一个 atoms 列表与 phase_state (PhaserState Enum 类型)对象 接受一个 atoms 列表与 route_state (RouterState Enum 类型)对象
### 直接输出呈现形式 ### 直接输出呈现形式
Phaser, 但显示数据有所不同\ Router, 但显示数据有所不同\
Phaser 不同, Procession 显示队列会对过长的 atom.ident 进行缩略(末尾 `>` 符号) Router 不同, Procession 显示队列会对过长的 atom.ident 进行缩略(末尾 `>` 符号)
```text ```text
| Type | Name | State | Progress | Queue | Current Atom | | Type | Name | State | Progress | Procession | Current Atom |
|:-----------|:-------|:--------|:-----------|:-----------------------|:------------------------------| | :--------- | :----- | :----- | :------- | :--------------------- | :---------------------------- |
| Procession | 新记忆 | active | 1 / 2 | ['秦孝公>', '君臣固>'] | 秦孝公据崤函之固, 拥雍州之地, | | Procession | 新记忆 | active | 1 / 2 | ['秦孝公>', '君臣固>'] | 秦孝公据崤函之固, 拥雍州之地, |
``` ```
| Type | Name | State | Progress | Queue | Current Atom | | Type | Name | State | Progress | Procession | Current Atom |
|:-----------|:-------|:--------|:-----------|:-----------------------|:------------------------------| | :--------- | :----- | :----- | :------- | :--------------------- | :---------------------------- |
| Procession | 新记忆 | active | 1 / 2 | ['秦孝公>', '君臣固>'] | 秦孝公据崤函之固, 拥雍州之地, | | Procession | 新记忆 | active | 1 / 2 | ['秦孝公>', '君臣固>'] | 秦孝公据崤函之固, 拥雍州之地, |
### 方法 ### 方法
@@ -142,11 +142,11 @@ Phaser 的 __repr__ 定义了此对象"官方的显示"用作直观的调试.\
判断是否为空队列(传入原子列表对象是空列表的队列) 判断是否为空队列(传入原子列表对象是空列表的队列)
#### get_fission(self) #### get_expander(self)
获取当前原子的 Fission 对象, 用于单原子调度展开 获取当前原子的 Expander 对象, 用于单原子调度展开
## Fission - 单原子调度控制器 ## Expander - 单原子调度控制器
### 属性 ### 属性
@@ -166,7 +166,7 @@ Phaser 的 __repr__ 定义了此对象"官方的显示"用作直观的调试.\
### 初始化 ### 初始化
接受 atom 对象和 phase 参数 接受 atom 对象和 route 参数
### 方法 ### 方法

View File

@@ -1,8 +1,8 @@
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from .fission import Fission from .expander import Expander
from .phaser import Phaser from .router import Router
from .procession import Procession from .procession import Procession
from .states import PhaserState, ProcessionState from .states import RouterState, ProcessionState
__all__ = ["PhaserState", "ProcessionState", "Procession", "Fission", "Phaser"] __all__ = ["RouterState", "ProcessionState", "Procession", "Expander", "Router"]

View File

@@ -8,42 +8,42 @@ import heurams.kernel.particles as pt
import heurams.kernel.puzzles as puz import heurams.kernel.puzzles as puz
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from .states import FissionState, PhaserState from .states import ExpanderState, RouterState
logger = get_logger(__name__) logger = get_logger(__name__)
class Fission(Machine): class Expander(Machine):
"""单原子调度展开器""" """单原子调度展开器"""
def __init__(self, atom: pt.Atom, phase=PhaserState.RECOGNITION): def __init__(self, atom: pt.Atom, route=RouterState.RECOGNITION):
self.phase = phase self.route = route
self.cursor = 0 self.cursor = 0
self.atom = atom self.atom = atom
self.current_puzzle_inf: dict self.current_puzzle_inf: dict
# phase 为 PhaserState 枚举实例, 需要获取其value # route 为 RouterState 枚举实例, 需要获取其value
phase_value = phase.value route_value = route.value
states = [ states = [
{"name": FissionState.EXAMMODE.value}, {"name": ExpanderState.EXAMMODE.value},
{"name": FissionState.RETRONLY.value}, {"name": ExpanderState.RETRONLY.value},
] ]
transitions = [ transitions = [
{ {
"trigger": "finish", "trigger": "finish",
"source": FissionState.EXAMMODE.value, "source": ExpanderState.EXAMMODE.value,
"dest": FissionState.RETRONLY.value, "dest": ExpanderState.RETRONLY.value,
}, },
] ]
if phase == PhaserState.FINISHED: if route == RouterState.FINISHED:
Machine.__init__( Machine.__init__(
self, self,
states=states, states=states,
transitions=transitions, transitions=transitions,
initial=FissionState.EXAMMODE.value, initial=ExpanderState.EXAMMODE.value,
) )
return return
orbital_schedule = atom.registry["orbital"]["phases"][phase_value] # type: ignore orbital_schedule = atom.registry["orbital"]["routes"][route_value] # type: ignore
orbital_puzzles = atom.registry["nucleon"]["puzzles"] orbital_puzzles = atom.registry["nucleon"]["puzzles"]
self.puzzles_inf = list() self.puzzles_inf = list()
self.min_ratings = [] self.min_ratings = []
@@ -81,7 +81,7 @@ class Fission(Machine):
self, self,
states=states, states=states,
transitions=transitions, transitions=transitions,
initial=FissionState.EXAMMODE.value, initial=ExpanderState.EXAMMODE.value,
) )
def get_puzzles_inf(self): def get_puzzles_inf(self):
@@ -118,11 +118,13 @@ class Fission(Machine):
dic = [ dic = [
{ {
"Type": "Fission", "Type": "Expander",
"Atom": truncate(self.atom.ident), "Atom": truncate(self.atom.ident),
"State": self.state, "State": self.state,
"Progress": f"{self.cursor + 1} / {len(self.puzzles_inf)}", "Progress": f"{self.cursor + 1} / {len(self.puzzles_inf)}",
"Queue": list(map(lambda f: truncate(f["alia"]), self.puzzles_inf)), "Procession": list(
map(lambda f: truncate(f["alia"]), self.puzzles_inf)
),
"Current Puzzle": f"{self.current_puzzle_inf['alia']}@{self.current_puzzle_inf['puzzle'].__name__}", # type: ignore "Current Puzzle": f"{self.current_puzzle_inf['alia']}@{self.current_puzzle_inf['puzzle'].__name__}", # type: ignore
} }
] ]

View File

@@ -4,8 +4,8 @@ from transitions import Machine
import heurams.kernel.particles as pt import heurams.kernel.particles as pt
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from .fission import Fission from .expander import Expander
from .states import PhaserState, ProcessionState from .states import RouterState, ProcessionState
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -13,11 +13,11 @@ logger = get_logger(__name__)
class Procession(Machine): class Procession(Machine):
"""队列: 标识单次记忆流程""" """队列: 标识单次记忆流程"""
def __init__(self, atoms: list, phase_state: PhaserState, name_: str = ""): def __init__(self, atoms: list, route_state: RouterState, name_: str = ""):
logger.debug( logger.debug(
"Procession.__init__: 原子数量=%d, phase=%s, name='%s'", "Procession.__init__: 原子数量=%d, route=%s, name='%s'",
len(atoms), len(atoms),
phase_state.value, route_state.value,
name_, name_,
) )
self.current_atom: pt.Atom | None self.current_atom: pt.Atom | None
@@ -25,7 +25,7 @@ class Procession(Machine):
self.current_atom = atoms[0] if atoms else None self.current_atom = atoms[0] if atoms else None
self.cursor = 0 self.cursor = 0
self.name_ = name_ self.name_ = name_
self.phase = phase_state self.route = route_state
states = [ states = [
{"name": ProcessionState.ACTIVE.value, "on_enter": "on_active"}, {"name": ProcessionState.ACTIVE.value, "on_enter": "on_active"},
@@ -113,8 +113,8 @@ class Procession(Machine):
logger.debug("Procession.is_empty: %s", empty) logger.debug("Procession.is_empty: %s", empty)
return empty return empty
def get_fission(self): def get_expander(self):
return Fission(atom=self.current_atom, phase=self.phase) # type: ignore return Expander(atom=self.current_atom, route=self.route) # type: ignore
def __repr__(self, style="pipe", ends="\n"): def __repr__(self, style="pipe", ends="\n"):
from heurams.services.textproc import truncate from heurams.services.textproc import truncate
@@ -125,7 +125,7 @@ class Procession(Machine):
"Name": self.name_, "Name": self.name_,
"State": self.state, "State": self.state,
"Progress": f"{self.cursor + 1} / {len(self.atoms)}", "Progress": f"{self.cursor + 1} / {len(self.atoms)}",
"Queue": list(map(lambda f: truncate(f.ident), self.atoms)), "Procession": list(map(lambda f: truncate(f.ident), self.atoms)),
"Current Atom": self.current_atom.ident, # type: ignore "Current Atom": self.current_atom.ident, # type: ignore
} }
] ]

View File

@@ -5,16 +5,16 @@ from heurams.kernel.particles.placeholders import AtomPlaceholder
from heurams.services.logger import get_logger from heurams.services.logger import get_logger
from .procession import Procession from .procession import Procession
from .states import PhaserState, ProcessionState from .states import RouterState, ProcessionState
logger = get_logger(__name__) logger = get_logger(__name__)
class Phaser(Machine): class Router(Machine):
"""全局调度阶段管理""" """全局调度阶段路由"""
def __init__(self, atoms: list[pt.Atom]) -> None: def __init__(self, atoms: list[pt.Atom]) -> None:
logger.debug("Phaser.__init__: 原子数量=%d", len(atoms)) logger.debug(f"Router.__init__: 原子数量={len(atoms)}")
self.atoms = atoms self.atoms = atoms
new_atoms = list() new_atoms = list()
@@ -26,56 +26,57 @@ class Phaser(Machine):
else: else:
old_atoms.append(i) old_atoms.append(i)
logger.debug("新原子数量=%d, 旧原子数量=%d", len(new_atoms), len(old_atoms)) logger.debug(f"新原子数量={len(new_atoms)}, 旧原子数量={len(old_atoms)}")
self.processions = list() self.processions = list()
"""路由中的所有队列"""
# TODO: 改进为基于配置文件的可选复习阶段 # TODO: 改进为基于配置文件的可选复习阶段
if len(old_atoms): if len(old_atoms):
self.processions.append( self.processions.append(
Procession(old_atoms, PhaserState.QUICK_REVIEW, "初始复习") Procession(old_atoms, RouterState.QUICK_REVIEW, "初始复习")
) )
logger.debug("创建初始复习 Procession") logger.debug("创建初始复习 Procession")
if len(new_atoms): if len(new_atoms):
self.processions.append( self.processions.append(
Procession(new_atoms, PhaserState.RECOGNITION, "新记忆") Procession(new_atoms, RouterState.RECOGNITION, "新记忆")
) )
logger.debug("创建新记忆 Procession") logger.debug("创建新记忆 Procession")
self.processions.append(Procession(atoms, PhaserState.FINAL_REVIEW, "总体复习")) self.processions.append(Procession(atoms, RouterState.FINAL_REVIEW, "总体复习"))
logger.debug("创建总体复习 Procession") logger.debug("创建总体复习 Procession")
logger.debug("Phaser 初始化完成, processions 数量=%d", len(self.processions)) logger.debug("Router 初始化完成, processions 数量=%d", len(self.processions))
# 设置transitions状态机 # 设置transitions状态机
states = [ states = [
{"name": PhaserState.UNSURE.value, "on_enter": "on_unsure"}, {"name": RouterState.UNSURE.value, "on_enter": "on_unsure"},
{"name": PhaserState.QUICK_REVIEW.value, "on_enter": "on_quick_review"}, {"name": RouterState.QUICK_REVIEW.value, "on_enter": "on_quick_review"},
{"name": PhaserState.RECOGNITION.value, "on_enter": "on_recognition"}, {"name": RouterState.RECOGNITION.value, "on_enter": "on_recognition"},
{"name": PhaserState.FINAL_REVIEW.value, "on_enter": "on_final_review"}, {"name": RouterState.FINAL_REVIEW.value, "on_enter": "on_final_review"},
{"name": PhaserState.FINISHED.value, "on_enter": "on_finished"}, {"name": RouterState.FINISHED.value, "on_enter": "on_finished"},
] ]
transitions = [ transitions = [
{"trigger": "to_unsure", "source": "*", "dest": PhaserState.UNSURE.value}, {"trigger": "to_unsure", "source": "*", "dest": RouterState.UNSURE.value},
{ {
"trigger": "to_quick_review", "trigger": "to_quick_review",
"source": "*", "source": "*",
"dest": PhaserState.QUICK_REVIEW.value, "dest": RouterState.QUICK_REVIEW.value,
}, },
{ {
"trigger": "to_recognition", "trigger": "to_recognition",
"source": "*", "source": "*",
"dest": PhaserState.RECOGNITION.value, "dest": RouterState.RECOGNITION.value,
}, },
{ {
"trigger": "to_final_review", "trigger": "to_final_review",
"source": "*", "source": "*",
"dest": PhaserState.FINAL_REVIEW.value, "dest": RouterState.FINAL_REVIEW.value,
}, },
{ {
"trigger": "to_finished", "trigger": "to_finished",
"source": "*", "source": "*",
"dest": PhaserState.FINISHED.value, "dest": RouterState.FINISHED.value,
}, },
] ]
@@ -83,54 +84,54 @@ class Phaser(Machine):
self, self,
states=states, states=states,
transitions=transitions, transitions=transitions,
initial=PhaserState.UNSURE.value, initial=RouterState.UNSURE.value,
) )
self.to_unsure() self.to_unsure()
def on_unsure(self): def on_unsure(self):
"""进入UNSURE状态时的回调""" """进入UNSURE状态时的回调"""
logger.debug("Phaser 进入 UNSURE 状态") logger.debug("Router 进入 UNSURE 状态")
def on_quick_review(self): def on_quick_review(self):
"""进入QUICK_REVIEW状态时的回调""" """进入QUICK_REVIEW状态时的回调"""
logger.debug("Phaser 进入 QUICK_REVIEW 状态") logger.debug("Router 进入 QUICK_REVIEW 状态")
def on_recognition(self): def on_recognition(self):
"""进入RECOGNITION状态时的回调""" """进入RECOGNITION状态时的回调"""
logger.debug("Phaser 进入 RECOGNITION 状态") logger.debug("Router 进入 RECOGNITION 状态")
def on_final_review(self): def on_final_review(self):
"""进入FINAL_REVIEW状态时的回调""" """进入FINAL_REVIEW状态时的回调"""
logger.debug("Phaser 进入 FINAL_REVIEW 状态") logger.debug("Router 进入 FINAL_REVIEW 状态")
def on_finished(self): def on_finished(self):
"""进入FINISHED状态时的回调""" """进入FINISHED状态时的回调"""
for i in self.atoms: for i in self.atoms:
i.lock(1) i.lock(1)
i.revise() i.revise()
logger.debug("Phaser 进入 FINISHED 状态") logger.debug("Router 进入 FINISHED 状态")
def current_procession(self): def current_procession(self):
logger.debug("Phaser.current_procession 被调用") logger.debug("Router.current_procession 被调用")
for i in self.processions: for i in self.processions:
i: Procession i: Procession
if i.state != ProcessionState.FINISHED.value: if i.state != ProcessionState.FINISHED.value:
# if i.phase == PhaserState.UNSURE: 此判断是不必要的 因为没有这种 Procession # if i.route == RouterState.UNSURE: 此判断是不必要的 因为没有这种 Procession
if i.phase == PhaserState.QUICK_REVIEW: if i.route == RouterState.QUICK_REVIEW:
self.to_quick_review() self.to_quick_review()
elif i.phase == PhaserState.RECOGNITION: elif i.route == RouterState.RECOGNITION:
self.to_recognition() self.to_recognition()
elif i.phase == PhaserState.FINAL_REVIEW: elif i.route == RouterState.FINAL_REVIEW:
self.to_final_review() self.to_final_review()
logger.debug("找到未完成的 Procession: phase=%s", i.phase) logger.debug("找到未完成的 Procession: route=%s", i.route)
return i return i
# 所有Procession都已完成 # 所有Procession都已完成
self.to_finished() self.to_finished()
logger.debug("所有 Procession 已完成, 状态设置为 FINISHED") logger.debug("所有 Procession 已完成, 状态设置为 FINISHED")
return Procession([AtomPlaceholder()], PhaserState.FINISHED) return Procession([AtomPlaceholder()], RouterState.FINISHED)
def __repr__(self, style="pipe", ends="\n"): def __repr__(self, style="pipe", ends="\n"):
from tabulate import tabulate as tabu from tabulate import tabulate as tabu
@@ -139,7 +140,7 @@ class Phaser(Machine):
lst = [ lst = [
{ {
"Type": "Phaser", "Type": "Router",
"State": self.state, "State": self.state,
"Processions": list(map(lambda f: (f.name_), self.processions)), "Processions": list(map(lambda f: (f.name_), self.processions)),
"Current Procession": "None" if not self.current_procession() else self.current_procession().name_, # type: ignore "Current Procession": "None" if not self.current_procession() else self.current_procession().name_, # type: ignore

View File

@@ -5,7 +5,7 @@ from heurams.services.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
class PhaserState(Enum): class RouterState(Enum):
UNSURE = "unsure" UNSURE = "unsure"
QUICK_REVIEW = "quick_review" QUICK_REVIEW = "quick_review"
RECOGNITION = "recognition" RECOGNITION = "recognition"
@@ -18,7 +18,7 @@ class ProcessionState(Enum):
FINISHED = "finished" FINISHED = "finished"
class FissionState(Enum): class ExpanderState(Enum):
EXAMMODE = "exammode" EXAMMODE = "exammode"
RETRONLY = "retronly" RETRONLY = "retronly"

View File

@@ -24,7 +24,7 @@ def get_daystamp() -> int:
def get_timestamp() -> float: def get_timestamp() -> float:
"""获取 UNIX 时间戳""" """获取 UNIX 时间戳"""
# 搞这个的原因是要支持可复现操作 # 搞这个函数的原因是要支持可复现操作
time_override = config_var.get()["services"]["timer"]["timestamp_override"] time_override = config_var.get()["services"]["timer"]["timestamp_override"]
if time_override != -1: if time_override != -1:
logger.debug("使用覆盖的时间戳: %f", time_override) logger.debug("使用覆盖的时间戳: %f", time_override)

View File

@@ -0,0 +1,3 @@
"""会话模块"""
class Session:
pass