'use client' import { useState, useEffect, useRef } from 'react' 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, 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 note1Id: string note2Id: string note1: { id: string title: string | null content: string } note2: { id: string title: string | null content: string } similarityScore: number insight: string insightDate: Date viewed: boolean feedback: string | null } interface MemoryEchoNotificationProps { onOpenNote?: (noteId: string) => void } export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationProps) { const { t } = useLanguage() const [insight, setInsight] = useState(null) const [isLoading, setIsLoading] = useState(false) const [isDismissed, setIsDismissed] = useState(false) const [showComparison, setShowComparison] = useState(false) const [fusionNotes, setFusionNotes] = useState>>([]) const [demoMode, setDemoMode] = useState(false) const pollingRef = useRef | null>(null) // Fetch insight on mount useEffect(() => { fetchInsight() return () => { if (pollingRef.current) clearInterval(pollingRef.current) } }, []) const fetchInsight = async () => { setIsLoading(true) try { const res = await fetch('/api/ai/echo') const data = await res.json() if (data.insight) { setInsight(data.insight) // If we got an insight, check if user is in demo mode setDemoMode(true) } } catch (error) { console.error('[MemoryEcho] Failed to fetch insight:', error) } finally { setIsLoading(false) } } // Start polling in demo mode after first dismiss useEffect(() => { if (isDismissed && demoMode && !pollingRef.current) { pollingRef.current = setInterval(async () => { try { const res = await fetch('/api/ai/echo') const data = await res.json() if (data.insight) { setInsight(data.insight) setIsDismissed(false) } } catch { // silent } }, 15000) // Poll every 15s } return () => { if (pollingRef.current) { clearInterval(pollingRef.current) pollingRef.current = null } } }, [isDismissed, demoMode]) const handleView = async () => { if (!insight) return try { await fetch('/api/ai/echo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'view', insightId: insight.id }) }) toast.success(t('toast.openingConnection')) setShowComparison(true) } catch (error) { console.error('[MemoryEcho] Failed to view connection:', error) toast.error(t('toast.openConnectionFailed')) } } const handleFeedback = async (feedback: 'thumbs_up' | 'thumbs_down') => { if (!insight) return try { await fetch('/api/ai/echo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'feedback', insightId: insight.id, feedback }) }) if (feedback === 'thumbs_up') { toast.success(t('toast.thanksFeedback')) } else { toast.success(t('toast.thanksFeedbackImproving')) } setIsDismissed(true) // Stop polling after explicit feedback if (pollingRef.current) { clearInterval(pollingRef.current) pollingRef.current = null } } catch (error) { console.error('[MemoryEcho] Failed to submit feedback:', error) toast.error(t('toast.feedbackFailed')) } } 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>) } const handleDismiss = () => { setIsDismissed(true) } if (isLoading || !insight) { return null } const note1Title = insight.note1.title || t('notification.untitled') const note2Title = insight.note2.title || t('notification.untitled') const similarityPercentage = Math.round(insight.similarityScore * 100) const comparisonNotes: Array> = [ { 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 ( <> {/* Comparison Modal */} { setShowComparison(false) setIsDismissed(true) }} notes={comparisonNotes} similarity={insight.similarityScore} onOpenNote={(noteId) => { setShowComparison(false) setIsDismissed(true) onOpenNote?.(noteId) }} onMergeNotes={handleMergeNotes} /> {/* Fusion Modal */} {fusionNotes.length > 0 && ( 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, aiProvider: 'fusion', 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 && (
{t('memoryEcho.title')} {t('memoryEcho.description')}
{/* AI-generated insight */}

{insight.insight}

{/* Connected notes */}
{note1Title} {note2Title} {t('memoryEcho.match', { percentage: similarityPercentage })}
{/* Action buttons */}
{/* Dismiss link */}
)} ) }