Files
Momento/memento-mobile/components/AISheet.tsx
Antigravity 0fa8978395
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m32s
CI / Deploy production (on server) (push) Has been skipped
feat: mobile app complet + flashcards fixes + drag handle améliorations
Mobile app:
- Révision flashcards : liste decks, session flip-card SM-2, couleurs harmonisées web
- Génération flashcards depuis note (FlashcardSheet + route /api/mobile/flashcards/generate)
- Audio Whisper : hook useAudioRecorder reécrit, MicButton avec erreurs
- IA : AISheet (améliorer/clarifier/résumer), TitleSheet (titre automatique)
- Suppression note (soft delete + confirmation Alert)
- Note du jour : titre lisible + HTML (plus JSON TipTap brut)
- Parser TipTap→HTML côté mobile (tipTapToHtml)
- Icône 🎓 dans header note → génération flashcards
- Endpoint flashcardGenerate dans config.ts

Web fixes:
- Bug flashcards groupées par carnet → deck par note (migration + schema)
- Bug filtre 'cartes dues' ignoré (suppression fallback buildSessionQueue)
- Suppression UI création deck manuelle (inutile)
- Fix setViewType is not defined dans home-client.tsx

Drag handle menu:
- Fix : clearNodes() avant transformation (heading→liste/code/citation)
- Ajout : option 'Texte' (paragraphe) dans Transformer en
- Ajout : Monter / Descendre le bloc
- Ajout : Copier le contenu du bloc
- Fix : sous-menu hover stable (délai 200ms)
- Fix : Supprimer en rouge via classe --danger (plus :first-child)
- i18n : nouvelles clés dans 15 locales

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 18:49:40 +00:00

143 lines
5.2 KiB
TypeScript

/**
* AISheet — bottom sheet IA avec modes d'amélioration + résultat
* Remplace les Alert.alert natifs par une UI propre au design Momento
*/
import { useState } from 'react'
import {
View, Text, TouchableOpacity, ActivityIndicator,
ScrollView, StyleSheet,
} from 'react-native'
import { Sparkles, Scissors, MessageSquare, Pencil, CheckCircle2, RefreshCw } from 'lucide-react-native'
import { BottomSheet } from '@/components/BottomSheet'
import { apiFetch } from '@/lib/api'
import { ENDPOINTS } from '@/lib/config'
import { C } from '@/lib/theme'
const MODES = [
{ key: 'improve', label: 'Améliorer le style', icon: Sparkles, color: C.brand },
{ key: 'fix_grammar', label: 'Corriger la grammaire', icon: CheckCircle2, color: '#5b7ec7' },
{ key: 'shorten', label: 'Raccourcir', icon: Scissors, color: '#4a9b61' },
{ key: 'clarify', label: 'Clarifier les idées', icon: MessageSquare,color: '#c77a3a' },
]
interface Props {
visible: boolean
onClose: () => void
text: string
onApply: (improved: string) => void
}
export function AISheet({ visible, onClose, text, onApply }: Props) {
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<string | null>(null)
const [selectedMode, setSelectedMode] = useState<string | null>(null)
const handleClose = () => {
setResult(null)
setSelectedMode(null)
onClose()
}
const handleMode = async (mode: string) => {
setSelectedMode(mode)
setLoading(true)
setResult(null)
try {
const res = await apiFetch(ENDPOINTS.aiImprove, {
method: 'POST',
body: JSON.stringify({ text, mode }),
})
const d = await res.json().catch(() => ({}))
if (!res.ok) {
setResult(`⚠️ ${d.error === 'quota_exceeded' ? 'Quota IA dépassé' : (d.error ?? 'Erreur serveur')}`)
return
}
setResult(d.improved ?? '')
} catch {
setResult('⚠️ Erreur réseau — vérifiez votre connexion.')
} finally {
setLoading(false)
}
}
const handleApply = () => {
if (result && !result.startsWith('⚠️')) {
onApply(result)
}
handleClose()
}
const handleRetry = () => {
if (selectedMode) handleMode(selectedMode)
}
const title = result ? 'Résultat IA' : 'Améliorer avec l\'IA'
return (
<BottomSheet visible={visible} onClose={handleClose} title={title}>
{!result && !loading && (
<View style={s.modeList}>
{MODES.map((m) => {
const Icon = m.icon
return (
<TouchableOpacity key={m.key} onPress={() => handleMode(m.key)} style={s.modeRow} activeOpacity={0.7}>
<View style={[s.modeIcon, { backgroundColor: m.color + '18' }]}>
<Icon size={18} color={m.color} />
</View>
<Text style={s.modeLabel}>{m.label}</Text>
</TouchableOpacity>
)
})}
</View>
)}
{loading && (
<View style={s.loadingBox}>
<ActivityIndicator color={C.brand} size="large" />
<Text style={s.loadingText}>Génération en cours</Text>
</View>
)}
{result && !loading && (
<View style={s.resultBox}>
<ScrollView style={s.resultScroll} showsVerticalScrollIndicator={false}>
<Text style={[s.resultText, result.startsWith('⚠️') && { color: '#e11d48' }]}>
{result}
</Text>
</ScrollView>
{!result.startsWith('⚠️') && (
<TouchableOpacity onPress={handleApply} style={s.applyBtn} activeOpacity={0.8}>
<Pencil size={15} color={C.white} />
<Text style={s.applyBtnText}>Remplacer le texte</Text>
</TouchableOpacity>
)}
<TouchableOpacity onPress={handleRetry} style={s.retryBtn} activeOpacity={0.8}>
<RefreshCw size={14} color={C.concrete} />
<Text style={s.retryText}>Réessayer</Text>
</TouchableOpacity>
</View>
)}
</BottomSheet>
)
}
const s = StyleSheet.create({
modeList: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 8 },
modeRow: {
flexDirection: 'row', alignItems: 'center', gap: 14,
paddingHorizontal: 12, paddingVertical: 14,
borderRadius: 14, marginBottom: 4,
},
modeIcon: { width: 40, height: 40, borderRadius: 12, alignItems: 'center', justifyContent: 'center' },
modeLabel: { fontSize: 15, fontWeight: '500', color: C.ink },
loadingBox: { alignItems: 'center', paddingVertical: 36, gap: 12 },
loadingText: { fontSize: 14, color: C.concrete },
resultBox: { paddingHorizontal: 20, paddingTop: 8 },
resultScroll: { maxHeight: 200, backgroundColor: C.white, borderWidth: 1, borderColor: C.border, borderRadius: 14, padding: 14, marginBottom: 14 },
resultText: { fontSize: 15, color: C.ink, lineHeight: 23 },
applyBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: C.ink, paddingVertical: 13, borderRadius: 14, justifyContent: 'center', marginBottom: 8 },
applyBtnText: { color: C.white, fontWeight: '700', fontSize: 14 },
retryBtn: { flexDirection: 'row', alignItems: 'center', gap: 6, justifyContent: 'center', paddingVertical: 10 },
retryText: { fontSize: 13, color: C.concrete },
})