feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
546
memento-note/components/memory-echo-section.tsx
Normal file
546
memento-note/components/memory-echo-section.tsx
Normal file
@@ -0,0 +1,546 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback, type ReactNode } from 'react'
|
||||
import { ChevronDown, ChevronUp, Sparkles, Link2, X, Loader2, HelpCircle } from 'lucide-react'
|
||||
import { LinkedNotePreviewDialog } from '@/components/linked-note-preview-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useLanguage } from '@/lib/i18n/LanguageProvider'
|
||||
import type { BlockSuggestion } from '@/components/block-picker'
|
||||
import { toast } from 'sonner'
|
||||
import { useNoteEditorContext } from '@/components/note-editor/note-editor-context'
|
||||
|
||||
interface ConnectionData {
|
||||
noteId: string
|
||||
title: string | null
|
||||
content: string
|
||||
createdAt: Date
|
||||
similarity: number
|
||||
daysApart: number
|
||||
}
|
||||
|
||||
interface LiveBlockRefHost {
|
||||
targetNoteId: string
|
||||
targetNoteTitle: string
|
||||
notebookName: string
|
||||
blockIds: string[]
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface MemoryEchoSectionProps {
|
||||
noteId: string
|
||||
onCompareNotes?: (noteIds: string[], meta?: { similarity?: number }) => void
|
||||
onMergeNotes?: (noteIds: string[]) => void
|
||||
}
|
||||
|
||||
interface PreviewTarget {
|
||||
noteId: string
|
||||
title: string | null
|
||||
excerpt: string
|
||||
}
|
||||
|
||||
function stripHtml(html: string): string {
|
||||
return html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
function excerpt(text: string, max = 150): string {
|
||||
const plain = stripHtml(text)
|
||||
if (plain.length <= max) return plain
|
||||
return `${plain.slice(0, max).trim()}…`
|
||||
}
|
||||
|
||||
async function resolveBlockForEmbed(sourceNoteId: string, hint: string): Promise<{ block: BlockSuggestion; mode: 'live' | 'citation' } | null> {
|
||||
const params = new URLSearchParams({ noteId: sourceNoteId, hint })
|
||||
const res = await fetch(`/api/blocks/resolve?${params}`)
|
||||
if (!res.ok) return null
|
||||
const data = await res.json()
|
||||
if (!data.block) return null
|
||||
return { block: data.block as BlockSuggestion, mode: data.mode === 'live' ? 'live' : 'citation' }
|
||||
}
|
||||
|
||||
function dispatchLiveBlockInsert(block: BlockSuggestion) {
|
||||
window.dispatchEvent(new CustomEvent('memento-insert-live-block', { detail: { block } }))
|
||||
}
|
||||
|
||||
function dispatchCitationInsert(payload: { noteId: string; noteTitle: string; excerpt: string }) {
|
||||
window.dispatchEvent(new CustomEvent('memento-insert-citation', { detail: { ...payload, atEnd: true } }))
|
||||
}
|
||||
|
||||
function ActionWithHelp({
|
||||
label,
|
||||
help,
|
||||
children,
|
||||
}: {
|
||||
label: string
|
||||
help: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-[240px] text-left">
|
||||
<p className="font-medium mb-0.5">{label}</p>
|
||||
<p className="text-background/80">{help}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export function MemoryEchoSection({
|
||||
noteId,
|
||||
onCompareNotes,
|
||||
onMergeNotes,
|
||||
}: MemoryEchoSectionProps) {
|
||||
const { t } = useLanguage()
|
||||
const editorCtx = useNoteEditorContext()
|
||||
const [connections, setConnections] = useState<ConnectionData[]>([])
|
||||
const [retroRefs, setRetroRefs] = useState<LiveBlockRefHost[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [isVisible, setIsVisible] = useState(true)
|
||||
const [consentRequired, setConsentRequired] = useState(false)
|
||||
const [embeddingId, setEmbeddingId] = useState<string | null>(null)
|
||||
const [helpOpen, setHelpOpen] = useState(false)
|
||||
const [previewTarget, setPreviewTarget] = useState<PreviewTarget | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
setIsLoading(true)
|
||||
setConsentRequired(false)
|
||||
setConnections([])
|
||||
setRetroRefs([])
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const [connRes, retroRes] = await Promise.all([
|
||||
fetch(`/api/ai/echo/connections?noteId=${noteId}&limit=10`),
|
||||
fetch(`/api/notes/${noteId}/live-block-refs`),
|
||||
])
|
||||
|
||||
if (cancelled) return
|
||||
|
||||
if (connRes.status === 403) {
|
||||
setConsentRequired(true)
|
||||
setConnections([])
|
||||
} else if (connRes.ok) {
|
||||
const data = await connRes.json()
|
||||
setConnections(data.connections || [])
|
||||
} else {
|
||||
setConnections([])
|
||||
}
|
||||
|
||||
if (retroRes.ok) {
|
||||
const data = await retroRes.json()
|
||||
setRetroRefs(data.refs || [])
|
||||
} else {
|
||||
setRetroRefs([])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MemoryEchoSection] Failed to fetch:', error)
|
||||
} finally {
|
||||
if (!cancelled) setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Lazy load léger : ne bloque pas l'ouverture de la note
|
||||
const timer = window.setTimeout(() => { void load() }, 400)
|
||||
return () => {
|
||||
cancelled = true
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
}, [noteId])
|
||||
|
||||
// Scroll doux vers la section quand une forte connexion apparaît (une fois par note)
|
||||
useEffect(() => {
|
||||
if (isLoading || connections.length === 0) return
|
||||
const top = connections[0]
|
||||
if (!top || top.similarity < 0.75) return
|
||||
const key = `memory-echo-scroll-${noteId}`
|
||||
if (sessionStorage.getItem(key)) return
|
||||
sessionStorage.setItem(key, '1')
|
||||
requestAnimationFrame(() => {
|
||||
document.getElementById('memory-echo-section')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
})
|
||||
}, [isLoading, connections, noteId])
|
||||
|
||||
const handleEmbed = useCallback(async (conn: ConnectionData) => {
|
||||
setEmbeddingId(conn.noteId)
|
||||
try {
|
||||
const hint = excerpt(stripHtml(conn.content), 300)
|
||||
const resolved = await resolveBlockForEmbed(conn.noteId, hint)
|
||||
const noteTitle = conn.title || t('memoryEcho.comparison.untitled')
|
||||
|
||||
if (resolved?.mode === 'live' && resolved.block.blockId) {
|
||||
const block = resolved.block
|
||||
if (noteId) {
|
||||
try {
|
||||
await fetch('/api/blocks/embed', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sourceNoteId: block.noteId,
|
||||
blockId: block.blockId,
|
||||
targetNoteId: noteId,
|
||||
}),
|
||||
})
|
||||
} catch {
|
||||
// non bloquant
|
||||
}
|
||||
}
|
||||
const insertedLive = editorCtx.richTextEditorRef.current?.insertLiveBlock(block, { atEnd: true })
|
||||
if (!insertedLive) {
|
||||
dispatchLiveBlockInsert(block)
|
||||
}
|
||||
if (insertedLive || editorCtx.richTextEditorRef.current?.getEditor()) {
|
||||
toast.success(t('memoryEcho.editorSection.embedSuccess'))
|
||||
} else {
|
||||
toast.error(t('memoryEcho.editorSection.embedFailed'))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const citationText = stripHtml(
|
||||
resolved?.block.content || conn.content || hint
|
||||
).slice(0, 1200)
|
||||
|
||||
if (!citationText.trim()) {
|
||||
toast.error(t('memoryEcho.editorSection.embedFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
const payload = { noteId: conn.noteId, noteTitle, excerpt: citationText }
|
||||
|
||||
if (editorCtx.state.isMarkdown) {
|
||||
const quoted = citationText.split('\n').map(line => `> ${line}`).join('\n')
|
||||
const mdCitation = `\n\n${quoted}\n\n— [${noteTitle}](/home?openNote=${conn.noteId})\n`
|
||||
editorCtx.actions.setContent(editorCtx.state.content + mdCitation)
|
||||
toast.success(t('memoryEcho.editorSection.citationSuccess'))
|
||||
return
|
||||
}
|
||||
|
||||
const inserted = editorCtx.richTextEditorRef.current?.insertCitation(payload, { atEnd: true })
|
||||
if (inserted) {
|
||||
toast.success(t('memoryEcho.editorSection.citationSuccess'))
|
||||
return
|
||||
}
|
||||
|
||||
dispatchCitationInsert(payload)
|
||||
toast.success(t('memoryEcho.editorSection.citationSuccess'))
|
||||
} catch {
|
||||
toast.error(t('memoryEcho.editorSection.embedFailed'))
|
||||
} finally {
|
||||
setEmbeddingId(null)
|
||||
}
|
||||
}, [t, noteId, editorCtx])
|
||||
|
||||
if (!isVisible) return null
|
||||
|
||||
const hasConnections = connections.length > 0
|
||||
const hasRetro = retroRefs.length > 0
|
||||
|
||||
if (!isLoading && !hasConnections && !hasRetro && !consentRequired) {
|
||||
return null
|
||||
}
|
||||
|
||||
const topConnection = connections[0]
|
||||
const restConnections = connections.slice(1)
|
||||
|
||||
return (
|
||||
<div id="memory-echo-section" className="mt-10 space-y-6 scroll-mt-24">
|
||||
{isLoading && (
|
||||
<div
|
||||
className="rounded-2xl border border-indigo-500/10 bg-gradient-to-br from-indigo-500/[0.03] to-transparent p-5 animate-pulse space-y-3"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4 text-indigo-400" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-[0.25em] text-indigo-500/60">
|
||||
{t('memoryEcho.editorSection.badgeLabel')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-4 bg-indigo-500/10 rounded w-3/4" />
|
||||
<div className="h-16 bg-indigo-500/5 rounded border-l-2 border-indigo-500/20" />
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{t('memoryEcho.editorSection.loading')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && consentRequired && (
|
||||
<div className="rounded-2xl border border-amber-500/20 bg-amber-500/[0.04] p-4 text-sm text-muted-foreground">
|
||||
{t('memoryEcho.editorSection.consentRequired')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && hasConnections && topConnection && (
|
||||
<div className="rounded-2xl border border-indigo-500/10 bg-gradient-to-br from-indigo-500/[0.03] to-transparent p-4 space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Sparkles className="h-4 w-4 text-indigo-500 animate-pulse shrink-0" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-[0.25em] text-indigo-600/80 dark:text-indigo-400/80 truncate">
|
||||
{t('memoryEcho.editorSection.badgeLabel')}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setHelpOpen(v => !v)}
|
||||
className="p-0.5 rounded-md text-indigo-500/70 hover:text-indigo-600 hover:bg-indigo-500/10 transition-colors"
|
||||
aria-expanded={helpOpen}
|
||||
aria-label={t('memoryEcho.editorSection.helpToggle')}
|
||||
>
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[11px] font-mono font-semibold px-2 py-0.5 rounded-full bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 border border-indigo-500/15">
|
||||
{t('memoryEcho.editorSection.affinityBadge', {
|
||||
percentage: Math.round(topConnection.similarity * 100),
|
||||
})}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0"
|
||||
onClick={() => setIsVisible(false)}
|
||||
title={t('memoryEcho.editorSection.close')}
|
||||
>
|
||||
<X className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground hidden sm:block">
|
||||
{t('memoryEcho.editorSection.intro')}
|
||||
</p>
|
||||
|
||||
{helpOpen && (
|
||||
<div className="rounded-xl border border-indigo-500/15 bg-indigo-500/[0.04] p-4 space-y-3 text-sm text-muted-foreground">
|
||||
<p className="font-medium text-foreground">{t('memoryEcho.editorSection.helpTitle')}</p>
|
||||
<ul className="space-y-2.5 list-none pl-0">
|
||||
<li><strong className="text-foreground">{t('memoryEcho.editorSection.viewLinkedNote')}</strong> — {t('memoryEcho.editorSection.helpView')}</li>
|
||||
<li><strong className="text-foreground">{t('memoryEcho.editorSection.embedPassage')}</strong> — {t('memoryEcho.editorSection.helpCite')}</li>
|
||||
<li><strong className="text-foreground">{t('memoryEcho.editorSection.compare')}</strong> — {t('memoryEcho.editorSection.helpCompare')}</li>
|
||||
<li><strong className="text-foreground">{t('memoryEcho.editorSection.merge')}</strong> — {t('memoryEcho.editorSection.helpMerge')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<blockquote className="border-l-2 border-indigo-500/20 pl-3 font-serif italic text-sm leading-relaxed text-foreground/85">
|
||||
« {excerpt(topConnection.content)} »
|
||||
</blockquote>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 pt-1 border-t border-indigo-500/10 flex-wrap text-[11px]">
|
||||
<span className="text-muted-foreground min-w-0 truncate">
|
||||
{t('memoryEcho.editorSection.detectedIn', {
|
||||
title: topConnection.title || t('memoryEcho.comparison.untitled'),
|
||||
})}
|
||||
</span>
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<ActionWithHelp
|
||||
label={t('memoryEcho.editorSection.viewLinkedNote')}
|
||||
help={t('memoryEcho.editorSection.helpView')}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="font-semibold text-muted-foreground hover:text-foreground hover:underline transition-colors"
|
||||
onClick={() => setPreviewTarget({
|
||||
noteId: topConnection.noteId,
|
||||
title: topConnection.title,
|
||||
excerpt: excerpt(topConnection.content, 500),
|
||||
})}
|
||||
>
|
||||
{t('memoryEcho.editorSection.view')}
|
||||
</button>
|
||||
</ActionWithHelp>
|
||||
<ActionWithHelp
|
||||
label={t('memoryEcho.editorSection.embedPassage')}
|
||||
help={t('memoryEcho.editorSection.helpCite')}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
disabled={embeddingId === topConnection.noteId}
|
||||
className="inline-flex items-center gap-1 font-bold text-indigo-600 dark:text-indigo-400 hover:underline disabled:opacity-50"
|
||||
onClick={() => void handleEmbed(topConnection)}
|
||||
>
|
||||
<Link2 className="h-3 w-3" />
|
||||
{embeddingId === topConnection.noteId
|
||||
? t('memoryEcho.editorSection.embedding')
|
||||
: t('memoryEcho.editorSection.embedPassage')}
|
||||
</button>
|
||||
</ActionWithHelp>
|
||||
{onCompareNotes && (
|
||||
<ActionWithHelp
|
||||
label={t('memoryEcho.editorSection.compare')}
|
||||
help={t('memoryEcho.editorSection.helpCompare')}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="font-semibold text-muted-foreground hover:text-foreground hover:underline transition-colors"
|
||||
onClick={() => onCompareNotes([noteId, topConnection.noteId], { similarity: topConnection.similarity })}
|
||||
>
|
||||
{t('memoryEcho.editorSection.compare')}
|
||||
</button>
|
||||
</ActionWithHelp>
|
||||
)}
|
||||
{onMergeNotes && (
|
||||
<ActionWithHelp
|
||||
label={t('memoryEcho.editorSection.merge')}
|
||||
help={t('memoryEcho.editorSection.helpMerge')}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="font-semibold text-purple-600 dark:text-purple-400 hover:underline transition-colors"
|
||||
onClick={() => onMergeNotes([noteId, topConnection.noteId])}
|
||||
>
|
||||
{t('memoryEcho.editorSection.merge')}
|
||||
</button>
|
||||
</ActionWithHelp>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{restConnections.length > 0 && (
|
||||
<div className="pt-2 border-t border-indigo-500/10">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1.5 text-xs font-medium text-indigo-600 dark:text-indigo-400 hover:underline"
|
||||
onClick={() => setIsExpanded(v => !v)}
|
||||
>
|
||||
{isExpanded
|
||||
? t('memoryEcho.editorSection.hideAll', { count: connections.length })
|
||||
: t('memoryEcho.editorSection.showAll', { count: connections.length })}
|
||||
{isExpanded ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-3 space-y-2 max-h-[280px] overflow-y-auto">
|
||||
{restConnections.map(conn => (
|
||||
<div
|
||||
key={conn.noteId}
|
||||
className="rounded-xl border border-border/60 p-3 bg-background/60 space-y-2"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h4 className="text-sm font-medium flex-1">
|
||||
{conn.title || t('memoryEcho.comparison.untitled')}
|
||||
</h4>
|
||||
<span className="text-[10px] font-mono px-2 py-0.5 rounded-full bg-indigo-500/10 text-indigo-700 dark:text-indigo-300">
|
||||
{Math.round(conn.similarity * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2 font-serif italic">
|
||||
« {excerpt(conn.content, 120)} »
|
||||
</p>
|
||||
<div className="flex items-center gap-3 text-[11px]">
|
||||
<button
|
||||
type="button"
|
||||
className="font-semibold text-muted-foreground hover:text-foreground hover:underline"
|
||||
onClick={() => setPreviewTarget({
|
||||
noteId: conn.noteId,
|
||||
title: conn.title,
|
||||
excerpt: excerpt(conn.content, 500),
|
||||
})}
|
||||
>
|
||||
{t('memoryEcho.editorSection.view')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={embeddingId === conn.noteId}
|
||||
className="inline-flex items-center gap-1 font-bold text-indigo-600 dark:text-indigo-400 hover:underline disabled:opacity-50"
|
||||
onClick={() => void handleEmbed(conn)}
|
||||
>
|
||||
<Link2 className="h-3 w-3" />
|
||||
{t('memoryEcho.editorSection.embedPassage')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && hasRetro && (
|
||||
<div className="rounded-2xl border border-blue-500/10 bg-blue-500/[0.02] p-5 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-60" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
|
||||
</span>
|
||||
<span className="text-[10px] font-bold uppercase tracking-[0.22em] text-blue-600/80 dark:text-blue-400/80">
|
||||
{t('memoryEcho.editorSection.retroTitle')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('memoryEcho.editorSection.retroDescription', { count: retroRefs.length })}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{retroRefs.map(ref => (
|
||||
<button
|
||||
key={ref.targetNoteId}
|
||||
type="button"
|
||||
onClick={() => setPreviewTarget({
|
||||
noteId: ref.targetNoteId,
|
||||
title: ref.targetNoteTitle,
|
||||
excerpt: '',
|
||||
})}
|
||||
className={cn(
|
||||
'w-full text-left rounded-xl border border-border/50 p-3',
|
||||
'hover:bg-muted/40 transition-colors flex items-center gap-3'
|
||||
)}
|
||||
>
|
||||
<Link2 className="h-4 w-4 text-blue-500 shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{ref.targetNoteTitle || t('memoryEcho.comparison.untitled')}
|
||||
</p>
|
||||
{ref.notebookName && (
|
||||
<p className="text-[11px] text-muted-foreground truncate">{ref.notebookName}</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{previewTarget && (
|
||||
<LinkedNotePreviewDialog
|
||||
isOpen
|
||||
onClose={() => setPreviewTarget(null)}
|
||||
noteId={previewTarget.noteId}
|
||||
initialTitle={previewTarget.title}
|
||||
initialExcerpt={previewTarget.excerpt}
|
||||
citationLoading={embeddingId === previewTarget.noteId}
|
||||
onInsertCitation={() => {
|
||||
void handleEmbed({
|
||||
noteId: previewTarget.noteId,
|
||||
title: previewTarget.title,
|
||||
content: previewTarget.excerpt || connections.find(c => c.noteId === previewTarget.noteId)?.content || '',
|
||||
createdAt: new Date(),
|
||||
similarity: connections.find(c => c.noteId === previewTarget.noteId)?.similarity ?? 0,
|
||||
daysApart: 0,
|
||||
})
|
||||
}}
|
||||
onCompare={
|
||||
onCompareNotes
|
||||
? () => onCompareNotes([noteId, previewTarget.noteId], {
|
||||
similarity: connections.find(c => c.noteId === previewTarget.noteId)?.similarity,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
onMerge={
|
||||
onMergeNotes
|
||||
? () => onMergeNotes([noteId, previewTarget.noteId])
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** @deprecated Use MemoryEchoSection */
|
||||
export { MemoryEchoSection as EditorConnectionsSection }
|
||||
Reference in New Issue
Block a user