fix: 完善

This commit is contained in:
2025-12-14 11:23:23 +08:00
parent baa7ac8ee9
commit 4f054ed2e5
7 changed files with 325 additions and 66 deletions

48
data/nucleon/test2.toml Normal file
View File

@@ -0,0 +1,48 @@
# Nucleon 是 HeurAMS 软件项目使用的基于 TOML 的专有源文件格式, 版本 4
# 建议使用的 MIME 类型: application/vnd.xyz.imwangzhiyu.heurams-nucleon.v4+toml
["__metadata__"]
["__metadata__.attribution"] # 版权元信息
author = "__heurams__"
group = "高考古诗文"
name = "过秦论"
license = "CC-BY-SA 4.0"
desc = "高考古诗文 - 过秦论"
["__metadata__.annotation"] # 键批注
note = "笔记"
keyword_note = "关键词翻译"
translation = "语句翻译"
["__metadata__.formation"] # 文件配置
delimiter = "/"
tts_text = "eval:nucleon['content'].replace('/', '')"
["__metadata__.orbital.puzzles"] # 谜题定义
# 我们称 "Recognition" 为 recognition 谜题的 alia
"Recognition" = { __origin__ = "recognition", __hint__ = "", primary = "eval:nucleon['content']", secondary = ["eval:nucleon['keyword_note']", "eval:nucleon['note']"], top_dim = ["eval:nucleon['translation']"] }
"SelectMeaning" = { __origin__ = "mcq", __hint__ = "eval:nucleon['content']", primary = "eval:nucleon['content']", mapping = "eval:nucleon['keyword_note']", jammer = "eval:nucleon['keyword_note']", max_riddles_num = "eval:default['mcq']['max_riddles_num']", prefix = "选择正确项: " }
"FillBlank" = { __origin__ = "cloze", __hint__ = "", text = "eval:nucleon['content']", delimiter = "eval:metadata['formation']['delimiter']", min_denominator = "eval:default['cloze']['min_denominator']"}
["__metadata__.orbital.schedule"] # 内置的推荐学习方案
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"]]
["秦孝公据崤函之固, 拥雍州之地,"]
note = []
content = "秦孝公/据/崤函/之固/, 拥/雍州/之地,/"
translation = "秦孝公占据着崤山和函谷关的险固地势,拥有雍州的土地,"
keyword_note = {"据"="占据", "崤函"="崤山和函谷关", "雍州"="古代九州之一"}
["君臣固守以窥周室,"]
note = []
content = "君臣/固守/以窥/周室,/"
translation = "君臣牢固地守卫着,借以窥视周王室的权力,"
keyword_note = {"窥"="窥视"}
["有席卷天下, 包举宇内, 囊括四海之意, 并吞八荒之心."]
note = []
content = "有/席卷/天下/, 包举/宇内/, 囊括/四海/之意/, 并吞/八荒/之心./"
translation = "有席卷天下,包办天宇之间,囊括四海的意图,并统天下的雄心。"
keyword_note = {"席卷"="像卷席子一样全部卷进去", "包举"="像打包一样全部拿走", "囊括"="像装口袋一样全部装进去", "八荒"="八方荒远之地"}

View File

@@ -51,11 +51,11 @@ class MemScreen(Screen):
def puzzle_widget(self): def puzzle_widget(self):
try: try:
print(self.phaser.state) #print(self.phaser.state)
self.fission = Fission(self.procession.current_atom, self.phaser.state) self.fission = Fission(self.procession.current_atom, self.phaser.state)
# print(1) # print(1)
puzzle_info = next(self.fission.generate()) puzzle_info = next(self.fission.generate())
print(puzzle_info) #print(puzzle_info)
return shim.puzzle2widget[puzzle_info["puzzle"]]( return shim.puzzle2widget[puzzle_info["puzzle"]](
atom=self.procession.current_atom, alia=puzzle_info["alia"] atom=self.procession.current_atom, alia=puzzle_info["alia"]
) )
@@ -90,7 +90,9 @@ class MemScreen(Screen):
forwards = 1 if new_rating >= 4 else 0 forwards = 1 if new_rating >= 4 else 0
self.rating = -1 self.rating = -1
if forwards: if forwards:
self.procession.forward(1) ret = self.procession.forward(1)
if ret == 0:
self.procession = self.phaser.current_procession() # type: ignore
self.load_puzzle() self.load_puzzle()
def action_play_voice(self): def action_play_voice(self):

View File

