refactor: 完成 0.4.0 版本更新
完成 0.4.0 版本更新, 为了消除此前提交消息风格不一致与错误提交超大文件的问题, 维持代码统计数据的准确性和提交消息风格的一致性, 重新初始化仓库; 旧的提交历史在 HeurAMS-legacy 仓库(https://gitea.imwangzhiyu.xyz/ajax/HeurAMS-legacy)
This commit is contained in:
2
src/heurams/interface/README.md
Normal file
2
src/heurams/interface/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Interface - 用户界面
|
||||
与界面系统**强绑定**的相关代码文件, "界面系统" 在此处是基本界面实现相关的 Textual 框架
|
||||
87
src/heurams/interface/__main__.py
Normal file
87
src/heurams/interface/__main__.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from textual.app import App
|
||||
from textual.widgets import Button
|
||||
from .screens.dashboard import DashboardScreen
|
||||
from .screens.nucreator import NucleonCreatorScreen
|
||||
from .screens.precache import PrecachingScreen
|
||||
from .screens.about import AboutScreen
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class HeurAMSApp(App):
|
||||
TITLE = "潜进"
|
||||
CSS_PATH = "css/main.tcss"
|
||||
SUB_TITLE = "启发式辅助记忆调度器"
|
||||
BINDINGS = [
|
||||
("q", "quit", "退出"),
|
||||
("d", "toggle_dark", "切换色调"),
|
||||
("1", "app.push_screen('dashboard')", "仪表盘"),
|
||||
("2", "app.push_screen('precache_all')", "缓存管理器"),
|
||||
("3", "app.push_screen('nucleon_creator')", "创建新单元"),
|
||||
("0", "app.push_screen('about')", "版本信息"),
|
||||
]
|
||||
SCREENS = {
|
||||
"dashboard": DashboardScreen,
|
||||
"nucleon_creator": NucleonCreatorScreen,
|
||||
"precache_all": PrecachingScreen,
|
||||
"about": AboutScreen,
|
||||
}
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.push_screen("dashboard")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
self.exit(event.button.id)
|
||||
|
||||
def action_do_nothing(self):
|
||||
print("DO NOTHING")
|
||||
self.refresh()
|
||||
|
||||
|
||||
def environment_check():
|
||||
from pathlib import Path
|
||||
|
||||
logger.debug("检查环境路径")
|
||||
|
||||
for i in config_var.get()["paths"].values():
|
||||
i = Path(i)
|
||||
if not i.exists():
|
||||
logger.info("创建目录: %s", i)
|
||||
print(f"创建 {i}")
|
||||
i.mkdir(exist_ok=True, parents=True)
|
||||
else:
|
||||
logger.debug("目录已存在: %s", i)
|
||||
print(f"找到 {i}")
|
||||
logger.debug("环境检查完成")
|
||||
|
||||
|
||||
def is_subdir(parent, child):
|
||||
try:
|
||||
child.relative_to(parent)
|
||||
logger.debug("is_subdir: %s 是 %s 的子目录", child, parent)
|
||||
return 1
|
||||
except:
|
||||
logger.debug("is_subdir: %s 不是 %s 的子目录", child, parent)
|
||||
return 0
|
||||
|
||||
|
||||
# 开发模式
|
||||
from heurams.context import rootdir, workdir, config_var
|
||||
from pathlib import Path
|
||||
from heurams.context import rootdir
|
||||
import os
|
||||
|
||||
if is_subdir(Path(rootdir), Path(os.getcwd())):
|
||||
os.chdir(Path(rootdir) / ".." / "..")
|
||||
print(f'转入开发数据目录: {Path(rootdir)/".."/".."}')
|
||||
|
||||
environment_check()
|
||||
|
||||
app = HeurAMSApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
|
||||
|
||||
def main():
|
||||
app.run()
|
||||
0
src/heurams/interface/css/main.tcss
Normal file
0
src/heurams/interface/css/main.tcss
Normal file
94
src/heurams/interface/screens/about.py
Normal file
94
src/heurams/interface/screens/about.py
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
Label,
|
||||
Static,
|
||||
Button,
|
||||
Markdown,
|
||||
)
|
||||
from textual.containers import ScrollableContainer, ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
|
||||
import heurams.services.version as version
|
||||
from heurams.context import *
|
||||
|
||||
|
||||
class AboutScreen(Screen):
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="about_container"):
|
||||
yield Label("[b]关于与版本信息[/b]")
|
||||
about_text = f"""
|
||||
# 关于 "潜进"
|
||||
|
||||
版本 {version.ver} {version.stage.capitalize()}
|
||||
|
||||
开发代号: {version.codename.capitalize()}
|
||||
|
||||
一个基于启发式算法的开放源代码记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划.
|
||||
|
||||
以 AGPL-3.0 开放源代码
|
||||
|
||||
开发人员:
|
||||
|
||||
- Wang Zhiyu([@pluvium27](https://github.com/pluvium27)): 项目作者
|
||||
|
||||
特别感谢:
|
||||
|
||||
- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SuperMemo-2 算法
|
||||
- [Thoughts Memo](https://www.zhihu.com/people/L.M.Sherlock): 文献参考
|
||||
|
||||
# 参与贡献
|
||||
|
||||
我们是一个年轻且包容的社区, 由技术人员, 设计师, 文书工作者, 以及创意人员共同构成,
|
||||
|
||||
通过我们协力开发的软件为所有人谋取福祉.
|
||||
|
||||
上述工作不可避免地让我们确立了下列价值观 (取自 KDE 宣言):
|
||||
|
||||
- 开放治理 确保更多人能参与我们的领导和决策进程;
|
||||
|
||||
- 自由软件 确保我们的工作成果随时能为所有人所用;
|
||||
|
||||
- 多样包容 确保所有人都能加入社区并参加工作;
|
||||
|
||||
- 创新精神 确保新思路能不断涌现并服务于所有人;
|
||||
|
||||
- 共同产权 确保我们能团结一致;
|
||||
|
||||
- 迎合用户 确保我们的成果对所有人有用.
|
||||
|
||||
综上所述, 在为我们共同目标奋斗的过程中, 我们认为上述价值观反映了我们社区的本质, 是我们始终如一地保持初心的关键所在.
|
||||
|
||||
这是一项立足于协作精神的事业, 它的运作和产出不受任何单一个人或者机构的操纵.
|
||||
|
||||
我们的共同目标是为人人带来高品质的辅助记忆 & 学习软件.
|
||||
|
||||
不管您来自何方, 我们都欢迎您加入社区并做出贡献.
|
||||
"""
|
||||
|
||||
# """
|
||||
# 学术数据
|
||||
|
||||
# "潜进" 的用户数据可用于科学方面的研究, 我们将在未来版本添加学术数据的收集和展示平台
|
||||
# """
|
||||
yield Markdown(about_text, classes="about-markdown")
|
||||
|
||||
yield Button(
|
||||
"返回主界面",
|
||||
id="back_button",
|
||||
variant="primary",
|
||||
classes="back-button",
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def action_go_back(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
def on_button_pressed(self, event) -> None:
|
||||
event.stop()
|
||||
if event.button.id == "back_button":
|
||||
self.action_go_back()
|
||||
153
src/heurams/interface/screens/dashboard.py
Normal file
153
src/heurams/interface/screens/dashboard.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
Label,
|
||||
ListView,
|
||||
ListItem,
|
||||
Button,
|
||||
Static,
|
||||
)
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
|
||||
from heurams.kernel.particles import *
|
||||
from heurams.context import *
|
||||
import heurams.services.version as version
|
||||
import heurams.services.timer as timer
|
||||
from .preparation import PreparationScreen
|
||||
from .about import AboutScreen
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
import pathlib
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class DashboardScreen(Screen):
|
||||
SUB_TITLE = "仪表盘"
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
yield ScrollableContainer(
|
||||
Label(f'欢迎使用 "潜进" 启发式辅助记忆调度器', classes="title-label"),
|
||||
Label(f"当前 UNIX 日时间戳: {timer.get_daystamp()}"),
|
||||
Label(f'时区修正: UTC+{config_var.get()["timezone_offset"] / 3600}'),
|
||||
Label("选择待学习或待修改的记忆单元集:", classes="title-label"),
|
||||
ListView(id="union-list", classes="union-list-view"),
|
||||
Label(
|
||||
f'"潜进" 启发式辅助记忆调度器 | 版本 {version.ver} {version.codename.capitalize()} 2025'
|
||||
),
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def item_desc_generator(self, filename) -> dict:
|
||||
"""简单分析以生成项目项显示文本
|
||||
|
||||
Returns:
|
||||
dict: 以数字为列表, 分别呈现单行字符串
|
||||
"""
|
||||
res = dict()
|
||||
filestem = pathlib.Path(filename).stem
|
||||
res[0] = f"{filename}\0"
|
||||
from heurams.kernel.particles.loader import load_electron
|
||||
import heurams.kernel.particles as pt
|
||||
|
||||
electron_file_path = pathlib.Path(config_var.get()["paths"]["electron_dir"]) / (
|
||||
filestem + ".json"
|
||||
)
|
||||
|
||||
logger.debug(f"电子文件路径: {electron_file_path}")
|
||||
|
||||
if electron_file_path.exists(): # 未找到则创建电子文件 (json)
|
||||
pass
|
||||
else:
|
||||
electron_file_path.touch()
|
||||
with open(electron_file_path, "w") as f:
|
||||
f.write("{}")
|
||||
electron_dict = load_electron(path=electron_file_path) # TODO: 取消硬编码扩展名
|
||||
logger.debug(electron_dict)
|
||||
is_due = 0
|
||||
is_activated = 0
|
||||
nextdate = 0x3F3F3F3F
|
||||
for i in electron_dict.values():
|
||||
i: pt.Electron
|
||||
logger.debug(i, i.is_due())
|
||||
if i.is_due():
|
||||
is_due = 1
|
||||
if i.is_activated():
|
||||
is_activated = 1
|
||||
nextdate = min(nextdate, i.nextdate())
|
||||
res[1] = f"下一次复习: {nextdate}\n"
|
||||
res[1] += f"{is_due if "需要复习" else "当前无需复习"}"
|
||||
if not is_activated:
|
||||
res[1] = " 尚未激活"
|
||||
return res
|
||||
|
||||
def on_mount(self) -> None:
|
||||
union_list_widget = self.query_one("#union-list", ListView)
|
||||
|
||||
probe = probe_all(0)
|
||||
|
||||
if len(probe["nucleon"]):
|
||||
for file in probe["nucleon"]:
|
||||
text = self.item_desc_generator(file)
|
||||
union_list_widget.append(
|
||||
ListItem(
|
||||
Label(text[0] + "\n" + text[1]),
|
||||
)
|
||||
)
|
||||
else:
|
||||
union_list_widget.append(
|
||||
ListItem(
|
||||
Static(
|
||||
"在 ./nucleon/ 中未找到任何内容源数据文件.\n请放置文件后重启应用.\n或者新建空的单元集."
|
||||
)
|
||||
)
|
||||
)
|
||||
union_list_widget.disabled = True
|
||||
|
||||
def on_list_view_selected(self, event) -> None:
|
||||
if not isinstance(event.item, ListItem):
|
||||
return
|
||||
|
||||
selected_label = event.item.query_one(Label)
|
||||
if "未找到任何 .toml 文件" in str(selected_label.renderable): # type: ignore
|
||||
return
|
||||
|
||||
selected_filename = pathlib.Path(
|
||||
str(selected_label.renderable)
|
||||
.partition("\0")[0] # 文件名末尾截断, 保留文件名
|
||||
.replace("*", "")
|
||||
) # 去除markdown加粗
|
||||
|
||||
nucleon_file_path = (
|
||||
pathlib.Path(config_var.get()["paths"]["nucleon_dir"]) / selected_filename
|
||||
)
|
||||
electron_file_path = pathlib.Path(config_var.get()["paths"]["electron_dir"]) / (
|
||||
str(selected_filename.stem) + ".json"
|
||||
)
|
||||
self.app.push_screen(PreparationScreen(nucleon_file_path, electron_file_path))
|
||||
|
||||
def on_button_pressed(self, event) -> None:
|
||||
if event.button.id == "new_nucleon_button":
|
||||
# 切换到创建单元
|
||||
from .nucreator import NucleonCreatorScreen
|
||||
|
||||
newscr = NucleonCreatorScreen()
|
||||
self.app.push_screen(newscr)
|
||||
elif event.button.id == "precache_all_button":
|
||||
# 切换到缓存管理器
|
||||
from .precache import PrecachingScreen
|
||||
|
||||
precache_screen = PrecachingScreen()
|
||||
self.app.push_screen(precache_screen)
|
||||
elif event.button.id == "about_button":
|
||||
from .about import AboutScreen
|
||||
|
||||
about_screen = AboutScreen()
|
||||
self.app.push_screen(about_screen)
|
||||
|
||||
def action_quit_app(self) -> None:
|
||||
self.app.exit()
|
||||
0
src/heurams/interface/screens/intelinote.py
Normal file
0
src/heurams/interface/screens/intelinote.py
Normal file
152
src/heurams/interface/screens/memorizor.py
Normal file
152
src/heurams/interface/screens/memorizor.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import Header, Footer, Label, Static, Button
|
||||
from textual.containers import Center, ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
from textual.reactive import reactive
|
||||
from enum import Enum, auto
|
||||
|
||||
from heurams.services.logger import get_logger
|
||||
from heurams.context import config_var
|
||||
from heurams.kernel.reactor import *
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as pz
|
||||
from .. import shim
|
||||
|
||||
|
||||
class AtomState(Enum):
|
||||
FAILED = auto()
|
||||
NORMAL = auto()
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MemScreen(Screen):
|
||||
BINDINGS = [
|
||||
("q", "pop_screen", "返回"),
|
||||
# ("p", "prev", "复习上一个"),
|
||||
("d", "toggle_dark", ""),
|
||||
("v", "play_voice", "朗读"),
|
||||
("0,1,2,3", "app.push_screen('about')", ""),
|
||||
]
|
||||
|
||||
if config_var.get()["quick_pass"]:
|
||||
BINDINGS.append(("k", "quick_pass", "跳过"))
|
||||
rating = reactive(-1)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
atoms: list,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(name, id, classes)
|
||||
self.atoms = atoms
|
||||
self.phaser = Phaser(atoms)
|
||||
# logger.debug(self.phaser.state)
|
||||
self.procession: Procession = self.phaser.current_procession() # type: ignore
|
||||
self.atom: pt.Atom = self.procession.current_atom
|
||||
# logger.debug(self.phaser.state)
|
||||
# self.procession.forward(1)
|
||||
for i in atoms:
|
||||
i.do_eval()
|
||||
|
||||
def on_mount(self):
|
||||
self.load_puzzle()
|
||||
pass
|
||||
|
||||
def puzzle_widget(self):
|
||||
try:
|
||||
logger.debug(self.phaser.state)
|
||||
logger.debug(self.procession.cursor)
|
||||
logger.debug(self.atom)
|
||||
self.fission = Fission(self.atom, self.phaser.state)
|
||||
puzzle_debug = next(self.fission.generate())
|
||||
# logger.debug(puzzle_debug)
|
||||
return shim.puzzle2widget[puzzle_debug["puzzle"]](
|
||||
atom=self.atom, alia=puzzle_debug["alia"]
|
||||
)
|
||||
except (KeyError, StopIteration, AttributeError) as e:
|
||||
logger.debug(f"调度展开出错: {e}")
|
||||
return Static("无法生成谜题")
|
||||
# logger.debug(shim.puzzle2widget[puzzle_debug["puzzle"]])
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer():
|
||||
yield Label(self._get_progress_text(), id="progress")
|
||||
|
||||
# self.mount(self.current_widget()) # type: ignore
|
||||
yield ScrollableContainer(id="puzzle-container")
|
||||
# yield Button("重新学习此单元", id="re-recognize", variant="warning")
|
||||
yield Footer()
|
||||
|
||||
def _get_progress_text(self):
|
||||
return f"当前进度: {self.procession.process() + 1}/{self.procession.total_length()}"
|
||||
|
||||
def update_display(self):
|
||||
progress_widget = self.query_one("#progress")
|
||||
progress_widget.update(self._get_progress_text()) # type: ignore
|
||||
|
||||
def load_puzzle(self):
|
||||
self.atom: pt.Atom = self.procession.current_atom
|
||||
container = self.query_one("#puzzle-container")
|
||||
for i in container.children:
|
||||
i.remove()
|
||||
container.mount(self.puzzle_widget())
|
||||
|
||||
def load_finished_widget(self):
|
||||
container = self.query_one("#puzzle-container")
|
||||
for i in container.children:
|
||||
i.remove()
|
||||
from heurams.interface.widgets.finished import Finished
|
||||
|
||||
container.mount(Finished())
|
||||
|
||||
def on_button_pressed(self, event):
|
||||
event.stop()
|
||||
|
||||
def watch_rating(self, old_rating, new_rating) -> None:
|
||||
if self.procession == 0:
|
||||
return
|
||||
if new_rating == -1:
|
||||
return
|
||||
forwards = 1 if new_rating >= 4 else 0
|
||||
self.rating = -1
|
||||
logger.debug(f"试图前进: {"允许" if forwards else "禁止"}")
|
||||
if forwards:
|
||||
ret = self.procession.forward(1)
|
||||
if ret == 0: # 若结束了此次队列
|
||||
self.procession = self.phaser.current_procession() # type: ignore
|
||||
if self.procession == 0: # 若所有队列都结束了
|
||||
logger.debug(f"记忆进程结束")
|
||||
for i in self.atoms:
|
||||
i: pt.Atom
|
||||
i.revise()
|
||||
i.persist("electron")
|
||||
self.load_finished_widget()
|
||||
return
|
||||
else:
|
||||
logger.debug(f"建立新队列 {self.procession.phase}")
|
||||
self.load_puzzle()
|
||||
else: # 若不通过
|
||||
self.procession.append()
|
||||
self.update_display()
|
||||
|
||||
def action_quick_pass(self):
|
||||
self.rating = 5
|
||||
self.atom.minimize(5)
|
||||
self.atom.registry["electron"].activate()
|
||||
self.atom.lock(1)
|
||||
|
||||
def action_play_voice(self):
|
||||
"""朗读当前内容"""
|
||||
pass
|
||||
|
||||
def action_toggle_dark(self):
|
||||
self.app.action_toggle_dark()
|
||||
|
||||
def action_pop_screen(self):
|
||||
self.app.pop_screen()
|
||||
171
src/heurams/interface/screens/nucreator.py
Normal file
171
src/heurams/interface/screens/nucreator.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
Label,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Markdown,
|
||||
)
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
|
||||
from heurams.services.version import ver
|
||||
import toml
|
||||
from pathlib import Path
|
||||
from heurams.context import config_var
|
||||
|
||||
|
||||
class NucleonCreatorScreen(Screen):
|
||||
BINDINGS = [("q", "go_back", "返回")]
|
||||
SUB_TITLE = "单元集创建向导"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(name=None, id=None, classes=None)
|
||||
|
||||
def search_templates(self):
|
||||
from pathlib import Path
|
||||
from heurams.context import config_var
|
||||
|
||||
template_dir = Path(config_var.get()["paths"]["template_dir"])
|
||||
templates = list()
|
||||
for i in template_dir.iterdir():
|
||||
if i.name.endswith(".toml"):
|
||||
try:
|
||||
import toml
|
||||
|
||||
with open(i, "r") as f:
|
||||
dic = toml.load(f)
|
||||
desc = dic["__metadata__.attribution"]["desc"]
|
||||
templates.append(desc + " (" + i.name + ")")
|
||||
except Exception as e:
|
||||
templates.append(f"无描述模板 ({i.name})")
|
||||
print(e)
|
||||
return templates
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="vice_container"):
|
||||
yield Label(f"[b]空白单元集创建向导\n")
|
||||
yield Markdown(
|
||||
"> 提示: 你可能注意到当选中文本框时底栏和操作按键绑定将被覆盖 \n只需选中(使用鼠标或 Tab)选择框即可恢复底栏功能"
|
||||
)
|
||||
yield Markdown("1. 键入单元集名称")
|
||||
yield Input(placeholder="单元集名称", id="name_input")
|
||||
yield Markdown(
|
||||
"> 单元集名称不应与现有单元集重复. \n> 新的单元集文件将创建在 ./nucleon/你输入的名称.toml"
|
||||
)
|
||||
yield Label(f"\n")
|
||||
yield Markdown("2. 选择单元集模板")
|
||||
LINES = self.search_templates()
|
||||
"""带有宏支持的空白单元集 ({ver})
|
||||
古诗词模板单元集 ({ver})
|
||||
英语词汇和短语模板单元集 ({ver})
|
||||
"""
|
||||
yield Select.from_values(LINES, prompt="选择类型", id="template_select")
|
||||
yield Markdown("> 新单元集的版本号将和主程序版本保持同步")
|
||||
yield Label(f"\n")
|
||||
yield Markdown("3. 输入常见附加元数据 (可选)")
|
||||
yield Input(placeholder="作者", id="author_input")
|
||||
yield Input(placeholder="内容描述", id="desc_input")
|
||||
yield Button(
|
||||
"新建空白单元集",
|
||||
id="submit_button",
|
||||
variant="primary",
|
||||
classes="start-button",
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self):
|
||||
self.query_one("#submit_button").focus()
|
||||
|
||||
def action_go_back(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self):
|
||||
self.app.exit()
|
||||
|
||||
def on_button_pressed(self, event) -> None:
|
||||
event.stop()
|
||||
if event.button.id == "submit_button":
|
||||
# 获取输入值
|
||||
name_input = self.query_one("#name_input")
|
||||
template_select = self.query_one("#template_select")
|
||||
author_input = self.query_one("#author_input")
|
||||
desc_input = self.query_one("#desc_input")
|
||||
|
||||
name = name_input.value.strip() # type: ignore
|
||||
author = author_input.value.strip() # type: ignore
|
||||
desc = desc_input.value.strip() # type: ignore
|
||||
selected = template_select.value # type: ignore
|
||||
|
||||
# 验证
|
||||
if not name:
|
||||
self.notify("单元集名称不能为空", severity="error")
|
||||
return
|
||||
|
||||
# 获取配置路径
|
||||
config = config_var.get()
|
||||
nucleon_dir = Path(config["paths"]["nucleon_dir"])
|
||||
template_dir = Path(config["paths"]["template_dir"])
|
||||
|
||||
# 检查文件是否已存在
|
||||
nucleon_path = nucleon_dir / f"{name}.toml"
|
||||
if nucleon_path.exists():
|
||||
self.notify(f"单元集 '{name}' 已存在", severity="error")
|
||||
return
|
||||
|
||||
# 确定模板文件
|
||||
if selected is None:
|
||||
self.notify("请选择一个模板", severity="error")
|
||||
return
|
||||
# selected 是描述字符串, 格式如 "描述 (filename.toml)"
|
||||
# 提取文件名
|
||||
import re
|
||||
|
||||
match = re.search(r"\(([^)]+)\)$", selected)
|
||||
if not match:
|
||||
self.notify("模板选择格式无效", severity="error")
|
||||
return
|
||||
template_filename = match.group(1)
|
||||
template_path = template_dir / template_filename
|
||||
if not template_path.exists():
|
||||
self.notify(f"模板文件不存在: {template_filename}", severity="error")
|
||||
return
|
||||
|
||||
# 加载模板
|
||||
try:
|
||||
with open(template_path, "r", encoding="utf-8") as f:
|
||||
template_data = toml.load(f)
|
||||
except Exception as e:
|
||||
self.notify(f"加载模板失败: {e}", severity="error")
|
||||
return
|
||||
|
||||
# 更新元数据
|
||||
metadata = template_data.get("__metadata__", {})
|
||||
attribution = metadata.get("attribution", {})
|
||||
if author:
|
||||
attribution["author"] = author
|
||||
if desc:
|
||||
attribution["desc"] = desc
|
||||
attribution["name"] = name
|
||||
# 可选: 设置版本
|
||||
attribution["version"] = ver
|
||||
metadata["attribution"] = attribution
|
||||
template_data["__metadata__"] = metadata
|
||||
|
||||
# 确保 nucleon_dir 存在
|
||||
nucleon_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 写入新文件
|
||||
try:
|
||||
with open(nucleon_path, "w", encoding="utf-8") as f:
|
||||
toml.dump(template_data, f)
|
||||
except Exception as e:
|
||||
self.notify(f"保存单元集失败: {e}", severity="error")
|
||||
return
|
||||
|
||||
self.notify(f"单元集 '{name}' 创建成功")
|
||||
self.app.pop_screen()
|
||||
246
src/heurams/interface/screens/precache.py
Normal file
246
src/heurams/interface/screens/precache.py
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
Label,
|
||||
Button,
|
||||
Static,
|
||||
ProgressBar,
|
||||
)
|
||||
from textual.containers import ScrollableContainer, Horizontal
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
import pathlib
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.services.hasher as hasher
|
||||
from heurams.context import *
|
||||
from textual.worker import get_current_worker
|
||||
|
||||
|
||||
class PrecachingScreen(Screen):
|
||||
"""预缓存音频文件屏幕
|
||||
|
||||
缓存记忆单元音频文件, 全部(默认) 或部分记忆单元(可选参数传入)
|
||||
|
||||
Args:
|
||||
nucleons (list): 可选列表, 仅包含 Nucleon 对象
|
||||
desc (list): 可选字符串, 包含对此次调用的文字描述
|
||||
"""
|
||||
|
||||
SUB_TITLE = "缓存管理器"
|
||||
BINDINGS = [("q", "go_back", "返回")]
|
||||
|
||||
def __init__(self, nucleons: list = [], desc: str = ""):
|
||||
super().__init__(name=None, id=None, classes=None)
|
||||
self.nucleons = nucleons
|
||||
self.is_precaching = False
|
||||
self.current_file = ""
|
||||
self.current_item = ""
|
||||
self.progress = 0
|
||||
self.total = len(nucleons)
|
||||
self.processed = 0
|
||||
self.precache_worker = None
|
||||
self.cancel_flag = 0
|
||||
self.desc = desc
|
||||
for i in nucleons:
|
||||
i: pt.Nucleon
|
||||
i.do_eval()
|
||||
# print("完成 EVAL")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="precache_container"):
|
||||
yield Label("[b]音频预缓存[/b]", classes="title-label")
|
||||
|
||||
if self.nucleons:
|
||||
yield Static(f"目标单元归属: [b]{self.desc}[/b]", classes="target-info")
|
||||
yield Static(f"单元数量: {len(self.nucleons)}", classes="target-info")
|
||||
else:
|
||||
yield Static("目标: 所有单元", classes="target-info")
|
||||
|
||||
yield Static(id="status", classes="status-info")
|
||||
yield Static(id="current_item", classes="current-item")
|
||||
yield ProgressBar(total=100, show_eta=False, id="progress_bar")
|
||||
|
||||
with Horizontal(classes="button-group"):
|
||||
if not self.is_precaching:
|
||||
yield Button("开始预缓存", id="start_precache", variant="primary")
|
||||
else:
|
||||
yield Button("取消预缓存", id="cancel_precache", variant="error")
|
||||
yield Button("清空缓存", id="clear_cache", variant="warning")
|
||||
yield Button("返回", id="go_back", variant="default")
|
||||
|
||||
yield Static("若您离开此界面, 未完成的缓存进程会自动停止.")
|
||||
yield Static('缓存程序支持 "断点续传".')
|
||||
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self):
|
||||
"""挂载时初始化状态"""
|
||||
self.update_status("就绪", "等待开始...")
|
||||
|
||||
def update_status(self, status, current_item="", progress=None):
|
||||
"""更新状态显示"""
|
||||
status_widget = self.query_one("#status", Static)
|
||||
item_widget = self.query_one("#current_item", Static)
|
||||
progress_bar = self.query_one("#progress_bar", ProgressBar)
|
||||
|
||||
status_widget.update(f"状态: {status}")
|
||||
item_widget.update(f"当前项目: {current_item}" if current_item else "")
|
||||
|
||||
if progress is not None:
|
||||
progress_bar.progress = progress
|
||||
progress_bar.advance(0) # 刷新显示
|
||||
|
||||
def precache_by_text(self, text: str):
|
||||
"""预缓存单段文本的音频"""
|
||||
from heurams.context import rootdir, workdir, config_var
|
||||
|
||||
cache_dir = pathlib.Path(config_var.get()["paths"]["cache_dir"])
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
cache_file = cache_dir / f"{hasher.get_md5(text)}.wav"
|
||||
if not cache_file.exists():
|
||||
try: # TODO: 调用模块消除tts耦合
|
||||
import edge_tts as tts
|
||||
|
||||
communicate = tts.Communicate(text, "zh-CN-XiaoxiaoNeural")
|
||||
communicate.save_sync(str(cache_file))
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"预缓存失败 '{text}': {e}")
|
||||
return 0
|
||||
return 1
|
||||
|
||||
def precache_by_nucleon(self, nucleon: pt.Nucleon):
|
||||
"""依据 Nucleon 缓存"""
|
||||
# print(nucleon.metadata['formation']['tts_text'])
|
||||
ret = self.precache_by_text(nucleon.metadata["formation"]["tts_text"])
|
||||
return ret
|
||||
# print(f"TTS 缓存: {nucleon.metadata['formation']['tts_text']}")
|
||||
|
||||
def precache_by_list(self, nucleons: list):
|
||||
"""依据 Nucleons 列表缓存"""
|
||||
for idx, nucleon in enumerate(nucleons):
|
||||
# print(f"PROC: {nucleon}")
|
||||
worker = get_current_worker()
|
||||
if worker and worker.is_cancelled: # 函数在worker中执行且已被取消
|
||||
return False
|
||||
text = nucleon.metadata["formation"]["tts_text"]
|
||||
# self.current_item = text[:30] + "..." if len(text) > 50 else text
|
||||
# print(text)
|
||||
self.processed += 1
|
||||
# print(self.processed)
|
||||
# print(self.total)
|
||||
progress = int((self.processed / self.total) * 100) if self.total > 0 else 0
|
||||
# print(progress)
|
||||
self.update_status(f"正处理 ({idx + 1}/{len(nucleons)})", text, progress)
|
||||
ret = self.precache_by_nucleon(nucleon)
|
||||
if not ret:
|
||||
self.update_status(
|
||||
"出错",
|
||||
f"处理失败, 跳过: {self.current_item}",
|
||||
)
|
||||
import time
|
||||
|
||||
time.sleep(1)
|
||||
if self.cancel_flag:
|
||||
worker.cancel()
|
||||
self.cancel_flag = 0
|
||||
return False
|
||||
return True
|
||||
|
||||
def precache_by_nucleons(self):
|
||||
# print("开始缓存")
|
||||
ret = self.precache_by_list(self.nucleons)
|
||||
# print(f"返回 {ret}")
|
||||
return ret
|
||||
|
||||
def precache_by_filepath(self, path: pathlib.Path):
|
||||
"""预缓存单个文件的所有内容"""
|
||||
lst = list()
|
||||
for i in pt.load_nucleon(path):
|
||||
lst.append(i[0])
|
||||
return self.precache_by_list(lst)
|
||||
|
||||
def precache_all_files(self):
|
||||
"""预缓存所有文件"""
|
||||
from heurams.context import rootdir, workdir, config_var
|
||||
|
||||
nucleon_path = pathlib.Path(config_var.get()["paths"]["nucleon_dir"])
|
||||
nucleon_files = [
|
||||
f for f in nucleon_path.iterdir() if f.suffix == ".toml"
|
||||
] # TODO: 解耦合
|
||||
|
||||
# 计算总项目数
|
||||
self.total = 0
|
||||
nu = list()
|
||||
for file in nucleon_files:
|
||||
try:
|
||||
for i in pt.load_nucleon(file):
|
||||
nu.append(i[0])
|
||||
except:
|
||||
continue
|
||||
self.total = len(nu)
|
||||
for i in nu:
|
||||
i: pt.Nucleon
|
||||
i.do_eval()
|
||||
return self.precache_by_list(nu)
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
event.stop()
|
||||
if event.button.id == "start_precache" and not self.is_precaching:
|
||||
# 开始预缓存
|
||||
if self.nucleons:
|
||||
self.precache_worker = self.run_worker(
|
||||
self.precache_by_nucleons,
|
||||
thread=True,
|
||||
exclusive=True,
|
||||
exit_on_error=True,
|
||||
)
|
||||
else:
|
||||
self.precache_worker = self.run_worker(
|
||||
self.precache_all_files,
|
||||
thread=True,
|
||||
exclusive=True,
|
||||
exit_on_error=True,
|
||||
)
|
||||
|
||||
elif event.button.id == "cancel_precache" and self.is_precaching:
|
||||
# 取消预缓存
|
||||
if self.precache_worker:
|
||||
self.precache_worker.cancel()
|
||||
self.is_precaching = False
|
||||
self.processed = 0
|
||||
self.progress = 0
|
||||
self.update_status("已取消", "预缓存操作被用户取消", 0)
|
||||
|
||||
elif event.button.id == "clear_cache":
|
||||
# 清空缓存
|
||||
try:
|
||||
import shutil
|
||||
from heurams.context import rootdir, workdir, config_var
|
||||
|
||||
shutil.rmtree(
|
||||
f"{config_var.get()["paths"]["cache_dir"]}", ignore_errors=True
|
||||
)
|
||||
self.update_status("已清空", "音频缓存已清空", 0)
|
||||
except Exception as e:
|
||||
self.update_status("错误", f"清空缓存失败: {e}")
|
||||
self.cancel_flag = 1
|
||||
self.processed = 0
|
||||
self.progress = 0
|
||||
|
||||
elif event.button.id == "go_back":
|
||||
self.action_go_back()
|
||||
|
||||
def action_go_back(self):
|
||||
if self.is_precaching and self.precache_worker:
|
||||
self.precache_worker.cancel()
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self):
|
||||
if self.is_precaching and self.precache_worker:
|
||||
self.precache_worker.cancel()
|
||||
self.app.exit()
|
||||
144
src/heurams/interface/screens/preparation.py
Normal file
144
src/heurams/interface/screens/preparation.py
Normal file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
Label,
|
||||
Static,
|
||||
Button,
|
||||
Markdown,
|
||||
)
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
from heurams.context import config_var
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.services.hasher as hasher
|
||||
from heurams.context import *
|
||||
from textual.reactive import reactive
|
||||
from textual.widget import Widget
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PreparationScreen(Screen):
|
||||
|
||||
SUB_TITLE = "准备记忆集"
|
||||
|
||||
BINDINGS = [
|
||||
("q", "go_back", "返回"),
|
||||
("p", "precache", "预缓存音频"),
|
||||
("d", "toggle_dark", ""),
|
||||
("0,1,2,3", "app.push_screen('about')", ""),
|
||||
]
|
||||
|
||||
scheduled_num = reactive(config_var.get()["scheduled_num"])
|
||||
|
||||
def __init__(self, nucleon_file: pathlib.Path, electron_file: pathlib.Path) -> None:
|
||||
super().__init__(name=None, id=None, classes=None)
|
||||
self.nucleon_file = nucleon_file
|
||||
self.electron_file = electron_file
|
||||
self.nucleons_with_orbital = pt.load_nucleon(self.nucleon_file)
|
||||
self.electrons = pt.load_electron(self.electron_file)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="vice_container"):
|
||||
yield Label(f"准备就绪: [b]{self.nucleon_file.stem}[/b]\n")
|
||||
yield Label(
|
||||
f"内容源文件: {config_var.get()['paths']['nucleon_dir']}/[b]{self.nucleon_file.name}[/b]"
|
||||
)
|
||||
yield Label(
|
||||
f"元数据文件: {config_var.get()['paths']['electron_dir']}/[b]{self.electron_file.name}[/b]"
|
||||
)
|
||||
yield Label(f"\n单元数量: {len(self.nucleons_with_orbital)}\n")
|
||||
yield Label(f"单次记忆数量: {self.scheduled_num}", id="schnum_label")
|
||||
|
||||
yield Button(
|
||||
"开始记忆",
|
||||
id="start_memorizing_button",
|
||||
variant="primary",
|
||||
classes="start-button",
|
||||
)
|
||||
yield Button(
|
||||
"预缓存音频",
|
||||
id="precache_button",
|
||||
variant="success",
|
||||
classes="precache-button",
|
||||
)
|
||||
|
||||
yield Static(f"\n单元预览:\n")
|
||||
yield Markdown(self._get_full_content().replace("/", ""), classes="full")
|
||||
yield Footer()
|
||||
|
||||
# def watch_scheduled_num(self, old_scheduled_num, new_scheduled_num):
|
||||
# logger.debug("响应", old_scheduled_num, "->", new_scheduled_num)
|
||||
# try:
|
||||
# one = self.query_one("#schnum_label")
|
||||
# one.update(f"单次记忆数量: {new_scheduled_num}") # type: ignore
|
||||
# except:
|
||||
# pass
|
||||
|
||||
def _get_full_content(self):
|
||||
content = ""
|
||||
for nucleon, orbital in self.nucleons_with_orbital:
|
||||
nucleon: pt.Nucleon
|
||||
# print(nucleon.payload)
|
||||
content += " - " + nucleon["content"] + " \n"
|
||||
return content
|
||||
|
||||
def action_go_back(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_precache(self):
|
||||
from ..screens.precache import PrecachingScreen
|
||||
|
||||
lst = list()
|
||||
for i in self.nucleons_with_orbital:
|
||||
lst.append(i[0])
|
||||
precache_screen = PrecachingScreen(lst)
|
||||
self.app.push_screen(precache_screen)
|
||||
|
||||
def action_quit_app(self):
|
||||
self.app.exit()
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
event.stop()
|
||||
logger.debug("按下按钮")
|
||||
if event.button.id == "start_memorizing_button":
|
||||
atoms = list()
|
||||
for nucleon, orbital in self.nucleons_with_orbital:
|
||||
atom = pt.Atom(nucleon.ident)
|
||||
atom.link("nucleon", nucleon)
|
||||
try:
|
||||
atom.link("electron", self.electrons[nucleon.ident])
|
||||
except KeyError:
|
||||
atom.link("electron", pt.Electron(nucleon.ident))
|
||||
atom.link("orbital", orbital)
|
||||
atom.link("nucleon_fmt", "toml")
|
||||
atom.link("electron_fmt", "json")
|
||||
atom.link("orbital_fmt", "toml")
|
||||
atom.link("nucleon_path", self.nucleon_file)
|
||||
atom.link("electron_path", self.electron_file)
|
||||
atom.link("orbital_path", None)
|
||||
atoms.append(atom)
|
||||
atoms_to_provide = list()
|
||||
left_new = self.scheduled_num
|
||||
for i in atoms:
|
||||
i: pt.Atom
|
||||
if i.registry["electron"].is_due():
|
||||
atoms_to_provide.append(i)
|
||||
else:
|
||||
if i.registry["electron"].is_activated():
|
||||
pass
|
||||
else:
|
||||
left_new -= 1
|
||||
if left_new >= 0:
|
||||
atoms_to_provide.append(i)
|
||||
logger.debug(f"ATP: {atoms_to_provide}")
|
||||
from .memorizor import MemScreen
|
||||
|
||||
memscreen = MemScreen(atoms_to_provide)
|
||||
self.app.push_screen(memscreen)
|
||||
elif event.button.id == "precache_button":
|
||||
self.action_precache()
|
||||
0
src/heurams/interface/screens/register.py
Normal file
0
src/heurams/interface/screens/register.py
Normal file
48
src/heurams/interface/screens/synctool.py
Normal file
48
src/heurams/interface/screens/synctool.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import (
|
||||
Header,
|
||||
Footer,
|
||||
Label,
|
||||
Button,
|
||||
Static,
|
||||
ProgressBar,
|
||||
)
|
||||
from textual.containers import ScrollableContainer, Horizontal
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
import pathlib
|
||||
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.services.hasher as hasher
|
||||
from heurams.context import *
|
||||
from textual.worker import get_current_worker
|
||||
|
||||
|
||||
class SyncScreen(Screen):
|
||||
|
||||
BINDINGS = [("q", "go_back", "返回")]
|
||||
|
||||
def __init__(self, nucleons: list = [], desc: str = ""):
|
||||
super().__init__(name=None, id=None, classes=None)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
with ScrollableContainer(id="sync_container"):
|
||||
pass
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self):
|
||||
"""挂载时初始化状态"""
|
||||
|
||||
def update_status(self, status, current_item="", progress=None):
|
||||
"""更新状态显示"""
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
event.stop()
|
||||
|
||||
def action_go_back(self):
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_quit_app(self):
|
||||
self.app.exit()
|
||||
0
src/heurams/interface/screens/type43.py
Normal file
0
src/heurams/interface/screens/type43.py
Normal file
36
src/heurams/interface/shim.py
Normal file
36
src/heurams/interface/shim.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Kernel 操作辅助函数库"""
|
||||
|
||||
import random
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as pz
|
||||
import heurams.interface.widgets as pzw
|
||||
from typing import TypedDict
|
||||
|
||||
staging = {} # 细粒度缓存区, 是 ident -> quality 的封装
|
||||
|
||||
|
||||
def report_to_staging(atom: pt.Atom, quality):
|
||||
staging[atom.ident] = min(quality, staging[atom.ident])
|
||||
|
||||
|
||||
def clear():
|
||||
staging = dict()
|
||||
|
||||
|
||||
def deploy_to_electron():
|
||||
for atom_ident, quality in staging.items():
|
||||
if pt.atom_registry[atom_ident].registry["electron"].is_activated:
|
||||
pt.atom_registry[atom_ident].registry["electron"].revisor(quality=quality)
|
||||
else:
|
||||
pt.atom_registry[atom_ident].registry["electron"].revisor(
|
||||
quality=quality, is_new_activation=True
|
||||
)
|
||||
clear()
|
||||
|
||||
|
||||
puzzle2widget = {
|
||||
pz.RecognitionPuzzle: pzw.Recognition,
|
||||
pz.ClozePuzzle: pzw.ClozePuzzle,
|
||||
pz.MCQPuzzle: pzw.MCQPuzzle,
|
||||
pz.BasePuzzle: pzw.BasePuzzleWidget,
|
||||
}
|
||||
7
src/heurams/interface/widgets/__init__.py
Normal file
7
src/heurams/interface/widgets/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
from .basic_puzzle import BasicEvaluation
|
||||
from .cloze_puzzle import ClozePuzzle
|
||||
from .finished import Finished
|
||||
from .mcq_puzzle import MCQPuzzle
|
||||
from .placeholder import Placeholder
|
||||
from .recognition import Recognition
|
||||
32
src/heurams/interface/widgets/base_puzzle_widget.py
Normal file
32
src/heurams/interface/widgets/base_puzzle_widget.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from typing import Iterable
|
||||
from textual.app import ComposeResult
|
||||
from textual.widget import Widget
|
||||
import heurams.kernel.particles as pt
|
||||
|
||||
|
||||
class BasePuzzleWidget(Widget):
|
||||
def __init__(
|
||||
self,
|
||||
*children: Widget,
|
||||
atom: pt.Atom,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
disabled: bool = False,
|
||||
markup: bool = True
|
||||
) -> None:
|
||||
super().__init__(
|
||||
*children,
|
||||
name=name,
|
||||
id=id,
|
||||
classes=classes,
|
||||
disabled=disabled,
|
||||
markup=markup
|
||||
)
|
||||
self.atom = atom
|
||||
|
||||
def compose(self) -> Iterable[Widget]:
|
||||
return super().compose()
|
||||
|
||||
def handler(self, rating) -> None:
|
||||
pass
|
||||
119
src/heurams/interface/widgets/basic_puzzle.py
Normal file
119
src/heurams/interface/widgets/basic_puzzle.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from textual.widgets import (
|
||||
Label,
|
||||
Static,
|
||||
Button,
|
||||
)
|
||||
from textual.containers import ScrollableContainer, Horizontal
|
||||
from textual.widget import Widget
|
||||
import heurams.kernel.particles as pt
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
from textual.message import Message
|
||||
|
||||
|
||||
class BasicEvaluation(BasePuzzleWidget):
|
||||
def __init__(
|
||||
self,
|
||||
*children: Widget,
|
||||
atom: pt.Atom,
|
||||
alia: str = "",
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
disabled: bool = False,
|
||||
markup: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
*children,
|
||||
atom=atom,
|
||||
name=name,
|
||||
id=id,
|
||||
classes=classes,
|
||||
disabled=disabled,
|
||||
markup=markup,
|
||||
)
|
||||
|
||||
class RatingChanged(Message):
|
||||
def __init__(self, rating: int) -> None:
|
||||
self.rating = rating # 评分值 (0-5)
|
||||
super().__init__()
|
||||
|
||||
# 反馈映射表
|
||||
feedback_mapping = {
|
||||
"feedback_5": {"rating": 5, "text": "完美回想"},
|
||||
"feedback_4": {"rating": 4, "text": "犹豫后正确"},
|
||||
"feedback_3": {"rating": 3, "text": "困难地正确"},
|
||||
"feedback_2": {"rating": 2, "text": "错误但熟悉"},
|
||||
"feedback_1": {"rating": 1, "text": "错误且不熟"},
|
||||
"feedback_0": {"rating": 0, "text": "完全空白"},
|
||||
}
|
||||
|
||||
def compose(self):
|
||||
# 显示主要内容
|
||||
yield Label(self.atom.registry["nucleon"]["content"], id="main")
|
||||
|
||||
# 显示评估说明(可选)
|
||||
yield Static("请评估你对这个内容的记忆程度: ", classes="instruction")
|
||||
|
||||
# 按钮容器
|
||||
with ScrollableContainer(id="button_container"):
|
||||
btn = {}
|
||||
btn["5"] = Button(
|
||||
"完美回想", variant="success", id="feedback_5", classes="choice"
|
||||
)
|
||||
btn["4"] = Button(
|
||||
"犹豫后正确", variant="success", id="feedback_4", classes="choice"
|
||||
)
|
||||
btn["3"] = Button(
|
||||
"困难地正确", variant="warning", id="feedback_3", classes="choice"
|
||||
)
|
||||
btn["2"] = Button(
|
||||
"错误但熟悉", variant="warning", id="feedback_2", classes="choice"
|
||||
)
|
||||
btn["1"] = Button(
|
||||
"错误且不熟", variant="error", id="feedback_1", classes="choice"
|
||||
)
|
||||
btn["0"] = Button(
|
||||
"完全空白", variant="error", id="feedback_0", classes="choice"
|
||||
)
|
||||
|
||||
# 布局按钮
|
||||
yield Horizontal(btn["5"], btn["4"])
|
||||
yield Horizontal(btn["3"], btn["2"])
|
||||
yield Horizontal(btn["1"], btn["0"])
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""处理按钮点击事件"""
|
||||
button_id = event.button.id
|
||||
|
||||
if button_id in self.feedback_mapping:
|
||||
feedback_info = self.feedback_mapping[button_id]
|
||||
|
||||
self.post_message(
|
||||
self.RatingChanged(
|
||||
rating=feedback_info["rating"],
|
||||
)
|
||||
)
|
||||
|
||||
event.button.add_class("selected")
|
||||
|
||||
self.disable_other_buttons(button_id)
|
||||
|
||||
def disable_other_buttons(self, selected_button_id: str) -> None:
|
||||
for button in self.query("Button.choice"):
|
||||
if button.id != selected_button_id:
|
||||
button.disabled = True
|
||||
|
||||
def enable_all_buttons(self) -> None:
|
||||
for button in self.query("Button.choice"):
|
||||
button.disabled = False
|
||||
|
||||
def on_key(self, event) -> None:
|
||||
if event.key in ["0", "1", "2", "3", "4", "5"]:
|
||||
button_id = f"feedback_{event.key}"
|
||||
if button_id in self.feedback_mapping:
|
||||
# 模拟按钮点击
|
||||
self.post_message(
|
||||
self.RatingChanged(
|
||||
rating=self.feedback_mapping[button_id]["rating"],
|
||||
)
|
||||
)
|
||||
109
src/heurams/interface/widgets/cloze_puzzle.py
Normal file
109
src/heurams/interface/widgets/cloze_puzzle.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from textual.widgets import (
|
||||
Label,
|
||||
Button,
|
||||
)
|
||||
from textual.widget import Widget
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as pz
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
import copy
|
||||
import random
|
||||
from textual.containers import Container
|
||||
from textual.message import Message
|
||||
from heurams.services.logger import get_logger
|
||||
from typing import TypedDict
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Setting(TypedDict):
|
||||
__origin__: str
|
||||
__hint__: str
|
||||
text: str
|
||||
delimiter: str
|
||||
min_denominator: str
|
||||
|
||||
|
||||
class ClozePuzzle(BasePuzzleWidget):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*children: Widget,
|
||||
atom: pt.Atom,
|
||||
alia: str = "",
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
disabled: bool = False,
|
||||
markup: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
*children,
|
||||
atom=atom,
|
||||
name=name,
|
||||
id=id,
|
||||
classes=classes,
|
||||
disabled=disabled,
|
||||
markup=markup,
|
||||
)
|
||||
self.inputlist = list()
|
||||
self.hashtable = {}
|
||||
self.alia = alia
|
||||
self._load()
|
||||
self.hashmap = dict()
|
||||
|
||||
def _load(self):
|
||||
setting = self.atom.registry["orbital"]["puzzles"][self.alia]
|
||||
self.puzzle = pz.ClozePuzzle(
|
||||
text=setting["text"],
|
||||
delimiter=setting["delimiter"],
|
||||
min_denominator=int(setting["min_denominator"]),
|
||||
)
|
||||
self.puzzle.refresh()
|
||||
self.ans = copy.copy(self.puzzle.answer) # 乱序
|
||||
random.shuffle(self.ans)
|
||||
|
||||
def compose(self):
|
||||
yield Label(self.puzzle.wording, id="sentence")
|
||||
yield Label(f"当前输入: {self.inputlist}", id="inputpreview")
|
||||
# 渲染当前问题的选项
|
||||
with Container(id="btn-container"):
|
||||
for i in self.ans:
|
||||
self.hashmap[str(hash(i))] = i
|
||||
btnid = f"sel000-{hash(i)}"
|
||||
logger.debug(f"建立按钮 {btnid}")
|
||||
yield Button(i, id=f"{btnid}")
|
||||
|
||||
yield Button("退格", id="delete")
|
||||
|
||||
def update_display(self):
|
||||
preview = self.query_one("#inputpreview")
|
||||
preview.update(f"当前输入: {self.inputlist}") # type: ignore
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
button_id = event.button.id
|
||||
|
||||
if button_id == "delete":
|
||||
if len(self.inputlist) > 0:
|
||||
self.inputlist.pop()
|
||||
self.update_display()
|
||||
else:
|
||||
answer_text = self.hashmap[button_id[7:]] # type: ignore
|
||||
self.inputlist.append(answer_text)
|
||||
self.update_display()
|
||||
|
||||
if len(self.inputlist) >= len(self.puzzle.answer):
|
||||
is_correct = self.inputlist == self.puzzle.answer
|
||||
rating = 4 if is_correct else 2
|
||||
self.handler(rating)
|
||||
self.screen.rating = rating # type: ignore
|
||||
|
||||
if not is_correct:
|
||||
self.inputlist = []
|
||||
self.update_display()
|
||||
|
||||
def handler(self, rating):
|
||||
if self.atom.lock():
|
||||
pass
|
||||
else:
|
||||
self.atom.minimize(rating)
|
||||
36
src/heurams/interface/widgets/finished.py
Normal file
36
src/heurams/interface/widgets/finished.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from textual.widgets import (
|
||||
Label,
|
||||
Button,
|
||||
)
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
class Finished(Widget):
|
||||
def __init__(
|
||||
self,
|
||||
*children: Widget,
|
||||
alia="",
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
disabled: bool = False,
|
||||
markup: bool = True
|
||||
) -> None:
|
||||
self.alia = alia
|
||||
super().__init__(
|
||||
*children,
|
||||
name=name,
|
||||
id=id,
|
||||
classes=classes,
|
||||
disabled=disabled,
|
||||
markup=markup
|
||||
)
|
||||
|
||||
def compose(self):
|
||||
yield Label("本次记忆进程结束", id="finished_msg")
|
||||
yield Button("返回上一级", id="back-to-menu")
|
||||
|
||||
def on_button_pressed(self, event):
|
||||
button_id = event.button.id
|
||||
if button_id == "back-to-menu":
|
||||
self.app.pop_screen()
|
||||
160
src/heurams/interface/widgets/mcq_puzzle.py
Normal file
160
src/heurams/interface/widgets/mcq_puzzle.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# 单项选择题
|
||||
from textual.widgets import (
|
||||
Label,
|
||||
Button,
|
||||
)
|
||||
from textual.containers import ScrollableContainer, Container
|
||||
from textual.widget import Widget
|
||||
import heurams.kernel.particles as pt
|
||||
import heurams.kernel.puzzles as pz
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
from typing import TypedDict
|
||||
from heurams.services.hasher import hash
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Setting(TypedDict):
|
||||
__origin__: str
|
||||
__hint__: str
|
||||
primary: str # 显示的提示文本
|
||||
mapping: dict # 谜题到答案的映射
|
||||
jammer: list # 干扰项
|
||||
max_riddles_num: int # 最大谜题数量
|
||||
prefix: str # 提示词前缀
|
||||
|
||||
|
||||
class MCQPuzzle(BasePuzzleWidget):
|
||||
def __init__(
|
||||
self,
|
||||
*children: Widget,
|
||||
atom: pt.Atom,
|
||||
alia: str = "",
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
disabled: bool = False,
|
||||
markup: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
*children,
|
||||
atom=atom,
|
||||
name=name,
|
||||
id=id,
|
||||
classes=classes,
|
||||
disabled=disabled,
|
||||
markup=markup,
|
||||
)
|
||||
self.inputlist = []
|
||||
self.alia = alia
|
||||
self.hashmap = dict()
|
||||
self.cursor = 0
|
||||
self.atom = atom
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
cfg = self.atom.registry["orbital"]["puzzles"][self.alia]
|
||||
self.puzzle = pz.MCQPuzzle(
|
||||
cfg["mapping"], cfg["jammer"], int(cfg["max_riddles_num"]), cfg["prefix"]
|
||||
)
|
||||
self.puzzle.refresh()
|
||||
|
||||
def compose(self):
|
||||
self.atom.registry["nucleon"].do_eval()
|
||||
setting: Setting = self.atom.registry["nucleon"].metadata["orbital"]["puzzles"][
|
||||
self.alia
|
||||
]
|
||||
logger.debug(f"Puzzle Setting: {setting}")
|
||||
current_options = self.puzzle.options[len(self.inputlist)]
|
||||
yield Label(setting["primary"], id="sentence")
|
||||
yield Label(self.puzzle.wording[len(self.inputlist)], id="puzzle")
|
||||
yield Label(f"当前输入: {self.inputlist}", id="inputpreview")
|
||||
|
||||
# 渲染当前问题的选项
|
||||
with Container(id="btn-container"):
|
||||
for i in current_options:
|
||||
self.hashmap[str(hash(i))] = i
|
||||
btnid = f"sel{str(self.cursor).zfill(3)}-{hash(i)}"
|
||||
logger.debug(f"建立按钮 {btnid}")
|
||||
yield Button(i, id=f"{btnid}")
|
||||
|
||||
yield Button("退格", id="delete")
|
||||
|
||||
def update_display(self, error=0):
|
||||
# 更新预览标签
|
||||
preview = self.query_one("#inputpreview")
|
||||
preview.update(f"当前输入: {self.inputlist}") # type: ignore
|
||||
logger.debug("已经更新预览标签")
|
||||
# 更新问题标签
|
||||
puzzle_label = self.query_one("#puzzle")
|
||||
current_question_index = len(self.inputlist)
|
||||
if current_question_index < len(self.puzzle.wording):
|
||||
puzzle_label.update(self.puzzle.wording[current_question_index]) # type: ignore
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""处理按钮点击事件"""
|
||||
event.stop()
|
||||
button_id = event.button.id
|
||||
|
||||
if button_id == "delete":
|
||||
# 退格处理
|
||||
if len(self.inputlist) > 0:
|
||||
self.inputlist.pop()
|
||||
self.refresh_buttons()
|
||||
self.update_display()
|
||||
|
||||
elif button_id.startswith("sel"): # type: ignore
|
||||
# 选项选择处理
|
||||
answer_text = self.hashmap[button_id[7:]] # type: ignore
|
||||
self.inputlist.append(answer_text)
|
||||
logger.debug(f"{self.inputlist}")
|
||||
# 检查是否完成所有题目
|
||||
if len(self.inputlist) >= len(self.puzzle.answer):
|
||||
is_correct = self.inputlist == self.puzzle.answer
|
||||
rating = 4 if is_correct else 2
|
||||
|
||||
self.screen.rating = rating # type: ignore
|
||||
self.handler(rating)
|
||||
# 重置输入(如果回答错误)
|
||||
if not is_correct:
|
||||
self.inputlist = []
|
||||
self.refresh_buttons()
|
||||
self.update_display()
|
||||
else:
|
||||
# 进入下一题
|
||||
self.refresh_buttons()
|
||||
self.update_display()
|
||||
|
||||
def refresh_buttons(self):
|
||||
"""刷新按钮显示(用于题目切换)"""
|
||||
# 移除所有选项按钮
|
||||
logger.debug("刷新按钮")
|
||||
self.cursor += 1
|
||||
container = self.query_one("#btn-container")
|
||||
buttons_to_remove = [
|
||||
child
|
||||
for child in container.children
|
||||
if hasattr(child, "id") and child.id and child.id.startswith("sel")
|
||||
]
|
||||
|
||||
for button in buttons_to_remove:
|
||||
logger.info(button)
|
||||
container.remove_children("#" + button.id) # type: ignore
|
||||
|
||||
# 添加当前题目的选项按钮
|
||||
current_question_index = len(self.inputlist)
|
||||
if current_question_index < len(self.puzzle.options):
|
||||
current_options = self.puzzle.options[current_question_index]
|
||||
for option in current_options:
|
||||
button_id = f"sel{str(self.cursor).zfill(3)}-{hash(option)}"
|
||||
if button_id not in self.hashmap:
|
||||
self.hashmap[button_id] = option
|
||||
new_button = Button(option, id=button_id)
|
||||
container.mount(new_button)
|
||||
|
||||
def handler(self, rating):
|
||||
if self.atom.lock():
|
||||
pass
|
||||
else:
|
||||
self.atom.minimize(rating)
|
||||
33
src/heurams/interface/widgets/placeholder.py
Normal file
33
src/heurams/interface/widgets/placeholder.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from textual.widgets import (
|
||||
Label,
|
||||
Button,
|
||||
)
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
class Placeholder(Widget):
|
||||
def __init__(
|
||||
self,
|
||||
*children: Widget,
|
||||
name: str | None = None,
|
||||
alia: str = "",
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
disabled: bool = False,
|
||||
markup: bool = True
|
||||
) -> None:
|
||||
super().__init__(
|
||||
*children,
|
||||
name=name,
|
||||
id=id,
|
||||
classes=classes,
|
||||
disabled=disabled,
|
||||
markup=markup
|
||||
)
|
||||
|
||||
def compose(self):
|
||||
yield Label("示例标签", id="testlabel")
|
||||
yield Button("示例按钮", id="testbtn", classes="choice")
|
||||
|
||||
def on_button_pressed(self, event):
|
||||
pass
|
||||
116
src/heurams/interface/widgets/recognition.py
Normal file
116
src/heurams/interface/widgets/recognition.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from textual.reactive import reactive
|
||||
from textual.widgets import (
|
||||
Markdown,
|
||||
Label,
|
||||
Static,
|
||||
Button,
|
||||
)
|
||||
from textual.containers import Center
|
||||
from textual.widget import Widget
|
||||
from typing import Dict
|
||||
import heurams.kernel.particles as pt
|
||||
import re
|
||||
from .base_puzzle_widget import BasePuzzleWidget
|
||||
from typing import TypedDict, List
|
||||
from textual.message import Message
|
||||
from heurams.services.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class RecognitionConfig(TypedDict):
|
||||
__origin__: str
|
||||
__hint__: str
|
||||
primary: str
|
||||
secondary: List[str]
|
||||
top_dim: List[str]
|
||||
|
||||
|
||||
class Recognition(BasePuzzleWidget):
|
||||
def __init__(
|
||||
self,
|
||||
*children: Widget,
|
||||
atom: pt.Atom,
|
||||
alia: str = "",
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
disabled: bool = False,
|
||||
markup: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
*children,
|
||||
atom=atom,
|
||||
name=name,
|
||||
id=id,
|
||||
classes=classes,
|
||||
disabled=disabled,
|
||||
markup=markup,
|
||||
)
|
||||
if alia == "":
|
||||
alia = "Recognition"
|
||||
self.alia = alia
|
||||
|
||||
def compose(self):
|
||||
cfg: RecognitionConfig = self.atom.registry["orbital"]["puzzles"][self.alia]
|
||||
delim = self.atom.registry["nucleon"].metadata["formation"]["delimiter"]
|
||||
replace_dict = {
|
||||
", ": ",",
|
||||
". ": ".",
|
||||
"; ": ";",
|
||||
": ": ":",
|
||||
f"{delim},": ",",
|
||||
f".{delim}": ".",
|
||||
f"{delim};": ";",
|
||||
f";{delim}": ";",
|
||||
f":{delim}": ":",
|
||||
}
|
||||
|
||||
nucleon = self.atom.registry["nucleon"]
|
||||
metadata = self.atom.registry["nucleon"].metadata
|
||||
primary = cfg["primary"]
|
||||
|
||||
with Center():
|
||||
yield Static(f"[dim]{cfg['top_dim']}[/]")
|
||||
yield Label("")
|
||||
|
||||
for old, new in replace_dict.items():
|
||||
primary = primary.replace(old, new)
|
||||
primary_splited = re.split(r"(?<=[,;:|])", cfg["primary"])
|
||||
for item in primary_splited:
|
||||
with Center():
|
||||
yield Label(
|
||||
f"[b][b]{item.replace(delim, ' ')}[/][/]",
|
||||
id="sentence" + str(hash(item)),
|
||||
)
|
||||
|
||||
for item in cfg["secondary"]:
|
||||
if isinstance(item, list):
|
||||
for j in item:
|
||||
yield Markdown(f"### {metadata['annotation'][item]}: {j}")
|
||||
continue
|
||||
if isinstance(item, Dict):
|
||||
total = ""
|
||||
for j, k in item.items(): # type: ignore
|
||||
total += f"> **{j}**: {k} \n"
|
||||
yield Markdown(total)
|
||||
if isinstance(item, str):
|
||||
yield Markdown(item)
|
||||
|
||||
with Center():
|
||||
yield Button("我已知晓", id="ok")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "ok":
|
||||
self.screen.rating = 5 # type: ignore
|
||||
self.handler(5)
|
||||
|
||||
def handler(self, rating):
|
||||
if not self.atom.registry["runtime"]["locked"]:
|
||||
if not self.atom.registry["electron"].is_activated():
|
||||
self.atom.registry["electron"].activate()
|
||||
logger.debug(f"激活原子 {self.atom}")
|
||||
self.atom.lock(1)
|
||||
self.atom.minimize(5)
|
||||
else:
|
||||
pass
|
||||
Reference in New Issue
Block a user