feat: 重构发现频道并全面优化 UI/UX,新增响应式布局、字体系统与图标组件#90
Conversation
- 在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中引入字体样式文件
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughReplaces 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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorAdd 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. Addaria-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 | 🟠 MajorUpgrade
@vitejs/plugin-reactto support Vite 8.
vitehas been upgraded to^8.0.9, but@vitejs/plugin-reactremains at^4.3.1. The installed version only supportsvite@^4.2.0 || ^5.0.0and will fail peer dependency checks when installing. Version6.0.1(latest) supportsvite@^8.0.0and 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 | 🟡 MinorRace 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:
Unmount clears a flag that may still be in use. Lines 121–125 unconditionally set
analyzing=falseon unmount. If the card unmounts (virtualization, view switch, filter change) whileanalyzeRepository(...)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 itstrueflag silently flipped tofalsewhen the original promise'sfinally(line 331) executes.No cancellation support.
analyzeRepository(inaiAnalysisHelper.ts) does not accept anAbortSignalparameter, even thoughgithubApi.getRepositoryReadme()and the underlying AI service methods support it. The README fetch and AI call will continue after unmount and callupdateRepository(...)with stale output.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 overwriteai_summaryandcustom_category.Recommended: Add an
AbortSignalparameter toAnalyzeRepositoryOptions, wire it through togithubApi.getRepositoryReadme()andaiService.analyzeRepository(), and abort on unmount. Guard thefinally/updateRepositorypath 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 | 🟡 MinorComment/implementation mismatch:
::beforerenders once, not per line.The comment on line 301 says "每行前面添加 $ 符号" (add
$before each line), but a::beforepseudo-element on.language-bashinserts 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 inMarkdownRenderer.🤖 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 | 🟡 MinorCopy-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 withoutnavigator.clipboard) the user clicks and nothing visible happens, with no fallback path (e.g., selecting a textarea).Consider a small
copied/copyFailedstate that swaps the button label briefly, and adocument.execCommand('copy')or textarea-selection fallback whennavigator.clipboardis 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.openmissingnoopener,noreferrer.Opening a URL with
target=_blankbut norel="noopener noreferrer"equivalent gives the opened page access towindow.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 | 🟡 MinorTOC extraction has two correctness issues.
- Matches headings inside fenced code blocks. The regex
/^(#{1,3})\s+(.+)$/gmruns over the raw markdown and doesn't know about``` fences, so shell/YAML/Dockerfile comments like# Install dependenciesinside code blocks will become spurious TOC entries with IDs that never match any rendered element. Clicking them no-ops.- 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
idMapby(text, occurrenceIndex)or haveMarkdownRendererconsumeitems(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 | 🟡 MinorMissing
font-weight: 700(bold) declarations.The font file only declares weights 400, 500, and 600, but the application extensively requests weight 700 via
font-boldTailwind classes and inlinefont-weight: 700styles (e.g., in headings,MarkdownRenderer, button labels, andsrc/index.css). This will trigger faux-bolding of the 600 face, producing suboptimal typographic quality. Add weight 700 faces with matchingunicode-rangesplits, or constrain usage to the declared weights.The referenced
.woff2binaries are present inpublic/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 matrixLines 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.tsxto match:has()support (Chrome/Edge 105+, Safari 15.4+, Firefox 121+), or avoid:has()by attaching a class to<pre>elements inMarkdownRendererbased 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 | 🟡 MinorCustom
xlbreakpoint (1300px) is intentional but scope claim is overstated.The
xl: 1300pxoverride 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 4xl: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 | 🟡 MinorBoth highlight.js themes are imported unconditionally — dark/light won't switch.
Because
github-dark.min.cssandgithub.min.cssare both imported, the second import's.hljsselectors 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:
- Use the theme that supports both via CSS variables (e.g.
highlight.js/styles/github.min.cssplus custom dark-mode overrides insrc/index.css— which you already scaffolded per the AI summary).- Dynamically import one stylesheet based on
useAppStore(state => state.theme).- Gate each sheet behind a
data-theme/.darkselector 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 | 🟡 MinorCard-wide copy/cut/select prevention blocks legitimate text selection.
userSelect: 'none'plusonCopy/onCut/onSelectpreventDefault applies to the entire card, which means users cannot select or copy the repository description, AI summary, tags, orfull_name— all of which are useful to copy (e.g. pastingowner/repointo 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 existingonClickis 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 | 🟡 MinorEmpty-string option isn't
nullfordiscoverySelectedTopic.When the user picks the placeholder option (
<option value="">), the value is""— casting"" as TopicCategory | nulldoes not producenull. The store ends up withdiscoverySelectedTopic === '', whichrefreshChannel('topic', ...)treats as falsy (so it happens to fall through to trending), but the type contract is violated and any strict check liketopic !== nullwould 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
childrenis 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/lineCountcomputation at Line 89 — whenchildrenisn't a string,lineCountis0and 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 | 🟡 MinorDead logic in
category_lockedcomputation.
shouldKeepLockedis defined aswasCategoryLocked && resolvedCategory !== undefined && resolvedCategory !== '', soshouldKeepLocked || wasCategoryLockedalways simplifies towasCategoryLocked— 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 beshouldKeepLocked. As written, a repo that was locked but whose AI analysis returned no matching category will remaincategory_locked: truewithcustom_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/onBlurhandlers: 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-describedbylinkage 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.
DataManagementPanelalready has ashowSuccess/showErrorMessagepattern. 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 onlyalerton 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 overrank/channel/platform: undefinedwhen constructingRepository.Setting discovery-only fields to
undefinedstill leaves the keys on the object (withundefinedvalues). For a cleanerRepositorypayload — 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: Avoiddocument.querySelectorAllfor React form state.Reading the export selection from the DOM couples this panel to a specific
.export-checkboxclass 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. aSet<string>toggled by each checkbox'sonChange) and passing it toexportData(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: ReusegetDefaultCategoryNamesfromaiAnalysisHelper.tsinstead of re-inlining the list.
src/services/aiAnalysisHelper.tsalready exportsgetDefaultCategoryNames(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: Bothforks_countandforksas required fields is redundant and error-prone.GitHub's REST API returns both (
forksis a legacy alias kept in sync withforks_count), and your codebase already reads them defensively (e.g.SubscriptionRepoCard.tsxLine 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 droppingforksin favor offorks_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 inDiscoveryView.tsx.Two concerns:
- 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 thelanguageparam, or passing in the canonical category list from the store.src/components/DiscoveryView.tsx(Lines 824-829) repeats the identical hardcoded list inline instead of calling this helper —handleAnalyzePageshould reusegetDefaultCategoryNames(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-controlledsearchKeywordswithout escaping into GitHub search qualifier.
searchByTopicinlinessearchKeywordsdirectly into the query string. Because the caller ingetTopicRepositoriespasses 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 likelanguage:Gooruser:fooin 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 inC#andC++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:addRepositoryID-generation scheme risks extremely large IDs and rare collisions.
newId = Math.max(timestamp, maxExistingId + 1) + randomwheretimestamp = Date.now()andrandom∈ [0, 9999] produces IDs in the ~1.7e12 range (JS epoch ms) — well within safe integer range, but:
- 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.- 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 + 1with 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:discoveryRepospersistence can grow unbounded and bloat IndexedDB storage.Discovery channels accumulate repositories across pages (via
appendDiscoveryRepos) and the fulldiscoveryReposmap is persisted to IndexedDB on every state change. A user who scrolls deeply across multiple channels can easily persist thousands ofDiscoveryRepoobjects (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 onnormalizePersistedStateat 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
⛔ Files ignored due to path filters (4)
dist/index.htmlis excluded by!**/dist/**package-lock.jsonis excluded by!**/package-lock.jsonpublic/fonts/inter-latin-ext.woff2is excluded by!**/*.woff2public/fonts/inter-latin.woff2is excluded by!**/*.woff2
📒 Files selected for processing (28)
index.htmlpackage.jsonpublic/fonts/inter.csssrc/App.tsxsrc/components/DiscoverySidebar.tsxsrc/components/DiscoveryView.tsxsrc/components/ErrorBoundary.tsxsrc/components/Header.tsxsrc/components/LoginScreen.tsxsrc/components/MarkdownRenderer.tsxsrc/components/ReadmeModal.tsxsrc/components/RepositoryCard.tsxsrc/components/ScrollToBottom.tsxsrc/components/SortAlgorithmTooltip.tsxsrc/components/SubscriptionDevCard.tsxsrc/components/SubscriptionRepoCard.tsxsrc/components/SubscriptionView.tsxsrc/components/settings/DataManagementPanel.tsxsrc/components/settings/GeneralPanel.tsxsrc/constants/project.tssrc/index.csssrc/services/aiAnalysisHelper.tssrc/services/githubApi.tssrc/services/updateService.tssrc/store/useAppStore.tssrc/types/index.tssrc/utils/clipboardUtils.tstailwind.config.js
💤 Files with no reviewable changes (2)
- src/components/SubscriptionView.tsx
- src/components/SubscriptionDevCard.tsx
使用动态分块函数替代静态配置,提高代码灵活性 移除过时注释以保持配置简洁
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>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/services/githubApi.ts (1)
455-475: Minor: redundant'All'entry inlanguageMap.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 toExclude<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
refactor(DiscoveryView): 使用ref优化滚动位置存储 fix(DataManagementPanel): 修复主题和视图模式类型检查
PR #90 审计反馈 —
|
|
有个严重问题,切顶部标签会白屏,无法恢复。另外之前加的Trending(https://github.com/trending)被搞没了,比起一成不变的榜单,这个trending榜更有意义 |
refactor(pagination): 重构分页组件,支持服务端分页和本地分页 feat(store): 添加discoveryCurrentPage状态管理 style(tooltip): 改进排序算法提示框的定位和响应式设计 fix(scroll): 修复页面切换时的滚动位置问题 perf(discovery): 优化数据加载逻辑和性能
|
官方没有提供趋势页面的API 原来增加的功能是通过Github Search实现的 |
|
还有啥地方要改的,我再打磨一下 |
我上个版本用的RSS订阅:
|
我下了workflow编译的之后,刷新了下趋势的feed,就遇见了切标签白屏的问题,开始是趋势页白,得关闭重开,后来变成每个页都白了,就没法继续往下测了。 |
|
好的,我把RSS趋势放到趋势页面下作为频道展示 |
切顶栏标签的时候会白。最开始是卡顿,然后后来是页面空白了。我先试试,再遇见把报错收集下。 |
|
在WIN上的构建产物没有问题 我改下吧 |
我是 mac 环境。上个 pr 刚加趋势标签的时候我遇见了一次频道分类死活显示不全,本地化也不生效。后来发现是第一版历史数据的问题。这次这个不知道是不是也是历史数据导致的。趋势里面的数据结构变了有可能冲突。 |
- 添加性能优化相关代码,包括虚拟列表、图片懒加载和性能监控 - 实现RSS趋势功能,支持从第三方源获取GitHub趋势数据 - 重构发现频道相关代码,优化类型定义和状态管理 - 添加代码分割和懒加载以提升首屏性能 - 优化排序算法提示弹窗的交互和样式 - 更新依赖项,添加esbuild用于构建优化
- 在RSS服务中添加基础URL常量并重构URL配置 - 优化RepositoryList组件的暂停/恢复和停止逻辑,使用useCallback提升性能 - 在应用状态管理中新增rssTimeRange字段并实现版本迁移
保留切换频道时的当前页码,而不是总是重置为1
- 重构项目发现模块,合并项目类型和时间范围为场景化时间范围 - 新增模态框可见性钩子,优化滚动按钮在模态框打开时的显示逻辑 - 修复暗黑模式下Markdown文本颜色问题 - 更新数据管理面板,支持更多状态的导入导出 - 调整发现页筛选器UI,优化用户体验
|
minimax2.7真是垃圾中的垃圾,我让他排障,他各种尝试都没有找到问题,在我再三要求他不要一直起新PR,不要合并的情况下直接把一堆他写的PR合并了。然后闯了祸就摆烂,说自己闯祸了,问我接下来该怎么办... |
* 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


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