From bd605a40913dfa3295aee0bfef834e3150e0e23f Mon Sep 17 00:00:00 2001 From: XZH Date: Sat, 22 Aug 2026 07:45:09 -0700 Subject: [PATCH] fix(kids): repair current-page learning CI --- deeptutor/api/routers/kids_admin.py | 13 +- .../immersive_reading/kids_word_hints.py | 20 +- deeptutor/services/path_service.py | 3 + scripts/build_kids_dictionary.py | 12 +- .../test_kids_interactive_books.py | 57 +++-- .../immersive_reading/test_kids_word_hints.py | 3 +- web/package-lock.json | 203 +++++++++++++++++- web/package.json | 1 + 8 files changed, 272 insertions(+), 40 deletions(-) diff --git a/deeptutor/api/routers/kids_admin.py b/deeptutor/api/routers/kids_admin.py index 1fbb65142a..7f69d5d0e6 100644 --- a/deeptutor/api/routers/kids_admin.py +++ b/deeptutor/api/routers/kids_admin.py @@ -215,7 +215,9 @@ async def list_available_interactive_books() -> dict: "id": book.id, "title": book.title or b_id, "description": book.description, - "status": book.status.value if hasattr(book.status, "value") else str(book.status), + "status": book.status.value + if hasattr(book.status, "value") + else str(book.status), "page_count": book.page_count, "chapter_count": book.chapter_count, "language": book.language, @@ -229,7 +231,10 @@ async def list_available_interactive_books() -> dict: b["assigned_profile_ids"] = [ a.profile_id for a in assignments - if (a.book_id == b["id"] or (a.content_type == "interactive_book" and a.document_id == b["id"])) + if ( + a.book_id == b["id"] + or (a.content_type == "interactive_book" and a.document_id == b["id"]) + ) and a.status == "active" ] @@ -243,9 +248,7 @@ class AssignInteractiveBookRequest(BaseModel): @router.post("/profiles/{profile_id}/interactive-books") -async def assign_interactive_book( - profile_id: str, request: AssignInteractiveBookRequest -) -> dict: +async def assign_interactive_book(profile_id: str, request: AssignInteractiveBookRequest) -> dict: """Assign an interactive book from BookEngine to a child profile.""" manager = get_kids_manager() if manager.get_profile(profile_id) is None: diff --git a/deeptutor/immersive_reading/kids_word_hints.py b/deeptutor/immersive_reading/kids_word_hints.py index fc3ec2f3db..f0611ee8cd 100644 --- a/deeptutor/immersive_reading/kids_word_hints.py +++ b/deeptutor/immersive_reading/kids_word_hints.py @@ -215,10 +215,12 @@ def _generate_thinking_clue(word: str, definition: str) -> str: if normalized_definition.startswith(("to ", "to\t")): return f"Think about an action! Can you guess what someone does when they {word}?" if normalized_definition.startswith(("a ", "an ", "the ", "something ")): - return f"Picture what this could be in the world! Can you guess what \"{word}\" is?" - if normalized_definition.startswith(("very ", "not ", "feeling ", "having ", "showing ", "full of ")): + return f'Picture what this could be in the world! Can you guess what "{word}" is?' + if normalized_definition.startswith( + ("very ", "not ", "feeling ", "having ", "showing ", "full of ") + ): return "Think about describing something! Can you guess what quality this word shows?" - return f"Look closely at the story clues! Can you guess what \"{word}\" means here?" + return f'Look closely at the story clues! Can you guess what "{word}" means here?' @dataclass(frozen=True) @@ -248,7 +250,11 @@ def _concise_chinese(value: str) -> str: if not value: return "" normalized = value.replace("\\n", "\n").replace("\r", "") - lines = [line.strip() for line in normalized.splitlines() if line.strip() and not line.strip().startswith("[")] + lines = [ + line.strip() + for line in normalized.splitlines() + if line.strip() and not line.strip().startswith("[") + ] if not lines: return "" preferred = lines[0] @@ -257,7 +263,11 @@ def _concise_chinese(value: str) -> str: preferred = line break cleaned = _POS_PREFIX_RE.sub("", preferred).strip() - parts = [p.strip() for p in re.split(r"[;,,;]", cleaned) if p.strip() and not p.strip().startswith(("(", "("))] + parts = [ + p.strip() + for p in re.split(r"[;,,;]", cleaned) + if p.strip() and not p.strip().startswith(("(", "(")) + ] if parts: return ",".join(parts[:3]) return cleaned diff --git a/deeptutor/services/path_service.py b/deeptutor/services/path_service.py index 65bf592848..9066e763d8 100644 --- a/deeptutor/services/path_service.py +++ b/deeptutor/services/path_service.py @@ -368,6 +368,9 @@ def get_book_pages_dir(self, book_id: str) -> Path: def get_book_page_file(self, book_id: str, page_id: str) -> Path: return self.get_book_pages_dir(book_id) / f"{page_id}.json" + def get_book_learning_captures_file(self, book_id: str) -> Path: + return self.get_book_root(book_id) / "learning_captures.json" + def get_book_assets_dir(self, book_id: str) -> Path: return self.get_book_root(book_id) / "assets" diff --git a/scripts/build_kids_dictionary.py b/scripts/build_kids_dictionary.py index 621738f49b..b0d9300a07 100644 --- a/scripts/build_kids_dictionary.py +++ b/scripts/build_kids_dictionary.py @@ -173,8 +173,7 @@ def write_database( """ ) connection.executemany( - "INSERT INTO entries(word, definition, part_of_speech, phonetic) " - "VALUES (?, ?, ?, ?)", + "INSERT INTO entries(word, definition, part_of_speech, phonetic) VALUES (?, ?, ?, ?)", ( (item.word, item.definition, item.part_of_speech, item.phonetic) for item in definitions @@ -231,9 +230,7 @@ def main(argv: Sequence[str] | None = None) -> int: parser.add_argument( "--output", type=Path, - default=Path( - "data/user/workspace/immersive_reading/dictionaries/kids_simple.db" - ), + default=Path("data/user/workspace/immersive_reading/dictionaries/kids_simple.db"), ) parser.add_argument( "--dump", @@ -257,10 +254,7 @@ def main(argv: Sequence[str] | None = None) -> int: if missing: preview = ", ".join(missing[:30]) suffix = "" if len(missing) <= 30 else ", ..." - print( - f"Skipped {len(missing)} YLE words without a usable definition: " - f"{preview}{suffix}" - ) + print(f"Skipped {len(missing)} YLE words without a usable definition: {preview}{suffix}") return 0 diff --git a/tests/immersive_reading/test_kids_interactive_books.py b/tests/immersive_reading/test_kids_interactive_books.py index a114622a49..2978e9187e 100644 --- a/tests/immersive_reading/test_kids_interactive_books.py +++ b/tests/immersive_reading/test_kids_interactive_books.py @@ -1,12 +1,14 @@ """Unit and integration tests for Kids Interactive Books (Math and Digital Books).""" import json - import time -import pytest + from fastapi import FastAPI from fastapi.testclient import TestClient +import pytest +import deeptutor.api.routers.kids as kids_router_module +import deeptutor.api.routers.kids_admin as kids_admin_router_module from deeptutor.book.models import ( Block, BlockType, @@ -24,8 +26,6 @@ KidsProfile, ) from deeptutor.immersive_reading.service import get_kids_manager -import deeptutor.api.routers.kids as kids_router_module -import deeptutor.api.routers.kids_admin as kids_admin_router_module @pytest.fixture @@ -51,8 +51,18 @@ def mock_interactive_book(tmp_path, monkeypatch) -> Book: book_dir = tmp_path / "book_workspace" book_dir.mkdir(parents=True, exist_ok=True) monkeypatch.setattr(storage.path_service, "get_book_dir", lambda: book_dir) - monkeypatch.setattr(storage.path_service, "get_book_root", lambda b_id: book_dir / f"book_{b_id}") - monkeypatch.setattr(storage.path_service, "ensure_book_root", lambda b_id: (book_dir / f"book_{b_id}") if (book_dir / f"book_{b_id}").mkdir(parents=True, exist_ok=True) is None else (book_dir / f"book_{b_id}")) + monkeypatch.setattr( + storage.path_service, "get_book_root", lambda b_id: book_dir / f"book_{b_id}" + ) + monkeypatch.setattr( + storage.path_service, + "ensure_book_root", + lambda b_id: ( + (book_dir / f"book_{b_id}") + if (book_dir / f"book_{b_id}").mkdir(parents=True, exist_ok=True) is None + else (book_dir / f"book_{b_id}") + ), + ) book_id = "math_fun_01" book = Book( @@ -111,7 +121,10 @@ def mock_interactive_book(tmp_path, monkeypatch) -> Book: id="blk_anim_1", type=BlockType.ANIMATION, title="图形变变变", - payload={"video_url": f"book_{book_id}/assets/triangles.mp4", "caption": "看三角形怎么拼成正方形"}, + payload={ + "video_url": f"book_{book_id}/assets/triangles.mp4", + "caption": "看三角形怎么拼成正方形", + }, ), Block( id="blk_quiz_1", @@ -162,7 +175,9 @@ def kids_client(clean_kids_manager, sample_profile, mock_interactive_book) -> Te return TestClient(app) -def test_kids_manager_assign_and_progress(clean_kids_manager, sample_profile, mock_interactive_book): +def test_kids_manager_assign_and_progress( + clean_kids_manager, sample_profile, mock_interactive_book +): """Verify assignment and progress tracking on KidsManager layer.""" assignment = clean_kids_manager.assign_interactive_book( sample_profile.id, @@ -230,7 +245,9 @@ def test_kids_manager_assign_and_progress(clean_kids_manager, sample_profile, mo assert prog.total_stars == 3 -def test_child_interactive_book_endpoints(kids_client, sample_profile, clean_kids_manager, mock_interactive_book): +def test_child_interactive_book_endpoints( + kids_client, sample_profile, clean_kids_manager, mock_interactive_book +): """Verify child endpoints: book manifest, sanitized page, quiz grading.""" # 1. Assign book to profile clean_kids_manager.assign_interactive_book( @@ -240,7 +257,9 @@ def test_child_interactive_book_endpoints(kids_client, sample_profile, clean_kid ) # 2. Get device token for child - auth_resp = kids_client.post("/api/v1/kids/select-profile", json={"profile_id": sample_profile.id}) + auth_resp = kids_client.post( + "/api/v1/kids/select-profile", json={"profile_id": sample_profile.id} + ) assert auth_resp.status_code == 200 token = auth_resp.json()["token"] headers = {"Authorization": f"Bearer {token}"} @@ -254,13 +273,17 @@ def test_child_interactive_book_endpoints(kids_client, sample_profile, clean_kid assert library[0]["book"]["title"] == "趣味数学:数与图形" # 4. Get interactive book detail - book_resp = kids_client.get(f"/api/v1/kids/interactive-books/{mock_interactive_book.id}", headers=headers) + book_resp = kids_client.get( + f"/api/v1/kids/interactive-books/{mock_interactive_book.id}", headers=headers + ) assert book_resp.status_code == 200 assert book_resp.json()["book"]["title"] == "趣味数学:数与图形" assert len(book_resp.json()["spine"]["chapters"]) == 1 # 5. Get page content — answers MUST be stripped from quiz block for child safety - page_resp = kids_client.get(f"/api/v1/kids/interactive-books/{mock_interactive_book.id}/pages/pg_01", headers=headers) + page_resp = kids_client.get( + f"/api/v1/kids/interactive-books/{mock_interactive_book.id}/pages/pg_01", headers=headers + ) assert page_resp.status_code == 200 page_data = page_resp.json()["page"] quiz_blk = next(b for b in page_data["blocks"] if b["type"] == "quiz") @@ -290,7 +313,9 @@ def test_child_interactive_book_endpoints(kids_client, sample_profile, clean_kid assert "三角形有3条边" in grade["per_question"][0]["explanation"] -def test_parent_admin_interactive_books(kids_client, sample_profile, clean_kids_manager, mock_interactive_book): +def test_parent_admin_interactive_books( + kids_client, sample_profile, clean_kids_manager, mock_interactive_book +): """Verify parent management endpoints for listing and assigning interactive books.""" # List available books in BookEngine list_resp = kids_client.get("/api/v1/kids-admin/available-books") @@ -302,7 +327,11 @@ def test_parent_admin_interactive_books(kids_client, sample_profile, clean_kids_ # Assign to profile assign_resp = kids_client.post( f"/api/v1/kids-admin/profiles/{sample_profile.id}/interactive-books", - json={"book_id": mock_interactive_book.id, "title": "自定义数学书名", "available_through_page_order": 0}, + json={ + "book_id": mock_interactive_book.id, + "title": "自定义数学书名", + "available_through_page_order": 0, + }, ) assert assign_resp.status_code == 200 assignment = assign_resp.json()["assignment"] diff --git a/tests/immersive_reading/test_kids_word_hints.py b/tests/immersive_reading/test_kids_word_hints.py index 093e362743..f4c674020f 100644 --- a/tests/immersive_reading/test_kids_word_hints.py +++ b/tests/immersive_reading/test_kids_word_hints.py @@ -68,8 +68,7 @@ def test_simple_wiktionary_parser_extracts_child_readable_definition(): assert parsed is not None assert parsed.definition == ( - "A picture is marks on a flat object, like paper, that show shapes, " - "people, or things." + "A picture is marks on a flat object, like paper, that show shapes, people, or things." ) assert parsed.part_of_speech == "noun" assert parsed.phonetic == "/ˈpɪktʃɚ/" diff --git a/web/package-lock.json b/web/package-lock.json index f465057d2d..4b4906bd04 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -26,6 +26,7 @@ "react-dom": "^19.0.0", "react-i18next": "^16.5.3", "react-markdown": "^10.1.0", + "react-reader": "^2.0.15", "react-syntax-highlighter": "^16.1.1", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", @@ -1643,7 +1644,7 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright": "1.57.0" @@ -1994,6 +1995,16 @@ "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", "license": "MIT" }, + "node_modules/@types/localforage": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.34.tgz", + "integrity": "sha512-tJxahnjm9dEI1X+hQSC5f2BSd/coZaqbIl1m3TCl0q9SVuC52XcXfV0XmoCU1+PmjyucuVITwoTnN8OlTbEXXA==", + "deprecated": "This is a stub types definition for localforage (https://github.com/localForage/localForage). localforage provides its own type definitions, so you don't need @types/localforage installed!", + "license": "MIT", + "dependencies": { + "localforage": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -2042,6 +2053,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2634,6 +2646,16 @@ "d3-transition": "^3.0.1" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", + "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", + "deprecated": "this version has critical issues, please update to the latest version", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -3642,7 +3664,6 @@ "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", "hasInstallScript": true, "license": "MIT", - "optional": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -3729,6 +3750,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, "node_modules/cytoscape": { @@ -3779,6 +3801,19 @@ "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", "license": "MIT" }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/d3": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", @@ -4551,6 +4586,23 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/epubjs": { + "version": "0.3.93", + "resolved": "https://registry.npmjs.org/epubjs/-/epubjs-0.3.93.tgz", + "integrity": "sha512-c06pNSdBxcXv3dZSbXAVLE1/pmleRhOT6mXNZo6INKmvuKpYB65MwU/lO7830czCtjIiK9i+KR+3S+p0wtljrw==", + "license": "BSD-2-Clause", + "dependencies": { + "@types/localforage": "0.0.34", + "@xmldom/xmldom": "^0.7.5", + "core-js": "^3.18.3", + "event-emitter": "^0.3.5", + "jszip": "^3.7.1", + "localforage": "^1.10.0", + "lodash": "^4.17.21", + "marks-pane": "^1.0.9", + "path-webpack": "0.0.3" + } + }, "node_modules/es-abstract": { "version": "1.24.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", @@ -4728,6 +4780,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5111,6 +5203,21 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -5185,6 +5292,16 @@ "node": ">=0.10.0" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/exceljs": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", @@ -5215,6 +5332,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -7117,6 +7243,24 @@ "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", "license": "ISC" }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/localforage/node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -7133,6 +7277,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash-es": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", @@ -7303,6 +7453,12 @@ "node": ">= 20" } }, + "node_modules/marks-pane": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/marks-pane/-/marks-pane-1.0.9.tgz", + "integrity": "sha512-Ahs4oeG90tbdPWwAJkAAoHg2lRR8lAs9mZXETNPO9hYg3AkjUJBKi1NQ4aaIQZVGrig7c/3NUV1jANl8rFTeMg==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8421,6 +8577,12 @@ } } }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -8779,6 +8941,12 @@ "dev": true, "license": "MIT" }, + "node_modules/path-webpack": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/path-webpack/-/path-webpack-0.0.3.tgz", + "integrity": "sha512-AmeDxedoo5svf7aB3FYqSAKqMxys014lVKBzy1o/5vv9CtU7U4wgGWL1dA2o6MOzcD53ScN4Jmiq6VbtLz1vIQ==", + "license": "MIT" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -8858,7 +9026,7 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.57.0" @@ -8877,7 +9045,7 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -9255,6 +9423,25 @@ "react": ">=18" } }, + "node_modules/react-reader": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/react-reader/-/react-reader-2.0.15.tgz", + "integrity": "sha512-xc+Mm1GXUxRG1Aa8E2uc1Vku01mwgF+GrDYif0C/J/D0QCWgDqvQx6UDMwjaR5x8Z9gIej9lAs72Tklg7wLBGg==", + "license": "ISC", + "dependencies": { + "epubjs": "^0.3.93", + "react-swipeable": "^7.0.2" + } + }, + "node_modules/react-swipeable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.2.tgz", + "integrity": "sha512-v1Qx1l+aC2fdxKa9aKJiaU/ZxmJ5o98RMoFwUqAAzVWUcxgfHFXDDruCKXhw6zIYXm6V64JiHgP9f6mlME5l8w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/react-syntax-highlighter": { "version": "16.1.1", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", @@ -10609,6 +10796,12 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -10704,7 +10897,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/web/package.json b/web/package.json index 3939e3bf7e..b91955f700 100644 --- a/web/package.json +++ b/web/package.json @@ -40,6 +40,7 @@ "react-dom": "^19.0.0", "react-i18next": "^16.5.3", "react-markdown": "^10.1.0", + "react-reader": "^2.0.15", "react-syntax-highlighter": "^16.1.1", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0",