epic-ux-design #1
@@ -5,8 +5,12 @@ import { useLanguage } from '@/lib/i18n/LanguageProvider'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
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 { ComparisonModal } from './comparison-modal'
|
||||
import { FusionModal } from './fusion-modal'
|
||||
import { createNote, updateNote } from '@/app/actions/notes'
|
||||
import { Note } from '@/lib/types'
|
||||
|
||||
interface MemoryEchoInsight {
|
||||
id: string
|
||||
@@ -38,7 +42,8 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
||||
const [insight, setInsight] = useState<MemoryEchoInsight | null>(null)
|
||||
const [isLoading, setIsLoading] = 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 pollingRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
@@ -58,8 +63,8 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
||||
|
||||
if (data.insight) {
|
||||
setInsight(data.insight)
|
||||
// Check if user is in demo mode by looking at frequency settings
|
||||
setDemoMode(true) // If we got an insight after dismiss, assume demo mode
|
||||
// If we got an insight, check if user is in demo mode
|
||||
setDemoMode(true)
|
||||
}
|
||||
} catch (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
|
||||
useEffect(() => {
|
||||
if (isDismissed && !pollingRef.current) {
|
||||
if (isDismissed && demoMode && !pollingRef.current) {
|
||||
pollingRef.current = setInterval(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/ai/echo')
|
||||
@@ -90,25 +95,20 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
||||
pollingRef.current = null
|
||||
}
|
||||
}
|
||||
}, [isDismissed])
|
||||
}, [isDismissed, demoMode])
|
||||
|
||||
const handleView = async () => {
|
||||
if (!insight) return
|
||||
|
||||
try {
|
||||
// Mark as viewed
|
||||
await fetch('/api/ai/echo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'view',
|
||||
insightId: insight.id
|
||||
})
|
||||
body: JSON.stringify({ action: 'view', insightId: insight.id })
|
||||
})
|
||||
|
||||
// Show success message and open modal
|
||||
toast.success(t('toast.openingConnection'))
|
||||
setShowModal(true)
|
||||
setShowComparison(true)
|
||||
} catch (error) {
|
||||
console.error('[MemoryEcho] Failed to view connection:', error)
|
||||
toast.error(t('toast.openConnectionFailed'))
|
||||
@@ -122,21 +122,15 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
|
||||
await fetch('/api/ai/echo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'feedback',
|
||||
insightId: insight.id,
|
||||
feedback
|
||||
})
|
||||
body: JSON.stringify({ action: 'feedback', insightId: insight.id, feedback })
|
||||
})
|
||||
|
||||
// Show feedback toast
|
||||
if (feedback === 'thumbs_up') {
|
||||
toast.success(t('toast.thanksFeedback'))
|
||||
} else {
|
||||
toast.success(t('toast.thanksFeedbackImproving'))
|
||||
}
|
||||
|
||||
// Dismiss notification
|
||||
setIsDismissed(true)
|
||||
// Stop polling after explicit feedback
|
||||
if (pollingRef.current) {
|
||||
@@ -149,225 +143,194 @@ 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 = () => {
|
||||
setIsDismissed(true)
|
||||
}
|
||||
|
||||
// Don't render notification if dismissed, loading, or no insight
|
||||
if (isDismissed || isLoading || !insight) {
|
||||
if (isLoading || !insight) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Calculate values for both notification and modal
|
||||
const note1Title = insight.note1.title || t('notification.untitled')
|
||||
const note2Title = insight.note2.title || t('notification.untitled')
|
||||
const similarityPercentage = Math.round(insight.similarityScore * 100)
|
||||
|
||||
// Render modal if requested
|
||||
if (showModal && insight) {
|
||||
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">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b dark:border-zinc-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-full">
|
||||
<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)
|
||||
}}
|
||||
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 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"
|
||||
>
|
||||
<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 */}
|
||||
<div
|
||||
onClick={() => {
|
||||
if (onOpenNote) {
|
||||
onOpenNote(insight.note2.id)
|
||||
setShowModal(false)
|
||||
}
|
||||
}}
|
||||
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 */}
|
||||
<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">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
const comparisonNotes: Array<Partial<Note>> = [
|
||||
{ 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 (
|
||||
<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">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-full">
|
||||
<Lightbulb className="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
||||
<>
|
||||
{/* Comparison Modal */}
|
||||
<ComparisonModal
|
||||
isOpen={showComparison}
|
||||
onClose={() => {
|
||||
setShowComparison(false)
|
||||
setIsDismissed(true)
|
||||
}}
|
||||
notes={comparisonNotes}
|
||||
similarity={insight.similarityScore}
|
||||
onOpenNote={(noteId) => {
|
||||
setShowComparison(false)
|
||||
setIsDismissed(true)
|
||||
onOpenNote?.(noteId)
|
||||
}}
|
||||
onMergeNotes={handleMergeNotes}
|
||||
/>
|
||||
|
||||
{/* Fusion Modal */}
|
||||
{fusionNotes.length > 0 && (
|
||||
<FusionModal
|
||||
isOpen={fusionNotes.length > 0}
|
||||
onClose={() => setFusionNotes([])}
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Notification Card — hidden when dismissed or modal open */}
|
||||
{!isDismissed && (
|
||||
<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">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-full">
|
||||
<Lightbulb className="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
{t('memoryEcho.title')}
|
||||
<Sparkles className="h-4 w-4 text-amber-500" />
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs mt-1">
|
||||
{t('memoryEcho.description')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 -mr-2 -mt-2"
|
||||
onClick={handleDismiss}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
{t('memoryEcho.title')}
|
||||
<Sparkles className="h-4 w-4 text-amber-500" />
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs mt-1">
|
||||
{t('memoryEcho.description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
{/* AI-generated insight */}
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-lg p-3 border border-amber-100 dark:border-amber-900/30">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{insight.insight}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 -mr-2 -mt-2"
|
||||
onClick={handleDismiss}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
{/* AI-generated insight */}
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-lg p-3 border border-amber-100 dark:border-amber-900/30">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{insight.insight}
|
||||
</p>
|
||||
</div>
|
||||
{/* Connected notes */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Badge variant="outline" className="border-primary/20 text-primary dark:border-primary/30 dark:text-primary-foreground">
|
||||
{note1Title}
|
||||
</Badge>
|
||||
<ArrowRight className="h-3 w-3 text-gray-400" />
|
||||
<Badge variant="outline" className="border-purple-200 text-purple-700 dark:border-purple-900 dark:text-purple-300">
|
||||
{note2Title}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="ml-auto text-xs">
|
||||
{t('memoryEcho.match', { percentage: similarityPercentage })}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connected notes */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Badge variant="outline" className="border-primary/20 text-primary dark:border-primary/30 dark:text-primary-foreground">
|
||||
{note1Title}
|
||||
</Badge>
|
||||
<ArrowRight className="h-3 w-3 text-gray-400" />
|
||||
<Badge variant="outline" className="border-purple-200 text-purple-700 dark:border-purple-900 dark:text-purple-300">
|
||||
{note2Title}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="ml-auto text-xs">
|
||||
{t('memoryEcho.match', { percentage: similarityPercentage })}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1 bg-amber-600 hover:bg-amber-700 text-white"
|
||||
onClick={handleView}
|
||||
>
|
||||
{t('memoryEcho.viewConnection')}
|
||||
</Button>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1 bg-amber-600 hover:bg-amber-700 text-white"
|
||||
onClick={handleView}
|
||||
>
|
||||
{t('memoryEcho.viewConnection')}
|
||||
</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">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-green-600 hover:text-green-700 hover:bg-green-50 dark:hover:bg-green-950/20"
|
||||
onClick={() => handleFeedback('thumbs_up')}
|
||||
title={t('memoryEcho.helpful')}
|
||||
<div className="flex items-center gap-1 border-l pl-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-green-600 hover:text-green-700 hover:bg-green-50 dark:hover:bg-green-950/20"
|
||||
onClick={() => handleFeedback('thumbs_up')}
|
||||
title={t('memoryEcho.helpful')}
|
||||
>
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20"
|
||||
onClick={() => handleFeedback('thumbs_down')}
|
||||
title={t('memoryEcho.notHelpful')}
|
||||
>
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dismiss link */}
|
||||
<button
|
||||
className="w-full text-center text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 py-1"
|
||||
onClick={handleDismiss}
|
||||
>
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20"
|
||||
onClick={() => handleFeedback('thumbs_down')}
|
||||
title={t('memoryEcho.notHelpful')}
|
||||
>
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dismiss link */}
|
||||
<button
|
||||
className="w-full text-center text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 py-1"
|
||||
onClick={handleDismiss}
|
||||
>
|
||||
{t('memoryEcho.dismiss')}
|
||||
</button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{t('memoryEcho.dismiss')}
|
||||
</button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getAIProvider } from '../factory'
|
||||
import { cosineSimilarity } from '@/lib/utils'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
export interface NoteConnection {
|
||||
@@ -52,10 +53,53 @@ export class MemoryEchoService {
|
||||
private readonly MIN_DAYS_APART_DEMO = 0 // No delay for demo mode
|
||||
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
|
||||
*/
|
||||
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
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
@@ -151,8 +195,8 @@ export class MemoryEchoService {
|
||||
note2Content: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
const config = await prisma.systemConfig.findFirst()
|
||||
const provider = getAIProvider(config || undefined)
|
||||
const config = await getSystemConfig()
|
||||
const provider = getAIProvider(config)
|
||||
|
||||
const note1Desc = note1Title || '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
|
||||
*/
|
||||
async getConnectionsForNote(noteId: string, userId: string): Promise<NoteConnection[]> {
|
||||
// Ensure all notes have embeddings before searching
|
||||
await this.ensureEmbeddings(userId)
|
||||
|
||||
// Get the note with embedding
|
||||
const targetNote = await prisma.note.findUnique({
|
||||
where: { id: noteId },
|
||||
|
||||
Reference in New Issue
Block a user