Lot J : interrupteur de collecte des questions dans l'écran Guide IA

Sans UI, le client ne pouvait pas exercer le refus dont il est pourtant
responsable. Le libellé dit ce que couper coûte (l'onglet cesse de se remplir) et
ce que couper ne fait pas : rien n'est effacé, les questions déjà enregistrées
vivent jusqu'à leur purge à 90 jours.

manager_api_new étendu à la main. flutter build web vert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Fransolet 2026-08-13 15:58:36 +02:00
parent 82cb4cff3e
commit 9af4f8e2d8
2 changed files with 56 additions and 0 deletions

View File

@ -46,6 +46,9 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
/// Nul tant que le serveur n'a pas répondu : la ligne « ~N questions » ne s'affiche pas.
int? _tokensPerQuestion;
/// Le client autorise-t-il l'enregistrement des questions de ses visiteurs ? (RGPD)
bool _collectionEnabled = true;
@override
void initState() {
super.initState();
@ -85,6 +88,7 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
_nameController.text = instance?.guideName ?? '';
_personaController.text = instance?.guidePersonaPrompt ?? '';
_voiceId = instance?.guideVoiceId ?? kGuideVoiceViva;
_collectionEnabled = instance?.isVisitorQuestionCollectionEnabled ?? true;
final ownLanguage = (instance?.guideFallbackMessages ?? [])
.where((m) => m.language == _editingLanguage && (m.value ?? '').isNotEmpty);
@ -186,6 +190,7 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
instance.guideName = _nameController.text.trim();
instance.guidePersonaPrompt = _personaController.text.trim();
instance.guideVoiceId = _voiceId;
instance.isVisitorQuestionCollectionEnabled = _collectionEnabled;
instance.guideFallbackMessages = [...otherLanguages, ...edited];
try {
@ -332,6 +337,8 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
const SizedBox(height: 16),
_channelsCard(l),
const SizedBox(height: 16),
_collectionCard(l),
const SizedBox(height: 16),
_identityCard(l),
],
);
@ -398,6 +405,43 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
);
}
/// Interrupteur de collecte (RGPD, lot J).
///
/// **C'est le client qui est responsable de traitement**, pas nous : il doit pouvoir
/// refuser. La collecte était inconditionnelle dès que l'assistant était actif.
///
/// Le texte dit ce que couper coûte sans quoi le gestionnaire le désactive « par
/// prudence » puis s'étonne que son onglet reste vide — et ce que couper ne fait pas :
/// les questions déjà enregistrées vivent jusqu'à leur purge, ce n'est pas un effacement.
Widget _collectionCard(AppLocalizations l) {
return _card(
title: 'Questions de vos visiteurs',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
contentPadding: EdgeInsets.zero,
value: _collectionEnabled,
onChanged: (v) => setState(() => _collectionEnabled = v),
title: const Text('Enregistrer les questions posées au guide',
style: TextStyle(fontSize: 14)),
subtitle: Text(
_collectionEnabled
? 'Alimente l\'onglet « Ce que demandent vos visiteurs » et le rapport des '
'questions restées sans réponse. Conservation 90 jours, puis seuls les '
'regroupements par thème sont gardés.'
: 'Aucune nouvelle question ne sera enregistrée. L\'onglet « Ce que demandent '
'vos visiteurs » cessera de se remplir. Les questions déjà enregistrées '
'restent jusqu\'à leur suppression automatique à 90 jours.',
style: TextStyle(fontSize: 12, color: kBodyTextColor),
),
activeThumbColor: kPrimaryColor,
),
],
),
);
}
Widget _card({required String title, String? subtitle, required Widget child}) {
return Card(
elevation: 0,

View File

@ -26,6 +26,7 @@ class InstanceDTO {
this.guideName,
this.guidePersonaPrompt,
this.guideVoiceId,
this.isVisitorQuestionCollectionEnabled,
this.guideFallbackMessages = const [],
this.subscriptionPlanId,
this.subscriptionPlan,
@ -109,6 +110,10 @@ class InstanceDTO {
/// Voix Gemini TTS : "Sulafat" (Viva) ou "Umbriel" (Marco).
String? guideVoiceId;
/// Le client autorise-t-il l'enregistrement des questions de ses visiteurs ? (RGPD)
/// Couper la collecte n'efface rien : les questions déjà là vivent jusqu'à leur purge.
bool? isVisitorQuestionCollectionEnabled;
/// Formulations de repli. Liste à plat : plusieurs entrées peuvent partager la même langue.
List<TranslationDTO> guideFallbackMessages;
@ -264,6 +269,11 @@ class InstanceDTO {
} else {
json[r'guideVoiceId'] = null;
}
if (this.isVisitorQuestionCollectionEnabled != null) {
json[r'isVisitorQuestionCollectionEnabled'] = this.isVisitorQuestionCollectionEnabled;
} else {
json[r'isVisitorQuestionCollectionEnabled'] = null;
}
json[r'guideFallbackMessages'] = this.guideFallbackMessages;
if (this.subscriptionPlanId != null) {
json[r'subscriptionPlanId'] = this.subscriptionPlanId;
@ -372,6 +382,8 @@ class InstanceDTO {
guideName: mapValueOfType<String>(json, r'guideName'),
guidePersonaPrompt: mapValueOfType<String>(json, r'guidePersonaPrompt'),
guideVoiceId: mapValueOfType<String>(json, r'guideVoiceId'),
isVisitorQuestionCollectionEnabled:
mapValueOfType<bool>(json, r'isVisitorQuestionCollectionEnabled'),
guideFallbackMessages: TranslationDTO.listFromJson(json[r'guideFallbackMessages']),
subscriptionPlanId: mapValueOfType<String>(json, r'subscriptionPlanId'),
subscriptionPlan: SubscriptionPlanDTO.fromJson(json[r'subscriptionPlan']),