feat(flashcards): révision SM-2, génération IA et page /revision
Livre US-FLASHCARDS avec decks, session de révision, stats et migration Prisma. Finalise le Web Clipper (i18n 15 langues) et corrige les erreurs ESLint bloquant la CI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -313,9 +313,19 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
}, [content, isMarkdown])
|
||||
|
||||
const resolveImagesForSave = useCallback((contentToSave: string): string[] => {
|
||||
const extracted = !isMarkdown ? extractImagesFromHTML(contentToSave) : []
|
||||
return Array.from(new Set([...images, ...extracted]))
|
||||
}, [images, isMarkdown])
|
||||
if (!contentToSave) return []
|
||||
if (!isMarkdown) {
|
||||
return extractImagesFromHTML(contentToSave)
|
||||
} else {
|
||||
const urls = new Set<string>()
|
||||
const matches = contentToSave.matchAll(/!\[.*?\]\((.*?)\)/g)
|
||||
for (const match of matches) {
|
||||
const src = match[1]?.trim()
|
||||
if (src) urls.add(src)
|
||||
}
|
||||
return Array.from(urls)
|
||||
}
|
||||
}, [isMarkdown])
|
||||
|
||||
const handleGenerateTitles = async () => {
|
||||
const fullContentForAI = [
|
||||
@@ -613,8 +623,13 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
if (contentToSave !== content) setContent(contentToSave)
|
||||
if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
|
||||
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
||||
if (removedImageUrls.length > 0) {
|
||||
cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
|
||||
const deletedImages = Array.from(new Set([
|
||||
...removedImageUrls,
|
||||
...images.filter(url => !imagesToSave.includes(url))
|
||||
]))
|
||||
if (deletedImages.length > 0) {
|
||||
cleanupOrphanedImages(deletedImages, note.id).catch(() => {})
|
||||
setRemovedImageUrls([])
|
||||
}
|
||||
await refreshLabels()
|
||||
onNoteSaved?.(result)
|
||||
@@ -732,8 +747,13 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
if (contentToSave !== content) setContent(contentToSave)
|
||||
if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
|
||||
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
||||
if (removedImageUrls.length > 0) {
|
||||
cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
|
||||
const deletedImages = Array.from(new Set([
|
||||
...removedImageUrls,
|
||||
...images.filter(url => !imagesToSave.includes(url))
|
||||
]))
|
||||
if (deletedImages.length > 0) {
|
||||
cleanupOrphanedImages(deletedImages, note.id).catch(() => {})
|
||||
setRemovedImageUrls([])
|
||||
}
|
||||
await refreshLabels()
|
||||
onNoteSaved?.(result)
|
||||
|
||||
@@ -18,8 +18,9 @@ import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles,
|
||||
Maximize2, Copy, ArrowLeft, ChevronRight, PanelRight, Check, Loader2, Save, MoreHorizontal,
|
||||
Trash2, LogOut, Wand2, Share2, Wind, Paperclip
|
||||
Trash2, LogOut, Wand2, Share2, Wind, Paperclip, GraduationCap
|
||||
} from 'lucide-react'
|
||||
import { FlashcardGenerateDialog } from '@/components/flashcards/flashcard-generate-dialog'
|
||||
import { NoteShareDialog } from './note-share-dialog'
|
||||
import { deleteNote, leaveSharedNote } from '@/app/actions/notes'
|
||||
import { emitNoteChange } from '@/lib/note-change-sync'
|
||||
@@ -41,6 +42,7 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
||||
const { t } = useLanguage()
|
||||
const [isConverting, setIsConverting] = useState(false)
|
||||
const [shareOpen, setShareOpen] = useState(false)
|
||||
const [flashcardsOpen, setFlashcardsOpen] = useState(false)
|
||||
|
||||
const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null
|
||||
|
||||
@@ -169,6 +171,17 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
||||
<Wind size={16} />
|
||||
</button>
|
||||
|
||||
{!readOnly && (
|
||||
<button
|
||||
title={t('flashcards.toolbarGenerate')}
|
||||
aria-label={t('flashcards.toolbarGenerate')}
|
||||
onClick={() => setFlashcardsOpen(true)}
|
||||
className="p-1.5 rounded-full border border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-all"
|
||||
>
|
||||
<GraduationCap size={16} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!readOnly && onToggleAttachments && (
|
||||
<button
|
||||
title={t('notes.attachments') || 'Attachments'}
|
||||
@@ -247,6 +260,16 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
||||
/>
|
||||
)}
|
||||
|
||||
<FlashcardGenerateDialog
|
||||
open={flashcardsOpen}
|
||||
onClose={() => setFlashcardsOpen(false)}
|
||||
noteId={note.id}
|
||||
noteTitle={state.title || note.title || 'Untitled'}
|
||||
onSaved={(deckId) => {
|
||||
window.open(`/revision?deckId=${encodeURIComponent(deckId)}`, '_self')
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
aria-label={t('notes.documentInfoAria')}
|
||||
onClick={() => { actions.setInfoOpen(!state.infoOpen); actions.setAiOpen(false) }}
|
||||
|
||||
Reference in New Issue
Block a user