Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 52 additions & 10 deletions src/components/RepositoryList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { Bot, ChevronDown, Pause, Play } from 'lucide-react';
import { RepositoryCard } from './RepositoryCard';

Expand Down Expand Up @@ -32,7 +32,6 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
const [showAISummary, setShowAISummary] = useState(true);
const [showDropdown, setShowDropdown] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [searchTime, setSearchTime] = useState<number | undefined>(undefined);

// 使用 useRef 来管理停止状态,确保在异步操作中能正确访问最新值
const shouldStopRef = useRef(false);
Expand Down Expand Up @@ -76,6 +75,43 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
);
});

// Infinite scroll (瀑布流按需加载)
const LOAD_BATCH = 50;
const [visibleCount, setVisibleCount] = useState(LOAD_BATCH);
const sentinelRef = useRef<HTMLDivElement | null>(null);

const startIndex = filteredRepositories.length === 0 ? 0 : 1;
const endIndex = Math.min(visibleCount, filteredRepositories.length);
const visibleRepositories = filteredRepositories.slice(0, visibleCount);

// Reset visible count when filters or data change
useEffect(() => {
setVisibleCount(LOAD_BATCH);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedCategory, repositories, filteredRepositories.length]);

// IntersectionObserver to load more on demand
useEffect(() => {
const node = sentinelRef.current;
if (!node) return;

const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (entry.isIntersecting) {
setVisibleCount((count) => {
if (count >= filteredRepositories.length) return count;
return Math.min(count + LOAD_BATCH, filteredRepositories.length);
});
}
},
{ root: null, rootMargin: '200px', threshold: 0 }
);

observer.observe(node);
return () => observer.disconnect();
}, [filteredRepositories.length]);

const handleAIAnalyze = async (analyzeUnanalyzedOnly: boolean = false, analyzeFailedOnly: boolean = false) => {
if (!githubToken) {
alert(language === 'zh' ? 'GitHub token 未找到,请重新登录。' : 'GitHub token not found. Please login again.');
Expand Down Expand Up @@ -136,7 +172,7 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
const concurrency = activeConfig.concurrency || 1;

// 并发分析函数
const analyzeRepository = async (repo: Repository, index: number) => {
const analyzeRepository = async (repo: Repository) => {
// 检查是否需要停止
if (shouldStopRef.current) {
return false;
Expand Down Expand Up @@ -201,9 +237,7 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
}

const batch = targetRepos.slice(i, i + concurrency);
const promises = batch.map((repo, batchIndex) =>
analyzeRepository(repo, i + batchIndex)
);
const promises = batch.map((repo) => analyzeRepository(repo));

await Promise.all(promises);

Expand Down Expand Up @@ -434,7 +468,10 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
<div className="text-sm text-gray-500 dark:text-gray-400">
<div className="flex items-center justify-between">
<div>
{t(`显示 ${filteredRepositories.length} 个仓库`, `Showing ${filteredRepositories.length} repositories`)}
{t(
`第 ${startIndex}-${endIndex} / 共 ${filteredRepositories.length} 个仓库`,
`Showing ${startIndex}-${endIndex} of ${filteredRepositories.length} repositories`
)}
{repositories.length !== filteredRepositories.length && (
<span className="ml-2 text-blue-600 dark:text-blue-400">
{t(`(从 ${repositories.length} 个中筛选)`, `(filtered from ${repositories.length})`)}
Expand Down Expand Up @@ -462,17 +499,22 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
</div>
</div>

{/* Repository Grid */}
{/* Repository Grid with consistent card widths */}
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{filteredRepositories.map(repo => (
{visibleRepositories.map(repo => (
<RepositoryCard
key={repo.id}
key={repo.id}
repository={repo}
showAISummary={showAISummary}
searchQuery={useAppStore.getState().searchFilters.query}
/>
))}
Comment on lines +502 to 511

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix stale store read and hook rule violation related to searchFilters.

  • Passing searchQuery={useAppStore.getState().searchFilters.query} won’t update on store changes.
  • Elsewhere (Line 298) useAppStore() is called conditionally inside an if block, violating the Rules of Hooks and risking runtime errors.

Apply these diffs:

Add searchFilters to the top-level selector so it subscribes and is safe to use everywhere:

   const {
     githubToken,
     aiConfigs,
     activeAIConfig,
     isLoading,
     setLoading,
     updateRepository,
     language,
     customCategories,
     analysisProgress,
-    setAnalysisProgress
+    setAnalysisProgress,
+    searchFilters
   } = useAppStore();

Use the subscribed value when rendering cards:

-            searchQuery={useAppStore.getState().searchFilters.query}
+            searchQuery={searchFilters.query}

Remove the conditional hook usage in the empty-state branch and reuse the subscribed searchFilters:

-    const { searchFilters } = useAppStore();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* Repository Grid with consistent card widths */}
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{filteredRepositories.map(repo => (
{visibleRepositories.map(repo => (
<RepositoryCard
key={repo.id}
key={repo.id}
repository={repo}
showAISummary={showAISummary}
searchQuery={useAppStore.getState().searchFilters.query}
/>
))}
{/* Repository Grid with consistent card widths */}
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{visibleRepositories.map(repo => (
<RepositoryCard
key={repo.id}
repository={repo}
showAISummary={showAISummary}
searchQuery={searchFilters.query}
/>
))}
🤖 Prompt for AI Agents
In src/components/RepositoryList.tsx around lines 298 and 502-511, fix a stale
store read and a Rules of Hooks violation: add searchFilters to the top-level
useAppStore selector so the component subscribes to searchFilters (e.g., include
searchFilters in the selector return), replace direct useAppStore.getState()
reads when rendering RepositoryCard with the subscribed searchFilters value, and
remove the conditional call to useAppStore() inside the empty-state branch by
reusing the top-level subscribed searchFilters variable so hooks are only called
unconditionally at the component top.

</div>

{/* Sentinel for on-demand loading */}
{visibleCount < filteredRepositories.length && (
<div ref={sentinelRef} className="h-8" />
)}
</div>
);
};
Loading