From e57927e37d3594e2c640135aace5e39c15e5274f Mon Sep 17 00:00:00 2001 From: Daniel Arroyo Date: Sun, 22 Mar 2026 17:37:40 -0300 Subject: [PATCH] feat: MVP-4 Sprint 2 - Co-usage sidebar and search cache - Add co-usage display to NoteConnections (notas vistas juntas) - Add search results cache with 50 entry limit and FIFO eviction - Improve contextual sidebar with usage-based suggestions --- src/app/notes/[id]/page.tsx | 3 +++ src/components/note-connections.tsx | 18 ++++++++++++++++-- src/components/search-bar.tsx | 23 ++++++++++++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/app/notes/[id]/page.tsx b/src/app/notes/[id]/page.tsx index dee9342..b26306c 100644 --- a/src/app/notes/[id]/page.tsx +++ b/src/app/notes/[id]/page.tsx @@ -2,6 +2,7 @@ import { prisma } from '@/lib/prisma' import { notFound } from 'next/navigation' import { getRelatedNotes } from '@/lib/related' import { getBacklinksForNote, getOutgoingLinksForNote } from '@/lib/backlinks' +import { getCoUsedNotes } from '@/lib/usage' import { NoteConnections } from '@/components/note-connections' import { MarkdownContent } from '@/components/markdown-content' import { DeleteNoteButton } from '@/components/delete-note-button' @@ -36,6 +37,7 @@ export default async function NoteDetailPage({ params }: { params: Promise<{ id: const related = await getRelatedNotes(id, 5) const backlinks = await getBacklinksForNote(id) const outgoingLinks = await getOutgoingLinksForNote(id) + const coUsedNotes = await getCoUsedNotes(id, 5) const noteType = note.type as NoteType return ( @@ -100,6 +102,7 @@ export default async function NoteDetailPage({ params }: { params: Promise<{ id: backlinks={backlinks} outgoingLinks={outgoingLinks} relatedNotes={related} + coUsedNotes={coUsedNotes} /> diff --git a/src/components/note-connections.tsx b/src/components/note-connections.tsx index 715c1b0..bd50b1f 100644 --- a/src/components/note-connections.tsx +++ b/src/components/note-connections.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' -import { ArrowRight, Link2, RefreshCw, ExternalLink } from 'lucide-react' +import { ArrowRight, Link2, RefreshCw, ExternalLink, Users } from 'lucide-react' interface BacklinkInfo { id: string @@ -30,6 +30,7 @@ interface NoteConnectionsProps { backlinks: BacklinkInfo[] outgoingLinks: BacklinkInfo[] relatedNotes: RelatedNote[] + coUsedNotes: { noteId: string; title: string; type: string; weight: number }[] } function ConnectionGroup({ @@ -84,9 +85,10 @@ export function NoteConnections({ backlinks, outgoingLinks, relatedNotes, + coUsedNotes, }: NoteConnectionsProps) { const hasAnyConnections = - backlinks.length > 0 || outgoingLinks.length > 0 || relatedNotes.length > 0 + backlinks.length > 0 || outgoingLinks.length > 0 || relatedNotes.length > 0 || coUsedNotes.length > 0 if (!hasAnyConnections) { return null @@ -136,6 +138,18 @@ export function NoteConnections({ }))} emptyMessage="No hay notas relacionadas" /> + + {/* Co-used notes - often viewed together */} + ({ + id: cu.noteId, + title: cu.title, + type: cu.type, + }))} + emptyMessage="No hay notas co-usadas" + /> ) diff --git a/src/components/search-bar.tsx b/src/components/search-bar.tsx index 1e534a3..ee4e53a 100644 --- a/src/components/search-bar.tsx +++ b/src/components/search-bar.tsx @@ -7,6 +7,10 @@ import { Button } from '@/components/ui/button' import { Search, Loader2 } from 'lucide-react' import { ScoredNote } from '@/lib/search' +// Simple in-memory cache for search results +const searchCache = new Map() +const MAX_CACHE_SIZE = 50 + export function SearchBar() { const [query, setQuery] = useState('') const [results, setResults] = useState([]) @@ -31,14 +35,31 @@ export function SearchBar() { } debounceRef.current = setTimeout(async () => { + // Check cache first + if (searchCache.has(query)) { + setResults(searchCache.get(query)!) + setIsOpen(true) + setSelectedIndex(-1) + return + } + setIsLoading(true) try { const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`) const json = await res.json() if (json.success) { - setResults(json.data.slice(0, 8)) + const results = json.data.slice(0, 8) + setResults(results) setIsOpen(true) setSelectedIndex(-1) + + // Cache the results + if (searchCache.size >= MAX_CACHE_SIZE) { + // Remove oldest entry (first in Map) + const firstKey = searchCache.keys().next().value + if (firstKey) searchCache.delete(firstKey) + } + searchCache.set(query, results) } } catch (error) { console.error('Search failed:', error)