mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-21 21:47:00 +08:00
refactor: 拆分 AI 搜索证据集合 Hook
This commit is contained in:
@@ -9,7 +9,6 @@ import type {
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
AISearchWorkspaceProps,
|
AISearchWorkspaceProps,
|
||||||
EvidenceItem,
|
|
||||||
SearchProgressByStage,
|
SearchProgressByStage,
|
||||||
SearchRange,
|
SearchRange,
|
||||||
SearchScope,
|
SearchScope,
|
||||||
@@ -42,8 +41,7 @@ import { createSearchResultResetState } from './searchState'
|
|||||||
import { useSearchHistory } from './hooks/useSearchHistory'
|
import { useSearchHistory } from './hooks/useSearchHistory'
|
||||||
import { useKnowledgeStatus } from './hooks/useKnowledgeStatus'
|
import { useKnowledgeStatus } from './hooks/useKnowledgeStatus'
|
||||||
import { useExternalProviderConsent } from './hooks/useExternalProviderConsent'
|
import { useExternalProviderConsent } from './hooks/useExternalProviderConsent'
|
||||||
|
import { EVIDENCE_PAGE_SIZE, useEvidenceCollection } from './hooks/useEvidenceCollection'
|
||||||
const EVIDENCE_PAGE_SIZE = 8
|
|
||||||
|
|
||||||
export function AISearchWorkspace({
|
export function AISearchWorkspace({
|
||||||
contacts,
|
contacts,
|
||||||
@@ -64,10 +62,6 @@ export function AISearchWorkspace({
|
|||||||
const [resultQuery, setResultQuery] = useState('')
|
const [resultQuery, setResultQuery] = useState('')
|
||||||
const [stage, setStage] = useState<SearchStage>('idle')
|
const [stage, setStage] = useState<SearchStage>('idle')
|
||||||
const [answer, setAnswer] = useState('')
|
const [answer, setAnswer] = useState('')
|
||||||
const [evidence, setEvidence] = useState<EvidenceItem[]>([])
|
|
||||||
const [evidenceCollection, setEvidenceCollection] = useState<EvidenceItem[]>([])
|
|
||||||
const [visibleEvidenceCount, setVisibleEvidenceCount] = useState(0)
|
|
||||||
const [selectedEvidence, setSelectedEvidence] = useState(0)
|
|
||||||
const [analysisError, setAnalysisError] = useState('')
|
const [analysisError, setAnalysisError] = useState('')
|
||||||
const [messageCount, setMessageCount] = useState(0)
|
const [messageCount, setMessageCount] = useState(0)
|
||||||
const [senderNames, setSenderNames] = useState<Record<string, string>>({})
|
const [senderNames, setSenderNames] = useState<Record<string, string>>({})
|
||||||
@@ -83,12 +77,24 @@ export function AISearchWorkspace({
|
|||||||
const [appLogPath, setAppLogPath] = useState('')
|
const [appLogPath, setAppLogPath] = useState('')
|
||||||
const searchRequestIdRef = useRef('')
|
const searchRequestIdRef = useRef('')
|
||||||
const composerRef = useRef<HTMLTextAreaElement>(null)
|
const composerRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const evidenceCardRefs = useRef(new Map<number, HTMLElement>())
|
const {
|
||||||
const [evidenceFlash, setEvidenceFlash] = useState({ index: -1, nonce: 0 })
|
evidence,
|
||||||
const visibleEvidence = useMemo(
|
setEvidence,
|
||||||
() => evidenceCollection.slice(0, visibleEvidenceCount),
|
evidenceCollection,
|
||||||
[evidenceCollection, visibleEvidenceCount]
|
setEvidenceCollection,
|
||||||
)
|
setVisibleEvidenceCount,
|
||||||
|
selectedEvidence,
|
||||||
|
setSelectedEvidence,
|
||||||
|
visibleEvidence,
|
||||||
|
hasMoreEvidence,
|
||||||
|
evidenceFlash,
|
||||||
|
setEvidenceResult,
|
||||||
|
clearEvidenceCollection,
|
||||||
|
loadMoreEvidence,
|
||||||
|
focusEvidence,
|
||||||
|
jumpToEvidence,
|
||||||
|
setEvidenceCardRef
|
||||||
|
} = useEvidenceCollection({ onOpenEvidence })
|
||||||
|
|
||||||
const {
|
const {
|
||||||
history,
|
history,
|
||||||
@@ -149,10 +155,7 @@ export function AISearchWorkspace({
|
|||||||
const reset = createSearchResultResetState()
|
const reset = createSearchResultResetState()
|
||||||
setAnalysisError(reset.analysisError)
|
setAnalysisError(reset.analysisError)
|
||||||
setAnswer(reset.answer)
|
setAnswer(reset.answer)
|
||||||
setEvidence(reset.evidence)
|
clearEvidenceCollection()
|
||||||
setEvidenceCollection(reset.evidenceCollection)
|
|
||||||
setVisibleEvidenceCount(reset.visibleEvidenceCount)
|
|
||||||
setSelectedEvidence(reset.selectedEvidence)
|
|
||||||
setCachedAt(reset.cachedAt)
|
setCachedAt(reset.cachedAt)
|
||||||
setSearchTrace(reset.searchTrace)
|
setSearchTrace(reset.searchTrace)
|
||||||
setSearchProgress(reset.searchProgress)
|
setSearchProgress(reset.searchProgress)
|
||||||
@@ -160,21 +163,6 @@ export function AISearchWorkspace({
|
|||||||
setSearchDetailsOpen(reset.searchDetailsOpen)
|
setSearchDetailsOpen(reset.searchDetailsOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusEvidence = (index: number): void => {
|
|
||||||
if (!Number.isInteger(index) || index < 0 || index >= evidenceCollection.length) return
|
|
||||||
setVisibleEvidenceCount((current) => Math.max(current, index + 1))
|
|
||||||
setSelectedEvidence(index)
|
|
||||||
setEvidenceFlash((current) => ({ index, nonce: current.nonce + 1 }))
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (evidenceFlash.index < 0) return
|
|
||||||
evidenceCardRefs.current.get(evidenceFlash.index)?.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'nearest'
|
|
||||||
})
|
|
||||||
}, [evidenceFlash])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
void Promise.all([window.api.getSettings(), window.api.getAppLogPath()]).then(
|
void Promise.all([window.api.getSettings(), window.api.getAppLogPath()]).then(
|
||||||
([settingsResult, logPath]) => {
|
([settingsResult, logPath]) => {
|
||||||
@@ -355,9 +343,7 @@ export function AISearchWorkspace({
|
|||||||
)
|
)
|
||||||
setSearchTrace(mapSearchResultToTrace(searchResult, evidenceItems.length))
|
setSearchTrace(mapSearchResultToTrace(searchResult, evidenceItems.length))
|
||||||
setAgentTrace(searchResult.agent.trace)
|
setAgentTrace(searchResult.agent.trace)
|
||||||
setEvidence(evidenceItems)
|
setEvidenceResult(evidenceItems, collectionItems)
|
||||||
setEvidenceCollection(collectionItems)
|
|
||||||
setVisibleEvidenceCount(Math.min(EVIDENCE_PAGE_SIZE, collectionItems.length))
|
|
||||||
const nextSenderNames = mapEvidenceSenderNames(evidenceItems)
|
const nextSenderNames = mapEvidenceSenderNames(evidenceItems)
|
||||||
setSenderNames(nextSenderNames)
|
setSenderNames(nextSenderNames)
|
||||||
setMessageCount(searchResult.knowledge.totalMessages)
|
setMessageCount(searchResult.knowledge.totalMessages)
|
||||||
@@ -1187,8 +1173,7 @@ export function AISearchWorkspace({
|
|||||||
<article
|
<article
|
||||||
key={`${messageIdentity(item.message)}-${index}-${evidenceFlash.index === index ? evidenceFlash.nonce : 0}`}
|
key={`${messageIdentity(item.message)}-${index}-${evidenceFlash.index === index ? evidenceFlash.nonce : 0}`}
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
if (node) evidenceCardRefs.current.set(index, node)
|
setEvidenceCardRef(index, node)
|
||||||
else evidenceCardRefs.current.delete(index)
|
|
||||||
}}
|
}}
|
||||||
className={`ai-search-evidence-card ${selectedEvidence === index ? 'active' : ''} ${evidenceFlash.index === index ? 'focus-flash' : ''}`}
|
className={`ai-search-evidence-card ${selectedEvidence === index ? 'active' : ''} ${evidenceFlash.index === index ? 'focus-flash' : ''}`}
|
||||||
style={{ animationDelay: `${Math.min(index, 7) * 45}ms` }}
|
style={{ animationDelay: `${Math.min(index, 7) * 45}ms` }}
|
||||||
@@ -1213,7 +1198,7 @@ export function AISearchWorkspace({
|
|||||||
className="ai-search-evidence-link"
|
className="ai-search-evidence-link"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
onOpenEvidence(item.contact, item.message.createTime)
|
jumpToEvidence(index)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
跳转到原聊天 ↗
|
跳转到原聊天 ↗
|
||||||
@@ -1227,15 +1212,11 @@ export function AISearchWorkspace({
|
|||||||
<span>分析完成后,这里会显示支持结论的原始消息。</span>
|
<span>分析完成后,这里会显示支持结论的原始消息。</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{visibleEvidence.length > 0 && visibleEvidence.length < evidenceCollection.length && (
|
{hasMoreEvidence && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ai-search-evidence-load-more"
|
className="ai-search-evidence-load-more"
|
||||||
onClick={() =>
|
onClick={loadMoreEvidence}
|
||||||
setVisibleEvidenceCount((current) =>
|
|
||||||
Math.min(current + EVIDENCE_PAGE_SIZE, evidenceCollection.length)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
加载更多证据
|
加载更多证据
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from 'react'
|
||||||
|
import type { Contact } from '../../../../../shared/types'
|
||||||
|
import type { EvidenceItem } from '../searchTypes'
|
||||||
|
|
||||||
|
export const EVIDENCE_PAGE_SIZE = 8
|
||||||
|
|
||||||
|
type UseEvidenceCollectionOptions = {
|
||||||
|
onOpenEvidence: (contact: Contact, createTime?: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEvidenceCollection({ onOpenEvidence }: UseEvidenceCollectionOptions): {
|
||||||
|
evidence: EvidenceItem[]
|
||||||
|
setEvidence: Dispatch<SetStateAction<EvidenceItem[]>>
|
||||||
|
evidenceCollection: EvidenceItem[]
|
||||||
|
setEvidenceCollection: Dispatch<SetStateAction<EvidenceItem[]>>
|
||||||
|
visibleEvidenceCount: number
|
||||||
|
setVisibleEvidenceCount: Dispatch<SetStateAction<number>>
|
||||||
|
selectedEvidence: number
|
||||||
|
setSelectedEvidence: Dispatch<SetStateAction<number>>
|
||||||
|
visibleEvidence: EvidenceItem[]
|
||||||
|
hasMoreEvidence: boolean
|
||||||
|
evidenceFlash: { index: number; nonce: number }
|
||||||
|
evidenceCardRefs: React.MutableRefObject<Map<number, HTMLElement>>
|
||||||
|
setEvidenceResult: (summaryEvidence: EvidenceItem[], collection: EvidenceItem[]) => void
|
||||||
|
clearEvidenceCollection: () => void
|
||||||
|
loadMoreEvidence: () => void
|
||||||
|
focusEvidence: (index: number) => void
|
||||||
|
jumpToEvidence: (index: number) => void
|
||||||
|
setEvidenceCardRef: (index: number, node: HTMLElement | null) => void
|
||||||
|
} {
|
||||||
|
const [evidence, setEvidence] = useState<EvidenceItem[]>([])
|
||||||
|
const [evidenceCollection, setEvidenceCollection] = useState<EvidenceItem[]>([])
|
||||||
|
const [visibleEvidenceCount, setVisibleEvidenceCount] = useState(0)
|
||||||
|
const [selectedEvidence, setSelectedEvidence] = useState(0)
|
||||||
|
const [evidenceFlash, setEvidenceFlash] = useState({ index: -1, nonce: 0 })
|
||||||
|
const evidenceCardRefs = useRef(new Map<number, HTMLElement>())
|
||||||
|
const visibleEvidence = useMemo(
|
||||||
|
() => evidenceCollection.slice(0, visibleEvidenceCount),
|
||||||
|
[evidenceCollection, visibleEvidenceCount]
|
||||||
|
)
|
||||||
|
const hasMoreEvidence =
|
||||||
|
visibleEvidence.length > 0 && visibleEvidence.length < evidenceCollection.length
|
||||||
|
|
||||||
|
const setEvidenceResult = (summaryEvidence: EvidenceItem[], collection: EvidenceItem[]): void => {
|
||||||
|
setEvidence(summaryEvidence)
|
||||||
|
setEvidenceCollection(collection)
|
||||||
|
setVisibleEvidenceCount(Math.min(EVIDENCE_PAGE_SIZE, collection.length))
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearEvidenceCollection = (): void => {
|
||||||
|
setEvidence([])
|
||||||
|
setEvidenceCollection([])
|
||||||
|
setVisibleEvidenceCount(0)
|
||||||
|
setSelectedEvidence(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMoreEvidence = (): void => {
|
||||||
|
setVisibleEvidenceCount((current) =>
|
||||||
|
Math.min(current + EVIDENCE_PAGE_SIZE, evidenceCollection.length)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusEvidence = (index: number): void => {
|
||||||
|
if (!Number.isInteger(index) || index < 0 || index >= evidenceCollection.length) return
|
||||||
|
setVisibleEvidenceCount((current) => Math.max(current, index + 1))
|
||||||
|
setSelectedEvidence(index)
|
||||||
|
setEvidenceFlash((current) => ({ index, nonce: current.nonce + 1 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const jumpToEvidence = (index: number): void => {
|
||||||
|
const item = evidenceCollection[index]
|
||||||
|
if (!item) return
|
||||||
|
onOpenEvidence(item.contact, item.message.createTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setEvidenceCardRef = (index: number, node: HTMLElement | null): void => {
|
||||||
|
if (node) evidenceCardRefs.current.set(index, node)
|
||||||
|
else evidenceCardRefs.current.delete(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (evidenceFlash.index < 0) return
|
||||||
|
evidenceCardRefs.current.get(evidenceFlash.index)?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'nearest'
|
||||||
|
})
|
||||||
|
}, [evidenceFlash])
|
||||||
|
|
||||||
|
return {
|
||||||
|
evidence,
|
||||||
|
setEvidence,
|
||||||
|
evidenceCollection,
|
||||||
|
setEvidenceCollection,
|
||||||
|
visibleEvidenceCount,
|
||||||
|
setVisibleEvidenceCount,
|
||||||
|
selectedEvidence,
|
||||||
|
setSelectedEvidence,
|
||||||
|
visibleEvidence,
|
||||||
|
hasMoreEvidence,
|
||||||
|
evidenceFlash,
|
||||||
|
evidenceCardRefs,
|
||||||
|
setEvidenceResult,
|
||||||
|
clearEvidenceCollection,
|
||||||
|
loadMoreEvidence,
|
||||||
|
focusEvidence,
|
||||||
|
jumpToEvidence,
|
||||||
|
setEvidenceCardRef
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import { act, renderHook } from '@testing-library/react'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { useEvidenceCollection } from '../../src/renderer/src/components/search/hooks/useEvidenceCollection'
|
||||||
|
import type { EvidenceItem } from '../../src/renderer/src/components/search/searchTypes'
|
||||||
|
import {
|
||||||
|
aiSearchContact,
|
||||||
|
makeCacheRecord,
|
||||||
|
makePipelineEvidence
|
||||||
|
} from './support/ai-search-fixtures'
|
||||||
|
|
||||||
|
const onOpenEvidence = vi.fn()
|
||||||
|
|
||||||
|
const makeEvidence = (count: number, offset = 0): EvidenceItem[] =>
|
||||||
|
Array.from({ length: count }, (_, index) => {
|
||||||
|
const item = makePipelineEvidence(index + offset + 1)
|
||||||
|
return {
|
||||||
|
evidenceId: item.id,
|
||||||
|
contact: aiSearchContact,
|
||||||
|
message: {
|
||||||
|
id: item.messageId,
|
||||||
|
from: item.senderId,
|
||||||
|
type: '检索消息',
|
||||||
|
datetime: new Date(item.timestamp).toLocaleString('zh-CN', { hour12: false }),
|
||||||
|
content: item.text,
|
||||||
|
isSender: false,
|
||||||
|
name: item.sender,
|
||||||
|
senderId: item.senderId,
|
||||||
|
createTime: Math.floor(item.timestamp / 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const useEvidenceHarness = () => useEvidenceCollection({ onOpenEvidence })
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
Object.defineProperty(window, 'api', {
|
||||||
|
configurable: true,
|
||||||
|
value: { runAiSearch: vi.fn() }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useEvidenceCollection', () => {
|
||||||
|
it('starts with zero Summary Evidence and zero Collection Evidence', () => {
|
||||||
|
const { result } = renderHook(() => useEvidenceHarness())
|
||||||
|
|
||||||
|
expect(result.current.evidence).toEqual([])
|
||||||
|
expect(result.current.evidenceCollection).toEqual([])
|
||||||
|
expect(result.current.visibleEvidence).toEqual([])
|
||||||
|
expect(result.current.hasMoreEvidence).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows one Evidence item without rendering a load-more affordance', () => {
|
||||||
|
const { result } = renderHook(() => useEvidenceHarness())
|
||||||
|
const summary = makeEvidence(1)
|
||||||
|
|
||||||
|
act(() => result.current.setEvidenceResult(summary, summary))
|
||||||
|
|
||||||
|
expect(result.current.evidence).toHaveLength(1)
|
||||||
|
expect(result.current.visibleEvidence).toHaveLength(1)
|
||||||
|
expect(result.current.hasMoreEvidence).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows all eight Summary Evidence items on the first page', () => {
|
||||||
|
const { result } = renderHook(() => useEvidenceHarness())
|
||||||
|
const summary = makeEvidence(8)
|
||||||
|
|
||||||
|
act(() => result.current.setEvidenceResult(summary, summary))
|
||||||
|
|
||||||
|
expect(result.current.evidence).toHaveLength(8)
|
||||||
|
expect(result.current.visibleEvidence).toHaveLength(8)
|
||||||
|
expect(result.current.hasMoreEvidence).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps Summary Evidence capped at eight while Collection contains more items', () => {
|
||||||
|
const { result } = renderHook(() => useEvidenceHarness())
|
||||||
|
const summary = makeEvidence(8)
|
||||||
|
const collection = makeEvidence(16)
|
||||||
|
|
||||||
|
act(() => result.current.setEvidenceResult(summary, collection))
|
||||||
|
|
||||||
|
expect(result.current.evidence).toHaveLength(8)
|
||||||
|
expect(result.current.evidenceCollection).toHaveLength(16)
|
||||||
|
expect(result.current.visibleEvidence).toHaveLength(8)
|
||||||
|
expect(result.current.hasMoreEvidence).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads more Collection Evidence in pages without invoking Search', () => {
|
||||||
|
const { result } = renderHook(() => useEvidenceHarness())
|
||||||
|
const summary = makeEvidence(8)
|
||||||
|
const collection = makeEvidence(16)
|
||||||
|
act(() => result.current.setEvidenceResult(summary, collection))
|
||||||
|
|
||||||
|
act(() => result.current.loadMoreEvidence())
|
||||||
|
|
||||||
|
expect(result.current.visibleEvidence).toHaveLength(16)
|
||||||
|
expect(result.current.hasMoreEvidence).toBe(false)
|
||||||
|
expect(window.api.runAiSearch).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not change Summary Evidence when loading more Collection Evidence', () => {
|
||||||
|
const { result } = renderHook(() => useEvidenceHarness())
|
||||||
|
const summary = makeEvidence(8)
|
||||||
|
const collection = makeEvidence(17)
|
||||||
|
act(() => result.current.setEvidenceResult(summary, collection))
|
||||||
|
const summaryIds = result.current.evidence.map((item) => item.evidenceId)
|
||||||
|
|
||||||
|
act(() => result.current.loadMoreEvidence())
|
||||||
|
|
||||||
|
expect(result.current.evidence.map((item) => item.evidenceId)).toEqual(summaryIds)
|
||||||
|
expect(result.current.evidence).toHaveLength(8)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves stable E1…En identifiers and ordering across pagination', () => {
|
||||||
|
const { result } = renderHook(() => useEvidenceHarness())
|
||||||
|
const collection = makeEvidence(17)
|
||||||
|
act(() => result.current.setEvidenceResult(collection.slice(0, 8), collection))
|
||||||
|
|
||||||
|
const firstPageIds = result.current.visibleEvidence.map((item) => item.evidenceId)
|
||||||
|
act(() => result.current.loadMoreEvidence())
|
||||||
|
const allIds = result.current.visibleEvidence.map((item) => item.evidenceId)
|
||||||
|
|
||||||
|
expect(firstPageIds).toEqual(['E1', 'E2', 'E3', 'E4', 'E5', 'E6', 'E7', 'E8'])
|
||||||
|
expect(allIds).toEqual([
|
||||||
|
'E1',
|
||||||
|
'E2',
|
||||||
|
'E3',
|
||||||
|
'E4',
|
||||||
|
'E5',
|
||||||
|
'E6',
|
||||||
|
'E7',
|
||||||
|
'E8',
|
||||||
|
'E9',
|
||||||
|
'E10',
|
||||||
|
'E11',
|
||||||
|
'E12',
|
||||||
|
'E13',
|
||||||
|
'E14',
|
||||||
|
'E15',
|
||||||
|
'E16'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selects and highlights an Evidence item while expanding visibility when needed', () => {
|
||||||
|
const { result } = renderHook(() => useEvidenceHarness())
|
||||||
|
const collection = makeEvidence(16)
|
||||||
|
act(() => result.current.setEvidenceResult(collection.slice(0, 8), collection))
|
||||||
|
|
||||||
|
act(() => result.current.focusEvidence(10))
|
||||||
|
|
||||||
|
expect(result.current.selectedEvidence).toBe(10)
|
||||||
|
expect(result.current.visibleEvidence).toHaveLength(11)
|
||||||
|
expect(result.current.evidenceFlash.index).toBe(10)
|
||||||
|
expect(result.current.evidenceFlash.nonce).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('scrolls the highlighted Evidence card into view', () => {
|
||||||
|
const { result } = renderHook(() => useEvidenceHarness())
|
||||||
|
const scrollIntoView = vi.fn()
|
||||||
|
const card = { scrollIntoView } as unknown as HTMLElement
|
||||||
|
const collection = makeEvidence(1)
|
||||||
|
act(() => result.current.setEvidenceResult(collection, collection))
|
||||||
|
act(() => {
|
||||||
|
result.current.setEvidenceCardRef(0, card)
|
||||||
|
result.current.focusEvidence(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'nearest' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes the selected Evidence contact and timestamp to the jump callback', () => {
|
||||||
|
const { result } = renderHook(() => useEvidenceHarness())
|
||||||
|
const collection = makeEvidence(1)
|
||||||
|
act(() => result.current.setEvidenceResult(collection, collection))
|
||||||
|
|
||||||
|
act(() => result.current.jumpToEvidence(0))
|
||||||
|
|
||||||
|
expect(onOpenEvidence).toHaveBeenCalledWith(aiSearchContact, collection[0].message.createTime)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears the previous request Evidence and selection before the next result is applied', () => {
|
||||||
|
const { result } = renderHook(() => useEvidenceHarness())
|
||||||
|
const first = makeEvidence(1)
|
||||||
|
const second = makeEvidence(1, 1)
|
||||||
|
act(() => {
|
||||||
|
result.current.setEvidenceResult(first, first)
|
||||||
|
result.current.focusEvidence(0)
|
||||||
|
result.current.clearEvidenceCollection()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.evidence).toEqual([])
|
||||||
|
expect(result.current.evidenceCollection).toEqual([])
|
||||||
|
expect(result.current.selectedEvidence).toBe(0)
|
||||||
|
expect(result.current.visibleEvidence).toEqual([])
|
||||||
|
|
||||||
|
act(() => result.current.setEvidenceResult(second, second))
|
||||||
|
expect(result.current.evidence.map((item) => item.evidenceId)).toEqual(['E2'])
|
||||||
|
expect(result.current.evidence).not.toContain(first[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepts legacy cache normalization where missing evidenceCollection falls back to evidence', () => {
|
||||||
|
const { result } = renderHook(() => useEvidenceHarness())
|
||||||
|
const cached = makeCacheRecord({
|
||||||
|
query: 'legacy evidence',
|
||||||
|
evidence: [makePipelineEvidence(1)]
|
||||||
|
})
|
||||||
|
const legacyEvidence = cached.evidence as EvidenceItem[]
|
||||||
|
|
||||||
|
act(() => result.current.setEvidenceResult(legacyEvidence, legacyEvidence))
|
||||||
|
|
||||||
|
expect(result.current.evidenceCollection).toBe(result.current.evidence)
|
||||||
|
expect(result.current.visibleEvidence).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('replaces, rather than merges, Collection Evidence between request boundaries', () => {
|
||||||
|
const { result } = renderHook(() => useEvidenceHarness())
|
||||||
|
const first = makeEvidence(9)
|
||||||
|
const second = makeEvidence(2, 20)
|
||||||
|
|
||||||
|
act(() => result.current.setEvidenceResult(first.slice(0, 8), first))
|
||||||
|
act(() => {
|
||||||
|
result.current.clearEvidenceCollection()
|
||||||
|
result.current.setEvidenceResult(second, second)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.evidenceCollection.map((item) => item.evidenceId)).toEqual(['E21', 'E22'])
|
||||||
|
expect(result.current.evidenceCollection).not.toContain(first[8])
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user