Semantic search, NFT/profile/analytics pages, valora→perpetua rename - #141
Conversation
…ils page, semantic search and basic seo
Code Review — PR 141OverviewThis PR delivers a significant feature batch: semantic (CLIP) image search, NFT details page, user profile page, analytics dashboard, engagement tracking, and a rename of the `valora` feature to `perpetua`. The rename itself is clean and thorough. Several new features contain correctness bugs worth fixing before merge. Bugs1. Impression dedup is broken in record_engagement_batchFile: `src/alex_backend/src/dialectica/api/updates.rs` The docstring says "Impressions: deduped per authenticated user (one per arweave_id per user)", but the implementation just increments the counter unconditionally — it never checks whether the caller already registered an impression: ```rust Either update the docstring to say impressions are not deduped, or mirror the views logic and store a set of principals. 2. get_trending inflates anonymous view countsFile: `src/alex_backend/src/dialectica/api/queries.rs` `get_trending` counts `viewers.0.0.len()` to rank content. Anonymous viewers push `None` without any dedup (`record_view` always appends `None` for anonymous callers). A single anonymous user refreshing a page repeatedly inflates that token's trending rank unboundedly. Consider capping anonymous contributions or counting them separately. 3. Timestamp regression in Comment.tsxFile: `src/alex_frontend/core/features/nft/components/Comment.tsx` The diff removes the nanoseconds-to-milliseconds conversion:
`comment.created_at` is `nat64` (nanoseconds), a BigInt. The old code converted it to milliseconds. Unless `convertTimestamp` was simultaneously updated to accept nanoseconds or BigInt, every comment's relative timestamp will now display incorrectly. Verify `convertTimestamp`'s expected input unit. 4. reset_embeddings hardcodes a magic number instead of the constantFile: `src/alex_backend/src/similarity/updates.rs` line 758 ```rust The constant `EMBEDDINGS_MEM_ID` is defined in `store.rs` specifically to avoid this. If the ID ever changes, `reset_embeddings` will silently operate on the wrong memory segment. Performance Concerns5. Linear scan in similarity search — O(n) per queryFile: `src/alex_backend/src/similarity/queries.rs` Both `search_similar` and `search_by_vector` iterate the entire `EMBEDDINGS` BTreeMap and compute cosine similarity against every entry. On an ICP canister with a 2-second query instruction limit, this will hit the instruction ceiling well before 10,000 embeddings. Consider documenting the expected scale or adding a hard cap on indexed entries. 6. get_indexed_ids has no paginationFile: `src/alex_backend/src/similarity/queries.rs` ```rust This returns every arweave ID in one call. If embeddings reach the tens of thousands, this query will exceed the response size limit. Add an optional offset/limit or remove if only used for debugging. 7. Trending sort is not cachedFile: `src/alex_frontend/core/features/alexandrian/api/createTokenFetcher.ts` `await alex_backend.get_trending(BigInt(100))` is called on every sort or page change in trending mode. This result should be cached with React Query (e.g., `staleTime: 5 * 60_000`) rather than re-fetched on every render cycle. Code Quality8. Significant logic duplication between NftPage and Arweave.tsxFile: `src/alex_frontend/lbry/src/pages/NftPage.tsx` The price-history rendering (~80 lines) and share-button logic (~60 lines) in `NftPage.tsx` are near-identical copies of code in `Arweave.tsx`. These should be extracted into shared components (e.g., ``, ``) rather than duplicated. 9. UserProfilePage does not guard against invalid principal formatFile: `src/alex_frontend/lbry/src/pages/UserProfilePage.tsx` `Principal.fromText(principal)` throws if the principal string is malformed. Since the route `/user/:principal` accepts arbitrary URL input, wrap this in try/catch or validate the format to avoid unhandled errors crashing the page. 10. engagementService drops data on flush failureFile: `src/alex_frontend/core/services/engagementService.ts` ```ts The pre-clear pattern avoids double-sending, but any network error silently drops all buffered engagement data. This may be acceptable for analytics, but a comment noting the tradeoff would help future readers. 11. Avatar fallback uses direct DOM mutationFile: `src/alex_frontend/lbry/src/pages/UserProfilePage.tsx` ```tsx This is fragile — it breaks if the DOM structure changes. Use `const [imgError, setImgError] = useState(false)` and conditionally render based on that state instead. 12. skipped counter is always 0 in store_embeddingsFile: `src/alex_backend/src/similarity/updates.rs` `StoreResult` has a `skipped` field, but nothing in `store_embeddings` ever increments it — entries either succeed (`stored += 1`) or push to `errors`. Either remove the field or increment it for duplicate/overwrite entries. Minor
Summary
The rename, new pages, and backend engagement features are well-structured. The main blockers before merge are the impression-dedup bug (1), the timestamp regression in comments (3), and the embedding memory-ID magic number (4). The linear search scalability (5) should also have an explicit documented limit before the feature is enabled in production. |
Code Review — PR #141OverviewThis is a well-scoped, multi-feature PR. The Bugs1. DID / Rust struct mismatch in
But the Rust struct in pub struct StoreResult {
pub stored: u64,
pub errors: Vec<String>,
}Candid will silently zero-fill 2. for i in 0..a.len() {
dot += a[i] * b[i]; // panics if b.len() < a.len()The call sites validate dimensions before calling, but since the function is for (x, y) in a.iter().zip(b.iter()) { ... }3. The old code was: convertTimestamp(Number(comment.created_at) / 1000000, 'relative')The new code passes the raw value: convertTimestamp(comment.created_at, 'relative')IC Performance4. N+1 canister calls in For each NFT, the analytics page fires 3 separate IC calls: const [viewResult, impResult, reactionResult] = await Promise.allSettled([
alexBackendActor.get_view_count(arweaveId),
alexBackendActor.get_impressions(arweaveId),
alexBackendActor.get_reaction_counts(arweaveId),
]);A creator with 50 NFTs makes 150 canister calls. This will be noticeably slow and cycle-expensive. Consider adding a batch endpoint (e.g. 5. fn get_indexed_ids() -> Vec<String>This returns every indexed arweave ID with no pagination. With hundreds of thousands of embeddings, the response will exceed IC's 2 MB reply limit and trap. Add a 6.
7. const tokenIds = await icrc7.icrc7_tokens_of(account, [], []);The empty Code Quality8. Balance data silently truncated at 49 NFTs const balResult = await nftManagerActor.get_nft_balances(tokenIds.slice(0, 49));NFTs beyond index 49 will silently show 9.
10. Redux slice name not updated
const perpetuaSlice = createSlice({
name: "valora", // should be "perpetua"
...
})And 11. Trending sort drops const trendingOrder = new Map(
trendingEntries.map(([arweaveId, viewCount]: [string, bigint], index: number) => [arweaveId, index])
);
12. { offset: BigInt(0), limit: BigInt(20) }With no "load more" button or count indicator, users with >20 shelves see a truncated list with no indication. Either add pagination or show a "+ N more" notice. 13. Static SEO title on profile page <title>User Profile | Alexandria</title>This is a missed opportunity — the dynamic username is available. Using Security14. NaN or Inf embeddings from a caller will produce NaN similarity scores. The Minor
Summary
The rename and new pages are solid work. The similarity module is well-structured. The main things I'd want fixed before merge are the DID mismatch (#1), the timestamp regression (#3), the N+1 analytics fetch (#4), and the engagement flush gap (#9). |
Code Review — PR #141OverviewThis is a large, multi-concern PR landing several independent features:
The work quality is generally solid. There are some meaningful issues to address before merging. Bugs1. Redux slice name not updated after rename ( The slice variable was renamed to 2. AnalyticsPage balance index mismatch ( For users with more than 49 NFTs, 3. Comment timestamp regression ( Before: After: ICP timestamps are nanoseconds. If Performance Concerns4. search_similar / search_by_vector — O(n) full scan ( Both search functions iterate the entire embedding store on every query call. At 512 dimensions x N embeddings, ICP's per-query instruction limit will be hit with only a few thousand entries. Consider adding a hard cap on embedded entries or documenting the scale limit in the DID until a proper ANN index is added. 5. get_trending — linear scan + full sort on every call ( Scans and sorts all view records with no caching. A pre-computed sorted index or a smaller enforced default limit (e.g. 50) would help. 6. get_indexed_ids — unbounded return ( Returns all indexed IDs with no limit. Add pagination or a hard cap to avoid exceeding ICP response size limits at scale. 7. AnalyticsPage — N+1 query pattern ( For 50 NFTs, this fires ~150 canister calls simultaneously (3 per NFT). Consider a batched backend endpoint or limit the per-NFT table to top N by views. Design Issues8. Engagement flush only wired to AlexandrianPage unmount (
9. Semantic search has a centralized external dependency ( Failures silently log to console with no user-facing feedback. At minimum, show a toast when the embedding server is unreachable. 10. createTokenFetcher uses unauthenticated actor for trending ( The imported Minor Items11. Anonymous view Vec grows unboundedly ( Anonymous callers append 12. clear_embeddings is inefficient ( Iterates and removes keys one by one. The existing 13. icrc7_tokens_of is unbounded in UserProfilePage ( Empty Positives
|
Summary
valora→perpetua(routes, pages, types)/nft/:tokenId)/user/:principal) with NFT history/dashboard/analytics)similaritymodule (store/queries/updates)engagementServicein core/servicesplan/