feat: Complete internationalization and code cleanup
## 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>
This commit is contained in:
376
keep-notes/components/fusion-modal.tsx
Normal file
376
keep-notes/components/fusion-modal.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { X, Link2, Sparkles, Edit, Check } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Note } from '@/lib/types'
|
||||
import { useLanguage } from '@/lib/i18n/LanguageProvider'
|
||||
|
||||
interface FusionModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
notes: Array<Partial<Note>>
|
||||
onConfirmFusion: (mergedNote: { title: string; content: string }, options: FusionOptions) => Promise<void>
|
||||
}
|
||||
|
||||
interface FusionOptions {
|
||||
archiveOriginals: boolean
|
||||
keepAllTags: boolean
|
||||
useLatestTitle: boolean
|
||||
createBacklinks: boolean
|
||||
}
|
||||
|
||||
export function FusionModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
notes,
|
||||
onConfirmFusion
|
||||
}: FusionModalProps) {
|
||||
const { t } = useLanguage()
|
||||
const [selectedNoteIds, setSelectedNoteIds] = useState<string[]>(notes.filter(n => n.id).map(n => n.id!))
|
||||
const [customPrompt, setCustomPrompt] = useState('')
|
||||
const [fusionPreview, setFusionPreview] = useState('')
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [generationError, setGenerationError] = useState<string | null>(null)
|
||||
const hasGeneratedRef = useRef(false)
|
||||
|
||||
const [options, setOptions] = useState<FusionOptions>({
|
||||
archiveOriginals: true,
|
||||
keepAllTags: true,
|
||||
useLatestTitle: false,
|
||||
createBacklinks: false
|
||||
})
|
||||
|
||||
const handleGenerateFusion = useCallback(async () => {
|
||||
setIsGenerating(true)
|
||||
setGenerationError(null)
|
||||
setFusionPreview('')
|
||||
|
||||
try {
|
||||
|
||||
// Call AI API to generate fusion
|
||||
const res = await fetch('/api/ai/echo/fusion', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
noteIds: selectedNoteIds,
|
||||
prompt: customPrompt
|
||||
})
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Failed to generate fusion')
|
||||
}
|
||||
|
||||
if (!data.fusedNote) {
|
||||
throw new Error('No fusion content returned from API')
|
||||
}
|
||||
|
||||
setFusionPreview(data.fusedNote)
|
||||
} catch (error) {
|
||||
console.error('[FusionModal] Failed to generate:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : t('memoryEcho.fusion.generateError')
|
||||
setGenerationError(errorMessage)
|
||||
} finally {
|
||||
setIsGenerating(false)
|
||||
}
|
||||
}, [selectedNoteIds, customPrompt])
|
||||
|
||||
// Auto-generate fusion preview when modal opens with selected notes
|
||||
useEffect(() => {
|
||||
// Reset generation state when modal closes
|
||||
if (!isOpen) {
|
||||
hasGeneratedRef.current = false
|
||||
setGenerationError(null)
|
||||
setFusionPreview('')
|
||||
return
|
||||
}
|
||||
|
||||
// Generate only once when modal opens and we have 2+ notes
|
||||
if (isOpen && selectedNoteIds.length >= 2 && !hasGeneratedRef.current && !isGenerating) {
|
||||
hasGeneratedRef.current = true
|
||||
handleGenerateFusion()
|
||||
}
|
||||
}, [isOpen, selectedNoteIds.length, isGenerating, handleGenerateFusion])
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (isGenerating) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!fusionPreview) {
|
||||
await handleGenerateFusion()
|
||||
return
|
||||
}
|
||||
|
||||
setIsGenerating(true)
|
||||
try {
|
||||
// Parse the preview into title and content
|
||||
const lines = fusionPreview.split('\n')
|
||||
const title = lines[0].replace(/^#+\s*/, '').trim()
|
||||
const content = lines.slice(1).join('\n').trim()
|
||||
|
||||
await onConfirmFusion(
|
||||
{ title, content },
|
||||
options
|
||||
)
|
||||
|
||||
onClose()
|
||||
} finally {
|
||||
setIsGenerating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const selectedNotes = notes.filter(n => n.id && selectedNoteIds.includes(n.id))
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-hidden flex flex-col p-0">
|
||||
{/* 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-purple-100 dark:bg-purple-900/30 rounded-full">
|
||||
<Link2 className="h-5 w-5 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">
|
||||
{t('memoryEcho.fusion.title')}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('memoryEcho.fusion.mergeNotes', { count: selectedNoteIds.length })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* Section 1: Note Selection */}
|
||||
<div className="p-6 border-b dark:border-zinc-700">
|
||||
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
||||
{t('memoryEcho.fusion.notesToMerge')}
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{notes.filter(n => n.id).map((note) => (
|
||||
<div
|
||||
key={note.id}
|
||||
className={cn(
|
||||
"flex items-start gap-3 p-3 rounded-lg border transition-colors",
|
||||
selectedNoteIds.includes(note.id!)
|
||||
? "border-purple-200 bg-purple-50 dark:bg-purple-950/20 dark:border-purple-800"
|
||||
: "border-gray-200 dark:border-zinc-700 opacity-50"
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
id={`note-${note.id}`}
|
||||
checked={selectedNoteIds.includes(note.id!)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked && note.id) {
|
||||
setSelectedNoteIds([...selectedNoteIds, note.id])
|
||||
} else if (note.id) {
|
||||
setSelectedNoteIds(selectedNoteIds.filter(id => id !== note.id))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`note-${note.id}`}
|
||||
className="flex-1 cursor-pointer"
|
||||
>
|
||||
<div className="font-medium text-sm text-gray-900 dark:text-gray-100">
|
||||
{note.title || t('memoryEcho.comparison.untitled')}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{note.createdAt ? new Date(note.createdAt).toLocaleDateString() : t('memoryEcho.fusion.unknownDate')}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Custom Prompt (Optional) */}
|
||||
<div className="p-6 border-b dark:border-zinc-700">
|
||||
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
||||
{t('memoryEcho.fusion.optionalPrompt')}
|
||||
</h3>
|
||||
<Textarea
|
||||
placeholder={t('memoryEcho.fusion.promptPlaceholder')}
|
||||
value={customPrompt}
|
||||
onChange={(e) => setCustomPrompt(e.target.value)}
|
||||
rows={3}
|
||||
className="resize-none"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="mt-2"
|
||||
onClick={handleGenerateFusion}
|
||||
disabled={isGenerating || selectedNoteIds.length < 2}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Sparkles className="h-4 w-4 mr-2 animate-spin" />
|
||||
{t('memoryEcho.fusion.generating')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
{t('memoryEcho.fusion.generateFusion')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{generationError && (
|
||||
<div className="mx-6 mt-4 p-4 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<p className="text-sm text-red-700 dark:text-red-400">
|
||||
{t('memoryEcho.fusion.error')}: {generationError}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section 3: Preview */}
|
||||
{fusionPreview && (
|
||||
<div className="p-6 border-b dark:border-zinc-700">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold flex items-center gap-2">
|
||||
{t('memoryEcho.fusion.previewTitle')}
|
||||
</h3>
|
||||
{!isEditing && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
{t('memoryEcho.fusion.modify')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<Textarea
|
||||
value={fusionPreview}
|
||||
onChange={(e) => setFusionPreview(e.target.value)}
|
||||
rows={10}
|
||||
className="resize-none font-mono text-sm"
|
||||
/>
|
||||
) : (
|
||||
<div className="border dark:border-zinc-700 rounded-lg p-4 bg-white dark:bg-zinc-900">
|
||||
<pre className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap font-sans">
|
||||
{fusionPreview}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section 4: Options */}
|
||||
<div className="p-6">
|
||||
<h3 className="text-sm font-semibold mb-3">{t('memoryEcho.fusion.optionsTitle')}</h3>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={options.archiveOriginals}
|
||||
onCheckedChange={(checked) =>
|
||||
setOptions({ ...options, archiveOriginals: !!checked })
|
||||
}
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{t('memoryEcho.fusion.archiveOriginals')}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={options.keepAllTags}
|
||||
onCheckedChange={(checked) =>
|
||||
setOptions({ ...options, keepAllTags: !!checked })
|
||||
}
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{t('memoryEcho.fusion.keepAllTags')}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={options.useLatestTitle}
|
||||
onCheckedChange={(checked) =>
|
||||
setOptions({ ...options, useLatestTitle: !!checked })
|
||||
}
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{t('memoryEcho.fusion.useLatestTitle')}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={options.createBacklinks}
|
||||
onCheckedChange={(checked) =>
|
||||
setOptions({ ...options, createBacklinks: !!checked })
|
||||
}
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{t('memoryEcho.fusion.createBacklinks')}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-6 border-t dark:border-zinc-700 bg-gray-50 dark:bg-zinc-800/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('memoryEcho.fusion.cancel')}
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{isEditing && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
{t('memoryEcho.fusion.finishEditing')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
disabled={selectedNoteIds.length < 2 || isGenerating}
|
||||
className="bg-purple-600 hover:bg-purple-700 text-white"
|
||||
>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Sparkles className="h-4 w-4 mr-2 animate-spin" />
|
||||
{t('memoryEcho.fusion.generating')}
|
||||
</>
|
||||
) : (
|
||||
t('memoryEcho.fusion.confirmFusion')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user