fix(memory-echo): fix broken AI provider config and auto-generate missing embeddings
- Fix critical bug: used prisma.systemConfig.findFirst() which returned
a single {key,value} record instead of the full config object needed
by getAIProvider(). Replaced with getSystemConfig() that returns all
config as a proper Record<string,string>.
- Add ensureEmbeddings() to auto-generate embeddings for notes that
lack them before searching for connections. This fixes the case where
notes created without an AI provider configured never got embeddings,
making Memory Echo silently return zero connections.
- Restore demo mode polling (15s interval after dismiss) in the
notification component.
- Integrate ComparisonModal and FusionModal in the notification card
with merge button for direct note fusion from the notification.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,12 @@ import { useLanguage } from '@/lib/i18n/LanguageProvider'
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Lightbulb, ThumbsUp, ThumbsDown, X, Sparkles, ArrowRight } from 'lucide-react'
|
import { Lightbulb, ThumbsUp, ThumbsDown, X, Sparkles, ArrowRight, GitMerge } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { ComparisonModal } from './comparison-modal'
|
||||||
|
import { FusionModal } from './fusion-modal'
|
||||||
|
import { createNote, updateNote } from '@/app/actions/notes'
|
||||||
|
import { Note } from '@/lib/types'
|
||||||
|
|
||||||
interface MemoryEchoInsight {
|
interface MemoryEchoInsight {
|
||||||
id: string
|
id: string
|
||||||
@@ -38,7 +42,8 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
|||||||
const [insight, setInsight] = useState<MemoryEchoInsight | null>(null)
|
const [insight, setInsight] = useState<MemoryEchoInsight | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [isDismissed, setIsDismissed] = useState(false)
|
const [isDismissed, setIsDismissed] = useState(false)
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showComparison, setShowComparison] = useState(false)
|
||||||
|
const [fusionNotes, setFusionNotes] = useState<Array<Partial<Note>>>([])
|
||||||
const [demoMode, setDemoMode] = useState(false)
|
const [demoMode, setDemoMode] = useState(false)
|
||||||
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
|
||||||
@@ -58,8 +63,8 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
|||||||
|
|
||||||
if (data.insight) {
|
if (data.insight) {
|
||||||
setInsight(data.insight)
|
setInsight(data.insight)
|
||||||
// Check if user is in demo mode by looking at frequency settings
|
// If we got an insight, check if user is in demo mode
|
||||||
setDemoMode(true) // If we got an insight after dismiss, assume demo mode
|
setDemoMode(true)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[MemoryEcho] Failed to fetch insight:', error)
|
console.error('[MemoryEcho] Failed to fetch insight:', error)
|
||||||
@@ -70,7 +75,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
|||||||
|
|
||||||
// Start polling in demo mode after first dismiss
|
// Start polling in demo mode after first dismiss
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDismissed && !pollingRef.current) {
|
if (isDismissed && demoMode && !pollingRef.current) {
|
||||||
pollingRef.current = setInterval(async () => {
|
pollingRef.current = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/ai/echo')
|
const res = await fetch('/api/ai/echo')
|
||||||
@@ -90,25 +95,20 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
|||||||
pollingRef.current = null
|
pollingRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isDismissed])
|
}, [isDismissed, demoMode])
|
||||||
|
|
||||||
const handleView = async () => {
|
const handleView = async () => {
|
||||||
if (!insight) return
|
if (!insight) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Mark as viewed
|
|
||||||
await fetch('/api/ai/echo', {
|
await fetch('/api/ai/echo', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ action: 'view', insightId: insight.id })
|
||||||
action: 'view',
|
|
||||||
insightId: insight.id
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Show success message and open modal
|
|
||||||
toast.success(t('toast.openingConnection'))
|
toast.success(t('toast.openingConnection'))
|
||||||
setShowModal(true)
|
setShowComparison(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[MemoryEcho] Failed to view connection:', error)
|
console.error('[MemoryEcho] Failed to view connection:', error)
|
||||||
toast.error(t('toast.openConnectionFailed'))
|
toast.error(t('toast.openConnectionFailed'))
|
||||||
@@ -122,21 +122,15 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
|||||||
await fetch('/api/ai/echo', {
|
await fetch('/api/ai/echo', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ action: 'feedback', insightId: insight.id, feedback })
|
||||||
action: 'feedback',
|
|
||||||
insightId: insight.id,
|
|
||||||
feedback
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Show feedback toast
|
|
||||||
if (feedback === 'thumbs_up') {
|
if (feedback === 'thumbs_up') {
|
||||||
toast.success(t('toast.thanksFeedback'))
|
toast.success(t('toast.thanksFeedback'))
|
||||||
} else {
|
} else {
|
||||||
toast.success(t('toast.thanksFeedbackImproving'))
|
toast.success(t('toast.thanksFeedbackImproving'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dismiss notification
|
|
||||||
setIsDismissed(true)
|
setIsDismissed(true)
|
||||||
// Stop polling after explicit feedback
|
// Stop polling after explicit feedback
|
||||||
if (pollingRef.current) {
|
if (pollingRef.current) {
|
||||||
@@ -149,131 +143,88 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleMergeNotes = async (noteIds: string[]) => {
|
||||||
|
if (!insight) return
|
||||||
|
const fetched = await Promise.all(noteIds.map(async (id) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/notes/${id}`)
|
||||||
|
if (!res.ok) return null
|
||||||
|
const data = await res.json()
|
||||||
|
return data.success && data.data ? data.data : null
|
||||||
|
} catch { return null }
|
||||||
|
}))
|
||||||
|
setFusionNotes(fetched.filter((n: any) => n !== null) as Array<Partial<Note>>)
|
||||||
|
}
|
||||||
|
|
||||||
const handleDismiss = () => {
|
const handleDismiss = () => {
|
||||||
setIsDismissed(true)
|
setIsDismissed(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't render notification if dismissed, loading, or no insight
|
if (isLoading || !insight) {
|
||||||
if (isDismissed || isLoading || !insight) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate values for both notification and modal
|
|
||||||
const note1Title = insight.note1.title || t('notification.untitled')
|
const note1Title = insight.note1.title || t('notification.untitled')
|
||||||
const note2Title = insight.note2.title || t('notification.untitled')
|
const note2Title = insight.note2.title || t('notification.untitled')
|
||||||
const similarityPercentage = Math.round(insight.similarityScore * 100)
|
const similarityPercentage = Math.round(insight.similarityScore * 100)
|
||||||
|
|
||||||
// Render modal if requested
|
const comparisonNotes: Array<Partial<Note>> = [
|
||||||
if (showModal && insight) {
|
{ id: insight.note1.id, title: insight.note1.title, content: insight.note1.content },
|
||||||
|
{ id: insight.note2.id, title: insight.note2.title, content: insight.note2.content }
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
|
<>
|
||||||
<div className="bg-white dark:bg-zinc-900 rounded-lg shadow-xl max-w-5xl w-full max-h-[90vh] overflow-hidden">
|
{/* Comparison Modal */}
|
||||||
{/* Header */}
|
<ComparisonModal
|
||||||
<div className="flex items-center justify-between p-6 border-b dark:border-zinc-700">
|
isOpen={showComparison}
|
||||||
<div className="flex items-center gap-3">
|
onClose={() => {
|
||||||
<div className="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-full">
|
setShowComparison(false)
|
||||||
<Sparkles className="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-semibold">{t('memoryEcho.title')}</h2>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
{t('connection.similarityInfo', { similarity: similarityPercentage })}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setShowModal(false)
|
|
||||||
setIsDismissed(true)
|
setIsDismissed(true)
|
||||||
}}
|
}}
|
||||||
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
notes={comparisonNotes}
|
||||||
>
|
similarity={insight.similarityScore}
|
||||||
<X className="h-6 w-6" />
|
onOpenNote={(noteId) => {
|
||||||
</button>
|
setShowComparison(false)
|
||||||
</div>
|
setIsDismissed(true)
|
||||||
|
onOpenNote?.(noteId)
|
||||||
{/* AI-generated insight */}
|
|
||||||
<div className="px-6 py-4 bg-amber-50 dark:bg-amber-950/20 border-b dark:border-zinc-700">
|
|
||||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{insight.insight}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Notes Grid */}
|
|
||||||
<div className="grid grid-cols-2 gap-6 p-6">
|
|
||||||
{/* Note 1 */}
|
|
||||||
<div
|
|
||||||
onClick={() => {
|
|
||||||
if (onOpenNote) {
|
|
||||||
onOpenNote(insight.note1.id)
|
|
||||||
setShowModal(false)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer border dark:border-zinc-700 rounded-lg p-4 hover:border-amber-300 dark:hover:border-amber-700 transition-colors"
|
onMergeNotes={handleMergeNotes}
|
||||||
>
|
/>
|
||||||
<h3 className="font-semibold text-primary dark:text-primary-foreground mb-2">
|
|
||||||
{note1Title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-4">
|
|
||||||
{insight.note1.content}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 mt-2">{t('memoryEcho.clickToView')}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Note 2 */}
|
{/* Fusion Modal */}
|
||||||
<div
|
{fusionNotes.length > 0 && (
|
||||||
onClick={() => {
|
<FusionModal
|
||||||
if (onOpenNote) {
|
isOpen={fusionNotes.length > 0}
|
||||||
onOpenNote(insight.note2.id)
|
onClose={() => setFusionNotes([])}
|
||||||
setShowModal(false)
|
notes={fusionNotes}
|
||||||
|
onConfirmFusion={async ({ title, content }, options) => {
|
||||||
|
await createNote({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
labels: options.keepAllTags
|
||||||
|
? [...new Set(fusionNotes.flatMap(n => n.labels || []))]
|
||||||
|
: fusionNotes[0].labels || [],
|
||||||
|
color: fusionNotes[0].color,
|
||||||
|
type: 'text',
|
||||||
|
isMarkdown: true,
|
||||||
|
autoGenerated: true,
|
||||||
|
notebookId: fusionNotes[0].notebookId ?? undefined
|
||||||
|
})
|
||||||
|
if (options.archiveOriginals) {
|
||||||
|
for (const n of fusionNotes) {
|
||||||
|
if (n.id) await updateNote(n.id, { isArchived: true })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
toast.success(t('toast.notesFusionSuccess'))
|
||||||
|
setFusionNotes([])
|
||||||
|
setIsDismissed(true)
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer border dark:border-zinc-700 rounded-lg p-4 hover:border-purple-300 dark:hover:border-purple-700 transition-colors"
|
/>
|
||||||
>
|
)}
|
||||||
<h3 className="font-semibold text-purple-600 dark:text-purple-400 mb-2">
|
|
||||||
{note2Title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-4">
|
|
||||||
{insight.note2.content}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 mt-2">{t('memoryEcho.clickToView')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Feedback Section */}
|
{/* Notification Card — hidden when dismissed or modal open */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-t dark:border-zinc-700 bg-gray-50 dark:bg-zinc-800/50">
|
{!isDismissed && (
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
{t('connection.isHelpful')}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => handleFeedback('thumbs_up')}
|
|
||||||
className={`px-4 py-2 rounded-lg flex items-center gap-2 transition-colors ${insight.feedback === 'thumbs_up'
|
|
||||||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
||||||
: 'hover:bg-green-50 text-green-600 dark:hover:bg-green-950/20'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<ThumbsUp className="h-4 w-4" />
|
|
||||||
{t('memoryEcho.helpful')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleFeedback('thumbs_down')}
|
|
||||||
className={`px-4 py-2 rounded-lg flex items-center gap-2 transition-colors ${insight.feedback === 'thumbs_down'
|
|
||||||
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
|
||||||
: 'hover:bg-red-50 text-red-600 dark:hover:bg-red-950/20'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<ThumbsDown className="h-4 w-4" />
|
|
||||||
{t('memoryEcho.notHelpful')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed bottom-4 right-4 z-50 max-w-md w-full animate-in slide-in-from-bottom-4 fade-in duration-500">
|
<div className="fixed bottom-4 right-4 z-50 max-w-md w-full animate-in slide-in-from-bottom-4 fade-in duration-500">
|
||||||
<Card className="border-amber-200 dark:border-amber-900 shadow-lg bg-gradient-to-br from-amber-50 to-white dark:from-amber-950/20 dark:to-background">
|
<Card className="border-amber-200 dark:border-amber-900 shadow-lg bg-gradient-to-br from-amber-50 to-white dark:from-amber-950/20 dark:to-background">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
@@ -337,6 +288,16 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
|||||||
{t('memoryEcho.viewConnection')}
|
{t('memoryEcho.viewConnection')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1 border-purple-200 text-purple-700 hover:bg-purple-50 dark:border-purple-800 dark:text-purple-400 dark:hover:bg-purple-950/20"
|
||||||
|
onClick={() => handleMergeNotes([insight.note1.id, insight.note2.id])}
|
||||||
|
>
|
||||||
|
<GitMerge className="h-4 w-4 mr-1" />
|
||||||
|
{t('memoryEcho.editorSection.merge')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<div className="flex items-center gap-1 border-l pl-2">
|
<div className="flex items-center gap-1 border-l pl-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -369,5 +330,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getAIProvider } from '../factory'
|
import { getAIProvider } from '../factory'
|
||||||
import { cosineSimilarity } from '@/lib/utils'
|
import { cosineSimilarity } from '@/lib/utils'
|
||||||
|
import { getSystemConfig } from '@/lib/config'
|
||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
|
|
||||||
export interface NoteConnection {
|
export interface NoteConnection {
|
||||||
@@ -52,10 +53,53 @@ export class MemoryEchoService {
|
|||||||
private readonly MIN_DAYS_APART_DEMO = 0 // No delay for demo mode
|
private readonly MIN_DAYS_APART_DEMO = 0 // No delay for demo mode
|
||||||
private readonly MAX_INSIGHTS_PER_USER = 100 // Prevent spam
|
private readonly MAX_INSIGHTS_PER_USER = 100 // Prevent spam
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate embeddings for notes that don't have one yet
|
||||||
|
*/
|
||||||
|
private async ensureEmbeddings(userId: string): Promise<void> {
|
||||||
|
const notesWithoutEmbeddings = await prisma.note.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
isArchived: false,
|
||||||
|
trashedAt: null,
|
||||||
|
noteEmbedding: { is: null }
|
||||||
|
},
|
||||||
|
select: { id: true, content: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (notesWithoutEmbeddings.length === 0) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = await getSystemConfig()
|
||||||
|
const provider = getAIProvider(config)
|
||||||
|
|
||||||
|
for (const note of notesWithoutEmbeddings) {
|
||||||
|
if (!note.content || note.content.trim().length === 0) continue
|
||||||
|
try {
|
||||||
|
const embedding = await provider.getEmbeddings(note.content)
|
||||||
|
if (embedding && embedding.length > 0) {
|
||||||
|
await prisma.noteEmbedding.upsert({
|
||||||
|
where: { noteId: note.id },
|
||||||
|
create: { noteId: note.id, embedding: JSON.stringify(embedding) },
|
||||||
|
update: { embedding: JSON.stringify(embedding) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip this note, continue with others
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Provider not configured — nothing we can do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find meaningful connections between user's notes
|
* Find meaningful connections between user's notes
|
||||||
*/
|
*/
|
||||||
async findConnections(userId: string, demoMode: boolean = false): Promise<NoteConnection[]> {
|
async findConnections(userId: string, demoMode: boolean = false): Promise<NoteConnection[]> {
|
||||||
|
// Ensure all notes have embeddings before searching for connections
|
||||||
|
await this.ensureEmbeddings(userId)
|
||||||
|
|
||||||
// Get all user's notes with embeddings
|
// Get all user's notes with embeddings
|
||||||
const notes = await prisma.note.findMany({
|
const notes = await prisma.note.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -151,8 +195,8 @@ export class MemoryEchoService {
|
|||||||
note2Content: string
|
note2Content: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const config = await prisma.systemConfig.findFirst()
|
const config = await getSystemConfig()
|
||||||
const provider = getAIProvider(config || undefined)
|
const provider = getAIProvider(config)
|
||||||
|
|
||||||
const note1Desc = note1Title || 'Untitled note'
|
const note1Desc = note1Title || 'Untitled note'
|
||||||
const note2Desc = note2Title || 'Untitled note'
|
const note2Desc = note2Title || 'Untitled note'
|
||||||
@@ -366,6 +410,9 @@ Explain in one brief sentence (max 15 words) why these notes are connected. Focu
|
|||||||
* Get all connections for a specific note
|
* Get all connections for a specific note
|
||||||
*/
|
*/
|
||||||
async getConnectionsForNote(noteId: string, userId: string): Promise<NoteConnection[]> {
|
async getConnectionsForNote(noteId: string, userId: string): Promise<NoteConnection[]> {
|
||||||
|
// Ensure all notes have embeddings before searching
|
||||||
|
await this.ensureEmbeddings(userId)
|
||||||
|
|
||||||
// Get the note with embedding
|
// Get the note with embedding
|
||||||
const targetNote = await prisma.note.findUnique({
|
const targetNote = await prisma.note.findUnique({
|
||||||
where: { id: noteId },
|
where: { id: noteId },
|
||||||
|
|||||||
Reference in New Issue
Block a user