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>
116 lines
3.8 KiB
TypeScript
116 lines
3.8 KiB
TypeScript
/**
|
|
* TitleSheet — suggère 3 titres IA dans un bottom sheet propre
|
|
*/
|
|
import { useState, useEffect } from 'react'
|
|
import {
|
|
View, Text, TouchableOpacity, ActivityIndicator, StyleSheet,
|
|
} from 'react-native'
|
|
import { Sparkles } from 'lucide-react-native'
|
|
import { BottomSheet } from '@/components/BottomSheet'
|
|
import { apiFetch } from '@/lib/api'
|
|
import { ENDPOINTS } from '@/lib/config'
|
|
import { C } from '@/lib/theme'
|
|
|
|
interface Props {
|
|
visible: boolean
|
|
onClose: () => void
|
|
content: string
|
|
onSelect: (title: string) => void
|
|
}
|
|
|
|
export function TitleSheet({ visible, onClose, content, onSelect }: Props) {
|
|
const [loading, setLoading] = useState(false)
|
|
const [suggestions, setSuggestions] = useState<string[]>([])
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (visible && content.trim()) {
|
|
fetchTitles()
|
|
}
|
|
}, [visible])
|
|
|
|
const fetchTitles = async () => {
|
|
setLoading(true)
|
|
setError(null)
|
|
setSuggestions([])
|
|
try {
|
|
const res = await apiFetch(ENDPOINTS.aiTitle, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ content }),
|
|
})
|
|
const d = await res.json().catch(() => ({}))
|
|
if (!res.ok) {
|
|
setError(d.error === 'quota_exceeded' ? 'Quota dépassé' : 'Erreur serveur')
|
|
return
|
|
}
|
|
const raw: unknown[] = d.suggestions ?? []
|
|
setSuggestions(raw.map((s) => (typeof s === 'string' ? s : (s as any).title ?? '')).filter(Boolean))
|
|
} catch {
|
|
setError('Erreur réseau')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleSelect = (t: string) => {
|
|
onSelect(t)
|
|
onClose()
|
|
}
|
|
|
|
return (
|
|
<BottomSheet visible={visible} onClose={onClose} title="Titres suggérés">
|
|
{loading && (
|
|
<View style={s.center}>
|
|
<ActivityIndicator color={C.brand} size="large" />
|
|
<Text style={s.hint}>Génération des titres…</Text>
|
|
</View>
|
|
)}
|
|
{error && !loading && (
|
|
<View style={s.center}>
|
|
<Text style={s.errorText}>{error}</Text>
|
|
<TouchableOpacity onPress={fetchTitles} style={s.retryBtn}>
|
|
<Text style={s.retryText}>Réessayer</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
{!loading && !error && suggestions.length > 0 && (
|
|
<View style={s.list}>
|
|
{suggestions.map((t, i) => (
|
|
<TouchableOpacity key={i} onPress={() => handleSelect(t)} style={s.row} activeOpacity={0.7}>
|
|
<View style={s.numBadge}>
|
|
<Text style={s.numText}>{i + 1}</Text>
|
|
</View>
|
|
<Text style={s.titleText}>{t}</Text>
|
|
<Sparkles size={14} color={C.brand} />
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
)}
|
|
{!loading && !error && suggestions.length === 0 && (
|
|
<View style={s.center}>
|
|
<Text style={s.hint}>Aucune suggestion disponible</Text>
|
|
</View>
|
|
)}
|
|
</BottomSheet>
|
|
)
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
center: { alignItems: 'center', paddingVertical: 32, gap: 12 },
|
|
hint: { fontSize: 14, color: C.concrete },
|
|
errorText: { fontSize: 14, color: '#e11d48' },
|
|
retryBtn: { paddingHorizontal: 20, paddingVertical: 10, borderRadius: 10, backgroundColor: C.border },
|
|
retryText: { fontSize: 13, color: C.ink, fontWeight: '600' },
|
|
list: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 8 },
|
|
row: {
|
|
flexDirection: 'row', alignItems: 'center', gap: 12,
|
|
paddingHorizontal: 12, paddingVertical: 14,
|
|
borderRadius: 14, marginBottom: 8,
|
|
backgroundColor: C.white, borderWidth: 1, borderColor: C.border,
|
|
marginHorizontal: 8,
|
|
},
|
|
numBadge: { width: 26, height: 26, borderRadius: 13, backgroundColor: '#f3ece4', alignItems: 'center', justifyContent: 'center' },
|
|
numText: { fontSize: 12, fontWeight: '700', color: C.brand },
|
|
titleText: { flex: 1, fontSize: 14, fontWeight: '500', color: C.ink },
|
|
})
|