diff --git a/app/api/v1/order/benefit/route.ts b/app/api/v1/order/benefit/route.ts index 3f059ab..da5bc94 100644 --- a/app/api/v1/order/benefit/route.ts +++ b/app/api/v1/order/benefit/route.ts @@ -3,7 +3,7 @@ import { config } from '@/lib/server/config' export const dynamic = 'force-dynamic' -export async function GET(request: NextRequest) { +export async function POST(request: NextRequest) { const cookieToken = request.cookies.get('token')?.value const authHeader = request.headers.get('authorization') console.log('[benefit] cookie:', cookieToken ? 'yes' : 'no', '| auth header:', authHeader || 'none') diff --git a/app/api/v1/search/stream/route.ts b/app/api/v1/search/stream/route.ts index 53d2024..11afc4a 100644 --- a/app/api/v1/search/stream/route.ts +++ b/app/api/v1/search/stream/route.ts @@ -290,6 +290,8 @@ export async function POST(request: NextRequest) { const authHeader = request.headers.get('authorization') // cookie token → "Bearer "; otherwise forward Authorization header as-is (e.g. "MR-xxx") const authorization = cookieToken ? `Bearer ${cookieToken}` : authHeader || undefined + console.log("============================00003",process.env.USE_MOCK_SEARCH); + const stream = config.USE_MOCK_SEARCH ? mockSearchStream(query, page, pageSize) : wispaperSearchStream(query, page, pageSize, authorization) diff --git a/components/LoadMoreButton.tsx b/components/LoadMoreButton.tsx index 642affd..9f49544 100644 --- a/components/LoadMoreButton.tsx +++ b/components/LoadMoreButton.tsx @@ -5,21 +5,33 @@ import { useSearch } from '@/hooks/useSearch' import { useT } from '@/i18n' export default function LoadMoreButton() { + const papers = useMatrixStore((s) => s.papers) + const visibleCount = useMatrixStore((s) => s.visibleCount) const hasMore = useMatrixStore((s) => s.hasMore) const currentPage = useMatrixStore((s) => s.currentPage) const incrementPage = useMatrixStore((s) => s.incrementPage) - const { loadMore, isSearching, isExtracting } = useSearch() + const showMore = useMatrixStore((s) => s.showMore) + const { loadMore, startExtractionForVisible, isSearching, isExtracting } = useSearch() const t = useT() - if (!hasMore) return null + // Nothing more to show locally or from backend + const hasHiddenPapers = visibleCount < papers.length + if (!hasHiddenPapers && !hasMore) return null const loading = isSearching || isExtracting const handleClick = () => { if (loading) return - const nextPage = currentPage + 1 - incrementPage() - loadMore(nextPage) + if (hasHiddenPapers) { + // Reveal next 10 locally and extract them + showMore() + startExtractionForVisible() + } else { + // Fetch more from backend + const nextPage = currentPage + 1 + incrementPage() + loadMore(nextPage) + } } return ( diff --git a/components/MatrixTable.tsx b/components/MatrixTable.tsx index 121ac55..a8b8e7e 100644 --- a/components/MatrixTable.tsx +++ b/components/MatrixTable.tsx @@ -23,15 +23,18 @@ const FROZEN_TOTAL = FIXED_COLS_WIDTH.title + FIXED_COLS_WIDTH.year + FIXED_COLS const DYNAMIC_COL_WIDTH = 200 export default function MatrixTable() { - const papers = useMatrixStore((s) => s.papers) + const allPapers = useMatrixStore((s) => s.papers) const columns = useMatrixStore((s) => s.columns) const isExtracting = useMatrixStore((s) => s.isExtracting) const cells = useMatrixStore((s) => s.cells) const newPaperIds = useMatrixStore((s) => s.newPaperIds) const clearNewPaperId = useMatrixStore((s) => s.clearNewPaperId) const totalSearched = useMatrixStore((s) => s.totalSearched) + const visibleCount = useMatrixStore((s) => s.visibleCount) const t = useT() + const papers = useMemo(() => allPapers.slice(0, visibleCount), [allPapers, visibleCount]) + // Notification counter for newly added papers const [notifyCount, setNotifyCount] = useState(0) const [showNotify, setShowNotify] = useState(false) @@ -64,7 +67,7 @@ export default function MatrixTable() { [cells], ) - if (papers.length === 0) { + if (allPapers.length === 0) { return (
@@ -129,7 +132,7 @@ export default function MatrixTable() { )} - {t('statsAdded', { count: papers.length })} + {t('statsAdded', { count: allPapers.length })} diff --git a/components/SearchBar.tsx b/components/SearchBar.tsx index f39ac3d..bb46ef4 100644 --- a/components/SearchBar.tsx +++ b/components/SearchBar.tsx @@ -14,6 +14,7 @@ export interface SearchBarHandle { const SearchBar = forwardRef(function SearchBar(_props, ref) { const [inputValue, setInputValue] = useState('') const [quota, setQuota] = useState<{ used: number; limit: number | null } | null>(null) + const [showLoginModal, setShowLoginModal] = useState(false) const query = useMatrixStore((s) => s.query) const { doSearch, isSearching } = useSearch() const typingRef = useRef | null>(null) @@ -21,11 +22,17 @@ const SearchBar = forwardRef(function SearchBar(_props, ref) { const t = useT() const fetchQuota = useCallback(() => { - fetch(quotaApi.benefit(), { headers: getVisitorHeaders(), credentials: 'include' }) - .then((r) => r.ok ? r.json() : null) + fetch(quotaApi.benefit(), { method: 'POST', headers: getVisitorHeaders(), credentials: 'include' }) + .then((r) => { + if (r.status === 401) { + setShowLoginModal(true) + return null + } + return r.ok ? r.json() : null + }) .then((data) => { if (data) setQuota(data) }) .catch(() => {}) - }, []) + }, [t]) // Fetch quota on mount useEffect(() => { fetchQuota() }, [fetchQuota]) @@ -134,6 +141,29 @@ const SearchBar = forwardRef(function SearchBar(_props, ref) { : t('searchQuota', { used: String(quota.used), limit: String(quota.limit) })}
)} + + {/* Login required modal */} + {showLoginModal && ( +
+
+
+
+ + + +
+

{t('loginRequired')}

+

{t('loginRequiredDesc')}

+ +
+
+
+ )}
) }) diff --git a/docker-compose.yml b/docker-compose.yml index 76a2ee9..7c767d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: dockerfile: Dockerfile container_name: table-search ports: - - "9992:80" + - "9991:80" env_file: - ./.env.local restart: unless-stopped diff --git a/hooks/useSearch.ts b/hooks/useSearch.ts index 29655d8..a5272c2 100644 --- a/hooks/useSearch.ts +++ b/hooks/useSearch.ts @@ -88,6 +88,17 @@ export function useSearch() { let newSessionId = '' const paperIds: string[] = [] const columnIds: string[] = [] + let extractionTriggered = false + + const tryEarlyExtraction = () => { + if (extractionTriggered) return + const { visibleCount } = useMatrixStore.getState() + if (paperIds.length >= visibleCount && columnIds.length > 0) { + extractionTriggered = true + const visiblePaperIds = paperIds.slice(0, visibleCount) + startExtraction(newSessionId, visiblePaperIds, [...columnIds]) + } + } const url = opts?.onboarding ? searchApi.onboarding() : searchApi.stream() const body = opts?.onboarding ? {} : { query, page: 1, page_size: 20 } @@ -111,15 +122,19 @@ export function useSearch() { } addColumn(col) columnIds.push(col.id) + // 收到 column 时检查是否已有足够论文,立即开始抽取 + tryEarlyExtraction() } }, onComplete: (data) => { setIsSearching(false) if (data?.searched) setTotalSearched(data.searched) if (paperIds.length === 0) setHasMore(false) - // Use sorted order from store, not SSE arrival order - const sortedPaperIds = useMatrixStore.getState().papers.map((p) => p.id) - startExtraction(newSessionId, sortedPaperIds, columnIds) + // 如果论文不足 10 篇,在 complete 时触发抽取 + if (!extractionTriggered && paperIds.length > 0 && columnIds.length > 0) { + extractionTriggered = true + startExtraction(newSessionId, paperIds, columnIds) + } }, onError: (err) => { console.error('Search SSE error:', err) @@ -134,8 +149,8 @@ export function useSearch() { const extractColumn = useCallback( async (columnId: string) => { - const { sessionId: sid, papers, columns: allCols } = useMatrixStore.getState() - const pids = papers.map((p) => p.id) + const { sessionId: sid, papers, columns: allCols, visibleCount } = useMatrixStore.getState() + const pids = papers.slice(0, visibleCount).map((p) => p.id) if (!sid || pids.length === 0) return const col = allCols.find((c) => c.id === columnId) @@ -163,6 +178,24 @@ export function useSearch() { [setIsExtracting, updateCell], ) + // ---------- 对新显示的论文触发提取 ---------- + + const startExtractionForVisible = useCallback(() => { + const { sessionId: sid, papers, columns, cells, visibleCount } = useMatrixStore.getState() + if (!sid || columns.length === 0) return + + const columnIds = columns.map((c) => c.id) + // Find visible papers that haven't been extracted yet + const visiblePapers = papers.slice(0, visibleCount) + const unextractedIds = visiblePapers + .filter((p) => !cells.has(`${p.id}_${columnIds[0]}`)) + .map((p) => p.id) + + if (unextractedIds.length > 0) { + startExtraction(sid, unextractedIds, columnIds) + } + }, [startExtraction]) + // ---------- 加载更多 + 回填 ---------- const loadMore = useCallback( @@ -175,7 +208,6 @@ export function useSearch() { searchClientRef.current = client const newPaperIds: string[] = [] - const existingColumnIds = state.columns.map((c) => c.id) await client.connectPost(searchApi.stream(), { query: state.query, @@ -195,13 +227,7 @@ export function useSearch() { setHasMore(false) return } - // 回填:用 store 中排序后的新论文 ID - const sortedNewIds = useMatrixStore.getState().papers - .filter((p) => newPaperIds.includes(p.id)) - .map((p) => p.id) - if (existingColumnIds.length > 0) { - startExtraction(state.sessionId!, sortedNewIds, existingColumnIds) - } + // 不自动提取,等用户点 Load More 显示后再提取 }, onError: (err) => { console.error('Load more error:', err) @@ -209,7 +235,7 @@ export function useSearch() { }, }, getVisitorHeaders()) }, - [addPaper, setIsSearching, setHasMore, startExtraction], + [addPaper, setIsSearching, setHasMore], ) // ---------- 恢复后端 paper cache ---------- @@ -234,5 +260,5 @@ export function useSearch() { [], ) - return { doSearch, extractColumn, loadMore, abortAll, restoreSession, isSearching, isExtracting } + return { doSearch, extractColumn, loadMore, startExtractionForVisible, abortAll, restoreSession, isSearching, isExtracting } } diff --git a/i18n/en.ts b/i18n/en.ts index 0c79503..72f7f0d 100644 --- a/i18n/en.ts +++ b/i18n/en.ts @@ -56,6 +56,11 @@ const en: Messages = { timeHoursAgo: '{count}h ago', timeDaysAgo: '{count}d ago', + // Auth + loginRequired: 'Login Required', + loginRequiredDesc: 'Please log in to access this feature.', + loginButton: 'Go to Login', + // Language toggle langEN: 'EN', langZH: '中文', diff --git a/i18n/types.ts b/i18n/types.ts index fe3abf6..bb9e50e 100644 --- a/i18n/types.ts +++ b/i18n/types.ts @@ -55,6 +55,11 @@ export interface Messages { timeHoursAgo: string // "{count}h ago" timeDaysAgo: string // "{count}d ago" + // Auth + loginRequired: string + loginRequiredDesc: string + loginButton: string + // Language toggle langEN: string langZH: string diff --git a/i18n/zh.ts b/i18n/zh.ts index d58e110..aa434f3 100644 --- a/i18n/zh.ts +++ b/i18n/zh.ts @@ -56,6 +56,11 @@ const zh: Messages = { timeHoursAgo: '{count} 小时前', timeDaysAgo: '{count} 天前', + // Auth + loginRequired: '需要登录', + loginRequiredDesc: '请先登录后再继续使用此功能。', + loginButton: '前往登录', + // Language toggle langEN: 'EN', langZH: '中文', diff --git a/lib/server/nacos.ts b/lib/server/nacos.ts index f511e27..d2f0029 100644 --- a/lib/server/nacos.ts +++ b/lib/server/nacos.ts @@ -60,15 +60,18 @@ function parseEnvContent(content: string): Record { } export async function fetchNacosConfig(): Promise { - const serverAddr = process.env.NACOS_SERVER_ADDR + const serverAddr = process.env.NACOS_ENDPOINT if (!serverAddr) { - console.log('[nacos] NACOS_SERVER_ADDR not set, skipping Nacos config fetch') + console.log('[nacos] NACOS_ENDPOINT not set, skipping Nacos config fetch') return } + const normalizedServerAddr = /^http/i.test(serverAddr) + ? serverAddr + : `http://${serverAddr}` const namespace = process.env.NACOS_NAMESPACE || 'public' - const dataId = process.env.NACOS_DATA_ID || 'table-search' - const group = process.env.NACOS_GROUP || 'DEFAULT_GROUP' + const dataId = process.env.NACOS_DATA_ID || '.env' + const group = process.env.NACOS_GROUP || 'table-search' const username = process.env.NACOS_USERNAME const password = process.env.NACOS_PASSWORD @@ -76,7 +79,7 @@ export async function fetchNacosConfig(): Promise { // Authenticate if credentials are provided let accessToken: string | undefined if (username && password) { - accessToken = await getAccessToken(serverAddr, username, password) + accessToken = await getAccessToken(normalizedServerAddr, username, password) } // Build config request URL @@ -89,7 +92,7 @@ export async function fetchNacosConfig(): Promise { params.set('accessToken', accessToken) } - const url = `${serverAddr}/nacos/v1/cs/configs?${params.toString()}` + const url = `${normalizedServerAddr}/nacos/v1/cs/configs?${params.toString()}` const res = await fetch(url) if (!res.ok) { @@ -104,7 +107,7 @@ export async function fetchNacosConfig(): Promise { for (const [key, value] of Object.entries(entries)) { process.env[key] = value injected++ - } + } console.log( `[nacos] Loaded config from ${dataId}@${group} (namespace=${namespace}): ` + @@ -113,7 +116,7 @@ export async function fetchNacosConfig(): Promise { console.log(`ENVIRONMENT=${process.env.ENVIRONMENT ?? 'not set'}`) } catch (err) { console.warn( - `[nacos] Failed to fetch config from ${serverAddr} — falling back to env vars:`, + `[nacos] Failed to fetch config from ${normalizedServerAddr} — falling back to env vars:`, err instanceof Error ? err.message : err, ) } diff --git a/stores/useMatrixStore.ts b/stores/useMatrixStore.ts index befd65e..47570fe 100644 --- a/stores/useMatrixStore.ts +++ b/stores/useMatrixStore.ts @@ -26,6 +26,7 @@ interface MatrixStore { hasMore: boolean newPaperIds: Set totalSearched: number + visibleCount: number // 搜索 setSessionId: (id: string) => void @@ -48,6 +49,7 @@ interface MatrixStore { // 分页 incrementPage: () => void setHasMore: (v: boolean) => void + showMore: () => void // 恢复 hydrateFromProject: (data: HydrateData) => void @@ -70,6 +72,7 @@ export const useMatrixStore = create((set, get) => ({ hasMore: true, newPaperIds: new Set(), totalSearched: 0, + visibleCount: 10, setSessionId: (sessionId) => set({ sessionId }), @@ -86,27 +89,14 @@ export const useMatrixStore = create((set, get) => ({ hasMore: true, newPaperIds: new Set(), totalSearched: 0, + visibleCount: 10, }), addPaper: (paper) => set((s) => { - const score = paper.score ?? 0 - const papers = [...s.papers] - // Binary search for descending score insertion - let lo = 0 - let hi = papers.length - while (lo < hi) { - const mid = (lo + hi) >>> 1 - if ((papers[mid].score ?? 0) >= score) { - lo = mid + 1 - } else { - hi = mid - } - } - papers.splice(lo, 0, paper) const newPaperIds = new Set(s.newPaperIds) newPaperIds.add(paper.id) - return { papers, newPaperIds } + return { papers: [...s.papers, paper], newPaperIds } }), clearNewPaperId: (id) => @@ -164,6 +154,8 @@ export const useMatrixStore = create((set, get) => ({ setHasMore: (hasMore) => set({ hasMore }), + showMore: () => set((s) => ({ visibleCount: s.visibleCount + 10 })), + hydrateFromProject: (data) => set({ sessionId: data.sessionId, @@ -177,6 +169,7 @@ export const useMatrixStore = create((set, get) => ({ currentPage: 1, hasMore: true, newPaperIds: new Set(), + visibleCount: data.papers.length, }), reset: () => @@ -192,5 +185,6 @@ export const useMatrixStore = create((set, get) => ({ hasMore: true, newPaperIds: new Set(), totalSearched: 0, + visibleCount: 10, }), }))