@@ -4,14 +4,11 @@ from textual.widgets import (
Header, Header,
Footer, Footer,
Label, Label,
Input,
Select,
Button, Button,
Markdown,
Static, Static,
ProgressBar, ProgressBar,
) )
from textual.containers import Container, Horizontal, Center from textual.containers import Container, Horizontal
from textual.containers import Container from textual.containers import Container
from textual.screen import Screen from textual.screen import Screen
import pathlib import pathlib
@@ -19,7 +16,7 @@ import pathlib
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 textual.worker import Worker, get_current_worker from textual.worker import get_current_worker
class PrecachingScreen(Screen): class PrecachingScreen(Screen):

View 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 Container, Horizontal
from textual.containers import Container
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 Container(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()

View File

@@ -1,13 +1,18 @@
# 单项选择题
from textual.widgets import ( from textual.widgets import (
Label, Label,
Button, Button,
) )
from textual.containers import (
Container
)
from textual.widget import Widget from textual.widget import Widget
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 .base_puzzle_widget import BasePuzzleWidget from .base_puzzle_widget import BasePuzzleWidget
from typing import TypedDict from typing import TypedDict
from bidict import bidict
from heurams.services.hasher import hash
class Setting(TypedDict): class Setting(TypedDict):
__origin__: str __origin__: str
@@ -42,7 +47,7 @@ class MCQPuzzle(BasePuzzleWidget):
) )
self.inputlist = [] self.inputlist = []
self.alia = alia self.alia = alia
self.hashtable = {} self.hashmap = bidict()
self._load() self._load()
def _load(self): def _load(self):
@@ -53,9 +58,7 @@ class MCQPuzzle(BasePuzzleWidget):
self.puzzle.refresh() self.puzzle.refresh()
def compose(self): def compose(self):
setting: Setting = self.atom.registry["nucleon"].metadata["orbital"]["puzzle"][ setting: Setting = self.atom.registry["nucleon"].metadata["orbital"]["puzzle"][self.alia]
self.alia
]
yield Label(setting["primary"], id="sentence") yield Label(setting["primary"], id="sentence")
yield Label(self.puzzle.wording[len(self.inputlist)], id="puzzle") yield Label(self.puzzle.wording[len(self.inputlist)], id="puzzle")
yield Label(f"当前输入: {self.inputlist}", id="inputpreview") yield Label(f"当前输入: {self.inputlist}", id="inputpreview")
@@ -63,8 +66,8 @@ class MCQPuzzle(BasePuzzleWidget):
# 渲染当前问题的选项 # 渲染当前问题的选项
current_options = self.puzzle.options[len(self.inputlist)] current_options = self.puzzle.options[len(self.inputlist)]
for i in current_options: for i in current_options:
self.hashtable[str(hash(i))] = i self.hashmap[str(hash(i))] = i
yield Button(i, id=f"select{hash(i)}") yield Button(i, id=f"select-{hash(i)}")
yield Button("退格", id="delete") yield Button("退格", id="delete")
@@ -79,12 +82,9 @@ class MCQPuzzle(BasePuzzleWidget):
if current_question_index < len(self.puzzle.wording): if current_question_index < len(self.puzzle.wording):
puzzle_label.update(self.puzzle.wording[current_question_index]) # type: ignore puzzle_label.update(self.puzzle.wording[current_question_index]) # type: ignore
# 发送输入变化消息
# 如果还有下一题,发送题目切换消息
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:
"""处理按钮点击事件""" """处理按钮点击事件"""
event.stop()
button_id = event.button.id button_id = event.button.id
if button_id == "delete": if button_id == "delete":
@@ -93,9 +93,10 @@ class MCQPuzzle(BasePuzzleWidget):
self.inputlist.pop() self.inputlist.pop()
self.refresh_buttons() self.refresh_buttons()
self.update_display() self.update_display()
elif button_id.startswith("select"): # type: ignore elif button_id.startswith("select"): # type: ignore
# 选项选择处理 # 选项选择处理
answer_text = self.hashtable[button_id[6:]] # type: ignore answer_text = self.hashmap[button_id[7:]] # type: ignore
self.inputlist.append(answer_text) self.inputlist.append(answer_text)
# 检查是否完成所有题目 # 检查是否完成所有题目
@@ -103,16 +104,7 @@ class MCQPuzzle(BasePuzzleWidget):
is_correct = self.inputlist == self.puzzle.answer is_correct = self.inputlist == self.puzzle.answer
rating = 4 if is_correct else 2 rating = 4 if is_correct else 2
# 发送完成消息 self.screen.rating = rating # type: ignore
self.post_message(
self.PuzzleCompleted(
atom=self.atom,
rating=rating,
is_correct=is_correct,
user_answers=self.inputlist.copy(),
correct_answers=self.puzzle.answer.copy(),
)
)
# 重置输入(如果回答错误) # 重置输入(如果回答错误)
if not is_correct: if not is_correct:
@@ -132,6 +124,7 @@ class MCQPuzzle(BasePuzzleWidget):
for child in self.children for child in self.children
if hasattr(child, "id") and child.id and child.id.startswith("select") if hasattr(child, "id") and child.id and child.id.startswith("select")
] ]
for button in buttons_to_remove: for button in buttons_to_remove:
self.remove_child(button) # type: ignore self.remove_child(button) # type: ignore
@@ -140,8 +133,8 @@ class MCQPuzzle(BasePuzzleWidget):
if current_question_index < len(self.puzzle.options): if current_question_index < len(self.puzzle.options):
current_options = self.puzzle.options[current_question_index] current_options = self.puzzle.options[current_question_index]
for option in current_options: for option in current_options:
button_id = f"select{hash(option)}" button_id = f"select-{hash(option)}"
if button_id not in self.hashtable: if button_id not in self.hashmap:
self.hashtable[button_id] = option self.hashmap[button_id] = option
new_button = Button(option, id=button_id) new_button = Button(option, id=button_id)
self.mount(new_button) self.mount(new_button)

