import { useEffect, useState } from 'react' import { View, Text, ActivityIndicator, TouchableOpacity, Share, Alert, TextInput, StyleSheet, } from 'react-native' import { SafeAreaView } from 'react-native-safe-area-context' import { useLocalSearchParams, useRouter } from 'expo-router' import { ArrowLeft, Share2, Pencil, Check, Trash2, GraduationCap } from 'lucide-react-native' import { WebView } from 'react-native-webview' import { apiFetch } from '@/lib/api' import { ENDPOINTS } from '@/lib/config' import { C } from '@/lib/theme' import { AISheet } from '@/components/AISheet' import { MicButton } from '@/components/MicButton' import { FlashcardSheet } from '@/components/FlashcardSheet' import { useAudioRecorder } from '@/lib/useAudioRecorder' interface Note { id: string title: string content: string updatedAt: string notebookName?: string } const AI_MODES = [ { key: 'improve', label: '✨ Améliorer le style' }, { key: 'fix_grammar', label: '🔤 Corriger la grammaire' }, { key: 'shorten', label: '✂️ Raccourcir' }, { key: 'clarify', label: '💡 Clarifier' }, ] /** Extrait le texte brut depuis le HTML TipTap */ function htmlToPlainText(html: string): string { return html .replace(//gi, '\n') .replace(/<\/p>/gi, '\n') .replace(/<\/h[1-6]>/gi, '\n') .replace(/<\/li>/gi, '\n') .replace(/<[^>]+>/g, '') .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/ /g, ' ') .replace(/\n{3,}/g, '\n\n') .trim() } function tipTapToHtml(node: any): string { if (!node) return '' if (node.type === 'doc') return (node.content ?? []).map(tipTapToHtml).join('') if (node.type === 'text') { let t = (node.text ?? '').replace(/&/g, '&').replace(//g, '>') if (node.marks?.some((m: any) => m.type === 'bold')) t = `${t}` if (node.marks?.some((m: any) => m.type === 'italic')) t = `${t}` if (node.marks?.some((m: any) => m.type === 'code')) t = `${t}` return t } const inner = (node.content ?? []).map(tipTapToHtml).join('') switch (node.type) { case 'paragraph': return `

${inner || '​'}

` case 'heading': return `${inner}` case 'bulletList': return `` case 'orderedList': return `
    ${inner}
` case 'listItem': return `
  • ${inner}
  • ` case 'blockquote': return `
    ${inner}
    ` case 'codeBlock': return `
    ${inner}
    ` case 'hardBreak': return '
    ' default: return inner } } function buildHtml(content: string, title: string) { const safeTitle = title.replace(//g, '>') let body: string const trimmed = content.trimStart() if (trimmed.startsWith('<')) { body = content } else if (trimmed.startsWith('{')) { try { body = tipTapToHtml(JSON.parse(content)) } catch { body = `

    ${content.replace(/\n/g, '
    ')}

    ` } } else { body = `

    ${content.replace(/\n/g, '
    ')}

    ` } return `
    ${safeTitle}

    ${body}
    ` } export default function NoteScreen() { const { id } = useLocalSearchParams<{ id: string }>() const [note, setNote] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [editMode, setEditMode] = useState(false) const [editTitle, setEditTitle] = useState('') const [editContent, setEditContent] = useState('') const [saving, setSaving] = useState(false) const [aiSheetOpen, setAiSheetOpen] = useState(false) const [flashcardSheetOpen, setFlashcardSheetOpen] = useState(false) const router = useRouter() // Audio en mode édition const { state: audioState, startRecording, stopAndTranscribe, cancelRecording } = useAudioRecorder( (text) => setEditContent((prev) => prev ? prev + ' ' + text : text) ) useEffect(() => { if (!id) return apiFetch(ENDPOINTS.note(id as string)) .then((r) => { if (!r.ok) throw new Error(`Erreur ${r.status}`); return r.json() }) .then((data) => setNote(data.note ?? null)) .catch((e) => setError(e.message)) .finally(() => setLoading(false)) }, [id]) const handleEdit = () => { if (!note) return setEditTitle(note.title) setEditContent(htmlToPlainText(note.content ?? '')) setEditMode(true) } const handleSave = async () => { if (!note || !editTitle.trim()) return setSaving(true) try { const res = await apiFetch(ENDPOINTS.note(note.id), { method: 'PUT', body: JSON.stringify({ title: editTitle.trim(), content: editContent }), }) if (!res.ok) throw new Error('Erreur de sauvegarde') setNote((prev) => prev ? { ...prev, title: editTitle.trim(), content: editContent } : prev) setEditMode(false) } catch (e: any) { Alert.alert('Erreur', e.message) } finally { setSaving(false) } } const handleMic = () => { if (audioState === 'idle' || audioState === 'error') startRecording() else if (audioState === 'recording') stopAndTranscribe() else cancelRecording() } const handleShare = async () => { if (!note) return await Share.share({ title: note.title, message: `${note.title}\nhttps://memento-note.com` }) } const handleDelete = () => { if (!note) return Alert.alert( 'Supprimer la note', `Mettre "${note.title}" à la corbeille ?`, [ { text: 'Annuler', style: 'cancel' }, { text: 'Supprimer', style: 'destructive', onPress: async () => { try { const res = await apiFetch(ENDPOINTS.note(note.id), { method: 'DELETE' }) if (!res.ok) throw new Error(`Erreur ${res.status}`) router.back() } catch (e: any) { Alert.alert('Erreur', e.message ?? 'Impossible de supprimer la note') } }, }, ] ) } return ( { if (editMode) setEditMode(false); else router.back() }} style={s.backBtn} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}> {editMode ? (editTitle || '…') : (note?.title ?? '…')} {note && !editMode && ( <> setFlashcardSheetOpen(true)} style={s.iconBtn} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}> )} {editMode && ( {saving ? : } )} {loading && } {error && {error}} {!loading && !error && !note && Note introuvable.} {/* Mode lecture */} {note && !editMode && ( )} {/* Mode édition */} {note && editMode && ( {/* Barre outils */} {audioState === 'recording' ? ● Enregistrement… Appuyez pour arrêter : setAiSheetOpen(true)} disabled={!editContent.trim()} style={[s.aiBtn, !editContent.trim() && { opacity: 0.35 }]} activeOpacity={0.8}> ✨ Améliorer avec l'IA } )} setAiSheetOpen(false)} text={editContent} onApply={(t) => setEditContent(t)} /> {note && ( setFlashcardSheetOpen(false)} noteId={note.id} noteTitle={note.title} /> )} ) } const s = StyleSheet.create({ safe: { flex: 1, backgroundColor: C.paper }, header: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: C.border, backgroundColor: C.paper }, backBtn: { padding: 4 }, headerTitle: { flex: 1, fontSize: 15, fontWeight: '600', color: C.ink }, iconBtn: { padding: 4 }, saveBtn: { backgroundColor: C.brand, padding: 7, borderRadius: 10 }, center: { flex: 1, alignItems: 'center', justifyContent: 'center' }, editTitle: { fontSize: 24, fontWeight: '800', color: C.ink, paddingHorizontal: 20, paddingTop: 16, paddingBottom: 8, letterSpacing: -0.5 }, editDivider: { height: 1, backgroundColor: C.border, marginHorizontal: 20, marginBottom: 12 }, editContent: { flex: 1, fontSize: 16, color: C.ink, lineHeight: 26, paddingHorizontal: 20, paddingBottom: 80 }, toolbar: { flexDirection: 'row', alignItems: 'center', gap: 10, paddingHorizontal: 16, paddingVertical: 12, borderTopWidth: 1, borderTopColor: C.border, backgroundColor: C.paper }, recordHint: { flex: 1, fontSize: 13, color: '#e11d48', fontWeight: '500' }, aiBtn: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: C.ink, paddingVertical: 11, paddingHorizontal: 14, borderRadius: 12, justifyContent: 'center' }, aiBtnText: { color: C.white, fontWeight: '600', fontSize: 14 }, })