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>
143 lines
5.2 KiB
TypeScript
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 },
|
|
})
|