From f00f26dfe01ba1facbef32eeeeafa537b04c3cb2 Mon Sep 17 00:00:00 2001 From: Gigi Date: Sat, 18 Oct 2025 10:05:56 +0200 Subject: [PATCH] feat(debug): add Highlight Loading section with streaming metrics - Add query mode selector (Article/#a, URL/#r, Author) - Stream highlight events as they arrive with onEvent callback - Track timing metrics: total load time and time-to-first-event - Display highlight summaries with content, tags, and metadata - Support EOSE-based completion via queryEvents helper - Mirror bookmark loading section UX for consistency --- src/components/Debug.tsx | 233 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 231 insertions(+), 2 deletions(-) diff --git a/src/components/Debug.tsx b/src/components/Debug.tsx index e85bdbe3..8a40f044 100644 --- a/src/components/Debug.tsx +++ b/src/components/Debug.tsx @@ -80,12 +80,23 @@ const Debug: React.FC = ({ // Individual event decryption results const [decryptedEvents, setDecryptedEvents] = useState>(new Map()) + // Highlight loading state + const [highlightMode, setHighlightMode] = useState<'article' | 'url' | 'author'>('article') + const [highlightArticleCoord, setHighlightArticleCoord] = useState('') + const [highlightUrl, setHighlightUrl] = useState('') + const [highlightAuthor, setHighlightAuthor] = useState('') + const [isLoadingHighlights, setIsLoadingHighlights] = useState(false) + const [highlightEvents, setHighlightEvents] = useState([]) + const [tLoadHighlights, setTLoadHighlights] = useState(null) + const [tFirstHighlight, setTFirstHighlight] = useState(null) + // Live timing state const [liveTiming, setLiveTiming] = useState<{ nip44?: { type: 'encrypt' | 'decrypt'; startTime: number } nip04?: { type: 'encrypt' | 'decrypt'; startTime: number } loadBookmarks?: { startTime: number } decryptBookmarks?: { startTime: number } + loadHighlights?: { startTime: number } }>({}) useEffect(() => { @@ -315,6 +326,87 @@ const Debug: React.FC = ({ DebugBus.info('debug', 'Cleared bookmark data') } + const handleLoadHighlights = async () => { + if (!relayPool) { + DebugBus.warn('debug', 'Cannot load highlights: missing relayPool') + return + } + + const getValue = () => { + if (highlightMode === 'article') return highlightArticleCoord.trim() + if (highlightMode === 'url') return highlightUrl.trim() + return highlightAuthor.trim() + } + + const value = getValue() + if (!value) { + DebugBus.warn('debug', 'Please provide a value to query') + return + } + + try { + setIsLoadingHighlights(true) + setHighlightEvents([]) + setTFirstHighlight(null) + DebugBus.info('debug', `Loading highlights (${highlightMode}: ${value})...`) + + const start = performance.now() + setLiveTiming(prev => ({ ...prev, loadHighlights: { startTime: start } })) + + let firstEventTime: number | null = null + const seenIds = new Set() + + // Import highlight services + const { queryEvents } = await import('../services/dataFetch') + const { KINDS } = await import('../config/kinds') + + // Build filter based on mode + let filter: { kinds: number[]; '#a'?: string[]; '#r'?: string[]; authors?: string[] } + if (highlightMode === 'article') { + filter = { kinds: [KINDS.Highlights], '#a': [value] } + } else if (highlightMode === 'url') { + filter = { kinds: [KINDS.Highlights], '#r': [value] } + } else { + filter = { kinds: [KINDS.Highlights], authors: [value] } + } + + const events = await queryEvents(relayPool, filter, { + onEvent: (evt) => { + if (seenIds.has(evt.id)) return + seenIds.add(evt.id) + + if (firstEventTime === null) { + firstEventTime = performance.now() - start + setTFirstHighlight(Math.round(firstEventTime)) + } + + setHighlightEvents(prev => [...prev, evt]) + } + }) + + const elapsed = Math.round(performance.now() - start) + setTLoadHighlights(elapsed) + setLiveTiming(prev => { + const { loadHighlights, ...rest } = prev + return rest + }) + + DebugBus.info('debug', `Loaded ${events.length} highlight events in ${elapsed}ms`) + } catch (err) { + console.error('Failed to load highlights:', err) + DebugBus.error('debug', `Failed to load highlights: ${err instanceof Error ? err.message : String(err)}`) + } finally { + setIsLoadingHighlights(false) + } + } + + const handleClearHighlights = () => { + setHighlightEvents([]) + setTLoadHighlights(null) + setTFirstHighlight(null) + DebugBus.info('debug', 'Cleared highlight data') + } + const handleBunkerLogin = async () => { if (!bunkerUri.trim()) { setBunkerError('Please enter a bunker URI') @@ -376,7 +468,7 @@ const Debug: React.FC = ({ return null } - const getBookmarkLiveTiming = (operation: 'loadBookmarks' | 'decryptBookmarks') => { + const getBookmarkLiveTiming = (operation: 'loadBookmarks' | 'decryptBookmarks' | 'loadHighlights') => { const timing = liveTiming[operation] if (timing) { const elapsed = Math.round(performance.now() - timing.startTime) @@ -390,7 +482,7 @@ const Debug: React.FC = ({ value?: string | number | null; mode?: 'nip44' | 'nip04'; type?: 'encrypt' | 'decrypt'; - bookmarkOp?: 'loadBookmarks' | 'decryptBookmarks'; + bookmarkOp?: 'loadBookmarks' | 'decryptBookmarks' | 'loadHighlights'; }) => { const liveValue = bookmarkOp ? getBookmarkLiveTiming(bookmarkOp) : (mode && type ? getLiveTiming(mode, type) : null) const isLive = !!liveValue @@ -647,6 +739,143 @@ const Debug: React.FC = ({ )} + {/* Highlight Loading Section */} +
+

Highlight Loading

+
Test highlight loading with EOSE-based queryEvents (kind: 9802)
+ +
+
Query Mode:
+
+ + + +
+
+ +
+ {highlightMode === 'article' && ( + setHighlightArticleCoord(e.target.value)} + disabled={isLoadingHighlights} + /> + )} + {highlightMode === 'url' && ( + setHighlightUrl(e.target.value)} + disabled={isLoadingHighlights} + /> + )} + {highlightMode === 'author' && ( + setHighlightAuthor(e.target.value)} + disabled={isLoadingHighlights} + /> + )} +
+ +
+ + +
+ +
+ + +
+ + {highlightEvents.length > 0 && ( +
+
Loaded Highlights ({highlightEvents.length}):
+
+ {highlightEvents.map((evt, idx) => { + const content = evt.content || '' + const shortContent = content.length > 100 ? content.substring(0, 100) + '...' : content + const aTag = evt.tags?.find((t: string[]) => t[0] === 'a')?.[1] + const rTag = evt.tags?.find((t: string[]) => t[0] === 'r')?.[1] + const eTag = evt.tags?.find((t: string[]) => t[0] === 'e')?.[1] + const contextTag = evt.tags?.find((t: string[]) => t[0] === 'context')?.[1] + + return ( +
+
Highlight #{idx + 1}
+
+
Author: {evt.pubkey.slice(0, 16)}...
+
Created: {new Date(evt.created_at * 1000).toLocaleString()}
+
+
+
Content:
+
"{shortContent}"
+
+ {contextTag && ( +
+
Context: {contextTag.substring(0, 60)}...
+
+ )} + {aTag &&
#a: {aTag}
} + {rTag &&
#r: {rTag}
} + {eTag &&
#e: {eTag.slice(0, 16)}...
} +
ID: {evt.id}
+
+ ) + })} +
+
+ )} +
+ {/* Debug Logs Section */}

Debug Logs