Skip to content

feat: 重构发现频道并全面优化 UI/UX,新增响应式布局、字体系统与图标组件#90

Merged
AmintaCCCP merged 21 commits intoAmintaCCCP:mainfrom
SummerRay160:main
Apr 22, 2026
Merged

feat: 重构发现频道并全面优化 UI/UX,新增响应式布局、字体系统与图标组件#90
AmintaCCCP merged 21 commits intoAmintaCCCP:mainfrom
SummerRay160:main

Conversation

@SummerRay160
Copy link
Copy Markdown
Contributor

@SummerRay160 SummerRay160 commented Apr 20, 2026

✨ 核心功能
发现频道重构:将原有订阅功能重构为「发现频道」,新增多种筛选与排序选项,支持更灵活的仓库/开发者发现体验。

AI 分析辅助:新增 AI 分析工具类,优化仓库分析逻辑,仓库卡片支持 Star 与 AI 分析操作。

剪贴板工具:实现安全的剪贴板读写工具函数,增强跨浏览器兼容性。

Markdown 渲染增强:改进渲染器,添加代码块一键复制和目录导航功能。

订阅状态管理:补充订阅相关的类型定义(SubscriptionRepo、SubscriptionDev)及应用状态字段,实现订阅频道的初始化与迁移逻辑。

🎨 UI / UX 优化
响应式布局:在 Tailwind 配置中添加 sm/md/lg/xl/2xl 屏幕断点,并将 Header 导航栏的响应式断点从 lg 改为 xl。

图标系统:将订阅侧边栏、仓库卡片、开发者卡片中的 emoji 图标全部替换为 lucide-react 图标,优化平台图标的显示效果与间距。

移动端导航:新增可滑动的标签导航组件,并添加底部活动指示器与渐变遮罩效果。

字体配置:添加 Inter 字体文件(含多种字重与字符集支持),在 Tailwind 中设为默认无衬线字体,并在 index.html 中引入。

布局细节:修复 SubscriptionView 组件侧边栏的 sticky 定位样式,移除构建时误提交的 dist/index.html。

🛠️ 组件与工具增强
ErrorBoundary:增强错误边界组件的捕获与降级能力。

DataManagementPanel:扩展数据导入导出功能,支持更多格式与场景。

ScrollToBottom:新增滚动到底部组件,提升长列表交互体验。

README 模态框:支持字体大小调整和目录导航。

代码高亮:添加 highlight.js 依赖,并在 index.css 中增强终端样式与代码高亮样式。

📦 构建与依赖
升级 Vite 及相关插件版本。

移除未使用的 Category 类型导入,清理旧订阅相关组件和类型定义。

Summary by CodeRabbit

  • New Features

    • Discovery view replaces subscription view with channels, filters, search, sorting, pagination and “load more”
    • Repository star/unstar (with confirmation), scroll-to-bottom button, and JSON data export/import (merge/replace)
    • In-page AI analysis controls for repositories
  • Improvements

    • Enhanced Markdown: syntax highlighting (highlight.js), copyable code blocks, image zoom/modal, in-modal TOC, heading navigation, adjustable font size
    • Redesigned error screen to report/copy details
    • Updated typography to Inter and refined responsive breakpoints; improved code/terminal styling
  • Utilities

    • Safer clipboard read/write helpers with localized messages

HappySummer added 9 commits April 20, 2026 15:23
- 在tailwind配置中添加sm-md-lg-xl-2xl屏幕断点
- 更新Header组件导航栏的响应式断点从lg改为xl
- 升级vite及相关插件版本
调整SubscriptionView组件中侧边栏的布局样式,添加sticky定位
移除构建过程中意外提交的dist/index.html文件
- 将订阅侧边栏、仓库卡片和开发者卡片中的emoji图标替换为lucide-react图标
- 为移动端添加可滑动的标签导航组件
- 优化平台图标的显示效果和间距
- 添加底部活动指示器和渐变遮罩效果
- 将订阅功能重构为发现频道,新增多种筛选和排序选项
- 添加AI分析辅助工具类,优化仓库分析逻辑
- 实现安全的剪贴板读写工具函数,增强兼容性
- 改进Markdown渲染器,添加代码复制和目录功能
- 优化仓库卡片组件,增加Star和AI分析操作
- 更新README模态框,支持字体大小调整和目录导航
- 移除旧的订阅相关组件和类型
- 在类型定义中添加 SubscriptionRepo 和 SubscriptionDev 等订阅相关类型
- 在应用状态中增加订阅相关的状态字段
- 实现订阅频道的初始化和迁移逻辑
- 移除未使用的 Category 类型导入
feat(ErrorBoundary): 增强错误边界组件功能
feat(DataManagementPanel): 扩展数据导入导出功能
style(index.css): 添加代码高亮和终端样式增强
feat(SubscriptionRepoCard): 改进Star操作逻辑和UI
feat(ScrollToBottom): 新增滚动到底部组件
build: 添加highlight.js依赖
添加Inter字体文件,包括多种字重和字符集支持
在tailwind配置中设置Inter为默认无衬线字体
在index.html中引入字体样式文件
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces the Subscription surface with a new Discovery system: adds discovery UI, store/actions, GitHub discovery APIs, AI analysis helpers and services, clipboard utilities, enhanced Markdown/README UX, new UI widgets, fonts/CSS, and removes legacy SubscriptionView/SubscriptionDevCard. Also updates build/tooling and some header/breakpoints.

Changes

Cohort / File(s) Summary
Font & Global Styles
index.html, public/fonts/inter.css, tailwind.config.js, src/index.css
Added local Inter font link and font-face CSS; configured Tailwind screens and sans font; large CSS additions for highlight.js, code blocks, terminal styling, image zoom/hover and prose rules.
Build deps & Config
package.json, vite.config.ts
Added highlight.js dependency; bumped Vite and plugin versions; switched Rollup manualChunks to id-based function.
Core Discovery UI
src/components/DiscoveryView.tsx, src/components/DiscoverySidebar.tsx, src/App.tsx
Added DiscoveryView with subcomponents (tabs, filters, pagination, analysis flow); renamed/refactored sidebar to Discovery (props/types changed); App.tsx now renders Discovery for the subscription route.
Repository Cards & Readme/Markdown
src/components/SubscriptionRepoCard.tsx, src/components/ReadmeModal.tsx, src/components/MarkdownRenderer.tsx
Repo card switched to DiscoveryRepo, added star/unstar, modal-open, AI analyze flows and optimistic state; README modal adds TOC/heading IDs/scroll progress/font-size; MarkdownRenderer adds CodeBlock (highlight.js + copy/line numbers), enhanced images (zoom, download, parent-link).
AI Analysis & Services
src/services/aiAnalysisHelper.ts, src/services/githubApi.ts, src/services/updateService.ts
Added analyzeRepository helper and failure constructor; extended GitHub API with discovery/search/star endpoints and query builders; updateService now derives version URL from project constants.
Store, Types & State
src/store/useAppStore.ts, src/types/index.ts
Reworked store for Discovery (channels, repos, pagination, filters, scroll positions), added analyzingRepositoryIds, discovery actions and persistence/migrations; introduced many discovery-related types and adjusted subscription state/types.
New UI Widgets
src/components/ScrollToBottom.tsx, src/components/SortAlgorithmTooltip.tsx
Added scroll-to-bottom button with container/window handling and localized label; added sort/algorithm tooltip component per channel.
Error & Project Constants
src/components/ErrorBoundary.tsx, src/constants/project.ts
Replaced clear-and-restart with report-issue flow, copyable error details and toggled details panel; added PROJECT_REPO_URL and PROJECT_ISSUES_URL.
Clipboard & Login
src/utils/clipboardUtils.ts, src/components/LoginScreen.tsx
Added safe clipboard read/write utilities with fallbacks and localized errors; LoginScreen now uses safeReadText.
Settings & Data Management
src/components/settings/DataManagementPanel.tsx, src/components/settings/GeneralPanel.tsx
Added JSON export/import (merge/replace), selective deletions, cleanup suggestions, and stats; GeneralPanel uses PROJECT_REPO_URL.
Removed Subscription UI
src/components/SubscriptionView.tsx, src/components/SubscriptionDevCard.tsx
Removed legacy SubscriptionView and SubscriptionDevCard (migration of functionality into Discovery components).
Other component tweaks
src/components/RepositoryCard.tsx, src/components/DiscoverySidebar.tsx, src/components/Header.tsx
RepositoryCard: per-repo analyzing tracking and refactor to use analyzeRepository; Sidebar renamed and simplified; Header breakpoints adjusted (lg→xl).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant UI as DiscoveryView
    participant Store as useAppStore
    participant GH as GitHubApiService
    participant AI as AIService

    User->>UI: select channel / request page
    UI->>Store: setSelectedDiscoveryChannel / setCurrentPage
    Store->>GH: fetch discovery repositories (trending/search/topic)
    GH-->>Store: PaginatedDiscoveryRepositories
    Store->>UI: discoveryRepos (render)

    User->>UI: click "Analyze page"
    UI->>Store: setAnalyzingRepository(repoId, true)
    UI->>GH: fetch README for repo
    GH-->>AI: return README
    UI->>AI: analyzeRepository(readme, categories)
    AI-->>Store: analysis result
    Store->>UI: updateDiscoveryRepo (show analysis)
    Store->>Store: setAnalyzingRepository(repoId, false)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Poem

