## Translation Files - Add 11 new language files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl) - Add 100+ missing translation keys across all 15 languages - New sections: notebook, pagination, ai.batchOrganization, ai.autoLabels - Update nav section with workspace, quickAccess, myLibrary keys ## Component Updates - Update 15+ components to use translation keys instead of hardcoded text - Components: notebook dialogs, sidebar, header, note-input, ghost-tags, etc. - Replace 80+ hardcoded English/French strings with t() calls - Ensure consistent UI across all supported languages ## Code Quality - Remove 77+ console.log statements from codebase - Clean up API routes, components, hooks, and services - Keep only essential error handling (no debugging logs) ## UI/UX Improvements - Update Keep logo to yellow post-it style (from-yellow-400 to-amber-500) - Change selection colors to #FEF3C6 (notebooks) and #EFB162 (nav items) - Make "+" button permanently visible in notebooks section - Fix grammar and syntax errors in multiple components ## Bug Fixes - Fix JSON syntax errors in it.json, nl.json, pl.json, zh.json - Fix syntax errors in notebook-suggestion-toast.tsx - Fix syntax errors in use-auto-tagging.ts - Fix syntax errors in paragraph-refactor.service.ts - Fix duplicate "fusion" section in nl.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Ou une version plus courte si vous préférez : feat(i18n): Add 15 languages, remove logs, update UI components - Create 11 new translation files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl) - Add 100+ translation keys: notebook, pagination, AI features - Update 15+ components to use translations (80+ strings) - Remove 77+ console.log statements from codebase - Fix JSON syntax errors in 4 translation files - Fix component syntax errors (toast, hooks, services) - Update logo to yellow post-it style - Change selection colors (#FEF3C6, #EFB162) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
338 lines
12 KiB
TypeScript
338 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
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 { toast } from 'sonner'
|
|
|
|
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 [insight, setInsight] = useState<MemoryEchoInsight | null>(null)
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [isDismissed, setIsDismissed] = useState(false)
|
|
const [showModal, setShowModal] = useState(false)
|
|
|
|
// Fetch insight on mount
|
|
useEffect(() => {
|
|
fetchInsight()
|
|
}, [])
|
|
|
|
const fetchInsight = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const res = await fetch('/api/ai/echo')
|
|
const data = await res.json()
|
|
|
|
if (data.insight) {
|
|
setInsight(data.insight)
|
|
}
|
|
} catch (error) {
|
|
console.error('[MemoryEcho] Failed to fetch insight:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
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
|
|
})
|
|
})
|
|
|
|
// Show success message and open modal
|
|
toast.success('Opening connection...')
|
|
setShowModal(true)
|
|
} catch (error) {
|
|
console.error('[MemoryEcho] Failed to view connection:', error)
|
|
toast.error('Failed to open connection')
|
|
}
|
|
}
|
|
|
|
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
|
|
})
|
|
})
|
|
|
|
// Show feedback toast
|
|
if (feedback === 'thumbs_up') {
|
|
toast.success('Thanks for your feedback!')
|
|
} else {
|
|
toast.success('Thanks! We\'ll use this to improve.')
|
|
}
|
|
|
|
// Dismiss notification
|
|
setIsDismissed(true)
|
|
} catch (error) {
|
|
console.error('[MemoryEcho] Failed to submit feedback:', error)
|
|
toast.error('Failed to submit feedback')
|
|
}
|
|
}
|
|
|
|
const handleDismiss = () => {
|
|
setIsDismissed(true)
|
|
}
|
|
|
|
// Don't render notification if dismissed, loading, or no insight
|
|
if (isDismissed || isLoading || !insight) {
|
|
return null
|
|
}
|
|
|
|
// Calculate values for both notification and modal
|
|
const note1Title = insight.note1.title || 'Untitled'
|
|
const note2Title = insight.note2.title || '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">💡 Memory Echo Discovery</h2>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
These notes are connected by {similarityPercentage}% similarity
|
|
</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-blue-600 dark:text-blue-400 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">Click to view note →</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">Click to view note →</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">
|
|
Is this connection helpful?
|
|
</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" />
|
|
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" />
|
|
Not Helpful
|
|
</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">
|
|
<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">
|
|
💡 I noticed something...
|
|
<Sparkles className="h-4 w-4 text-amber-500" />
|
|
</CardTitle>
|
|
<CardDescription className="text-xs mt-1">
|
|
Proactive connections between your notes
|
|
</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>
|
|
</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-blue-200 text-blue-700 dark:border-blue-900 dark:text-blue-300">
|
|
{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">
|
|
{similarityPercentage}% match
|
|
</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}
|
|
>
|
|
View Connection
|
|
</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="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="Not Helpful"
|
|
>
|
|
<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}
|
|
>
|
|
Dismiss for now
|
|
</button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|