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
This commit is contained in:
@@ -2,6 +2,7 @@ import { prisma } from '@/lib/prisma'
|
|||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
import { getRelatedNotes } from '@/lib/related'
|
import { getRelatedNotes } from '@/lib/related'
|
||||||
import { getBacklinksForNote, getOutgoingLinksForNote } from '@/lib/backlinks'
|
import { getBacklinksForNote, getOutgoingLinksForNote } from '@/lib/backlinks'
|
||||||
|
import { getCoUsedNotes } from '@/lib/usage'
|
||||||
import { NoteConnections } from '@/components/note-connections'
|
import { NoteConnections } from '@/components/note-connections'
|
||||||
import { MarkdownContent } from '@/components/markdown-content'
|
import { MarkdownContent } from '@/components/markdown-content'
|
||||||
import { DeleteNoteButton } from '@/components/delete-note-button'
|
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 related = await getRelatedNotes(id, 5)
|
||||||
const backlinks = await getBacklinksForNote(id)
|
const backlinks = await getBacklinksForNote(id)
|
||||||
const outgoingLinks = await getOutgoingLinksForNote(id)
|
const outgoingLinks = await getOutgoingLinksForNote(id)
|
||||||
|
const coUsedNotes = await getCoUsedNotes(id, 5)
|
||||||
const noteType = note.type as NoteType
|
const noteType = note.type as NoteType
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -100,6 +102,7 @@ export default async function NoteDetailPage({ params }: { params: Promise<{ id:
|
|||||||
backlinks={backlinks}
|
backlinks={backlinks}
|
||||||
outgoingLinks={outgoingLinks}
|
outgoingLinks={outgoingLinks}
|
||||||
relatedNotes={related}
|
relatedNotes={related}
|
||||||
|
coUsedNotes={coUsedNotes}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
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 {
|
interface BacklinkInfo {
|
||||||
id: string
|
id: string
|
||||||
@@ -30,6 +30,7 @@ interface NoteConnectionsProps {
|
|||||||
backlinks: BacklinkInfo[]
|
backlinks: BacklinkInfo[]
|
||||||
outgoingLinks: BacklinkInfo[]
|
outgoingLinks: BacklinkInfo[]
|
||||||
relatedNotes: RelatedNote[]
|
relatedNotes: RelatedNote[]
|
||||||
|
coUsedNotes: { noteId: string; title: string; type: string; weight: number }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConnectionGroup({
|
function ConnectionGroup({
|
||||||
@@ -84,9 +85,10 @@ export function NoteConnections({
|
|||||||
backlinks,
|
backlinks,
|
||||||
outgoingLinks,
|
outgoingLinks,
|
||||||
relatedNotes,
|
relatedNotes,
|
||||||
|
coUsedNotes,
|
||||||
}: NoteConnectionsProps) {
|
}: NoteConnectionsProps) {
|
||||||
const hasAnyConnections =
|
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) {
|
if (!hasAnyConnections) {
|
||||||
return null
|
return null
|
||||||
@@ -136,6 +138,18 @@ export function NoteConnections({
|
|||||||
}))}
|
}))}
|
||||||
emptyMessage="No hay notas relacionadas"
|
emptyMessage="No hay notas relacionadas"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Co-used notes - often viewed together */}
|
||||||
|
<ConnectionGroup
|
||||||
|
title="Co-usadas"
|
||||||
|
icon={Users}
|
||||||
|
notes={coUsedNotes.map((cu) => ({
|
||||||
|
id: cu.noteId,
|
||||||
|
title: cu.title,
|
||||||
|
type: cu.type,
|
||||||
|
}))}
|
||||||
|
emptyMessage="No hay notas co-usadas"
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Search, Loader2 } from 'lucide-react'
|
import { Search, Loader2 } from 'lucide-react'
|
||||||
import { ScoredNote } from '@/lib/search'
|
import { ScoredNote } from '@/lib/search'
|
||||||
|
|
||||||
|
// Simple in-memory cache for search results
|
||||||
|
const searchCache = new Map<string, ScoredNote[]>()
|
||||||
|
const MAX_CACHE_SIZE = 50
|
||||||
|
|
||||||
export function SearchBar() {
|
export function SearchBar() {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [results, setResults] = useState<ScoredNote[]>([])
|
const [results, setResults] = useState<ScoredNote[]>([])
|
||||||
@@ -31,14 +35,31 @@ export function SearchBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
debounceRef.current = setTimeout(async () => {
|
debounceRef.current = setTimeout(async () => {
|
||||||
|
// Check cache first
|
||||||
|
if (searchCache.has(query)) {
|
||||||
|
setResults(searchCache.get(query)!)
|
||||||
|
setIsOpen(true)
|
||||||
|
setSelectedIndex(-1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
|
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
if (json.success) {
|
if (json.success) {
|
||||||
setResults(json.data.slice(0, 8))
|
const results = json.data.slice(0, 8)
|
||||||
|
setResults(results)
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
setSelectedIndex(-1)
|
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) {
|
} catch (error) {
|
||||||
console.error('Search failed:', error)
|
console.error('Search failed:', error)
|
||||||
|
|||||||
Reference in New Issue
Block a user