import { discoveryChannelName, discoveryPlatformName } from '../i18n/discoveryNames'; import { useT } from '../i18n/useT'; import type { AppLanguage } from '../i18n/languages'; import { Button } from './ui/button'; import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react'; import { RefreshCw, TrendingUp, Bot, Check, Loader2, Rocket, Tag, Search, Crown, Filter, ChevronDown, Globe, X, Calendar, Newspaper, Users } from 'lucide-react'; import { SiAndroid, SiApple, SiLinux, SiX, SiTelegram } from '@icons-pack/react-simple-icons'; import { SiWindows } from './SiWindows'; import { useAppStore } from '../store/useAppStore'; import { useDiscoveryActions } from '../features/discovery/hooks/useDiscoveryActions'; import { useWebScrapeSupport } from '../hooks/useWebScrapeSupport'; import { DiscoverySidebar } from './DiscoverySidebar'; import { DiscoveryChannelMenu } from './DiscoveryChannelMenu'; import { TrendingHistoryPanel } from '../features/discovery/components/TrendingHistoryPanel'; import { useTrendingSnapshotCapture } from '../features/discovery/hooks/useTrendingSnapshotCapture'; import { SubscriptionRepoCard } from './SubscriptionRepoCard'; import { CodeSearchView } from './CodeSearchView'; import { SortAlgorithmTooltip } from './SortAlgorithmTooltip'; import { ScrollToBottom } from './ScrollToBottom'; import { XTweetSettingsModal } from './XTweetSettingsModal'; import { TelegramSettingsModal } from './TelegramSettingsModal'; import { Input } from './ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger } from './ui/dropdown-menu'; import type { DiscoveryChannel, DiscoveryChannelId, DiscoveryChannelIcon, DiscoveryPlatform, ProgrammingLanguage, SortBy, SortOrder, TopicCategory, TrendingTimeRange } from '../types'; const discoveryChannelIconMap: Record = { trending: , rocket: , star: , tag: , tweet: , telegram: , weekly: , search: , }; const discoveryChannelStyleMap: Record = { trending: { gradient: 'from-muted to-muted/60 dark:from-muted/40 dark:to-muted/20', shadow: 'shadow-subtle', largeIcon: , }, rocket: { gradient: 'from-muted to-muted/60 dark:from-muted/40 dark:to-muted/20', shadow: 'shadow-subtle', largeIcon: , }, star: { gradient: 'from-muted to-muted/60 dark:from-muted/40 dark:to-muted/20', shadow: 'shadow-subtle', largeIcon: , }, tag: { gradient: 'from-muted to-muted/60 dark:from-muted/40 dark:to-muted/20', shadow: 'shadow-subtle', largeIcon: , }, tweet: { gradient: 'from-muted to-muted/60 dark:from-muted/40 dark:to-muted/20', shadow: 'shadow-subtle', largeIcon: , }, telegram: { gradient: 'from-muted to-muted/60 dark:from-muted/40 dark:to-muted/20', shadow: 'shadow-subtle', largeIcon: , }, weekly: { gradient: 'from-muted to-muted/60 dark:from-muted/40 dark:to-muted/20', shadow: 'shadow-subtle', largeIcon: , }, search: { gradient: 'from-muted to-muted/60 dark:from-muted/40 dark:to-muted/20', shadow: 'shadow-subtle', largeIcon: , }, }; interface MobileTabNavProps { channels: { id: DiscoveryChannelId; name: string; nameEn: string; icon: React.ReactNode }[]; allChannels: DiscoveryChannel[]; selectedChannel: DiscoveryChannelId; onChannelSelect: (channel: DiscoveryChannelId) => void; onToggleChannel: (channel: DiscoveryChannelId) => void; language: AppLanguage; } const MobileTabNav: React.FC = ({ channels, allChannels, selectedChannel, onChannelSelect, onToggleChannel, language }) => { const scrollContainerRef = useRef(null); const tabRefs = useRef>(new Map()); const [indicatorStyle, setIndicatorStyle] = useState({ translateX: 0, width: 0 }); const isScrollingRef = useRef(false); const scrollTimeoutRef = useRef | null>(null); const rafRef = useRef(null); const updateIndicator = useCallback(() => { if (rafRef.current) { cancelAnimationFrame(rafRef.current); } rafRef.current = requestAnimationFrame(() => { const activeButton = tabRefs.current.get(selectedChannel); if (activeButton && scrollContainerRef.current) { const container = scrollContainerRef.current; const translateX = activeButton.offsetLeft - container.scrollLeft; const width = activeButton.offsetWidth; setIndicatorStyle({ translateX, width }); } }); }, [selectedChannel]); const scrollToActiveTab = useCallback(() => { const activeButton = tabRefs.current.get(selectedChannel); if (activeButton && scrollContainerRef.current) { const container = scrollContainerRef.current; const scrollLeft = activeButton.offsetLeft - (container.offsetWidth / 2) + (activeButton.offsetWidth / 2); container.scrollTo({ left: Math.max(0, scrollLeft), behavior: 'smooth', }); } }, [selectedChannel]); useEffect(() => { updateIndicator(); }, [updateIndicator, channels]); useEffect(() => { scrollToActiveTab(); const timer = setTimeout(() => { updateIndicator(); }, 350); return () => clearTimeout(timer); }, [selectedChannel, scrollToActiveTab, updateIndicator]); const handleScroll = useCallback(() => { if (!isScrollingRef.current) { isScrollingRef.current = true; } updateIndicator(); if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } scrollTimeoutRef.current = setTimeout(() => { isScrollingRef.current = false; }, 150); }, [updateIndicator]); useEffect(() => { return () => { if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } if (rafRef.current) { cancelAnimationFrame(rafRef.current); } }; }, []); return (
{channels.map((channel) => ( ))}
{/* Active indicator */}
); }; interface PlatformFilterProps { platform: DiscoveryPlatform; onPlatformChange: (platform: DiscoveryPlatform) => void; language: AppLanguage; } const PlatformFilter: React.FC = ({ platform, onPlatformChange, language }) => { const t = useT('discovery'); const platforms: { id: DiscoveryPlatform; name: string; nameEn: string; icon: React.ReactNode }[] = [ { id: 'All', name: '全部平台', nameEn: 'All Platforms', icon: }, { id: 'Android', name: 'Android', nameEn: 'Android', icon: }, { id: 'Macos', name: 'macOS', nameEn: 'macOS', icon: }, { id: 'Windows', name: 'Windows', nameEn: 'Windows', icon: }, { id: 'Linux', name: 'Linux', nameEn: 'Linux', icon: }, ]; const selectedPlatform = platforms.find(p => p.id === platform); return ( onPlatformChange(value as DiscoveryPlatform)}> {platforms.map((p) => ( {p.icon} {discoveryPlatformName(p, language)} ))} ); }; interface CustomSelectOption { value: string; label: string; icon?: React.ReactNode; } interface CustomSelectProps { value: string; onChange: (value: string) => void; options: CustomSelectOption[]; className?: string; dropdownClassName?: string; ariaLabel?: string; /** 禁用整个下拉(语义搜索模式下排序无意义时使用) */ disabled?: boolean; } const CustomSelect: React.FC = ({ value, onChange, options, className = '', dropdownClassName = '', ariaLabel, disabled = false, }) => { const selectedOption = options.find(o => o.value === value); return ( {options.map((option) => ( {option.icon && {option.icon}} {option.label} ))} ); }; interface LoadMoreButtonProps { onLoadMore: () => void; isLoading: boolean; hasMore: boolean; totalCount: number; language: AppLanguage; } /** 语义搜索四阶段(顺序即管道顺序),UI 据此渲染进度指示。 */ const SEMANTIC_PHASE_ORDER = ['expanding', 'recalling', 'enriching', 'reranking'] as const; /** 语义搜索过程状态:已完成打勾、当前阶段旋转 + 脉冲 + 明细(如补全计数)、未到阶段弱化。 */ const SemanticSearchProgress: React.FC<{ phase: string; detail: string | null }> = ({ phase, detail }) => { const t = useT('discovery'); const currentIndex = SEMANTIC_PHASE_ORDER.indexOf(phase as typeof SEMANTIC_PHASE_ORDER[number]); return (
{SEMANTIC_PHASE_ORDER.map((step, index) => { const done = currentIndex > index; const active = currentIndex === index; return ( {done ? : active ? : } {t(`discoveryView.search-phase-${step}`)} {active && detail && ({detail})} ); })}
); }; const LoadMoreButton: React.FC = ({ onLoadMore, isLoading, hasMore, totalCount, }) => { const t = useT('discovery'); if (!hasMore) { return (
{t('discoveryView.all-loaded')}
{t('discoveryView.total-totalcount-items', { totalCount: totalCount })}
); } return (
); }; interface DataStatsProps { currentCount: number; totalCount: number; language: AppLanguage; } const DataStats: React.FC = ({ currentCount, totalCount }) => { const t = useT('discovery'); return (
{totalCount > 0 && currentCount < totalCount ? t('discoveryView.items-count-with-total', { currentCount, totalCount }) : t('discoveryView.items-count', { currentCount })}
); }; export const DiscoveryView: React.FC = React.memo(() => { const scrollContainerRef = useRef(null); const toggleDiscoveryChannel = useAppStore(state => state.toggleDiscoveryChannel); const { supported: scrapeSupport, settled: scrapeSupportSettled } = useWebScrapeSupport(); const { githubToken, language, discoveryChannels, discoveryRepos, discoveryLastRefresh, discoveryIsLoading, discoveryIsLoadingMore, discoveryLoadMoreError, selectedDiscoveryChannel, setSelectedDiscoveryChannel, setDiscoveryScrollPosition, analysisProgress, discoveryPlatform, setDiscoveryPlatform, discoveryLanguage, setDiscoveryLanguage, discoverySortBy, setDiscoverySortBy, discoverySortOrder, setDiscoverySortOrder, discoverySearchQuery, setDiscoverySearchQuery, discoverySearchMode, setDiscoverySearchMode, discoverySearchPhase, discoverySearchPhaseDetail, discoverySearchEmptyReason, discoverySelectedTopic, setDiscoverySelectedTopic, discoveryHasMore, discoveryNextPage, discoveryTotalCount, trendingTimeRange, setTrendingTimeRange, weeklyOnlyCollected, setWeeklyOnlyCollected, weeklySyncStatus, xTweetFollows, xTweetAuth, xTweetAuthRevision, xTweetSyncStatus, telegramFollows, telegramSyncStatus, t, isAnalyzing, refreshChannel, handleAnalyzePage, handleAbortAnalysis, } = useDiscoveryActions(scrollContainerRef); const [searchInput, setSearchInput] = useState(discoverySearchQuery); const sidebarRef = useRef(null); // X 推文频道:关注列表设置弹窗 const [tweetSettingsOpen, setTweetSettingsOpen] = useState(false); // Telegram 频道:关注频道设置弹窗 const [telegramSettingsOpen, setTelegramSettingsOpen] = useState(false); // 工具栏显示状态 const [isToolbarVisible, setIsToolbarVisible] = useState(true); const lastScrollY = useRef(0); const scrollTimeoutRef = useRef | null>(null); // 用于在频道切换时直接读取最新滚动位置,避免订阅整个 map 导致 effect 重跑 const discoveryScrollPositionsRef = useRef>({}); // 用于记录最近一次自动拉取的频道,防止空频道无限循环拉取 const autoFetchChannelRef = useRef(null); const appliedTopicRef = useRef<{ topic: string | null; platform: DiscoveryPlatform } | null>(null); const isAnalyzingThisChannel = isAnalyzing && analysisProgress.total > 0; const isDesktopSafeMode = useMemo(() => { if (typeof window === 'undefined') return false; return window.location.protocol === 'file:' || navigator.userAgent.includes('Electron'); }, []); const safeDiscoveryChannels = useMemo(() => { if (!Array.isArray(discoveryChannels)) return []; // 可用性未确定(启动探测中)先全部显示,避免 Web+后端用户的可用频道闪烁; // 纯浏览器模式(无桌面壳、无后端)无法抓取 X 推文与 Telegram,落定后直接隐藏 if (!scrapeSupportSettled || scrapeSupport) return discoveryChannels.filter(Boolean); return discoveryChannels.filter(channel => Boolean(channel) && channel.id !== 'x-tweet' && channel.id !== 'telegram'); }, [discoveryChannels, scrapeSupport, scrapeSupportSettled]); // 抓取支持落定后(纯浏览器模式):隐藏频道移出列表,确保仍有一个可见启用 // 频道(水合期间不做该决策,必要时在此恢复「趋势」),并把选中的被隐藏频 // 道回退到可见频道 useEffect(() => { if (!scrapeSupportSettled || scrapeSupport) return; const visibleEnabledExists = safeDiscoveryChannels.some(ch => ch.enabled && ch.id !== 'code-search'); if (!visibleEnabledExists) { if (!safeDiscoveryChannels.find(ch => ch.id === 'trending')?.enabled) toggleDiscoveryChannel('trending'); setSelectedDiscoveryChannel('trending'); return; } if (selectedDiscoveryChannel === 'x-tweet' || selectedDiscoveryChannel === 'telegram') { const fallback = safeDiscoveryChannels.find(ch => ch.enabled && ch.id !== 'code-search')?.id ?? 'trending'; setSelectedDiscoveryChannel(fallback); } }, [scrapeSupport, scrapeSupportSettled, selectedDiscoveryChannel, safeDiscoveryChannels, toggleDiscoveryChannel, setSelectedDiscoveryChannel]); // 获取当前频道的所有仓库 const allRepos = useMemo( () => (discoveryRepos && discoveryRepos[selectedDiscoveryChannel]) || [], [discoveryRepos, selectedDiscoveryChannel] ); const currentLastRefresh = discoveryLastRefresh?.[selectedDiscoveryChannel] ?? null; // Trending 快照(开发守则 §7):只记 trending 频道,每天同一「周期 × 平台」保留一份 useTrendingSnapshotCapture( discoveryRepos?.trending ?? [], selectedDiscoveryChannel === 'trending', trendingTimeRange, discoveryPlatform, discoveryLastRefresh?.trending ?? null, ); // 从 store 获取当前频道的总数量 const currentTotalCount = discoveryTotalCount?.[selectedDiscoveryChannel] ?? 0; const currentIsLoading = discoveryIsLoading?.[selectedDiscoveryChannel] ?? false; const currentIsLoadingMore = discoveryIsLoadingMore?.[selectedDiscoveryChannel] ?? false; const currentLoadMoreError = discoveryLoadMoreError?.[selectedDiscoveryChannel] ?? null; const currentChannel = safeDiscoveryChannels.find(ch => ch.id === selectedDiscoveryChannel); const currentChannelIcon = currentChannel?.icon || 'trending'; const currentChannelStyle = discoveryChannelStyleMap[currentChannelIcon] || discoveryChannelStyleMap.trending; const currentChannelIconNode = discoveryChannelIconMap[currentChannelIcon] || discoveryChannelIconMap.trending; // 周刊同步/补全进度文案(工具栏与空态加载两处共用) const weeklyStatusText = weeklySyncStatus ? (weeklySyncStatus.phase === 'syncing' ? t('discoveryView.syncing-weekly-submissions-v1-scanned', { v1: weeklySyncStatus.current }) : t('discoveryView.fetching-repo-details-v1-v2', { v1: weeklySyncStatus.current, v2: weeklySyncStatus.total })) : null; // X 推文同步/补全进度文案 const xTweetStatusText = xTweetSyncStatus ? (xTweetSyncStatus.phase === 'syncing' ? t('discoveryView.fetching-timelines-v1-v2', { v1: xTweetSyncStatus.current, v2: xTweetSyncStatus.total }) : t('discoveryView.fetching-repo-details-v1-v2', { v1: xTweetSyncStatus.current, v2: xTweetSyncStatus.total })) : null; // Telegram 频道同步/补全进度文案 const telegramStatusText = telegramSyncStatus ? (telegramSyncStatus.phase === 'syncing' ? t('discoveryView.fetching-channel-messages-v1-v2', { v1: telegramSyncStatus.current, v2: telegramSyncStatus.total }) : t('discoveryView.fetching-repo-details-v1-v2', { v1: telegramSyncStatus.current, v2: telegramSyncStatus.total })) : null; // 切换频道时恢复滚动位置,并自动加载空数据 useEffect(() => { // 恢复当前频道的滚动位置(从 ref 读取最新值,避免订阅整个 map) const savedPosition = discoveryScrollPositionsRef.current[selectedDiscoveryChannel] || 0; window.scrollTo({ top: savedPosition, behavior: 'auto' }); // 取消持久化后,首次打开或切换到空频道时自动加载(代码搜索走本地实时请求,不参与自动拉取) const hasRepos = useAppStore.getState().discoveryRepos[selectedDiscoveryChannel]?.length > 0; const isLoading = useAppStore.getState().discoveryIsLoading[selectedDiscoveryChannel]; if (selectedDiscoveryChannel !== 'topic' && selectedDiscoveryChannel !== 'code-search' && selectedDiscoveryChannel !== 'search' && !hasRepos && !isLoading && autoFetchChannelRef.current !== selectedDiscoveryChannel) { autoFetchChannelRef.current = selectedDiscoveryChannel; refreshChannel(selectedDiscoveryChannel, 1, false); } }, [selectedDiscoveryChannel, refreshChannel]); useEffect(() => { if (selectedDiscoveryChannel === 'search' && discoverySearchQuery.trim()) { refreshChannel('search', 1, false); } }, [selectedDiscoveryChannel, discoverySearchQuery, refreshChannel]); // 趋势时间范围改变时刷新数据 useEffect(() => { if (selectedDiscoveryChannel === 'trending' && trendingTimeRange) { refreshChannel('trending', 1, false); } }, [trendingTimeRange, selectedDiscoveryChannel, refreshChannel]); // 周刊收录过滤器切换时重建列表:只在偏好值真正变化时触发(挂载/切频道不触发, // 避免与空频道自动加载 effect 双重调用导致增量同步被中止重启) const prevWeeklyOnlyCollectedRef = useRef(weeklyOnlyCollected); useEffect(() => { if (prevWeeklyOnlyCollectedRef.current === weeklyOnlyCollected) return; prevWeeklyOnlyCollectedRef.current = weeklyOnlyCollected; if (selectedDiscoveryChannel === 'weekly') { refreshChannel('weekly', 1, false); } }, [weeklyOnlyCollected, selectedDiscoveryChannel, refreshChannel]); // X 推文关注列表/鉴权变化时重建列表:只在集合真正变化时触发(挂载/切频道 // 不触发,避免与空频道自动加载 effect 双重调用导致同步被中止重启) // 修订号区分"不同 Cookie 之间的切换",布尔值做不到 const xTweetFollowsSignature = `${xTweetFollows.map(follow => follow.handle.toLowerCase()).sort().join(',')}|${xTweetAuth !== null}|${xTweetAuthRevision ?? 0}`; const prevXTweetFollowsRef = useRef(xTweetFollowsSignature); useEffect(() => { if (prevXTweetFollowsRef.current === xTweetFollowsSignature) return; prevXTweetFollowsRef.current = xTweetFollowsSignature; if (selectedDiscoveryChannel === 'x-tweet') { refreshChannel('x-tweet', 1, false); } }, [xTweetFollowsSignature, selectedDiscoveryChannel, refreshChannel]); // Telegram 关注列表变化时重建列表:只在关注集真正变化时触发(挂载/切频道 // 不触发,避免与空频道自动加载 effect 双重调用导致同步被中止重启) const telegramFollowsSignature = telegramFollows.map(follow => follow.channel.toLowerCase()).sort().join(','); const prevTelegramFollowsRef = useRef(telegramFollowsSignature); useEffect(() => { if (prevTelegramFollowsRef.current === telegramFollowsSignature) return; prevTelegramFollowsRef.current = telegramFollowsSignature; if (selectedDiscoveryChannel === 'telegram') { refreshChannel('telegram', 1, false); } }, [telegramFollowsSignature, selectedDiscoveryChannel, refreshChannel]); // 主题改变时刷新数据 useEffect(() => { if (selectedDiscoveryChannel !== 'topic' || !githubToken) return; const applied = appliedTopicRef.current; if (applied?.topic === discoverySelectedTopic && applied.platform === discoveryPlatform) return; appliedTopicRef.current = { topic: discoverySelectedTopic, platform: discoveryPlatform }; refreshChannel('topic', 1, false); }, [githubToken, discoverySelectedTopic, discoveryPlatform, selectedDiscoveryChannel, refreshChannel]); const formatLastRefresh = useCallback((timestamp: string | null) => { if (!timestamp) return ''; const date = new Date(timestamp); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMin = Math.floor(diffMs / (1000 * 60)); if (diffMin < 1) return t('discoveryView.just-now'); if (diffMin < 60) return t('discoveryView.diffmin-m-ago', { diffMin: diffMin }); const diffHours = Math.floor(diffMin / 60); if (diffHours < 24) return t('discoveryView.diffhours-h-ago', { diffHours: diffHours }); return date.toLocaleDateString(); }, [t]); // 处理滚动事件:保存滚动位置、控制工具栏显示、控制侧栏固定 const handleScroll = useCallback(() => { // 获取页面滚动位置(支持window滚动和元素滚动) const currentScrollY = window.scrollY || window.pageYOffset || 0; // 控制工具栏显示/隐藏 if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } // 向上滚动或接近顶部时显示工具栏,向下滚动时隐藏 if (currentScrollY < 50 || currentScrollY < lastScrollY.current) { setIsToolbarVisible(true); } else if (currentScrollY > lastScrollY.current + 10) { setIsToolbarVisible(false); } lastScrollY.current = currentScrollY; // 滚动停止后重新显示工具栏 scrollTimeoutRef.current = setTimeout(() => { setIsToolbarVisible(true); }, 1500); }, []); // 监听 window 滚动事件 useEffect(() => { window.addEventListener('scroll', handleScroll, { passive: true }); return () => { window.removeEventListener('scroll', handleScroll); if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } }; }, [handleScroll]); const handleSearch = useCallback(() => { if (selectedDiscoveryChannel === 'search') { const query = searchInput.trim(); if (!query) return; if (query === discoverySearchQuery) refreshChannel('search', 1, false); else setDiscoverySearchQuery(query); } }, [selectedDiscoveryChannel, discoverySearchQuery, searchInput, setDiscoverySearchQuery, refreshChannel]); // 搜索模式切换:关键词 ↔ AI 语义。这里只写 store;已有查询时的自动重搜由 // useDiscoveryActions 的模式变化 effect 触发(该 effect 在 latestStateRef 同步 // 之后执行,refreshChannel 读到的已是新模式,请求签名校验才能通过) const handleSearchModeChange = useCallback((value: string) => { setDiscoverySearchMode(value as 'keyword' | 'semantic'); }, [setDiscoverySearchMode]); const handleLoadMore = useCallback(async () => { if (!discoveryHasMore[selectedDiscoveryChannel]) { return; } if (currentIsLoading) { return; } const nextPage = discoveryNextPage[selectedDiscoveryChannel]; if (!nextPage) { return; } await refreshChannel(selectedDiscoveryChannel, nextPage, true); }, [ discoveryHasMore, discoveryNextPage, selectedDiscoveryChannel, currentIsLoading, refreshChannel ]); const refreshAll = useCallback(async () => { const enabledChannels = safeDiscoveryChannels.filter(ch => ch.enabled && ch.id !== 'code-search'); for (const channel of enabledChannels) { await refreshChannel(channel.id, 1, false); } }, [safeDiscoveryChannels, refreshChannel]); const mobileChannels = useMemo(() => { return safeDiscoveryChannels .filter(ch => ch.enabled) .map(ch => ({ ...ch, icon: discoveryChannelIconMap[ch.icon] || , })); }, [safeDiscoveryChannels]); return (
{/* Mobile Tab Navigation */} { if (channel === selectedDiscoveryChannel) { return; } const scrollTop = window.scrollY; discoveryScrollPositionsRef.current[selectedDiscoveryChannel] = scrollTop; setDiscoveryScrollPosition(selectedDiscoveryChannel, scrollTop); setSelectedDiscoveryChannel(channel); }} language={language} />
{/* 顶部工具栏 - 随滚动显示/隐藏 */}
{/* 第一行:标题和刷新按钮 */}
{currentChannelIconNode}

{language === 'zh' ? currentChannel?.name : discoveryChannelName(currentChannel ?? { id: 'search', name: '仓库搜索', nameEn: 'Repo Search' }, language)}

{currentLastRefresh && (

{t('discoveryView.updated')} {formatLastRefresh(currentLastRefresh)}

)}
{selectedDiscoveryChannel !== 'code-search' && (
{selectedDiscoveryChannel === 'hot-release' && (
{t('discoveryView.each-refresh-shows-different-content')}
)}
)}
{/* 第二行:筛选和操作按钮(代码搜索频道使用自有工具条,此处隐藏仓库维度控件) */} {selectedDiscoveryChannel !== 'code-search' && (
{selectedDiscoveryChannel === 'trending' && (
)} {selectedDiscoveryChannel === 'topic' && ( )} {selectedDiscoveryChannel === 'weekly' && ( )} {selectedDiscoveryChannel === 'weekly' && weeklyStatusText && ( {weeklyStatusText} )} {selectedDiscoveryChannel === 'x-tweet' && xTweetStatusText && ( {xTweetStatusText} )} {selectedDiscoveryChannel === 'x-tweet' && ( )} {selectedDiscoveryChannel === 'telegram' && telegramStatusText && ( {telegramStatusText} )} {selectedDiscoveryChannel === 'telegram' && ( )}
{!currentChannel?.sourceUrl && selectedDiscoveryChannel !== 'weekly' && selectedDiscoveryChannel !== 'x-tweet' && selectedDiscoveryChannel !== 'telegram' && ( )} {isAnalyzingThisChannel ? (
0 ? `${Math.min((analysisProgress.current / analysisProgress.total) * 100, 100)}%` : '0%', }} />
{analysisProgress.current}/{analysisProgress.total}
) : ( )}
)}
{/* 内容区域 */}
{selectedDiscoveryChannel === 'code-search' && } {selectedDiscoveryChannel !== 'code-search' && ( <> {selectedDiscoveryChannel === 'search' && (
{/* 搜索模式下拉:关键词(默认,行为不变)/ AI 语义(全网仓库) */} }, { value: 'semantic', label: t('discoveryView.search-mode-semantic'), icon: }, ]} />
setSearchInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} placeholder={discoverySearchMode === 'semantic' ? t('discoveryView.search-mode-semantic-placeholder') : t('discoveryView.search-repositories-2')} className="ui-field h-auto w-full py-2.5 pl-10 pr-4 text-foreground dark:text-foreground" />
{discoverySearchMode === 'semantic' && (

{t('discoveryView.search-mode-hint')}

)} {discoverySearchPhase && ( )}
{/* 语义模式由 AI 按相关性排序,排序选项无意义,禁用 */} setDiscoverySortBy(value as SortBy)} ariaLabel={t('discoveryView.discovery-sort-field')} disabled={discoverySearchMode === 'semantic'} options={[ { value: 'BestMatch', label: t('discoveryView.best-match') }, { value: 'MostStars', label: t('discoveryView.most-stars') }, { value: 'MostForks', label: t('discoveryView.most-forks') }, ]} /> setDiscoverySortOrder(value as SortOrder)} ariaLabel={t('discoveryView.discovery-sort-order')} disabled={discoverySearchMode === 'semantic'} options={[ { value: 'Descending', label: t('discoveryView.descending') }, { value: 'Ascending', label: t('discoveryView.ascending') }, ]} />
)} {currentIsLoading && allRepos.length === 0 && (

{t('discoveryView.fetching-data')}

{selectedDiscoveryChannel === 'weekly' && weeklyStatusText ? weeklyStatusText : selectedDiscoveryChannel === 'x-tweet' && xTweetStatusText ? xTweetStatusText : selectedDiscoveryChannel === 'telegram' && telegramStatusText ? telegramStatusText : t('discoveryView.waiting-for-github-api-response')}

)} {!currentIsLoading && allRepos.length === 0 && (
{selectedDiscoveryChannel === 'search' ? ( <> {isDesktopSafeMode ? (
{currentChannelIconNode}
) : (
{currentChannelStyle.largeIcon}
)}

{t('discoveryView.repo-search')}

{/* 仅当语义搜索真的给出了空态原因(召回为空/AI 判空)才显示语义空态文案: 语义模式但回退到关键词搜索(如 AI 配置不完整)时 reason 为 null, 此时显示通用关键词文案,不能宣称"语义召回为空" */} {discoverySearchMode === 'semantic' && discoverySearchQuery.trim() && discoverySearchEmptyReason ? (discoverySearchEmptyReason === 'ai_empty' ? t('discoveryView.semantic-empty-ai') : t('discoveryView.semantic-empty-recall')) : t('discoveryView.enter-keywords-to-search-github-repositories')}

) : selectedDiscoveryChannel === 'x-tweet' ? ( <> {isDesktopSafeMode ? (
{currentChannelIconNode}
) : (
{currentChannelStyle.largeIcon}
)}

{t('discoveryView.x-tweets')}

{t('discoveryView.fetches-github-projects-from-followed-accounts-l')}

) : selectedDiscoveryChannel === 'telegram' ? ( <> {isDesktopSafeMode ? (
{currentChannelIconNode}
) : (
{currentChannelStyle.largeIcon}
)}

{t('discoveryView.telegram-channels')}

{t('discoveryView.fetches-github-projects-from-followed-channels-m')}

) : selectedDiscoveryChannel === 'weekly' ? ( <> {isDesktopSafeMode ? (
{currentChannelIconNode}
) : (
{currentChannelStyle.largeIcon}
)}

{t('discoveryView.ruanyifeng-weekly')}

{t('discoveryView.sync-open-source-submissions-from-the-weekly-ear')}

) : ( <> {isDesktopSafeMode ? (
{currentChannelIconNode}
) : (
{currentChannelStyle.largeIcon}
)}

{t('discoveryView.no-data-yet')}

{t('discoveryView.click-refresh-to-fetch-latest-rankings')}

)}
)} {selectedDiscoveryChannel === 'trending' && ( )} {allRepos.length > 0 && (
{allRepos.map((repo, index) => (
))}
)} {currentIsLoadingMore && (
{t('discoveryView.loading-more')}
)} {currentLoadMoreError && (
{currentLoadMoreError}
)} {/* Page Info */} {!currentIsLoading && allRepos.length > 0 && (
{t('discoveryView.total')} {allRepos.length} {t('discoveryView.items')}
)} {/* Load More Button */} {!currentIsLoading && !currentIsLoadingMore && allRepos.length > 0 && ( )} )}
{/* 滚动到底部按钮 */} {/* X 推文频道关注列表设置 */} setTweetSettingsOpen(false)} /> {/* Telegram 频道关注列表设置 */} setTelegramSettingsOpen(false)} />
); }); DiscoveryView.displayName = 'DiscoveryView';