Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion app/api/v1/order/benefit/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 2 additions & 0 deletions app/api/v1/search/stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,8 @@ export async function POST(request: NextRequest) {
const authHeader = request.headers.get('authorization')
// cookie token → "Bearer <token>"; 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)
Expand Down
22 changes: 17 additions & 5 deletions components/LoadMoreButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
9 changes: 6 additions & 3 deletions components/MatrixTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -64,7 +67,7 @@ export default function MatrixTable() {
[cells],
)

if (papers.length === 0) {
if (allPapers.length === 0) {
return (
<div className="text-center py-16">
<div className="inline-flex items-center gap-2 text-gray-400">
Expand Down Expand Up @@ -129,7 +132,7 @@ export default function MatrixTable() {
)}
<span className="flex items-center gap-1">
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
{t('statsAdded', { count: papers.length })}
{t('statsAdded', { count: allPapers.length })}
</span>
<span className="w-1 h-1 rounded-full bg-gray-300" />
<span className="flex items-center gap-1">
Expand Down
36 changes: 33 additions & 3 deletions components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,25 @@ export interface SearchBarHandle {
const SearchBar = forwardRef<SearchBarHandle>(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<ReturnType<typeof setTimeout> | null>(null)
const prevSearchingRef = useRef(false)
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])
Expand Down Expand Up @@ -134,6 +141,29 @@ const SearchBar = forwardRef<SearchBarHandle>(function SearchBar(_props, ref) {
: t('searchQuota', { used: String(quota.used), limit: String(quota.limit) })}
</div>
)}

{/* Login required modal */}
{showLoginModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="bg-white rounded-2xl shadow-2xl p-6 sm:p-8 max-w-sm w-full mx-4 animate-in fade-in zoom-in-95">
<div className="flex flex-col items-center text-center">
<div className="w-12 h-12 rounded-full bg-amber-100 flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m0 0v2m0-2h2m-2 0H10m9.364-7.364A9 9 0 1112 3a9 9 0 017.364 4.636z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t('loginRequired')}</h3>
<p className="text-sm text-gray-500 mb-6">{t('loginRequiredDesc')}</p>
<button
onClick={() => { window.location.href = '/app/search' }}
className="w-full px-4 py-2.5 bg-indigo-600 text-white text-sm font-medium rounded-xl hover:bg-indigo-700 transition-colors"
>
{t('loginButton')}
</button>
</div>
</div>
</div>
)}
</div>
)
})
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
dockerfile: Dockerfile
container_name: table-search
ports:
- "9992:80"
- "9991:80"
env_file:
- ./.env.local
restart: unless-stopped
56 changes: 41 additions & 15 deletions hooks/useSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand All @@ -195,21 +227,15 @@ 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)
setIsSearching(false)
},
}, getVisitorHeaders())
},
[addPaper, setIsSearching, setHasMore, startExtraction],
[addPaper, setIsSearching, setHasMore],
)

// ---------- 恢复后端 paper cache ----------
Expand All @@ -234,5 +260,5 @@ export function useSearch() {
[],
)

return { doSearch, extractColumn, loadMore, abortAll, restoreSession, isSearching, isExtracting }
return { doSearch, extractColumn, loadMore, startExtractionForVisible, abortAll, restoreSession, isSearching, isExtracting }
}
5 changes: 5 additions & 0 deletions i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '中文',
Expand Down
5 changes: 5 additions & 0 deletions i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ const zh: Messages = {
timeHoursAgo: '{count} 小时前',
timeDaysAgo: '{count} 天前',

// Auth
loginRequired: '需要登录',
loginRequiredDesc: '请先登录后再继续使用此功能。',
loginButton: '前往登录',

// Language toggle
langEN: 'EN',
langZH: '中文',
Expand Down
19 changes: 11 additions & 8 deletions lib/server/nacos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,26 @@ function parseEnvContent(content: string): Record<string, string> {
}

export async function fetchNacosConfig(): Promise<void> {
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

try {
// 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
Expand All @@ -89,7 +92,7 @@ export async function fetchNacosConfig(): Promise<void> {
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) {
Expand All @@ -104,7 +107,7 @@ export async function fetchNacosConfig(): Promise<void> {
for (const [key, value] of Object.entries(entries)) {
process.env[key] = value
injected++
}
}

console.log(
`[nacos] Loaded config from ${dataId}@${group} (namespace=${namespace}): ` +
Expand All @@ -113,7 +116,7 @@ export async function fetchNacosConfig(): Promise<void> {
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,
)
}
Expand Down
Loading
Loading