From ff641088148bbd92c2a7cb54e159ec86b9b08a7f Mon Sep 17 00:00:00 2001 From: Chai Date: Thu, 20 Aug 2026 13:49:58 -0400 Subject: [PATCH 1/3] feat(chat): add chapter notes review suggestion --- .../app-expo/src/screens/BookChatScreen.tsx | 1 + .../src/screens/book-chat-suggestions.test.ts | 44 +++++++++++++++++++ .../app/src/components/chat/ChatPanel.tsx | 1 + packages/core/src/i18n/locales/en/chat.json | 3 +- packages/core/src/i18n/locales/es/chat.json | 3 +- packages/core/src/i18n/locales/fr/chat.json | 3 +- packages/core/src/i18n/locales/ja/chat.json | 3 +- packages/core/src/i18n/locales/ko/chat.json | 3 +- .../core/src/i18n/locales/zh-TW/chat.json | 3 +- packages/core/src/i18n/locales/zh/chat.json | 3 +- 10 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 packages/app-expo/src/screens/book-chat-suggestions.test.ts diff --git a/packages/app-expo/src/screens/BookChatScreen.tsx b/packages/app-expo/src/screens/BookChatScreen.tsx index 208c38953..dbece2758 100644 --- a/packages/app-expo/src/screens/BookChatScreen.tsx +++ b/packages/app-expo/src/screens/BookChatScreen.tsx @@ -345,6 +345,7 @@ export function BookChatScreen({ route, navigation }: Props) { t("chat.suggestions.summarizeChapter"), t("chat.suggestions.explainConcepts"), t("chat.suggestions.analyzeAuthor"), + t("chat.suggestions.reviewChapterNotes"), ], [t], ); diff --git a/packages/app-expo/src/screens/book-chat-suggestions.test.ts b/packages/app-expo/src/screens/book-chat-suggestions.test.ts new file mode 100644 index 000000000..0cdd8a5af --- /dev/null +++ b/packages/app-expo/src/screens/book-chat-suggestions.test.ts @@ -0,0 +1,44 @@ +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const screensDir = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(screensDir, "../../../.."); +const suggestionKey = 't("chat.suggestions.reviewChapterNotes")'; + +const clients = [ + resolve(repoRoot, "packages/app/src/components/chat/ChatPanel.tsx"), + resolve(screensDir, "BookChatScreen.tsx"), +] as const; + +const localizedCopy = { + en: "Review my notes for this chapter", + zh: "点评我对这一章的笔记", + "zh-TW": "點評我對這一章的筆記", + ja: "この章のメモをレビュー", + ko: "이 챕터에 대한 내 노트 검토", + fr: "Relire mes notes sur ce chapitre", + es: "Revisar mis notas de este capítulo", +} as const; + +describe("book chat suggestions", () => { + for (const client of clients) { + it(`${client} offers chapter note review`, () => { + expect(readFileSync(client, "utf8")).toContain(suggestionKey); + }); + } + + for (const [locale, copy] of Object.entries(localizedCopy)) { + it(`${locale} provides the chapter note review copy`, () => { + const messages = JSON.parse( + readFileSync( + resolve(repoRoot, `packages/core/src/i18n/locales/${locale}/chat.json`), + "utf8", + ), + ) as { chat: { suggestions: { reviewChapterNotes?: string } } }; + + expect(messages.chat.suggestions.reviewChapterNotes).toBe(copy); + }); + } +}); diff --git a/packages/app/src/components/chat/ChatPanel.tsx b/packages/app/src/components/chat/ChatPanel.tsx index 355f0156f..a9dd8b1eb 100644 --- a/packages/app/src/components/chat/ChatPanel.tsx +++ b/packages/app/src/components/chat/ChatPanel.tsx @@ -246,6 +246,7 @@ export function ChatPanel({ book, onNavigateToCitation }: ChatPanelProps) { t("chat.suggestions.summarizeChapter"), t("chat.suggestions.explainConcepts"), t("chat.suggestions.analyzeAuthor"), + t("chat.suggestions.reviewChapterNotes"), ]; return ( diff --git a/packages/core/src/i18n/locales/en/chat.json b/packages/core/src/i18n/locales/en/chat.json index fc1a91d71..9e12e0373 100644 --- a/packages/core/src/i18n/locales/en/chat.json +++ b/packages/core/src/i18n/locales/en/chat.json @@ -26,7 +26,8 @@ "generateNotes": "Generate study notes", "summarizeChapter": "Summarize this chapter", "explainConcepts": "Explain the key concepts", - "analyzeAuthor": "Analyze the author's argument" + "analyzeAuthor": "Analyze the author's argument", + "reviewChapterNotes": "Review my notes for this chapter" }, "aiAssistant": "AI Reading Assistant", "aiAssistantDesc": "Analyze content, answer questions, and help you understand your books better.", diff --git a/packages/core/src/i18n/locales/es/chat.json b/packages/core/src/i18n/locales/es/chat.json index fcfb5587a..e5d1dcc88 100644 --- a/packages/core/src/i18n/locales/es/chat.json +++ b/packages/core/src/i18n/locales/es/chat.json @@ -25,7 +25,8 @@ "generateNotes": "Generar notas de estudio", "summarizeChapter": "Resumir este capítulo", "explainConcepts": "Explicar los conceptos clave", - "analyzeAuthor": "Analizar el argumento del autor" + "analyzeAuthor": "Analizar el argumento del autor", + "reviewChapterNotes": "Revisar mis notas de este capítulo" }, "aiAssistant": "Asistente de lectura IA", "aiAssistantDesc": "Analiza contenido, responde preguntas y te ayuda a entender mejor tus libros.", diff --git a/packages/core/src/i18n/locales/fr/chat.json b/packages/core/src/i18n/locales/fr/chat.json index e8ed5b24e..0f2eec3f2 100644 --- a/packages/core/src/i18n/locales/fr/chat.json +++ b/packages/core/src/i18n/locales/fr/chat.json @@ -25,7 +25,8 @@ "generateNotes": "Générer des notes d'étude", "summarizeChapter": "Résumer ce chapitre", "explainConcepts": "Expliquer les concepts clés", - "analyzeAuthor": "Analyser l'argumentation de l'auteur" + "analyzeAuthor": "Analyser l'argumentation de l'auteur", + "reviewChapterNotes": "Relire mes notes sur ce chapitre" }, "aiAssistant": "Assistant de lecture IA", "aiAssistantDesc": "Analysez le contenu, répondez aux questions et comprenez mieux vos livres.", diff --git a/packages/core/src/i18n/locales/ja/chat.json b/packages/core/src/i18n/locales/ja/chat.json index 243d0f4c7..a55af14cb 100644 --- a/packages/core/src/i18n/locales/ja/chat.json +++ b/packages/core/src/i18n/locales/ja/chat.json @@ -25,7 +25,8 @@ "generateNotes": "学習ノートを生成", "summarizeChapter": "この章を要約", "explainConcepts": "主要な概念を説明", - "analyzeAuthor": "著者の主張を分析" + "analyzeAuthor": "著者の主張を分析", + "reviewChapterNotes": "この章のメモをレビュー" }, "aiAssistant": "AI読書アシスタント", "aiAssistantDesc": "コンテンツを分析し、質問に答え、本の理解を深めるお手伝いをします。", diff --git a/packages/core/src/i18n/locales/ko/chat.json b/packages/core/src/i18n/locales/ko/chat.json index 9087df1ca..3025a6910 100644 --- a/packages/core/src/i18n/locales/ko/chat.json +++ b/packages/core/src/i18n/locales/ko/chat.json @@ -25,7 +25,8 @@ "generateNotes": "학습 노트 생성", "summarizeChapter": "이 챕터 요약", "explainConcepts": "핵심 개념 설명", - "analyzeAuthor": "저자의 논점 분석" + "analyzeAuthor": "저자의 논점 분석", + "reviewChapterNotes": "이 챕터에 대한 내 노트 검토" }, "aiAssistant": "AI 독서 어시스턴트", "aiAssistantDesc": "내용을 분석하고, 질문에 답하며, 책을 더 잘 이해하도록 도와줘요.", diff --git a/packages/core/src/i18n/locales/zh-TW/chat.json b/packages/core/src/i18n/locales/zh-TW/chat.json index d910492d4..fa11ebe6d 100644 --- a/packages/core/src/i18n/locales/zh-TW/chat.json +++ b/packages/core/src/i18n/locales/zh-TW/chat.json @@ -25,7 +25,8 @@ "generateNotes": "產生學習筆記", "summarizeChapter": "總結這一章", "explainConcepts": "解釋關鍵概念", - "analyzeAuthor": "分析作者的論點" + "analyzeAuthor": "分析作者的論點", + "reviewChapterNotes": "點評我對這一章的筆記" }, "aiAssistant": "AI 閱讀助手", "aiAssistantDesc": "分析內容、回答問題,幫助你更好地理解書籍。", diff --git a/packages/core/src/i18n/locales/zh/chat.json b/packages/core/src/i18n/locales/zh/chat.json index 8d3c3fdac..72661582c 100644 --- a/packages/core/src/i18n/locales/zh/chat.json +++ b/packages/core/src/i18n/locales/zh/chat.json @@ -26,7 +26,8 @@ "generateNotes": "生成学习笔记", "summarizeChapter": "总结这一章", "explainConcepts": "解释关键概念", - "analyzeAuthor": "分析作者的论点" + "analyzeAuthor": "分析作者的论点", + "reviewChapterNotes": "点评我对这一章的笔记" }, "aiAssistant": "AI 阅读助手", "aiAssistantDesc": "分析内容、回答问题,帮助你更好地理解书籍。", From bb113a4859749ef2144e3ff60d4e48d0c74ac1be Mon Sep 17 00:00:00 2001 From: Chai Date: Thu, 20 Aug 2026 13:52:21 -0400 Subject: [PATCH 2/3] fix(ai): retain notes access for chapter requests --- .../src/screens/book-chat-suggestions.test.ts | 4 +- .../ai/__tests__/reading-agent-tools.test.ts | 38 ++++++++++++++++++- packages/core/src/ai/agents/reading-agent.ts | 3 ++ .../core/src/i18n/locales/zh-TW/chat.json | 2 +- packages/core/src/i18n/locales/zh/chat.json | 2 +- 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/app-expo/src/screens/book-chat-suggestions.test.ts b/packages/app-expo/src/screens/book-chat-suggestions.test.ts index 0cdd8a5af..2f71ed31f 100644 --- a/packages/app-expo/src/screens/book-chat-suggestions.test.ts +++ b/packages/app-expo/src/screens/book-chat-suggestions.test.ts @@ -14,8 +14,8 @@ const clients = [ const localizedCopy = { en: "Review my notes for this chapter", - zh: "点评我对这一章的笔记", - "zh-TW": "點評我對這一章的筆記", + zh: "点评我对本章的笔记", + "zh-TW": "點評我對本章的筆記", ja: "この章のメモをレビュー", ko: "이 챕터에 대한 내 노트 검토", fr: "Relire mes notes sur ce chapitre", diff --git a/packages/core/src/ai/__tests__/reading-agent-tools.test.ts b/packages/core/src/ai/__tests__/reading-agent-tools.test.ts index d96d8ac1c..5feade5f8 100644 --- a/packages/core/src/ai/__tests__/reading-agent-tools.test.ts +++ b/packages/core/src/ai/__tests__/reading-agent-tools.test.ts @@ -576,7 +576,43 @@ describe("streamReadingAgent tool registration", () => { expect(fourth.attemptedQueries).toEqual(["张三疯那一章讲了什么", "张三疯", "张三疯"]); }); - it("keeps RAG fallback available for current-page questions on indexed books", async () => { + it.each(["Review my notes for this chapter", "点评我对本章的笔记"])( + "keeps note access for the chapter review suggestion: %s", + async (prompt) => { + let capturedTools: any[] = []; + createReactAgentMock.mockImplementation((config) => { + capturedTools = config.tools; + return { + streamEvents: vi.fn(() => ({ + [Symbol.asyncIterator]: async function* () { + // no-op stream + }, + })), + }; + }); + + for await (const event of streamReadingAgent( + { + aiConfig: makeAIConfig(), + book: null, + bookId: "book-1", + semanticContext: null, + enabledSkills: [], + isVectorized: true, + getAvailableTools, + }, + prompt, + )) { + void event; + } + + const toolNames = capturedTools.map((tool) => tool.name); + expect(toolNames).toContain("getAnnotations"); + expect(toolNames).toContain("getCurrentChapter"); + }, + ); + + it("keeps RAG fallback available for current-page questions on indexed books", async () => { let capturedTools: any[] = []; createReactAgentMock.mockImplementation((config) => { capturedTools = config.tools; diff --git a/packages/core/src/ai/agents/reading-agent.ts b/packages/core/src/ai/agents/reading-agent.ts index 37bf5f654..030620993 100644 --- a/packages/core/src/ai/agents/reading-agent.ts +++ b/packages/core/src/ai/agents/reading-agent.ts @@ -170,6 +170,7 @@ const CATEGORY_TOOL_ORDER: Record = { "getCurrentChapter", "getSurroundingContext", "getReadingProgress", + "getAnnotations", "resolveChapterReference", "ragSearch", "ragContext", @@ -341,6 +342,7 @@ function getFocusedToolNames( "getCurrentChapter", "getSurroundingContext", "getReadingProgress", + "getAnnotations", "resolveChapterReference", "ragSearch", "ragContext", @@ -352,6 +354,7 @@ function getFocusedToolNames( "getCurrentChapter", "getSurroundingContext", "getReadingProgress", + "getAnnotations", "fallbackChapterContext", "addCitation", ], diff --git a/packages/core/src/i18n/locales/zh-TW/chat.json b/packages/core/src/i18n/locales/zh-TW/chat.json index fa11ebe6d..da8d6b927 100644 --- a/packages/core/src/i18n/locales/zh-TW/chat.json +++ b/packages/core/src/i18n/locales/zh-TW/chat.json @@ -26,7 +26,7 @@ "summarizeChapter": "總結這一章", "explainConcepts": "解釋關鍵概念", "analyzeAuthor": "分析作者的論點", - "reviewChapterNotes": "點評我對這一章的筆記" + "reviewChapterNotes": "點評我對本章的筆記" }, "aiAssistant": "AI 閱讀助手", "aiAssistantDesc": "分析內容、回答問題,幫助你更好地理解書籍。", diff --git a/packages/core/src/i18n/locales/zh/chat.json b/packages/core/src/i18n/locales/zh/chat.json index 72661582c..bb45a65a6 100644 --- a/packages/core/src/i18n/locales/zh/chat.json +++ b/packages/core/src/i18n/locales/zh/chat.json @@ -27,7 +27,7 @@ "summarizeChapter": "总结这一章", "explainConcepts": "解释关键概念", "analyzeAuthor": "分析作者的论点", - "reviewChapterNotes": "点评我对这一章的笔记" + "reviewChapterNotes": "点评我对本章的笔记" }, "aiAssistant": "AI 阅读助手", "aiAssistantDesc": "分析内容、回答问题,帮助你更好地理解书籍。", From 6fedf3cec92189a17c5e6d89a34a741f41e793b1 Mon Sep 17 00:00:00 2001 From: Chai Date: Thu, 20 Aug 2026 13:57:14 -0400 Subject: [PATCH 3/3] style: format chapter notes routing test --- .../ai/__tests__/reading-agent-tools.test.ts | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/packages/core/src/ai/__tests__/reading-agent-tools.test.ts b/packages/core/src/ai/__tests__/reading-agent-tools.test.ts index 5feade5f8..78924bd23 100644 --- a/packages/core/src/ai/__tests__/reading-agent-tools.test.ts +++ b/packages/core/src/ai/__tests__/reading-agent-tools.test.ts @@ -576,43 +576,43 @@ describe("streamReadingAgent tool registration", () => { expect(fourth.attemptedQueries).toEqual(["张三疯那一章讲了什么", "张三疯", "张三疯"]); }); - it.each(["Review my notes for this chapter", "点评我对本章的笔记"])( - "keeps note access for the chapter review suggestion: %s", - async (prompt) => { - let capturedTools: any[] = []; - createReactAgentMock.mockImplementation((config) => { - capturedTools = config.tools; - return { - streamEvents: vi.fn(() => ({ - [Symbol.asyncIterator]: async function* () { - // no-op stream - }, - })), - }; - }); - - for await (const event of streamReadingAgent( - { - aiConfig: makeAIConfig(), - book: null, - bookId: "book-1", - semanticContext: null, - enabledSkills: [], - isVectorized: true, - getAvailableTools, - }, - prompt, - )) { - void event; - } - - const toolNames = capturedTools.map((tool) => tool.name); - expect(toolNames).toContain("getAnnotations"); - expect(toolNames).toContain("getCurrentChapter"); - }, - ); - - it("keeps RAG fallback available for current-page questions on indexed books", async () => { + it.each(["Review my notes for this chapter", "点评我对本章的笔记"])( + "keeps note access for the chapter review suggestion: %s", + async (prompt) => { + let capturedTools: Array<{ name: string }> = []; + createReactAgentMock.mockImplementation((config) => { + capturedTools = config.tools; + return { + streamEvents: vi.fn(() => ({ + [Symbol.asyncIterator]: async function* () { + // no-op stream + }, + })), + }; + }); + + for await (const event of streamReadingAgent( + { + aiConfig: makeAIConfig(), + book: null, + bookId: "book-1", + semanticContext: null, + enabledSkills: [], + isVectorized: true, + getAvailableTools, + }, + prompt, + )) { + void event; + } + + const toolNames = capturedTools.map((tool) => tool.name); + expect(toolNames).toContain("getAnnotations"); + expect(toolNames).toContain("getCurrentChapter"); + }, + ); + + it("keeps RAG fallback available for current-page questions on indexed books", async () => { let capturedTools: any[] = []; createReactAgentMock.mockImplementation((config) => { capturedTools = config.tools;