View File

@@ -1,58 +1,228 @@
# mcq.py # mcq.py
from .base import BasePuzzle from .base import BasePuzzle
import random import random
from typing import List, Dict, Optional, Union
class MCQPuzzle(BasePuzzle): class MCQPuzzle(BasePuzzle):
"""选择题谜题生成器""" """选择题谜题生成器
该类用于生成和管理选择题谜题, 支持多个题目同时生成,
每个题目包含问题, 正确答案和干扰项选项.
Attributes:
prefix (str): 题目前缀文本
mapping (Dict[str, str]): 问题和正确答案的映射字典
jammer (List[str]): 干扰项列表
max_riddles_num (int): 最大题目数量限制
wording (Union[str, List[str]]): 题目文本内容
answer (Union[str, List[str]]): 正确答案列表
options (List[List[str]]): 每个题目的选项列表
"""
def __init__( def __init__(
self, mapping: dict, jammer: list, max_riddles_num: int = 2, prefix: str = "" self,
): mapping: Dict[str, str],
jammer: List[str],
max_riddles_num: int = 2,
prefix: str = "",
) -> None:
"""初始化选择题谜题生成器
Args:
mapping: 问题和正确答案的映射字典, 键为问题, 值为正确答案
jammer: 干扰项列表, 用于生成错误选项
max_riddles_num: 每次生成的最大题目数量, 范围限制在1-5之间
prefix: 题目前缀文本, 会显示在每个题目之前
"""
self.prefix = prefix self.prefix = prefix
self.mapping = mapping self.mapping = mapping
self.jammer = list(set(jammer + list(mapping.values()))) self.max_riddles_num = max(1, min(max_riddles_num, 5))
# 初始化干扰项, 确保至少有4个选项
self._init_jammer(jammer)
# 初始化题目状态
self._reset_puzzle_state()
def _init_jammer(self, jammer: List[str]) -> None:
"""初始化干扰项列表
合并传入的干扰项和所有正确答案, 确保去重后至少有4个干扰项.
Args:
jammer: 传入的干扰项列表
"""
# 合并正确答案和传入的干扰项, 并去重
unique_jammers = set(jammer + list(self.mapping.values()))
self.jammer = list(unique_jammers)
# 确保至少有4个干扰项
while len(self.jammer) < 4: while len(self.jammer) < 4:
self.jammer.append(" ") self.jammer.append(" ")
self.max_riddles_num = max(1, min(max_riddles_num, 5))
self.wording = "选择题 - 尚未刷新谜题"
self.answer = ["选择题 - 尚未刷新谜题"]
self.options = []
def refresh(self): def _reset_puzzle_state(self) -> None:
"""刷新谜题,根据题目数量生成适当数量的谜题""" """重置谜题状态为初始值
将题目文本, 答案和选项重置为默认状态.
"""
self.wording: Union[str, List[str]] = "选择题 - 尚未刷新谜题"
self.answer: Union[str, List[str]] = ["选择题 - 尚未刷新谜题"]
self.options: List[List[str]] = []
def refresh(self) -> None:
"""刷新谜题, 生成指定数量的选择题
从mapping中随机选择指定数量的问题, 为每个问题生成包含正确答案
和干扰项的选项列表, 并更新谜题状态.
Raises:
ValueError: 当mapping为空时不会抛出异常, 但会设置空谜题状态
"""
if not self.mapping: if not self.mapping:
self.wording = "无可用题目" self._set_empty_puzzle()
self.answer = ["无答案"]
self.options = []
return return
num_questions = min(self.max_riddles_num, len(self.mapping)) num_questions = min(self.max_riddles_num, len(self.mapping))
questions = random.sample(list(self.mapping.items()), num_questions) selected_questions = random.sample(list(self.mapping.items()), num_questions)
puzzles = []
answers = []
all_options = []
for question, correct_answer in questions: puzzles: List[str] = []
options = [correct_answer] answers: List[str] = []
available_jammers = [j for j in self.jammer if j != correct_answer] all_options: List[List[str]] = []
if len(available_jammers) >= 3:
selected_jammers = random.sample(available_jammers, 3) for question, correct_answer in selected_questions:
else: options = self._generate_options(correct_answer)
selected_jammers = random.choices(available_jammers, k=3)
options.extend(selected_jammers)
random.shuffle(options)
puzzles.append(question) puzzles.append(question)
answers.append(correct_answer) answers.append(correct_answer)
all_options.append(options) all_options.append(options)
question_texts = [] self.wording = self._format_questions(puzzles)
for i, puzzle in enumerate(puzzles):
question_texts.append(f"{self.prefix}:\n {i+1}. {puzzle}")
self.wording = question_texts
self.answer = answers self.answer = answers
self.options = all_options self.options = all_options
def __str__(self): def _set_empty_puzzle(self) -> None:
return f"{self.wording}\n正确答案: {', '.join(self.answer)}" """设置为空谜题状态
当没有可用的题目时, 设置相应的提示信息.
"""
self.wording = "无可用题目"
self.answer = ["无答案"]
self.options = []
def _generate_options(self, correct_answer: str) -> List[str]:
"""为单个问题生成选项列表(包含正确答案和干扰项)
Args:
correct_answer: 当前问题的正确答案
Returns:
包含4个选项的列表, 其中一个是正确答案, 三个是干扰项
Note:
如果可用干扰项不足3个, 会使用重复的干扰项填充
"""
options = [correct_answer]
# 获取可用的干扰项(排除正确答案)
available_jammers = [
jammer for jammer in self.jammer if jammer != correct_answer
]
# 选择3个干扰项
if len(available_jammers) >= 3:
selected_jammers = random.sample(available_jammers, 3)
else:
selected_jammers = random.choices(available_jammers, k=3)
options.extend(selected_jammers)
random.shuffle(options)
return options
def _format_questions(self, puzzles: List[str]) -> List[str]:
"""格式化问题列表为可读的文本
Args:
puzzles: 原始问题文本列表
Returns:
格式化后的问题文本列表, 包含编号和前缀
Example:
输入: ["问题1", "问题2"]
输出: ["前缀:\\n 1. 问题1", "前缀:\\n 2. 问题2"]
"""
if not puzzles:
return []
formatted_questions = []
for i, puzzle in enumerate(puzzles, 1):
question_text = (
f"{self.prefix}:\n {i}. {puzzle}" if self.prefix else f"{i}. {puzzle}"
)
formatted_questions.append(question_text)
return formatted_questions
def __str__(self) -> str:
"""返回谜题的字符串表示
Returns:
包含所有问题和正确答案的格式化字符串
Example:
选择题 - 尚未刷新谜题
正确答案: 选择题 - 尚未刷新谜题
"""
if isinstance(self.wording, list):
question_text = "\n".join(self.wording)
else:
question_text = self.wording
if isinstance(self.answer, list):
answer_text = ", ".join(self.answer)
else:
answer_text = str(self.answer)
return f"{question_text}\n正确答案: {answer_text}"
def get_question_count(self) -> int:
"""获取当前生成的题目数量
Returns:
当前题目的数量, 如果尚未刷新则返回 0
"""
if isinstance(self.wording, list):
return len(self.wording)
elif self.wording == "选择题 - 尚未刷新谜题" or self.wording == "无可用题目":
return 0
else:
return 1
def get_correct_answer_for_question(self, question_index: int) -> Optional[str]:
"""获取指定题目的正确答案
Args:
question_index: 题目索引(从0开始)
Returns:
指定题目的正确答案, 如果索引无效则返回None
"""
if not isinstance(self.answer, list):
return None
if 0 <= question_index < len(self.answer):
return self.answer[question_index]
return None
def get_options_for_question(self, question_index: int) -> Optional[List[str]]:
"""获取指定题目的选项列表
Args:
question_index: 题目索引(从0开始)
Returns:
指定题目的选项列表, 如果索引无效则返回None
"""
if 0 <= question_index < len(self.options):
return self.options[question_index]
return None

View File

@@ -25,6 +25,7 @@ class Procession:
return 1 # 成功 return 1 # 成功
except IndexError as e: except IndexError as e:
print(f"{e}") print(f"{e}")
self.state = ProcessionState.FINISHED
return 0 return 0
def append(self, atom=None): def append(self, atom=None):