🐇
I hopped through commits, found channels anew,
Stars and summaries, and syntax in view;
I nibbled fonts, zoomed pictures so bright,
Paged and analyzed through day and night —
Hop on, discover — the code feels light! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly describes the main changes: restructuring discovery channels and comprehensive UI/UX optimizations including new responsive layouts, font system, and icon components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/components/Header.tsx (1)

175-284: ⚠️ Potential issue | 🟠 Major

Add accessible labels to icon-only navigation buttons.

The tablet nav is now icon-only up to xl, and the desktop Trending button can also become icon-only when wrapped. Add aria-labels so these controls keep accessible names.

♿ Proposed fix
             <button
               onClick={() => setCurrentView('subscription')}
+              aria-label={isTextWrapped ? t('趋势', 'Trending') : undefined}
+              title={isTextWrapped ? t('趋势', 'Trending') : undefined}
               className={`${isTextWrapped ? 'p-2.5' : 'px-4 py-2'} rounded-lg font-medium transition-colors ${
                 currentView === 'subscription'
                   ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
@@
             <button
               onClick={() => setCurrentView('repositories')}
+              aria-label={t('仓库', 'Repositories')}
               className={`p-2.5 rounded-lg transition-colors ${
                 currentView === 'repositories'
                   ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
@@
             <button
               onClick={() => setCurrentView('releases')}
+              aria-label={t('发布', 'Releases')}
               className={`p-2.5 rounded-lg transition-colors ${
                 currentView === 'releases'
                   ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
@@
             <button
               onClick={() => setCurrentView('subscription')}
+              aria-label={t('趋势', 'Trending')}
               className={`p-2.5 rounded-lg transition-colors ${
                 currentView === 'subscription'
                   ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
@@
             <button
               onClick={() => setCurrentView('settings')}
+              aria-label={t('设置', 'Settings')}
               className={`p-2.5 rounded-lg transition-colors ${
                 currentView === 'settings'
                   ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Header.tsx` around lines 175 - 284, The icon-only buttons lack
accessible names: add aria-label attributes to each button in the tablet nav
(the nav with className "hidden md:flex xl:hidden...") and to the desktop
buttons that can become icon-only when isTextWrapped is true (the buttons that
call setCurrentView('repositories'|'releases'|'subscription'|'settings') and
render icons Search/Calendar/TrendingUp/Settings); set aria-label to the same
localized string used for title/text (use t('仓库','Repositories'),
t('发布','Releases'), t('趋势','Trending'), t('设置','Settings')) so screen readers
always get a proper label even when only the icon is shown.
package.json (1)

39-51: ⚠️ Potential issue | 🟠 Major

Upgrade @vitejs/plugin-react to support Vite 8.

vite has been upgraded to ^8.0.9, but @vitejs/plugin-react remains at ^4.3.1. The installed version only supports vite@^4.2.0 || ^5.0.0 and will fail peer dependency checks when installing. Version 6.0.1 (latest) supports vite@^8.0.0 and is compatible with your Vite upgrade.

Fix
-    "@vitejs/plugin-react": "^4.3.1",
+    "@vitejs/plugin-react": "^6.0.1",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 39 - 51, Update the `@vitejs/plugin-react`
dependency to a release compatible with Vite 8: change the version string for
"@vitejs/plugin-react" in package.json to "^6.0.1" (or the latest 6.x that
supports vite@^8.0.0), then reinstall dependencies and regenerate the lockfile
(npm install / yarn install / pnpm install) to resolve peer deps; verify builds
and dev server by running the existing vite scripts (e.g., the dev/build
commands) to ensure no other peer conflicts remain.
src/components/RepositoryCard.tsx (1)

118-333: ⚠️ Potential issue | 🟡 Minor

Race condition between unmount cleanup and in-flight AI analysis; missing cancellation token.

The analyzing state is tracked in the shared store keyed by repoId, but the component treats it as local:

  1. Unmount clears a flag that may still be in use. Lines 121–125 unconditionally set analyzing=false on unmount. If the card unmounts (virtualization, view switch, filter change) while analyzeRepository(...) is in flight, the cleanup clears the flag. A subsequently remounted card for the same repo—or a different component (e.g., SubscriptionRepoCard)—that starts its own analysis will have its true flag silently flipped to false when the original promise's finally (line 331) executes.

  2. No cancellation support. analyzeRepository (in aiAnalysisHelper.ts) does not accept an AbortSignal parameter, even though githubApi.getRepositoryReadme() and the underlying AI service methods support it. The README fetch and AI call will continue after unmount and call updateRepository(...) with stale output.

  3. Stale data can overwrite fresh results. updateRepository() performs a blind replacement with no timestamp validation. If two analyses race and the older one resolves after a newer one, it will overwrite ai_summary and custom_category.

Recommended: Add an AbortSignal parameter to AnalyzeRepositoryOptions, wire it through to githubApi.getRepositoryReadme() and aiService.analyzeRepository(), and abort on unmount. Guard the finally/updateRepository path to skip stale writes (e.g., compare timestamps or track whether the request was aborted). Alternatively, use a local ref to track whether this instance started the analysis, so unmount cleanup only clears the flag if this component is responsible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/RepositoryCard.tsx` around lines 118 - 333, The component
unmount cleanup unconditionally clears the shared analyzing flag
(analyzingRepositoryIds / setAnalyzingRepository) while handleAIAnalyze starts
an async analyzeRepository call without cancellation, allowing stale analysis
results to overwrite fresher ones via updateRepository; update the
AnalyzeRepositoryOptions and analyzeRepository(...) signature to accept an
AbortSignal and pass it through to githubApi.getRepositoryReadme and
aiService.analyzeRepository, create an AbortController in the component and
abort it on unmount, and change handleAIAnalyze to attach that signal and skip
calling updateRepository and the setAnalyzingRepository(false) finally-block if
the signal was aborted (or compare analyzed_at timestamps) so the unmount only
clears the flag for the instance that actually started the request.
🟡 Minor comments (12)
src/index.css-301-313 (1)

301-313: ⚠️ Potential issue | 🟡 Minor

Comment/implementation mismatch: ::before renders once, not per line.

The comment on line 301 says "每行前面添加 $ 符号" (add $ before each line), but a ::before pseudo-element on .language-bash inserts a single $ at the start of the entire code block. The actual behavior only prompts the first line, which is fine for single-line snippets but misleading for multi-line examples. Either update the comment to reflect single-prompt behavior, or render per-line via a JS transform in MarkdownRenderer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.css` around lines 301 - 313, The comment claiming "每行前面添加 $ 符号" is
incorrect because .language-bash::before (and
.language-sh/.language-shell/.language-zsh) only adds a single $ at the start of
the block; either update that comment to state it adds a single prompt for the
whole block, or implement per-line prompts by transforming code block contents
in MarkdownRenderer (split lines, wrap each line in a span or div with a class
like .language-bash-line, then apply a ::before to .language-bash-line to render
a $ per line); update the comment accordingly if you choose the JS transform
route.
src/components/ErrorBoundary.tsx-28-29 (1)

28-29: ⚠️ Potential issue | 🟡 Minor

Copy-error button never tells the user what happened.

  • strings.copied ("已复制!" / "Copied!") is defined on Line 29 but never referenced, so on success the button label stays as "Copy Error Info" with zero feedback.
  • handleCopyError (Lines 61-78) only logs on failure; on non-secure contexts (or browsers without navigator.clipboard) the user clicks and nothing visible happens, with no fallback path (e.g., selecting a textarea).

Consider a small copied/copyFailed state that swaps the button label briefly, and a document.execCommand('copy') or textarea-selection fallback when navigator.clipboard is unavailable, since the error-boundary is exactly where clipboard APIs may be unreliable.

Also applies to: 61-78, 104-110

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ErrorBoundary.tsx` around lines 28 - 29, The copy button never
shows feedback and has no fallback; update the ErrorBoundary component by adding
a transient UI state (e.g., copied and copyFailed booleans) and use those to
swap the button label instead of always showing strings.copyError — update
wherever the button is rendered to display strings.copied or a failure label
briefly when those states change. Modify handleCopyError to attempt
navigator.clipboard.writeText when available, and otherwise fall back to
creating a hidden textarea, select + document.execCommand('copy'), then remove
it; set copied = true on success, copyFailed = true on failure, and reset both
after a short timeout. Ensure you reference and update the existing
strings.copied constant and the handleCopyError function so the UI and clipboard
fallback are wired together.
src/components/ErrorBoundary.tsx-53-55 (1)

53-55: ⚠️ Potential issue | 🟡 Minor

window.open missing noopener,noreferrer.

Opening a URL with target=_blank but no rel="noopener noreferrer" equivalent gives the opened page access to window.opener, enabling reverse tabnabbing and leaking the referrer. Even though the target is our own GitHub issues page, the redirect chain is outside our control and this is easy to get right.

🔒 Proposed fix
   handleReportIssue = () => {
-    window.open(PROJECT_ISSUES_URL, '_blank');
+    window.open(PROJECT_ISSUES_URL, '_blank', 'noopener,noreferrer');
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ErrorBoundary.tsx` around lines 53 - 55, The handleReportIssue
method opens an external page with window.open(PROJECT_ISSUES_URL, '_blank')
which allows reverse tabnabbing; update handleReportIssue to call
window.open(PROJECT_ISSUES_URL, '_blank', 'noopener,noreferrer') and then
defensively set the new window's opener to null (e.g., const w =
window.open(...); if (w) w.opener = null;) so the function (handleReportIssue)
no longer exposes window.opener while still opening the PROJECT_ISSUES_URL in a
new tab.
src/components/ReadmeModal.tsx-49-65 (1)

49-65: ⚠️ Potential issue | 🟡 Minor

TOC extraction has two correctness issues.

  1. Matches headings inside fenced code blocks. The regex /^(#{1,3})\s+(.+)$/gm runs over the raw markdown and doesn't know about ``` fences, so shell/YAML/Dockerfile comments like # Install dependencies inside code blocks will become spurious TOC entries with IDs that never match any rendered element. Clicking them no-ops.
  2. Duplicate heading text collapses in idMap. idMap.set(text, id) keyed by text means repeated headings (e.g., multiple "Installation" / "Usage" sections, extremely common in long READMEs) overwrite each other, so navigating to an earlier duplicate scrolls to the last one. The TOC list itself still shows them, which makes the misrouting more confusing.

Suggestions:

  • Strip fenced code blocks before scanning (e.g., content.replace(/```[\s\S]*?```/g, '')).
  • Key idMap by (text, occurrenceIndex) or have MarkdownRenderer consume items (with its own sequential IDs) directly rather than mapping by text.
♻️ Sketch of fix
 const extractToc = useCallback((content: string): { items: TocItem[], idMap: Map<string, string> } => {
   const items: TocItem[] = [];
   const idMap = new Map<string, string>();
-  const regex = /^(#{1,3})\s+(.+)$/gm;
+  // Strip fenced code blocks so `# comments` inside them aren't treated as headings.
+  const sanitized = content.replace(/```[\s\S]*?```/g, '').replace(/~~~[\s\S]*?~~~/g, '');
+  const regex = /^(#{1,3})\s+(.+)$/gm;
   let match;
   let idCounter = 0;

-  while ((match = regex.exec(content)) !== null) {
+  while ((match = regex.exec(sanitized)) !== null) {
     const level = match[1].length;
     const text = match[2].trim();
     const id = `heading-${idCounter++}`;
     items.push({ id, text, level });
-    idMap.set(text, id);
+    // Only keep the first occurrence under the plain text key; callers that need
+    // the Nth occurrence should walk `items` instead.
+    if (!idMap.has(text)) idMap.set(text, id);
   }

   return { items, idMap };
 }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ReadmeModal.tsx` around lines 49 - 65, The extractToc function
is producing spurious headings from fenced code and collapsing duplicate
headings in idMap; fix it by first stripping fenced code blocks (e.g., remove
```...``` and ~~~...~~~ from the content into a sanitized string) before running
the existing regex, keep the existing heading id generation
(heading-{idCounter++}) and items push, and change idMap population so it does
not overwrite earlier occurrences (for example only set idMap.set(text, id) if
the key is absent) or switch callers to rely on the ordered items array instead
of a text-keyed map; update references to regex.exec to run against the
sanitized variable and preserve the function signature of extractToc.
public/fonts/inter.css-1-53 (1)

1-53: ⚠️ Potential issue | 🟡 Minor

Missing font-weight: 700 (bold) declarations.

The font file only declares weights 400, 500, and 600, but the application extensively requests weight 700 via font-bold Tailwind classes and inline font-weight: 700 styles (e.g., in headings, MarkdownRenderer, button labels, and src/index.css). This will trigger faux-bolding of the 600 face, producing suboptimal typographic quality. Add weight 700 faces with matching unicode-range splits, or constrain usage to the declared weights.

The referenced .woff2 binaries are present in public/fonts/.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public/fonts/inter.css` around lines 1 - 53, The CSS declares Inter
`@font-face` blocks for weights 400, 500, and 600 but not 700, causing faux-bold
rendering; add two new `@font-face` blocks for font-family 'Inter' with
font-weight: 700 (one using src: url('./inter-latin-ext.woff2') and the other
using src: url('./inter-latin.woff2')) and duplicate the matching unicode-range,
font-style: normal and font-display: swap entries used by the existing
400/500/600 blocks so the browser can load a true 700 face instead of synthetic
bolding.
src/index.css-279-560 (1)

279-560: ⚠️ Potential issue | 🟡 Minor

:has() selector incompatible with advertised browser support matrix

Lines 282–289, 520–523, 527–530, 535–542, 546–553, and 557–559 rely on the :has() relational pseudo-class, which is not supported in:

  • Firefox 75–120
  • Safari 13–15.3
  • Chrome 80–104
  • Edge 80–104

However, ErrorBoundary.tsx (line 166) explicitly advertises support for "Chrome 80+ / Firefox 75+ / Safari 13+ / Edge 80+". Users on these older but advertised versions will silently lose terminal backgrounds, gradients, line-height adjustments, and shell prompt prefixes, though the base markdown rendering remains functional.

Fix: Either tighten the advertised browser matrix in ErrorBoundary.tsx to match :has() support (Chrome/Edge 105+, Safari 15.4+, Firefox 121+), or avoid :has() by attaching a class to <pre> elements in MarkdownRenderer based on language and targeting that class instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.css` around lines 279 - 560, The CSS uses relational :has()
selectors (e.g. .prose pre:has(code.language-bash) and similar rules for
cmd/powershell and line-height) which are unsupported in older browsers
advertised in ErrorBoundary.tsx; either update the support matrix in
ErrorBoundary.tsx to require browsers with :has() (Chrome/Edge 105+, Safari
15.4+, Firefox 121+) or remove :has() usage by changing MarkdownRenderer to add
explicit classes on the <pre> elements (e.g. add pre--language-bash,
pre--language-cmd, pre--language-powershell) and then update the CSS selectors
to target .prose pre.pre--language-bash, .prose pre.pre--language-cmd, etc., and
adjust the line-height and prompt ::before rules to use those classes instead of
:has().
tailwind.config.js-6-12 (1)

6-12: ⚠️ Potential issue | 🟡 Minor

Custom xl breakpoint (1300px) is intentional but scope claim is overstated.

The xl: 1300px override is deliberate UI design with explicit documentation in code comments (Header.tsx: "Navigation - Desktop (≥1300px)"). However, the impact is much more limited than suggested: only 4 xl: usages exist in the entire codebase (2 in Header.tsx, 2 in DiscoveryView.tsx), all intentional and tied to specific responsive behaviors.

The concern about "silently breaking layouts" from third-party code is not substantiated—the codebase does not depend on default Tailwind breakpoints elsewhere. The suggested custom-named screen (hdxl: '1300px') adds unnecessary complexity for an intentional design choice that is already well-documented. No refactoring needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tailwind.config.js` around lines 6 - 12, The custom xl breakpoint set to
'1300px' in tailwind.config.js is intentional and only used in specific places;
preserve screens.xl = '1300px' but add a concise inline comment in
tailwind.config.js documenting that this override is deliberate and limited to
Header.tsx ("Navigation - Desktop (≥1300px)") and DiscoveryView.tsx usages; do
not rename to a new screen or refactor usages—just document the intent and point
to Header.tsx and DiscoveryView.tsx so future readers know the scope.
src/components/MarkdownRenderer.tsx-8-13 (1)

8-13: ⚠️ Potential issue | 🟡 Minor

Both highlight.js themes are imported unconditionally — dark/light won't switch.

Because github-dark.min.css and github.min.css are both imported, the second import's .hljs selectors simply override the first globally. In dark mode users will still see the light (or dark, depending on import order) color palette baked in. Pick one of these approaches:

  1. Use the theme that supports both via CSS variables (e.g. highlight.js/styles/github.min.css plus custom dark-mode overrides in src/index.css — which you already scaffolded per the AI summary).
  2. Dynamically import one stylesheet based on useAppStore(state => state.theme).
  3. Gate each sheet behind a data-theme / .dark selector by wrapping the imports in scoped CSS.

As-is, one of the imports is dead CSS weight.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/MarkdownRenderer.tsx` around lines 8 - 13, The two
Highlight.js themes (github-dark.min.css and github.min.css) are both imported
statically in MarkdownRenderer which causes one to always override the other;
instead remove the static imports and load the appropriate stylesheet
dynamically based on the current theme from useAppStore (e.g., call
useAppStore(state => state.theme) inside MarkdownRenderer, then in a useEffect
import() the matching CSS file (or toggle a single theme file plus CSS variable
overrides) and clean up previous link if needed); update MarkdownRenderer to
perform this dynamic import (or switch to the single-variable-compatible
stylesheet and add dark-mode overrides in your global CSS) so only the active
theme's styles are applied.
src/components/SubscriptionRepoCard.tsx-261-268 (1)

261-268: ⚠️ Potential issue | 🟡 Minor

Card-wide copy/cut/select prevention blocks legitimate text selection.

userSelect: 'none' plus onCopy/onCut/onSelect preventDefault applies to the entire card, which means users cannot select or copy the repository description, AI summary, tags, or full_name — all of which are useful to copy (e.g. pasting owner/repo into a terminal). If the intent is only to avoid interfering with the click-to-open-README gesture, consider dropping these handlers entirely; cursor-pointer + the existing onClick is already sufficient. Otherwise, scope the selection lock to the rank badge / action buttons only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SubscriptionRepoCard.tsx` around lines 261 - 268, The card
currently disables all text selection and clipboard actions via inline style
userSelect: 'none' and handlers onCopy/onCut/onSelect on the outer div (the
element with onClick={handleCardClick}), which blocks legitimate copying of repo
full_name, description, summary and tags; remove the global userSelect and the
onCopy/onCut/onSelect handlers from the card container (the div using
handleCardClick and cursor-pointer) so normal text selection/copy works, and if
selection locking is still required only for specific UI controls then move the
style/handlers to those elements (e.g., the rank badge or action buttons)
instead.
src/components/DiscoveryView.tsx-1030-1045 (1)

1030-1045: ⚠️ Potential issue | 🟡 Minor

Empty-string option isn't null for discoverySelectedTopic.

When the user picks the placeholder option (<option value="">), the value is "" — casting "" as TopicCategory | null does not produce null. The store ends up with discoverySelectedTopic === '', which refreshChannel('topic', ...) treats as falsy (so it happens to fall through to trending), but the type contract is violated and any strict check like topic !== null would misbehave. Normalize explicitly:

🔧 Proposed fix
-                    onChange={(e) => setDiscoverySelectedTopic(e.target.value as TopicCategory | null)}
+                    onChange={(e) =>
+                      setDiscoverySelectedTopic(
+                        e.target.value === '' ? null : (e.target.value as TopicCategory)
+                      )
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/DiscoveryView.tsx` around lines 1030 - 1045, The select's
onChange casts the empty string to TopicCategory | null but leaves
discoverySelectedTopic as "" instead of null; update the onChange handler for
the select that uses discoverySelectedTopic and setDiscoverySelectedTopic so it
normalizes the value: read e.target.value, if it's an empty string call
setDiscoverySelectedTopic(null) else cast to TopicCategory and set that value
(this ensures discoverySelectedTopic is actually null for the placeholder and
preserves strict checks and callers like refreshChannel('topic', ...)).
src/components/MarkdownRenderer.tsx-71-88 (1)

71-88: ⚠️ Potential issue | 🟡 Minor

String(children) can mis-serialize non-string code content.

When children is not a string (e.g. ['line1', 'line2']), String(children) produces "line1,line2" with an embedded comma — the copy-to-clipboard value is corrupted. For fenced code, react-markdown usually yields a single string, but this is not guaranteed across plugins. Safer:

🔧 Proposed fix
-    const codeText = typeof children === 'string'
-      ? children
-      : String(children);
+    const flatten = (node: React.ReactNode): string => {
+      if (node == null || typeof node === 'boolean') return '';
+      if (typeof node === 'string' || typeof node === 'number') return String(node);
+      if (Array.isArray(node)) return node.map(flatten).join('');
+      if (React.isValidElement(node)) return flatten((node.props as { children?: React.ReactNode }).children);
+      return '';
+    };
+    const codeText = flatten(children);

The same issue applies to the codeLines/lineCount computation at Line 89 — when children isn't a string, lineCount is 0 and line numbers are silently dropped. Reusing the flattened text fixes both.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/MarkdownRenderer.tsx` around lines 71 - 88, handleCopy
currently uses String(children) which corrupts non-string code content (e.g.
arrays) and the same unsafe assumption is used when computing
codeLines/lineCount; normalize children into a single text string first (e.g.
flatten React children or use
React.Children.toArray(children).map(String).join('\n') or an equivalent that
preserves line breaks) and then pass that normalizedText to safeWriteText in
handleCopy and use it for computing codeLines/lineCount so line numbers and
clipboard text remain correct; update references inside handleCopy, the
codeLines/lineCount logic, and any other places that read children to use the
new normalizedText variable instead.
src/services/aiAnalysisHelper.ts-50-61 (1)

50-61: ⚠️ Potential issue | 🟡 Minor

Dead logic in category_locked computation.

shouldKeepLocked is defined as wasCategoryLocked && resolvedCategory !== undefined && resolvedCategory !== '', so shouldKeepLocked || wasCategoryLocked always simplifies to wasCategoryLocked — the first operand adds nothing. If the intent is "preserve the lock only when we actually have a category to lock to", this should just be shouldKeepLocked. As written, a repo that was locked but whose AI analysis returned no matching category will remain category_locked: true with custom_category: undefined, which is a confusing state.

🔧 Proposed fix
-  const wasCategoryLocked = !!(repository as Repository).category_locked;
-  const shouldKeepLocked = wasCategoryLocked && resolvedCategory !== undefined && resolvedCategory !== '';
-
     return {
       summary: analysis.summary,
       tags: analysis.tags,
       platforms: analysis.platforms,
       custom_category: resolvedCategory,
-    category_locked: shouldKeepLocked || wasCategoryLocked,
+    category_locked: !!(repository as Repository).category_locked
+      && resolvedCategory !== undefined
+      && resolvedCategory !== '',
       analyzed_at: new Date().toISOString(),
       analysis_failed: false,
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/aiAnalysisHelper.ts` around lines 50 - 61, The category lock
computation contains dead logic: shouldKeepLocked is defined as
wasCategoryLocked && resolvedCategory !== undefined && resolvedCategory !== '',
but the returned category_locked uses shouldKeepLocked || wasCategoryLocked
which always equals wasCategoryLocked; update the return to set category_locked
to shouldKeepLocked (so a previously locked repo only remains locked when
resolvedCategory is present/non-empty). Adjust the return object in the same
block where wasCategoryLocked, shouldKeepLocked and resolvedCategory are defined
to use category_locked: shouldKeepLocked.
🧹 Nitpick comments (12)
src/components/SortAlgorithmTooltip.tsx (1)

73-107: A11y and button-type polish for the Info tooltip.

  • No onFocus/onBlur handlers: keyboard users who tab onto the Info button can never see the tooltip content, which is the component's only purpose.
  • The button has no type="button" — defaults to "submit" if this component is ever rendered inside a <form> (the discovery filters area is form-ish).
  • The popover has no role="tooltip" / aria-describedby linkage to the trigger, so screen readers won't announce it.
♻️ Minimal a11y fix
-      <button
-        onMouseEnter={() => setIsVisible(true)}
-        onMouseLeave={() => setIsVisible(false)}
-        onClick={() => setIsVisible(!isVisible)}
-        className="p-1 rounded-full text-gray-400 hover:text-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-colors"
-      >
+      <button
+        type="button"
+        aria-describedby={isVisible ? 'sort-algo-tooltip' : undefined}
+        aria-expanded={isVisible}
+        onMouseEnter={() => setIsVisible(true)}
+        onMouseLeave={() => setIsVisible(false)}
+        onFocus={() => setIsVisible(true)}
+        onBlur={() => setIsVisible(false)}
+        onClick={() => setIsVisible(v => !v)}
+        className="p-1 rounded-full text-gray-400 hover:text-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-colors"
+      >
         <Info className="w-4 h-4" />
       </button>

       {isVisible && (
-        <div className="absolute top-full mt-2 left-1/2 -translate-x-1/2 z-[9999]" style={{ zIndex: 9999 }}>
+        <div id="sort-algo-tooltip" role="tooltip" className="absolute top-full mt-2 left-1/2 -translate-x-1/2 z-[9999]" style={{ zIndex: 9999 }}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SortAlgorithmTooltip.tsx` around lines 73 - 107,
SortAlgorithmTooltip's Info button needs basic accessibility: add onFocus={() =>
setIsVisible(true)} and onBlur={() => setIsVisible(false)} to mirror
onMouseEnter/onMouseLeave so keyboard users can open the tooltip, set the
button's type to "button" to prevent accidental form submissions, and wire up
ARIA by giving the tooltip container a stable id (e.g., `${id}-tooltip` or
generate one) with role="tooltip" and adding aria-describedby on the trigger
button pointing to that id so screen readers announce the content; update
references to isVisible and setIsVisible accordingly and ensure the tooltip id
is unique per instance.
src/components/SubscriptionRepoCard.tsx (2)

172-178: alert() for success notification is jarring; reuse the existing toast pattern.

DataManagementPanel already has a showSuccess/showErrorMessage pattern. Bringing up a blocking modal for every star action (including the "Successfully starred" case) interrupts discovery browsing. Consider a non-blocking toast/snackbar, or simply rely on the star icon state change as implicit confirmation and only alert on errors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SubscriptionRepoCard.tsx` around lines 172 - 178, The code
uses alert(...) for success and error feedback in SubscriptionRepoCard which is
jarring; replace these blocking alerts with the existing non-blocking toast API
from DataManagementPanel (use showSuccess for the success path where
alert(t('已成功添加 Star'...)) is called and use showErrorMessage for the catch path
instead of alert(errorMessage)), keep the optimistic rollback via
setOptimisticStarred(null) and console.error('Failed to star repository:',
error) in the catch, and remove the success alert so the star icon state
(setOptimisticStarred/related UI) and showSuccess are the only confirmations.

150-161: Prefer destructuring over rank/channel/platform: undefined when constructing Repository.

Setting discovery-only fields to undefined still leaves the keys on the object (with undefined values). For a cleaner Repository payload — and to avoid accidental persistence/diff noise — destructure them out:

🔧 Proposed refactor
-      // 将DiscoveryRepo转换为Repository并添加到本地,保留AI分析结果
-      const repositoryToAdd = {
-        ...repo,
-        // 移除Discovery/Subscription特有的字段
-        rank: undefined,
-        channel: undefined,
-        platform: undefined,
-        // 添加Star时间
-        starred_at: new Date().toISOString(),
-      };
+      // 将DiscoveryRepo转换为Repository并添加到本地,保留AI分析结果
+      const { rank: _r, channel: _c, platform: _p, ...rest } = repo;
+      const repositoryToAdd: Repository = {
+        ...rest,
+        starred_at: new Date().toISOString(),
+      };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SubscriptionRepoCard.tsx` around lines 150 - 161, The current
construction of repositoryToAdd leaves discovery-only keys present with
undefined values (rank/channel/platform); instead, destructure those keys out of
repo and build the Repository from the remaining properties so the keys are
omitted: extract { rank, channel, platform, ...rest } = repo (ensuring you don’t
drop other fields like any AI analysis fields), then create repositoryToAdd from
rest plus starred_at and pass that to addRepository to avoid persisting
undefined keys.
src/components/settings/DataManagementPanel.tsx (1)

1162-1171: Avoid document.querySelectorAll for React form state.

Reading the export selection from the DOM couples this panel to a specific .export-checkbox class and breaks if the markup is ever reused elsewhere, if StrictMode/portals render duplicates, or if the panel is embedded in another page. Prefer tracking the selection in React state (e.g. a Set<string> toggled by each checkbox's onChange) and passing it to exportData(selectedTypes).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/settings/DataManagementPanel.tsx` around lines 1162 - 1171,
The button handler currently reads selected export types from the DOM via
document.querySelectorAll('.export-checkbox:checked'), which couples UI to
markup; change DataManagementPanel to track selected export types in React state
(e.g. a Set<string> or string[] called selectedExportTypes) updated by each
checkbox's onChange handler (use the checkbox's data-type or value inside the
onChange for the checkbox component) and replace the DOM query in the button's
onClick to call exportData(Array.from(selectedExportTypes)) after validating
selectedExportTypes.length; update the checkbox inputs to derive their checked
value from selectedExportTypes and toggle membership in the set on change.
src/components/DiscoveryView.tsx (1)

819-829: Reuse getDefaultCategoryNames from aiAnalysisHelper.ts instead of re-inlining the list.

src/services/aiAnalysisHelper.ts already exports getDefaultCategoryNames(customCategories) that returns this exact list. Duplicating the fixed Chinese labels here guarantees drift the next time either side is edited.

🔧 Proposed refactor
-    const allCategories = useAppStore
-      .getState()
-      .customCategories.map(c => c.name);
-    const categoryNames = [
-      ...allCategories,
-      '全部分类', 'Web应用', '移动应用', '桌面应用', '数据库',
-      'AI/机器学习', '开发工具', '安全工具', '游戏', '设计工具',
-      '效率工具', '教育学习', '社交网络', '数据分析',
-    ];
+    const categoryNames = getDefaultCategoryNames(useAppStore.getState().customCategories);

(and add the import at the top)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/DiscoveryView.tsx` around lines 819 - 829, Replace the
duplicated hard-coded category list with the shared helper: import and call
getDefaultCategoryNames from aiAnalysisHelper and use its return for
categoryNames instead of building the array inline; specifically, keep computing
allCategories via useAppStore.getState().customCategories.map(c => c.name) then
set categoryNames = getDefaultCategoryNames(allCategories). Add the import for
getDefaultCategoryNames at the top of the file and remove the inline literal
array so the component uses the single source of truth.
src/types/index.ts (1)

7-9: Both forks_count and forks as required fields is redundant and error-prone.

GitHub's REST API returns both (forks is a legacy alias kept in sync with forks_count), and your codebase already reads them defensively (e.g. SubscriptionRepoCard.tsx Line 426: repo.forks_count ?? repo.forks ?? 0). Forcing both as required introduces a risk of drift (e.g. a partial DTO from the backend adapter sets only one and fails type checks elsewhere). Consider making one required and the other optional, or dropping forks in favor of forks_count.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types/index.ts` around lines 7 - 9, The type definition currently
requires both forks_count and forks which is redundant and brittle; update the
DTO in src/types/index.ts so only forks_count is required and forks is optional
(e.g., make the forks property possibly undefined) or remove forks entirely;
ensure callers like SubscriptionRepoCard (which defensively reads
repo.forks_count ?? repo.forks ?? 0) still compile by leaving forks as optional
if you don't remove it, and run type checks to confirm no other code assumes
forks is always present.
src/services/aiAnalysisHelper.ts (1)

72-80: Hardcoded Chinese labels ignore UI language and are duplicated in DiscoveryView.tsx.

Two concerns:

  1. The fixed list ('全部分类', 'Web应用', ...) is returned regardless of the user's UI language, so English users' AI prompts will be mixed-language. Consider localizing via the language param, or passing in the canonical category list from the store.
  2. src/components/DiscoveryView.tsx (Lines 824-829) repeats the identical hardcoded list inline instead of calling this helper — handleAnalyzePage should reuse getDefaultCategoryNames(customCategories) to avoid drift.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/aiAnalysisHelper.ts` around lines 72 - 80,
getDefaultCategoryNames currently returns a hardcoded Chinese category list
which ignores UI language and is duplicated in DiscoveryView.tsx; change
getDefaultCategoryNames to accept a language parameter (or pull canonical
categories from the store) and return localized labels accordingly, and update
DiscoveryView.tsx to remove the inline hardcoded list and call
getDefaultCategoryNames(customCategories, language) (or the store-backed
variant) from handleAnalyzePage so both places share the same canonical,
localized category array; ensure function signature and call sites
(getDefaultCategoryNames, handleAnalyzePage, customCategories usage) are updated
consistently.
src/services/githubApi.ts (2)

590-620: Inject user-controlled searchKeywords without escaping into GitHub search qualifier.

searchByTopic inlines searchKeywords directly into the query string. Because the caller in getTopicRepositories passes curated strings this is currently safe, but the method is also reachable from other call sites (and is an easy footgun for future use) — any embedded qualifier like language:Go or user:foo in the keywords would silently override filter logic. Consider validating/escaping the keywords or documenting the method as expecting only plain search terms.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubApi.ts` around lines 590 - 620, searchByTopic builds the
GitHub search query by inlining user-controlled searchKeywords, which allows
injected qualifiers (e.g., "language:Go" or "user:foo") to override filters; fix
by sanitizing or escaping searchKeywords before concatenation in searchByTopic:
either strip any tokens that match GitHub qualifier patterns (like
/\b[a-zA-Z_-]+:[^\s]+/), reject keywords containing ':' and return an error, or
escape/encode such characters so they are treated as plain text; update the code
around searchByTopic (and callers such as getTopicRepositories) to use the
sanitizedKeyword variable when constructing query and add a small unit test to
exercise a keyword with a qualifier to ensure it no longer injects a filter.

455-475: Quote language values containing special characters.

GitHub's search syntax supports quoting language qualifiers (e.g., language:"C#"), which is more robust than unquoted values. While properly encoded queries will function, quoting guards against edge cases where special characters in C# and C++ could be misinterpreted (e.g., + decoded as space on proxies, # as URL fragment boundary).

-    return `language:${languageMap[language]}`;
+    const value = languageMap[language];
+    // Quote values that contain special characters (#, +, spaces)
+    return /[#+ ]/.test(value) ? `language:"${value}"` : `language:${value}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubApi.ts` around lines 455 - 475, The buildLanguageQuery
function currently returns unquoted language qualifiers which can break for
special characters; update buildLanguageQuery (and the languageMap it uses) so
values with special characters are wrapped in double quotes (e.g., change 'C#'
-> '"C#"', 'C++' -> '"C++"') and ensure the returned string uses the quoted
value (return `language:${languageMap[language]}` with the mapped value already
quoted) so GitHub search receives language:"C#" style qualifiers.
src/store/useAppStore.ts (3)

588-623: addRepository ID-generation scheme risks extremely large IDs and rare collisions.

newId = Math.max(timestamp, maxExistingId + 1) + random where timestamp = Date.now() and random ∈ [0, 9999] produces IDs in the ~1.7e12 range (JS epoch ms) — well within safe integer range, but:

  1. If two calls happen in the same millisecond and draw the same random (≈1/10k chance per concurrent call), IDs collide. You explicitly mention wanting to "avoid race conditions" in the comment, but the current scheme doesn't actually prevent this.
  2. Mixing a clock-based ID with real GitHub repo IDs (which are sequential integers, often < 1e9) means future equality/hash operations across both populations can surprise readers who assume IDs come from the same distribution.

A simpler and collision-free alternative is to use maxExistingId + 1 with a negative-id range reserved for locally-added repos, or to use a UUID string if the schema supports it.

♻️ Minimal adjustment
-          const timestamp = Date.now();
-          const random = Math.floor(Math.random() * 10000);
-          const maxExistingId = state.repositories.length > 0
-            ? Math.max(...state.repositories.map(r => r.id))
-            : 0;
-          const newId = Math.max(timestamp, maxExistingId + 1) + random;
+          const maxExistingId = state.repositories.length > 0
+            ? Math.max(...state.repositories.map(r => r.id))
+            : 0;
+          // Reserve a high offset to clearly distinguish locally-added repos from GitHub IDs.
+          const LOCAL_ID_BASE = 1e12;
+          const newId = Math.max(maxExistingId + 1, LOCAL_ID_BASE);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/useAppStore.ts` around lines 588 - 623, The addRepository
ID-generation uses timestamp+random (newId) which can collide under concurrent
calls and produces huge numeric IDs; change addRepository so new IDs are
deterministic and collision-free by either (a) assigning newId = maxExistingId +
1 (compute max from state.repositories.map(r => r.id)) or (b) switch to a UUID
string for id if schema permits; if you choose to distinguish local-only repos
prefer a reserved negative id range or a UUID to avoid mixing with GitHub
numeric ids; update the branch that pushes the new repo (the else block that
sets updatedRepositories and the returned repositories/searchResults) to use the
new id strategy and ensure id types remain consistent across state.

1169-1182: discoveryRepos persistence can grow unbounded and bloat IndexedDB storage.

Discovery channels accumulate repositories across pages (via appendDiscoveryRepos) and the full discoveryRepos map is persisted to IndexedDB on every state change. A user who scrolls deeply across multiple channels can easily persist thousands of DiscoveryRepo objects (including AI-analysis fields), which bloats the storage backing, slows rehydration, and duplicates data already available from the API.

Consider persisting only a bounded window per channel (e.g., the first page, or the last N results) and relying on network refresh for older pages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/useAppStore.ts` around lines 1169 - 1182, The persisted
discoveryRepos map currently saves all accumulated pages (populated by
appendDiscoveryRepos) and can grow unbounded; change the persistence logic in
useAppStore so that before persisting discoveryRepos you truncate each channel's
array to a bounded window (e.g., only the first page or last N items per
channel) or persist only metadata (totalCount/nextPage) plus a small slice of
items, ensuring appendDiscoveryRepos and rehydration still work by relying on
network fetches for older pages; locate the serialization/rehydration code that
references discoveryRepos in useAppStore and implement trimming of each
channel's repository array there (or switch to storing summaries instead of full
DiscoveryRepo objects).

1221-1272: Migration guards using falsy checks will incorrectly re-initialize valid persisted values.

Guards like if (state && !state.discoveryPlatform) state.discoveryPlatform = 'All' use truthy checks, which means an explicit empty-string value (if ever persisted due to a bug) would be silently overwritten. More importantly, the pattern doesn't generalize — none of the per-channel discovery maps (discoveryRepos, discoveryLastRefresh, discoveryIsLoading, discoveryHasMore, discoveryNextPage, discoveryTotalCount, discoveryScrollPositions) are initialized here when migrating from versions ≤ 4. They rely entirely on normalizePersistedState at merge time, which (as noted in another comment) doesn't fully backfill defaults either.

Prefer typeof state.X === 'undefined' and initialize the full set of discovery maps in migration for version 5 so rehydration is deterministic regardless of which path runs first.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/useAppStore.ts` around lines 1221 - 1272, The migration uses falsy
checks that can overwrite valid persisted values; change guards like if (state
&& !state.discoveryPlatform) to explicit undefined checks (typeof
state.discoveryPlatform === 'undefined') and apply the same pattern for
discoveryLanguage, discoverySortBy, discoverySortOrder and
selectedDiscoveryChannel; additionally, during the subscriptionChannels
migration (where defaultSubscriptionChannels, state.subscriptionChannels, and
defaultChannelsMap are handled) ensure you initialize the full set of discovery
maps used elsewhere—discoveryRepos, discoveryLastRefresh, discoveryIsLoading,
discoveryHasMore, discoveryNextPage, discoveryTotalCount,
discoveryScrollPositions—when typeof state.<map> === 'undefined' so rehydration
is deterministic regardless of normalizePersistedState execution order and you
do not clobber intentionally falsy values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 04059237-f6b0-4b27-acff-aed13c6c5a72

📥 Commits

Reviewing files that changed from the base of the PR and between bae04bc and c285f7f.

⛔ Files ignored due to path filters (4)
  • dist/index.html is excluded by !**/dist/**
  • package-lock.json is excluded by !**/package-lock.json
  • public/fonts/inter-latin-ext.woff2 is excluded by !**/*.woff2
  • public/fonts/inter-latin.woff2 is excluded by !**/*.woff2
📒 Files selected for processing (28)
  • index.html
  • package.json
  • public/fonts/inter.css
  • src/App.tsx
  • src/components/DiscoverySidebar.tsx
  • src/components/DiscoveryView.tsx
  • src/components/ErrorBoundary.tsx
  • src/components/Header.tsx
  • src/components/LoginScreen.tsx
  • src/components/MarkdownRenderer.tsx
  • src/components/ReadmeModal.tsx
  • src/components/RepositoryCard.tsx
  • src/components/ScrollToBottom.tsx
  • src/components/SortAlgorithmTooltip.tsx
  • src/components/SubscriptionDevCard.tsx
  • src/components/SubscriptionRepoCard.tsx
  • src/components/SubscriptionView.tsx
  • src/components/settings/DataManagementPanel.tsx
  • src/components/settings/GeneralPanel.tsx
  • src/constants/project.ts
  • src/index.css
  • src/services/aiAnalysisHelper.ts
  • src/services/githubApi.ts
  • src/services/updateService.ts
  • src/store/useAppStore.ts
  • src/types/index.ts
  • src/utils/clipboardUtils.ts
  • tailwind.config.js
💤 Files with no reviewable changes (2)
  • src/components/SubscriptionView.tsx
  • src/components/SubscriptionDevCard.tsx

Comment thread src/components/DiscoveryView.tsx Outdated
Comment thread src/components/DiscoveryView.tsx
Comment thread src/components/DiscoveryView.tsx
Comment thread src/components/settings/DataManagementPanel.tsx
Comment thread src/components/settings/DataManagementPanel.tsx
Comment thread src/components/settings/DataManagementPanel.tsx
Comment thread src/services/githubApi.ts
Comment thread src/services/githubApi.ts
Comment thread src/store/useAppStore.ts
HappySummer and others added 5 commits April 21, 2026 06:01
使用动态分块函数替代静态配置,提高代码灵活性
移除过时注释以保持配置简洁
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/services/githubApi.ts (1)

455-475: Minor: redundant 'All' entry in languageMap.

The early return on line 456 already handles 'All', so the 'All': '' entry in the map is dead. Harmless, but you can drop it (and change the type to Exclude<ProgrammingLanguage, 'All'>) for a tighter contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubApi.ts` around lines 455 - 475, Remove the redundant 'All'
key from the languageMap in buildLanguageQuery and tighten the map's type to
exclude 'All' by using Record<Exclude<ProgrammingLanguage, 'All'>, string>; keep
the early return for language === 'All' as-is, update the map literal to only
include concrete languages (Kotlin, Java, JavaScript, TypeScript, Python, Swift,
Rust, Go, CSharp, CPlusPlus, C, Dart, Ruby, PHP) and return
`language:${languageMap[language]}` unchanged so the function still returns an
empty string early for 'All' and maps other languages via the narrowed type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/services/githubApi.ts`:
- Around line 636-648: The topic keyword mapping in topicKeywords (used by
searchByTopic) is using space-separated terms which GitHub interprets as AND;
update each entry in topicKeywords (e.g., keys
'ai','ml','database','web','mobile','devtools','security','game') to join
alternative tokens with the OR operator (e.g., "artificial-intelligence OR
machine-learning OR ai") so the searchByTopic call receives OR-separated
keywords and returns repos matching any of the terms instead of requiring all of
them.

---

Nitpick comments:
In `@src/services/githubApi.ts`:
- Around line 455-475: Remove the redundant 'All' key from the languageMap in
buildLanguageQuery and tighten the map's type to exclude 'All' by using
Record<Exclude<ProgrammingLanguage, 'All'>, string>; keep the early return for
language === 'All' as-is, update the map literal to only include concrete
languages (Kotlin, Java, JavaScript, TypeScript, Python, Swift, Rust, Go,
CSharp, CPlusPlus, C, Dart, Ruby, PHP) and return
`language:${languageMap[language]}` unchanged so the function still returns an
empty string early for 'All' and maps other languages via the narrowed type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 059e5105-522d-40de-b152-42140d7b7b5c

📥 Commits

Reviewing files that changed from the base of the PR and between 4285674 and 42cc4e6.

📒 Files selected for processing (1)
  • src/services/githubApi.ts

Comment thread src/services/githubApi.ts
HappySummer added 2 commits April 21, 2026 06:28
refactor(DiscoveryView): 使用ref优化滚动位置存储
fix(DataManagementPanel): 修复主题和视图模式类型检查
@AmintaCCCP
Copy link
Copy Markdown
Owner

PR #90 审计反馈 — feat: 重构发现频道并全面优化 UI/UX

🦞 发现了不少好东西,尤其是发现重构和 Markdown 增强,但这里有几个核心坑需要填一下:

🔴 严重问题 (需优先修复)

  1. 依赖冲突 (Vite 8): package.jsonvite 升到了 ^8.0.9,但 @vitejs/plugin-react 还是 ^4.3.1(仅支持 Vite 4/5)。这会导致 peer dependency 报错,建议升到 ^6.0.1
  2. AI 分析竞态条件 (RepositoryCard.tsx): 卸载清理函数无条件清除全局 analyzingRepositoryIds。如果多个卡片同时分析或切换视图,旧请求的 finally 会把新请求的 loading 状态关掉。且 analyzeRepository 没接入 AbortSignal,卸载后请求依然会跑完并写入陈旧数据。

🟠 主要问题

  1. a11y 缺失: Header 的平板导航和折叠后的桌面按钮全是图标,没给 aria-label,屏幕阅读器完全抓不到。
  2. 安全隐患: ErrorBoundary.tsx 里的 window.open 没加 noopener,noreferrer,存在反向标签劫持风险。
  3. TOC 提取漏洞 (ReadmeModal.tsx): 正则会误抓代码块里的 # 注释;且 idMap 用标题文本做 key,遇到重复标题(比如多个 'Installation')导航会跳转到最后一个。建议提取前先 strip 掉 fenced code。

🟡 次要优化点

  • 字体缺失: inter.css 声明了 400/500/600 但没给 700,导致全站的 font-bold 都是假粗体。
  • 主题冲突: MarkdownRenderer.tsx 同时 import 了深浅两个 highlight.js css,后者的样式会强行覆盖前者。
  • 选择锁定 (SubscriptionRepoCard.tsx): 卡片容器加了 userSelect: 'none' 和剪贴板拦截,用户想复制仓库全名或描述都不行,建议范围收窄。
  • 逻辑冗余: aiAnalysisHelper.ts 里的 category_locked 计算存在死逻辑(shouldKeepLocked || wasCategoryLocked 恒等于 wasCategoryLocked)。

整体架构改得很棒,解决掉这几个关键点后合并会安全很多。建议后续考虑把字体/图标大修拆成更小的 PR,34个文件改动量确实太大了。

@AmintaCCCP
Copy link
Copy Markdown
Owner

有个严重问题,切顶部标签会白屏,无法恢复。另外之前加的Trending(https://github.com/trending)被搞没了,比起一成不变的榜单,这个trending榜更有意义

refactor(pagination): 重构分页组件,支持服务端分页和本地分页
feat(store): 添加discoveryCurrentPage状态管理
style(tooltip): 改进排序算法提示框的定位和响应式设计
fix(scroll): 修复页面切换时的滚动位置问题
perf(discovery): 优化数据加载逻辑和性能
@SummerRay160
Copy link
Copy Markdown
Contributor Author

官方没有提供趋势页面的API 原来增加的功能是通过Github Search实现的
类似的功能我放到【热门发布】里了

@SummerRay160
Copy link
Copy Markdown
Contributor Author

还有啥地方要改的,我再打磨一下

@AmintaCCCP
Copy link
Copy Markdown
Owner

官方没有提供趋势页面的API 原来增加的功能是通过Github Search实现的 类似的功能我放到【热门发布】里了

我上个版本用的RSS订阅:

@AmintaCCCP
Copy link
Copy Markdown
Owner

还有啥地方要改的,我再打磨一下

我下了workflow编译的之后,刷新了下趋势的feed,就遇见了切标签白屏的问题,开始是趋势页白,得关闭重开,后来变成每个页都白了,就没法继续往下测了。

@SummerRay160
Copy link
Copy Markdown
Contributor Author

SummerRay160 commented Apr 21, 2026

好的,我把RSS趋势放到趋势页面下作为频道展示
不知道白屏出现的现象是什么,在浏览器上显示正常。。。(可能是加了字体CDN的原因)

@AmintaCCCP
Copy link
Copy Markdown
Owner

好的,我把RSS趋势放到趋势页面下作为频道展示

不知道白屏出现的现象是什么,在浏览器上显示正常。。。(可能是加了字体CDN的原因)

切顶栏标签的时候会白。最开始是卡顿,然后后来是页面空白了。我先试试,再遇见把报错收集下。

@SummerRay160
Copy link
Copy Markdown
Contributor Author

在WIN上的构建产物没有问题 我改下吧

@AmintaCCCP
Copy link
Copy Markdown
Owner

在WIN上的构建产物没有问题 我改下吧

我是 mac 环境。上个 pr 刚加趋势标签的时候我遇见了一次频道分类死活显示不全,本地化也不生效。后来发现是第一版历史数据的问题。这次这个不知道是不是也是历史数据导致的。趋势里面的数据结构变了有可能冲突。

HappySummer added 4 commits April 22, 2026 02:19
- 添加性能优化相关代码,包括虚拟列表、图片懒加载和性能监控
- 实现RSS趋势功能,支持从第三方源获取GitHub趋势数据
- 重构发现频道相关代码,优化类型定义和状态管理
- 添加代码分割和懒加载以提升首屏性能
- 优化排序算法提示弹窗的交互和样式
- 更新依赖项,添加esbuild用于构建优化
- 在RSS服务中添加基础URL常量并重构URL配置
- 优化RepositoryList组件的暂停/恢复和停止逻辑,使用useCallback提升性能
- 在应用状态管理中新增rssTimeRange字段并实现版本迁移
保留切换频道时的当前页码,而不是总是重置为1
- 重构项目发现模块,合并项目类型和时间范围为场景化时间范围
- 新增模态框可见性钩子,优化滚动按钮在模态框打开时的显示逻辑
- 修复暗黑模式下Markdown文本颜色问题
- 更新数据管理面板,支持更多状态的导入导出
- 调整发现页筛选器UI,优化用户体验
@AmintaCCCP
Copy link
Copy Markdown
Owner

还是会白屏,而且存储加载不出来
image
image

@AmintaCCCP AmintaCCCP merged commit 73a8fd0 into AmintaCCCP:main Apr 22, 2026
5 checks passed
@AmintaCCCP
Copy link
Copy Markdown
Owner

minimax2.7真是垃圾中的垃圾,我让他排障,他各种尝试都没有找到问题,在我再三要求他不要一直起新PR,不要合并的情况下直接把一堆他写的PR合并了。然后闯了祸就摆烂,说自己闯祸了,问我接下来该怎么办...

AmintaCCCP added a commit that referenced this pull request Apr 22, 2026
* fix(desktop): prevent discovery view renderer crash

* fix: resolve mac desktop white screen when switching top tabs

Root causes fixed:
1. normalizePersistedState was missing defensive handling for 4 discovery
   runtime state fields (discoveryIsLoading, discoveryHasMore, discoveryNextPage,
   discoveryScrollPositions). If old persisted data had these fields in a wrong
   format (e.g. array instead of object), spreading them in store actions like
   { ...state.discoveryIsLoading, [channel]: loading } would produce corrupted
   state and cause React render errors → white screen.

2. migrate() function also lacked a reset for discoveryIsLoading /
   discoveryScrollPositions, so old-format data survived into the merge step.

3. DiscoveryView had no local ErrorBoundary. Any render error inside it
   propagated all the way to the root boundary (main.tsx), wiping the entire
   UI. Wrapping it in a scoped ErrorBoundary now shows a recovery UI instead
   of a blank page when switching to the discovery tab.

Changes:
- src/store/useAppStore.ts: add normalizePersistedState entries for
  discoveryIsLoading (reset to false), discoveryHasMore (safe object merge),
  discoveryNextPage (safe object merge), discoveryScrollPositions (reset to 0);
  add migrate() block to reset discoveryIsLoading and discoveryScrollPositions.
- src/App.tsx: wrap <DiscoveryView /> in <ErrorBoundary> for scoped recovery.

* fix(desktop): disable persistent storage for massive discoveryRepos object

* fix(desktop): debounce Zustand persistence serialization to prevent V8 memory crashes on tab switch

* fix(desktop): optimize rendering of SubscriptionRepoCard for desktop safe mode

* fix(desktop): move useEffect below refreshChannel definition to fix TDZ reference error during production build initialization
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants