├── .gitignore ├── CHANGELOG.md ├── README.md ├── README.zh.md ├── assets └── icon.png ├── locales ├── en │ └── messages.json └── zh │ └── messages.json ├── package.json ├── postcss.config.js ├── src ├── background.ts ├── components │ ├── Reader.tsx │ ├── ReaderContent.tsx │ ├── agent │ │ ├── InputArea.tsx │ │ ├── LoginPrompt.tsx │ │ ├── MessageBubble.tsx │ │ ├── MessageList.tsx │ │ ├── ThinkingIndicator.tsx │ │ ├── index.tsx │ │ └── types.ts │ ├── reader │ │ ├── ReaderDivider.tsx │ │ ├── ReaderToolbar.tsx │ │ ├── SelectionToolbar.tsx │ │ └── index.ts │ └── settings │ │ ├── Settings.tsx │ │ └── sections │ │ ├── AlignmentSection.tsx │ │ ├── FontFamilySection.tsx │ │ ├── FontSizeSection.tsx │ │ ├── SpacingSection.tsx │ │ ├── ThemeSection.tsx │ │ └── WidthSection.tsx ├── config │ ├── theme.ts │ └── ui.ts ├── content.tsx ├── context │ ├── I18nContext.tsx │ ├── ReaderContext.tsx │ └── ThemeContext.tsx ├── hooks │ ├── useArticle.ts │ ├── useStoredSettings.ts │ └── useTextSelection.ts ├── styles │ ├── global.css │ └── tailwind.output.css ├── types │ ├── api.ts │ ├── chat.ts │ └── global.d.ts └── utils │ ├── SessionManager.ts │ ├── auth.ts │ ├── export.ts │ ├── i18n.ts │ ├── language.ts │ ├── llm.ts │ ├── llmClient.ts │ ├── logger.ts │ ├── parser.ts │ └── themeManager.ts ├── tailwind.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # 依赖目录 2 | node_modules/ 3 | pnpm-lock.yaml 4 | package-lock.json 5 | yarn.lock 6 | 7 | # 构建输出 8 | dist/ 9 | build/ 10 | 11 | # Plasmo 特有目录 12 | .plasmo/ 13 | out/ 14 | 15 | # 开发环境 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | # 编译缓存 23 | .turbo/ 24 | .cache/ 25 | *.tsbuildinfo 26 | 27 | # 日志 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | pnpm-debug.log* 32 | 33 | # 编辑器目录 34 | .idea/ 35 | .vscode/ 36 | .vim/ 37 | *.sublime-workspace 38 | *.sublime-project 39 | 40 | # 系统文件 41 | .DS_Store 42 | Thumbs.db 43 | ehthumbs.db 44 | Desktop.ini 45 | 46 | # 临时文件 47 | *.swp 48 | *.swo 49 | *~ 50 | *.tmp 51 | *.bak 52 | .cursor/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.0 (2025-03-26) 4 | 5 | ### Features 6 | - Initial stable release of ReadLite 7 | - Reader mode for comfortable web reading 8 | - Support for multiple languages 9 | - Customizable reading experience (font, width, theme) 10 | - Language detection for optimal text display 11 | - Browser extension integration 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReadLite - Simple Reading Mode 2 | 3 | A browser extension that provides a clean, distraction-free reading experience with AI summarization capabilities. 4 | 5 | ![ReadLite Screenshot](assets/screenshot.png) 6 | 7 | ## Features 8 | 9 | - **Clean Reader Interface**: Transform cluttered web pages into a beautiful, distraction-free reading experience 10 | - **AI Article Summarization**: Get instant summaries and insights about what you're reading 11 | - **Multiple Themes**: Choose from Light, Dark, Sepia, and Paper themes to suit your preference 12 | - **Adjustable Typography**: Customize font size, line spacing, and width for optimal reading comfort 13 | - **Article Saving**: Save articles as markdown for offline reading 14 | 15 | ## Usage 16 | 17 | 1. Install the extension from the Chrome Web Store (coming soon) 18 | 2. Navigate to any article or blog post 19 | 3. Click the ReadLite icon in your browser toolbar 20 | 4. Enjoy a clean reading experience 21 | 5. Use the AI button to get summaries and ask questions about the article 22 | 23 | ## Development 24 | 25 | ### Prerequisites 26 | - Node.js (v16+) 27 | - Yarn or npm 28 | 29 | ### Setup 30 | ```bash 31 | # Clone the repository 32 | git clone https://github.com/yourusername/read-lite.git 33 | cd read-lite 34 | 35 | # Install dependencies 36 | yarn install 37 | 38 | # Start development server 39 | yarn dev 40 | ``` 41 | 42 | ### Build for production 43 | ```bash 44 | yarn build 45 | ``` 46 | 47 | ## Technical Details 48 | 49 | This extension is built with: 50 | - [Plasmo Framework](https://www.plasmo.com/) - Browser extension framework 51 | - [React](https://reactjs.org/) - UI library 52 | - [Mozilla Readability](https://github.com/mozilla/readability) - Content extraction 53 | - [Marked](https://marked.js.org/) - Markdown parsing 54 | 55 | ## License 56 | 57 | MIT 58 | 59 | ## Translation 60 | 61 | - [中文说明](./README.zh.md) 62 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # ReadLite - 简洁阅读模式 2 | 3 | 一款提供清爽无干扰阅读体验的浏览器扩展,具备 AI 文章摘要功能。 4 | 5 | ![ReadLite截图](assets/screenshot.png) 6 | 7 | ## 功能特色 8 | 9 | - **干净的阅读界面**:将杂乱的网页转换为美观、无干扰的阅读体验 10 | - **AI文章摘要**:获取即时摘要和对正在阅读内容的见解 11 | - **多种主题**:可选择亮色、暗色、棕褐色和纸张模式,满足个人偏好 12 | - **可调整排版**:自定义字体大小、行间距和页面宽度,获得最佳阅读舒适度 13 | - **文章保存**:将文章保存为markdown格式供离线阅读 14 | 15 | 16 | ## 使用方法 17 | 18 | 1. 从Chrome网上应用店安装扩展(即将推出) 19 | 2. 浏览任何文章或博客帖子 20 | 3. 点击浏览器工具栏中的ReadLite图标 21 | 4. 享受清爽的阅读体验 22 | 5. 使用AI按钮获取摘要或提问关于文章的问题 23 | 24 | ## 开发 25 | 26 | ### 前提条件 27 | - Node.js (v16+) 28 | - Yarn或npm 29 | 30 | ### 安装 31 | ```bash 32 | # 克隆仓库 33 | git clone https://github.com/yourusername/read-lite.git 34 | cd read-lite 35 | 36 | # 安装依赖 37 | yarn install 38 | 39 | # 启动开发服务器 40 | yarn dev 41 | ``` 42 | 43 | ### 生产构建 44 | ```bash 45 | yarn build 46 | ``` 47 | 48 | ## 技术细节 49 | 50 | 本扩展使用以下技术构建: 51 | - [Plasmo Framework](https://www.plasmo.com/) - 浏览器扩展框架 52 | - [React](https://reactjs.org/) - UI库 53 | - [Mozilla Readability](https://github.com/mozilla/readability) - 内容提取 54 | - [Marked](https://marked.js.org/) - Markdown解析 55 | 56 | ## 许可证 57 | 58 | MIT 59 | 60 | ## 其他语言 61 | 62 | - [English](./README.md) -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readlite/readlite-plugin/1f26f00528a94bb3314d33797c788ba4f9c4fcf0/assets/icon.png -------------------------------------------------------------------------------- /locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "ReadLite", 4 | "description": "Name of the extension." 5 | }, 6 | "extensionDescription": { 7 | "message": "Provide a clean, distraction-free reading experience for any article on the web.", 8 | "description": "Description of the extension." 9 | }, 10 | "displaySettings": { 11 | "message": "Display Settings", 12 | "description": "Title for the display settings panel." 13 | }, 14 | "readingTheme": { 15 | "message": "Theme", 16 | "description": "Label for the reading theme selection." 17 | }, 18 | "light": { 19 | "message": "Light", 20 | "description": "Light theme option." 21 | }, 22 | "dark": { 23 | "message": "Dark", 24 | "description": "Dark theme option." 25 | }, 26 | "paper": { 27 | "message": "Paper", 28 | "description": "Paper theme option." 29 | }, 30 | "eyecare": { 31 | "message": "Eyecare", 32 | "description": "Eyecare theme option." 33 | }, 34 | "highcontrast": { 35 | "message": "High Contrast", 36 | "description": "High Contrast theme option." 37 | }, 38 | "fontSize": { 39 | "message": "Font Size", 40 | "description": "Label for font size adjustment." 41 | }, 42 | "fontFamily": { 43 | "message": "Font", 44 | "description": "Label for font selection." 45 | }, 46 | "contentWidth": { 47 | "message": "Content Width", 48 | "description": "Label for page width adjustment." 49 | }, 50 | "narrow": { 51 | "message": "Narrow", 52 | "description": "Narrow width option." 53 | }, 54 | "standard": { 55 | "message": "Standard", 56 | "description": "Standard width option." 57 | }, 58 | "wide": { 59 | "message": "Wide", 60 | "description": "Wide width option." 61 | }, 62 | "lineSpacing": { 63 | "message": "Line Spacing", 64 | "description": "Label for line spacing adjustment." 65 | }, 66 | "tight": { 67 | "message": "Tight", 68 | "description": "Tight line spacing option." 69 | }, 70 | "normal": { 71 | "message": "Normal", 72 | "description": "Normal line spacing option." 73 | }, 74 | "relaxed": { 75 | "message": "Relaxed", 76 | "description": "Relaxed line spacing option." 77 | }, 78 | "system": { 79 | "message": "System", 80 | "description": "System font option." 81 | }, 82 | "currentSize": { 83 | "message": "Current Size", 84 | "description": "Label for displaying current font size." 85 | }, 86 | "currentFont": { 87 | "message": "Current Font", 88 | "description": "Label for displaying current font." 89 | }, 90 | "textAlignment": { 91 | "message": "Text Alignment", 92 | "description": "Label for text alignment options." 93 | }, 94 | "left": { 95 | "message": "Left", 96 | "description": "Left alignment option." 97 | }, 98 | "right": { 99 | "message": "Right", 100 | "description": "Right alignment option." 101 | }, 102 | "center": { 103 | "message": "Center", 104 | "description": "Center alignment option." 105 | }, 106 | "justify": { 107 | "message": "Justify", 108 | "description": "Justify alignment option." 109 | }, 110 | "recommendedForChinese": { 111 | "message": "For Chinese", 112 | "description": "Label for fonts recommended for Chinese text." 113 | }, 114 | "recommendedForEnglish": { 115 | "message": "For English", 116 | "description": "Label for fonts recommended for English text." 117 | }, 118 | "recommendedForBoth": { 119 | "message": "Universal", 120 | "description": "Label for fonts recommended for both languages." 121 | }, 122 | "allFonts": { 123 | "message": "All Fonts", 124 | "description": "Label for showing all available fonts." 125 | }, 126 | "chineseFonts": { 127 | "message": "Chinese Fonts", 128 | "description": "Label for Chinese fonts section." 129 | }, 130 | "englishFonts": { 131 | "message": "English Fonts", 132 | "description": "Label for English fonts section." 133 | }, 134 | "detected": { 135 | "message": "Detected", 136 | "description": "Label for detected language." 137 | }, 138 | "chinese": { 139 | "message": "Chinese", 140 | "description": "Chinese language name." 141 | }, 142 | "english": { 143 | "message": "English", 144 | "description": "English language name." 145 | }, 146 | "theme": { 147 | "message": "Theme", 148 | "description": "Theme section label." 149 | }, 150 | "extractingArticle": { 151 | "message": "Extracting article...", 152 | "description": "Message shown when extracting article content." 153 | }, 154 | "couldNotExtract": { 155 | "message": "Could not extract article content", 156 | "description": "Error message when article extraction fails." 157 | }, 158 | "closeReaderMode": { 159 | "message": "Close Reader Mode", 160 | "description": "Label for button to close reader mode." 161 | }, 162 | "close": { 163 | "message": "Close", 164 | "description": "Close button label." 165 | }, 166 | "download": { 167 | "message": "Download", 168 | "description": "Label for downloading article as Markdown." 169 | }, 170 | "settings": { 171 | "message": "Settings", 172 | "description": "Label for the settings button/panel." 173 | }, 174 | "agent": { 175 | "message": "Agent", 176 | "description": "Label/tooltip for the AI agent button." 177 | }, 178 | "askQuestion": { 179 | "message": "Ask a question", 180 | "description": "Placeholder or action label in AI panel." 181 | }, 182 | "summarize": { 183 | "message": "Summarize article", 184 | "description": "Action label in AI panel to summarize." 185 | }, 186 | "fontGeorgia": { 187 | "message": "Classic web serif font", 188 | "description": "Description for Georgia font." 189 | }, 190 | "fontPalatino": { 191 | "message": "Classic elegant serif", 192 | "description": "Description for Palatino font." 193 | }, 194 | "fontBookerly": { 195 | "message": "Kindle default reading font", 196 | "description": "Description for Bookerly font." 197 | }, 198 | "fontTimesNewRoman": { 199 | "message": "Traditional classic serif", 200 | "description": "Description for Times New Roman font." 201 | }, 202 | "fontArial": { 203 | "message": "Common sans-serif font", 204 | "description": "Description for Arial font." 205 | }, 206 | "fontSourceHanSans": { 207 | "message": "Clean modern sans-serif", 208 | "description": "Description for Source Han Sans font (English)." 209 | }, 210 | "fontSourceHanSerif": { 211 | "message": "Professional Chinese serif", 212 | "description": "Description for Source Han Serif font (English)." 213 | }, 214 | "fontPingFang": { 215 | "message": "Apple default Chinese font", 216 | "description": "Description for PingFang font (English)." 217 | }, 218 | "fontHiraginoSansGB": { 219 | "message": "Clear modern sans-serif", 220 | "description": "Description for Hiragino Sans GB font (English)." 221 | }, 222 | "fontMicrosoftYaHei": { 223 | "message": "Windows default Chinese font", 224 | "description": "Description for Microsoft YaHei font (English)." 225 | }, 226 | "refreshLanguage": { 227 | "message": "Sync browser language", 228 | "description": "Action/tooltip for language sync." 229 | }, 230 | "clearChat": { 231 | "message": "Clear chat", 232 | "description": "Label for the clear chat button in Agent UI." 233 | }, 234 | "selectModel": { 235 | "message": "Select Model", 236 | "description": "Label for the model selector in Agent UI." 237 | }, 238 | "comingSoon": { 239 | "message": "Coming soon", 240 | "description": "Label for features that will be available in the future." 241 | }, 242 | "welcomeMessage": { 243 | "message": "I'm here to help you understand what's currently visible on your screen. As you scroll, my context updates to focus on what you're reading now.", 244 | "description": "Initial welcome message shown in Agent UI." 245 | }, 246 | "contextTypeScreen": { 247 | "message": "Screen", 248 | "description": "Label for screen context type" 249 | }, 250 | "contextTypeArticle": { 251 | "message": "Article", 252 | "description": "Label for article context type" 253 | }, 254 | "contextTypeSelection": { 255 | "message": "Selection", 256 | "description": "Label for selection context type" 257 | }, 258 | "contextSelectionDesc": { 259 | "message": "Use selected text as context", 260 | "description": "Description for selection context" 261 | }, 262 | "explainSelection": { 263 | "message": "Explain this selection", 264 | "description": "Default query for selected text" 265 | }, 266 | "loading": { 267 | "message": "Loading...", 268 | "description": "Message shown when content is loading." 269 | }, 270 | "login": { 271 | "message": "Log in", 272 | "description": "Label for the login button." 273 | }, 274 | "loginRequired": { 275 | "message": "Login Required", 276 | "description": "Title for the login required message." 277 | }, 278 | "loginMessage": { 279 | "message": "Please log in to use the AI assistant feature. Login is free and helps us prevent abuse.", 280 | "description": "Message explaining why login is required." 281 | }, 282 | "loginButton": { 283 | "message": "Log In", 284 | "description": "Label for the login button in the login UI." 285 | }, 286 | "loginSafe": { 287 | "message": "Safe login, no privacy concerns", 288 | "description": "Message indicating login is safe" 289 | }, 290 | "minimize": { 291 | "message": "Close", 292 | "description": "Text for minimize/close button" 293 | }, 294 | "thinking": { 295 | "message": "Thinking...", 296 | "description": "Indicator when AI is thinking" 297 | }, 298 | "agentContext": { 299 | "message": "Context", 300 | "description": "Label for the context selector" 301 | }, 302 | "agentDefaultModel": { 303 | "message": "Default", 304 | "description": "Default model option" 305 | }, 306 | "agentInputPlaceholder": { 307 | "message": "Type your message...", 308 | "description": "Placeholder text for input field" 309 | }, 310 | "agentSendMessage": { 311 | "message": "Send message", 312 | "description": "Title for send button" 313 | }, 314 | "agentSending": { 315 | "message": "Sending...", 316 | "description": "Text shown when message is being sent" 317 | }, 318 | "agentSend": { 319 | "message": "Send", 320 | "description": "Text for send button" 321 | }, 322 | "startConversation": { 323 | "message": "Start a new conversation", 324 | "description": "Text shown when no messages exist" 325 | }, 326 | "emptyStateDescription": { 327 | "message": "Select a context mode and ask any question to start the conversation.", 328 | "description": "Description for empty state" 329 | }, 330 | "errorOccurred": { 331 | "message": "An error occurred", 332 | "description": "Text shown when an error occurs" 333 | }, 334 | "errorMessage": { 335 | "message": "Unable to complete the request. Please try again later.", 336 | "description": "Default error message" 337 | }, 338 | "retry": { 339 | "message": "Retry", 340 | "description": "Text for retry button" 341 | }, 342 | "modelSelectorTitle": { 343 | "message": "Select AI Model", 344 | "description": "Title for model selector" 345 | }, 346 | "noModelsAvailable": { 347 | "message": "No models available", 348 | "description": "Text when no models are available" 349 | }, 350 | "contextScreenDesc": { 351 | "message": "Only use currently visible content", 352 | "description": "Description for screen context" 353 | }, 354 | "contextArticleDesc": { 355 | "message": "Use the entire article as context", 356 | "description": "Description for article context" 357 | }, 358 | "collapseToolbar": { 359 | "message": "Collapse Toolbar", 360 | "description": "Label for the collapse toolbar button" 361 | }, 362 | "custom": { 363 | "message": "Custom", 364 | "description": "Custom theme option." 365 | }, 366 | "customTheme": { 367 | "message": "Custom Theme", 368 | "description": "Label for custom theme section." 369 | }, 370 | "background": { 371 | "message": "Background", 372 | "description": "Label for background color setting." 373 | }, 374 | "text": { 375 | "message": "Text", 376 | "description": "Label for text color setting." 377 | }, 378 | "accent": { 379 | "message": "Accent", 380 | "description": "Label for accent color setting." 381 | }, 382 | "border": { 383 | "message": "Border", 384 | "description": "Label for border color setting." 385 | }, 386 | "select": { 387 | "message": "Select", 388 | "description": "Label for color select button." 389 | }, 390 | "lightBackgrounds": { 391 | "message": "Light backgrounds", 392 | "description": "Label for light background color presets." 393 | }, 394 | "darkBackgrounds": { 395 | "message": "Dark backgrounds", 396 | "description": "Label for dark background color presets." 397 | }, 398 | "eyecareBackgrounds": { 399 | "message": "Eye-care backgrounds", 400 | "description": "Label for eye-care background color presets." 401 | }, 402 | "darkText": { 403 | "message": "Dark text", 404 | "description": "Label for dark text color presets." 405 | }, 406 | "lightText": { 407 | "message": "Light text", 408 | "description": "Label for light text color presets." 409 | }, 410 | "accentColors": { 411 | "message": "Accent colors", 412 | "description": "Label for accent color presets." 413 | }, 414 | "highlight": { 415 | "message": "Highlight", 416 | "description": "Label for the highlight button in selection toolbar." 417 | }, 418 | "note": { 419 | "message": "Note", 420 | "description": "Label for the note button in selection toolbar." 421 | }, 422 | "addNote": { 423 | "message": "Add Note", 424 | "description": "Label for the note button in selection toolbar." 425 | }, 426 | "copy": { 427 | "message": "Copy", 428 | "description": "Label for the copy button in selection toolbar." 429 | }, 430 | "copied": { 431 | "message": "Copied", 432 | "description": "Label shown after text has been copied." 433 | }, 434 | "ai": { 435 | "message": "AI", 436 | "description": "Short label for the AI assistant button in selection toolbar." 437 | }, 438 | "aiAssistant": { 439 | "message": "AI Assistant", 440 | "description": "Tooltip for the AI assistant button in selection toolbar." 441 | }, 442 | "askAI": { 443 | "message": "Ask AI", 444 | "description": "Label for the AI assistant button in selection toolbar." 445 | }, 446 | "removeHighlight": { 447 | "message": "Remove Highlight", 448 | "description": "Label for removing a highlight in selection toolbar." 449 | }, 450 | "delete": { 451 | "message": "Delete", 452 | "description": "Label for the delete button in selection toolbar." 453 | }, 454 | "query": { 455 | "message": "Query", 456 | "description": "Label for the query button in selection toolbar." 457 | }, 458 | "share": { 459 | "message": "Share", 460 | "description": "Label for the share button in selection toolbar." 461 | }, 462 | "selectHighlightColor": { 463 | "message": "Select highlight color", 464 | "description": "Label for the highlight color picker dialog." 465 | }, 466 | "selectedText": { 467 | "message": "Selected Text", 468 | "description": "Label for selected text reference block" 469 | } 470 | } -------------------------------------------------------------------------------- /locales/zh/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "读点东西", 4 | "description": "Name of the extension." 5 | }, 6 | "extensionDescription": { 7 | "message": "为任何网页文章提供清晰无干扰的阅读体验。", 8 | "description": "Description of the extension." 9 | }, 10 | "displaySettings": { 11 | "message": "显示设置", 12 | "description": "Title for the display settings panel." 13 | }, 14 | "readingTheme": { 15 | "message": "阅读主题", 16 | "description": "Label for the reading theme selection." 17 | }, 18 | "light": { 19 | "message": "明亮", 20 | "description": "Light theme option." 21 | }, 22 | "dark": { 23 | "message": "暗黑", 24 | "description": "Dark theme option." 25 | }, 26 | "paper": { 27 | "message": "纸张", 28 | "description": "Paper theme option." 29 | }, 30 | "eyecare": { 31 | "message": "护眼", 32 | "description": "Eyecare theme option." 33 | }, 34 | "highcontrast": { 35 | "message": "高对比度", 36 | "description": "High Contrast theme option." 37 | }, 38 | "fontSize": { 39 | "message": "字号", 40 | "description": "Label for font size adjustment." 41 | }, 42 | "fontFamily": { 43 | "message": "字体", 44 | "description": "Label for font selection." 45 | }, 46 | "contentWidth": { 47 | "message": "页面宽度", 48 | "description": "Label for page width adjustment." 49 | }, 50 | "narrow": { 51 | "message": "窄", 52 | "description": "Narrow width option." 53 | }, 54 | "standard": { 55 | "message": "标准", 56 | "description": "Standard width option." 57 | }, 58 | "wide": { 59 | "message": "宽", 60 | "description": "Wide width option." 61 | }, 62 | "lineSpacing": { 63 | "message": "行间距", 64 | "description": "Label for line spacing adjustment." 65 | }, 66 | "tight": { 67 | "message": "紧凑", 68 | "description": "Tight line spacing option." 69 | }, 70 | "normal": { 71 | "message": "标准", 72 | "description": "Normal line spacing option." 73 | }, 74 | "relaxed": { 75 | "message": "宽松", 76 | "description": "宽松行间距选项" 77 | }, 78 | "system": { 79 | "message": "系统", 80 | "description": "系统字体选项" 81 | }, 82 | "currentSize": { 83 | "message": "当前字号", 84 | "description": "显示当前字体大小标签" 85 | }, 86 | "currentFont": { 87 | "message": "当前字体", 88 | "description": "Label for displaying current font." 89 | }, 90 | "textAlignment": { 91 | "message": "文本对齐", 92 | "description": "Label for text alignment options." 93 | }, 94 | "left": { 95 | "message": "左对齐", 96 | "description": "Left alignment option." 97 | }, 98 | "right": { 99 | "message": "右对齐", 100 | "description": "Right alignment option." 101 | }, 102 | "center": { 103 | "message": "居中", 104 | "description": "Center alignment option." 105 | }, 106 | "justify": { 107 | "message": "两端对齐", 108 | "description": "Justify alignment option." 109 | }, 110 | "recommendedForChinese": { 111 | "message": "推荐中文", 112 | "description": "Label for fonts recommended for Chinese text." 113 | }, 114 | "recommendedForEnglish": { 115 | "message": "推荐英文", 116 | "description": "Label for fonts recommended for English text." 117 | }, 118 | "recommendedForBoth": { 119 | "message": "通用字体", 120 | "description": "Label for fonts recommended for both languages." 121 | }, 122 | "allFonts": { 123 | "message": "所有字体", 124 | "description": "Label for showing all available fonts." 125 | }, 126 | "chineseFonts": { 127 | "message": "中文字体", 128 | "description": "Label for Chinese fonts section." 129 | }, 130 | "englishFonts": { 131 | "message": "英文字体", 132 | "description": "Label for English fonts section." 133 | }, 134 | "detected": { 135 | "message": "已检测", 136 | "description": "Label for detected language." 137 | }, 138 | "chinese": { 139 | "message": "中文", 140 | "description": "Chinese language name." 141 | }, 142 | "english": { 143 | "message": "英文", 144 | "description": "English language name." 145 | }, 146 | "theme": { 147 | "message": "主题", 148 | "description": "Theme section label." 149 | }, 150 | "extractingArticle": { 151 | "message": "提取文章中...", 152 | "description": "Message shown when extracting article content." 153 | }, 154 | "couldNotExtract": { 155 | "message": "无法提取文章内容", 156 | "description": "Error message when article extraction fails." 157 | }, 158 | "closeReaderMode": { 159 | "message": "关闭阅读模式", 160 | "description": "Label for button to close reader mode." 161 | }, 162 | "close": { 163 | "message": "关闭", 164 | "description": "Close button label." 165 | }, 166 | "download": { 167 | "message": "下载", 168 | "description": "下载文章为 Markdown 格式标签" 169 | }, 170 | "settings": { 171 | "message": "设置", 172 | "description": "设置按钮/面板的标签" 173 | }, 174 | "agent": { 175 | "message": "Agent", 176 | "description": "AI 助手按钮的标签/提示" 177 | }, 178 | "askQuestion": { 179 | "message": "请输入问题", 180 | "description": "AI 面板中的占位符或操作标签" 181 | }, 182 | "summarize": { 183 | "message": "总结文章", 184 | "description": "AI 面板中总结文章的操作标签" 185 | }, 186 | "fontGeorgia": { 187 | "message": "经典网页衬线字体", 188 | "description": "Georgia 字体的描述" 189 | }, 190 | "fontPalatino": { 191 | "message": "经典优雅的衬线字体", 192 | "description": "Palatino 字体的描述" 193 | }, 194 | "fontBookerly": { 195 | "message": "Kindle默认阅读字体", 196 | "description": "Bookerly 字体的描述" 197 | }, 198 | "fontTimesNewRoman": { 199 | "message": "传统经典衬线字体", 200 | "description": "Times New Roman 字体的描述" 201 | }, 202 | "fontArial": { 203 | "message": "通用无衬线字体", 204 | "description": "Arial 字体的描述" 205 | }, 206 | "fontSourceHanSans": { 207 | "message": "现代清晰的无衬线字体", 208 | "description": "思源黑体字体的描述(中文)" 209 | }, 210 | "fontSourceHanSerif": { 211 | "message": "专业中文衬线字体", 212 | "description": "思源宋体字体的描述(中文)" 213 | }, 214 | "fontPingFang": { 215 | "message": "苹果设备默认中文字体", 216 | "description": "苹方字体的描述(中文)" 217 | }, 218 | "fontHiraginoSansGB": { 219 | "message": "清晰的现代无衬线字体", 220 | "description": "冬青黑体字体的描述(中文)" 221 | }, 222 | "fontMicrosoftYaHei": { 223 | "message": "Windows系统默认中文字体", 224 | "description": "微软雅黑字体的描述(中文)" 225 | }, 226 | "refreshLanguage": { 227 | "message": "同步浏览器语言", 228 | "description": "语言同步的操作/提示" 229 | }, 230 | "clearChat": { 231 | "message": "清除对话", 232 | "description": "Agent UI 中清除对话按钮的标签" 233 | }, 234 | "selectModel": { 235 | "message": "选择模型", 236 | "description": "Agent UI 中模型选择器的标签" 237 | }, 238 | "comingSoon": { 239 | "message": "即将推出", 240 | "description": "未来将提供的功能的标签" 241 | }, 242 | "welcomeMessage": { 243 | "message": "我可以帮助你理解当前屏幕上显示的内容。当你滚动页面时,我的上下文会更新以关注你正在阅读的内容。", 244 | "description": "Agent UI 中显示的初始欢迎消息" 245 | }, 246 | "contextTypeScreen": { 247 | "message": "屏幕", 248 | "description": "Agent UI 中屏幕上下文类型的标签" 249 | }, 250 | "contextTypeArticle": { 251 | "message": "文章", 252 | "description": "Agent UI 中文章上下文类型的标签" 253 | }, 254 | "contextTypeSelection": { 255 | "message": "选择", 256 | "description": "Agent UI 中选择上下文类型的标签" 257 | }, 258 | "loading": { 259 | "message": "加载中...", 260 | "description": "内容加载时显示的消息" 261 | }, 262 | "login": { 263 | "message": "登录", 264 | "description": "登录按钮的标签" 265 | }, 266 | "loginRequired": { 267 | "message": "需要登录", 268 | "description": "登录提示标题" 269 | }, 270 | "loginMessage": { 271 | "message": "请登录以使用AI助手功能。登录是免费的,有助于我们防止滥用。", 272 | "description": "解释为什么需要登录的消息" 273 | }, 274 | "loginButton": { 275 | "message": "登录", 276 | "description": "登录界面中登录按钮的标签" 277 | }, 278 | "loginSafe": { 279 | "message": "安全登录,无需担心隐私", 280 | "description": "表示登录安全的消息" 281 | }, 282 | "minimize": { 283 | "message": "关闭", 284 | "description": "最小化/关闭按钮的文本" 285 | }, 286 | "thinking": { 287 | "message": "思考中...", 288 | "description": "AI思考时的指示器" 289 | }, 290 | "agentContext": { 291 | "message": "上下文", 292 | "description": "上下文选择器的标签" 293 | }, 294 | "agentDefaultModel": { 295 | "message": "默认", 296 | "description": "默认模型选项" 297 | }, 298 | "agentInputPlaceholder": { 299 | "message": "输入您的消息...", 300 | "description": "输入框的占位文本" 301 | }, 302 | "agentSendMessage": { 303 | "message": "发送消息", 304 | "description": "发送按钮的标题" 305 | }, 306 | "agentSending": { 307 | "message": "发送中...", 308 | "description": "消息发送中显示的文本" 309 | }, 310 | "agentSend": { 311 | "message": "发送", 312 | "description": "发送按钮的文本" 313 | }, 314 | "startConversation": { 315 | "message": "开始新对话", 316 | "description": "无消息时显示的文本" 317 | }, 318 | "emptyStateDescription": { 319 | "message": "选择上下文模式并提问以开始对话", 320 | "description": "空状态的描述" 321 | }, 322 | "errorOccurred": { 323 | "message": "发生错误", 324 | "description": "发生错误时显示的文本" 325 | }, 326 | "errorMessage": { 327 | "message": "无法完成请求,请稍后再试", 328 | "description": "默认错误消息" 329 | }, 330 | "retry": { 331 | "message": "重试", 332 | "description": "重试按钮的文本" 333 | }, 334 | "modelSelectorTitle": { 335 | "message": "选择AI模型", 336 | "description": "模型选择器的标题" 337 | }, 338 | "noModelsAvailable": { 339 | "message": "无可用模型", 340 | "description": "无可用模型时的文本" 341 | }, 342 | "contextScreenDesc": { 343 | "message": "仅使用当前可见内容", 344 | "description": "屏幕上下文的描述" 345 | }, 346 | "contextArticleDesc": { 347 | "message": "使用整篇文章作为上下文", 348 | "description": "文章上下文的描述" 349 | }, 350 | "contextSelectionDesc": { 351 | "message": "使用选中文本作为上下文", 352 | "description": "Description for selection context" 353 | }, 354 | "explainSelection": { 355 | "message": "解释这段选中内容", 356 | "description": "Default query for selected text" 357 | }, 358 | "collapseToolbar": { 359 | "message": "折叠工具栏", 360 | "description": "折叠工具栏按钮的标签" 361 | }, 362 | "custom": { 363 | "message": "自定义", 364 | "description": "Custom theme option." 365 | }, 366 | "customTheme": { 367 | "message": "自定义主题", 368 | "description": "Label for custom theme section." 369 | }, 370 | "background": { 371 | "message": "背景", 372 | "description": "Label for background color setting." 373 | }, 374 | "text": { 375 | "message": "文本", 376 | "description": "Label for text color setting." 377 | }, 378 | "accent": { 379 | "message": "强调", 380 | "description": "Label for accent color setting." 381 | }, 382 | "border": { 383 | "message": "边框", 384 | "description": "Label for border color setting." 385 | }, 386 | "select": { 387 | "message": "选择", 388 | "description": "Label for color select button." 389 | }, 390 | "lightBackgrounds": { 391 | "message": "浅色背景", 392 | "description": "Label for light background color presets." 393 | }, 394 | "darkBackgrounds": { 395 | "message": "深色背景", 396 | "description": "Label for dark background color presets." 397 | }, 398 | "eyecareBackgrounds": { 399 | "message": "护眼背景", 400 | "description": "Label for eye-care background color presets." 401 | }, 402 | "darkText": { 403 | "message": "深色文本", 404 | "description": "Label for dark text color presets." 405 | }, 406 | "lightText": { 407 | "message": "浅色文本", 408 | "description": "Label for light text color presets." 409 | }, 410 | "accentColors": { 411 | "message": "强调色", 412 | "description": "Label for accent color presets." 413 | }, 414 | "highlight": { 415 | "message": "划线", 416 | "description": "Label for the highlight button in selection toolbar." 417 | }, 418 | "note": { 419 | "message": "想法", 420 | "description": "Label for the note button in selection toolbar." 421 | }, 422 | "addNote": { 423 | "message": "写想法", 424 | "description": "Label for the note button in selection toolbar." 425 | }, 426 | "copy": { 427 | "message": "复制", 428 | "description": "Label for the copy button in selection toolbar." 429 | }, 430 | "copied": { 431 | "message": "已复制", 432 | "description": "Label shown after text has been copied." 433 | }, 434 | "ai": { 435 | "message": "AI", 436 | "description": "Short label for the AI assistant button in selection toolbar." 437 | }, 438 | "aiAssistant": { 439 | "message": "AI 助手", 440 | "description": "Tooltip for the AI assistant button in selection toolbar." 441 | }, 442 | "askAI": { 443 | "message": "问 AI", 444 | "description": "Label for the AI assistant button in selection toolbar." 445 | }, 446 | "removeHighlight": { 447 | "message": "删除划线", 448 | "description": "Label for removing a highlight in selection toolbar." 449 | }, 450 | "delete": { 451 | "message": "删除", 452 | "description": "Label for the delete button in selection toolbar." 453 | }, 454 | "query": { 455 | "message": "查询", 456 | "description": "Label for the query button in selection toolbar." 457 | }, 458 | "share": { 459 | "message": "分享", 460 | "description": "Label for the share button in selection toolbar." 461 | }, 462 | "selectHighlightColor": { 463 | "message": "选择划线颜色", 464 | "description": "Label for the highlight color picker dialog." 465 | }, 466 | "selectedText": { 467 | "message": "选中文本", 468 | "description": "Label for selected text reference block" 469 | } 470 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "read-lite", 3 | "displayName": "ReadLite - Simple Reading Mode", 4 | "version": "1.0.5", 5 | "description": "A simple reading mode extension to make web reading more comfortable", 6 | "author": "ReadLite Team", 7 | "license": "MIT", 8 | "scripts": { 9 | "clean": "rm -rf .plasmo/ build/", 10 | "dev": "npm run build:tailwind && plasmo dev", 11 | "build": "npm run build:tailwind && plasmo build", 12 | "package": "npm run build:tailwind && plasmo build --zip", 13 | "test": "jest", 14 | "lint": "eslint src --ext .ts,.tsx", 15 | "lint:fix": "eslint src --ext .ts,.tsx --fix", 16 | "format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"", 17 | "build:tailwind": "npx tailwindcss -i ./src/styles/global.css -o ./src/styles/tailwind.output.css", 18 | "watch:tailwind": "npx tailwindcss -i ./src/styles/global.css -o ./src/styles/tailwind.output.css --watch" 19 | }, 20 | "dependencies": { 21 | "@emotion/react": "^11.14.0", 22 | "@emotion/styled": "^11.14.0", 23 | "@heroicons/react": "^2.2.0", 24 | "@mozilla/readability": "^0.6.0", 25 | "@mui/icons-material": "^7.0.1", 26 | "@mui/material": "^7.0.1", 27 | "@plasmohq/storage": "^1.9.0", 28 | "dompurify": "^3.2.4", 29 | "franc-min": "^6.2.0", 30 | "html2canvas": "^1.4.1", 31 | "marked": "^15.0.7", 32 | "plasmo": "^0.90.3", 33 | "react": "^18.2.0", 34 | "react-dom": "^18.2.0", 35 | "turndown": "^7.2.0", 36 | "turndown-plugin-gfm": "^1.0.2" 37 | }, 38 | "devDependencies": { 39 | "@types/chrome": "^0.0.312", 40 | "@types/dompurify": "^3.0.5", 41 | "@types/jest": "^29.5.11", 42 | "@types/marked": "^5.0.2", 43 | "@types/react": "^19.0.12", 44 | "@types/react-dom": "^19.0.4", 45 | "@types/turndown": "^5.0.5", 46 | "autoprefixer": "^10.4.21", 47 | "eslint": "^8.55.0", 48 | "jest": "^29.7.0", 49 | "jimp": "^1.6.0", 50 | "postcss": "^8.5.3", 51 | "prettier": "^3.1.0", 52 | "tailwindcss": "^3.3.3", 53 | "typescript": "^5.3.3" 54 | }, 55 | "manifest": { 56 | "default_locale": "en", 57 | "name": "__MSG_extensionName__", 58 | "description": "__MSG_extensionDescription__", 59 | "permissions": [ 60 | "activeTab", 61 | "scripting", 62 | "tabs", 63 | "storage" 64 | ], 65 | "host_permissions": [ 66 | "" 67 | ], 68 | "action": { 69 | "default_title": "ReadLite - Simple Reading Mode" 70 | }, 71 | "background": { 72 | "service_worker": "background.js", 73 | "type": "module" 74 | }, 75 | "web_accessible_resources": [ 76 | { 77 | "resources": [ 78 | "src/styles/tailwind.output.css" 79 | ], 80 | "matches": [ 81 | "" 82 | ] 83 | } 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /src/components/agent/InputArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState, KeyboardEvent } from 'react'; 2 | import { ContextType } from './types'; 3 | import { Model } from '../../types/api'; 4 | import { 5 | ChevronDownIcon, 6 | ChevronLeftIcon, 7 | ChevronRightIcon, 8 | TrashIcon, 9 | XMarkIcon 10 | } from '@heroicons/react/24/outline'; 11 | 12 | interface InputAreaProps { 13 | inputText: string; 14 | setInputText: (text: string) => void; 15 | isLoading: boolean; 16 | isProcessing: boolean; 17 | onSendMessage: () => void; 18 | disableSend: boolean; 19 | contextType: ContextType | null; 20 | setContextType: (type: ContextType | null) => void; 21 | contextOptions: { value: ContextType; label: string }[]; 22 | selectedModel: Model | null; 23 | setSelectedModel: (model: Model | null) => void; 24 | availableModels: Model[]; 25 | t: (key: string) => string; 26 | isAuth?: boolean; 27 | onClearConversation?: () => void; 28 | onLogin?: () => void; 29 | onClose?: () => void; 30 | selectedText?: string; 31 | } 32 | 33 | // QuoteIcon component for consistent use across components 34 | const QuoteIcon = ({ className }: { className?: string }) => ( 35 | 36 | 37 | 38 | ); 39 | 40 | const InputArea: React.FC = ({ 41 | inputText, 42 | setInputText, 43 | isLoading, 44 | isProcessing, 45 | onSendMessage, 46 | disableSend, 47 | contextType, 48 | setContextType, 49 | contextOptions = [], 50 | selectedModel, 51 | setSelectedModel, 52 | availableModels = [], 53 | t, 54 | isAuth = false, 55 | onClearConversation, 56 | onLogin, 57 | onClose, 58 | selectedText = '', 59 | }) => { 60 | const textareaRef = useRef(null); 61 | const [showContextMenu, setShowContextMenu] = useState(false); 62 | const [showModelMenu, setShowModelMenu] = useState(false); 63 | const [isFocused, setIsFocused] = useState(false); 64 | const [isToolbarExpanded, setIsToolbarExpanded] = useState(true); 65 | const [isReferenceExpanded, setIsReferenceExpanded] = useState(true); 66 | 67 | // Should show reference block 68 | const shouldShowReference = 69 | contextType === 'selection' && 70 | selectedText && 71 | selectedText.trim().length > 0; 72 | 73 | // Toggle reference visibility 74 | const toggleReference = () => { 75 | setIsReferenceExpanded(!isReferenceExpanded); 76 | }; 77 | 78 | useEffect(() => { 79 | // Focus the textarea when the component mounts 80 | textareaRef.current?.focus(); 81 | }, []); 82 | 83 | const handleKeyDown = (e: KeyboardEvent) => { 84 | if (e.key === 'Enter' && !e.shiftKey) { 85 | e.preventDefault(); 86 | if (!disableSend && !isLoading && inputText.trim() !== '') { 87 | onSendMessage(); 88 | } 89 | } 90 | }; 91 | 92 | // Set minimum height to 3 lines and maximum height to 8 lines 93 | const adjustTextareaHeight = () => { 94 | const textarea = textareaRef.current; 95 | if (textarea) { 96 | // Reset height to auto to get accurate scrollHeight 97 | textarea.style.height = 'auto'; 98 | 99 | // Get line height (approx 20px per line) 100 | const lineHeight = 20; 101 | const minHeight = lineHeight * 3; // 3 lines minimum 102 | const maxHeight = lineHeight * 8; // 8 lines maximum 103 | 104 | // Set height based on content but within min/max constraints 105 | textarea.style.height = `${Math.max(minHeight, Math.min(textarea.scrollHeight, maxHeight))}px`; 106 | } 107 | }; 108 | 109 | useEffect(() => { 110 | adjustTextareaHeight(); 111 | }, [inputText]); 112 | 113 | const handleChange = (e: React.ChangeEvent) => { 114 | setInputText(e.target.value); 115 | }; 116 | 117 | const handleSelectContext = (type: ContextType | null) => { 118 | setContextType(type); 119 | setShowContextMenu(false); 120 | textareaRef.current?.focus(); 121 | }; 122 | 123 | const handleSelectModel = (model: Model) => { 124 | setSelectedModel(model); 125 | setShowModelMenu(false); 126 | textareaRef.current?.focus(); 127 | }; 128 | 129 | const getContextLabel = () => { 130 | if (!contextType || !contextOptions || !Array.isArray(contextOptions)) return ''; 131 | const option = contextOptions.find(opt => opt?.value === contextType); 132 | return option?.label || ''; 133 | }; 134 | 135 | const handleFocus = () => setIsFocused(true); 136 | const handleBlur = () => setIsFocused(false); 137 | 138 | // Common classes for menu items 139 | const menuItemClass = "px-3 py-2 hover:bg-bg-tertiary cursor-pointer transition-colors duration-150 text-xs"; 140 | 141 | // Common classes for toolbar buttons 142 | const toolbarButtonClass = "p-1 rounded text-text-secondary/70 hover:text-accent hover:bg-accent/10 transition-colors"; 143 | 144 | return ( 145 |
146 | {/* Reference block for selections */} 147 | {shouldShowReference && ( 148 |
149 |
153 | 154 | {t('selectedText') || 'Selected Text'} 155 | {isReferenceExpanded ? ( 156 | 157 | ) : ( 158 | 159 | )} 160 |
161 | 162 | {isReferenceExpanded && ( 163 |
165 | {selectedText} 166 |
167 | )} 168 |
169 | )} 170 | 171 |
172 | {/* Input container with focus state styling */} 173 |
179 | {/* Top toolbar with context and model selectors */} 180 |
181 |
182 | {isToolbarExpanded ? ( 183 | 198 | ) : ( 199 | 206 | )} 207 | 208 | {/* Context menu dropdown */} 209 | {showContextMenu && contextOptions && Array.isArray(contextOptions) && ( 210 |
212 | {contextOptions.map((option) => ( 213 |
handleSelectContext(option.value)} 217 | > 218 | {option.label} 219 |
220 | ))} 221 |
222 | )} 223 |
224 | 225 |
226 | {isToolbarExpanded && ( 227 | <> 228 | {/* Login button - only shown when not authenticated */} 229 | {!isAuth && onLogin && ( 230 | 236 | )} 237 | 238 | {/* Model selector */} 239 | 251 | 252 | )} 253 | 254 | {/* Toolbar collapse button */} 255 | {isToolbarExpanded && ( 256 | 263 | )} 264 | 265 | {/* Clear conversation button - only shown when authenticated */} 266 | {isAuth && onClearConversation && ( 267 | 274 | )} 275 | 276 | {/* Close button */} 277 | {onClose && ( 278 | 285 | )} 286 | 287 | {/* Model selection menu */} 288 | {showModelMenu && availableModels && Array.isArray(availableModels) && ( 289 |
291 | {availableModels.map((model) => ( 292 |
handleSelectModel(model)} 296 | > 297 | {model.label} 298 |
299 | ))} 300 |
301 | )} 302 |
303 |
304 | 305 | {/* Text input area */} 306 |
307 |