refactor: 完成 0.4.0 版本更新
完成 0.4.0 版本更新, 为了消除此前提交消息风格不一致与错误提交超大文件的问题, 维持代码统计数据的准确性和提交消息风格的一致性, 重新初始化仓库; 旧的提交历史在 HeurAMS-legacy 仓库(https://gitea.imwangzhiyu.xyz/ajax/HeurAMS-legacy)
This commit is contained in:
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