Stats : lire voiceSessions, et l'aperçu cesse de polluer le rapport du client

_voiceSessions lisait appTypeDistribution['Voice'], qui ne se serait jamais
rempli : le vocal est un mode d'interaction, pas une plateforme. Le serveur
expose désormais « sessions ayant utilisé le vocal », qui est vrai.

Et l'aperçu de conversation du Guide IA journalisait chaque essai comme une
question de visiteur : le gestionnaire qui testait sa personnalité polluait son
propre onglet « Ce que demandent vos visiteurs » et gonflait le bloc des
questions sans réponse. isVisitorQuestion: false.

manager_api_new étendu à la main (voiceSessions, renommage isVisitorQuestion).
flutter analyze lib sans erreur, flutter build web vert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Fransolet 2026-08-13 14:32:55 +02:00
parent 04e73a8832
commit 82cb4cff3e
4 changed files with 27 additions and 7 deletions

View File

@ -630,6 +630,10 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
instanceId: ctx.instanceId,
appType: AppType.Web,
language: _editingLanguage,
// C'est le gestionnaire qui teste sa personnalité, pas un visiteur qui pose une
// question : sans ce drapeau, chaque essai atterrissait dans son propre onglet
// « Ce que demandent vos visiteurs » et gonflait le bloc des questions sans réponse.
isVisitorQuestion: false,
));
if (!mounted) return;
setState(() {

View File

@ -202,8 +202,14 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
double _contentsPerVisit(StatsSummaryDTO stats) =>
stats.totalSessions == 0 ? 0 : _totalViews(stats) / stats.totalSessions;
int _voiceSessions(StatsSummaryDTO stats) =>
stats.appTypeDistribution[AppType.Voice.name] ?? 0;
/// Lisait `appTypeDistribution['Voice']`, qui ne se remplira jamais : **le vocal
/// n'est pas une plateforme mais un mode d'interaction**. Un visiteur passe des
/// lunettes à l'écran dans une même session, or la distribution ne compte qu'une
/// entrée par session, tranchée sur l'événement le plus ancien — le chiffre aurait
/// dépendu de l'ordre dans lequel le visiteur a touché ses appareils.
///
/// Le serveur expose désormais « sessions ayant utilisé le vocal », qui est vrai.
int _voiceSessions(StatsSummaryDTO stats) => stats.voiceSessions ?? 0;
double _voiceShare(StatsSummaryDTO stats) => stats.totalSessions == 0
? 0

View File

@ -21,7 +21,7 @@ class AiChatRequest {
this.conversationId,
this.history = const [],
this.isVoice = false,
this.isAutoTriggered = false,
this.isVisitorQuestion = true,
});
String? message;
@ -50,9 +50,11 @@ class AiChatRequest {
/// true = interaction vocale (lunettes) prompt audio-friendly, pas de navigation
bool? isVoice;
/// true = tour déclenché par le système (mode proactif), pas demandé par le visiteur.
/// Le backend compte les jetons mais ne journalise pas la question : elle n'en est pas une.
bool? isAutoTriggered;
/// false = ce tour n'est pas une question de visiteur (prompt du mode proactif, ou
/// gestionnaire testant sa personnalité dans l'aperçu du Guide IA). Le backend compte
/// les jetons mais ne journalise rien. Défaut à true : ne pas envoyer le champ revient
/// à journaliser, ce qui est le bon comportement pour une app visiteur.
bool? isVisitorQuestion;
@override
bool operator ==(Object other) =>
@ -117,7 +119,7 @@ class AiChatRequest {
json[r'conversationId'] = null;
}
json[r'isVoice'] = this.isVoice ?? false;
json[r'isAutoTriggered'] = this.isAutoTriggered ?? false;
json[r'isVisitorQuestion'] = this.isVisitorQuestion ?? true;
return json;
}

View File

@ -15,6 +15,7 @@ class StatsSummaryDTO {
this.visitsByDay = const [],
this.languageDistribution = const {},
this.appTypeDistribution = const {},
this.voiceSessions,
this.topPois = const [],
this.topAgendaEvents = const [],
this.quizStats = const [],
@ -30,6 +31,12 @@ class StatsSummaryDTO {
List<DayStatDTO> visitsByDay;
Map<String, int> languageDistribution;
Map<String, int> appTypeDistribution;
/// Sessions ayant utilisé le vocal au moins une fois. Volontairement **hors** de
/// `appTypeDistribution` : le vocal est un mode d'interaction, pas une plateforme —
/// une même session mêle la voix et le tactile.
int? voiceSessions;
List<PoiStatDTO> topPois;
List<AgendaEventStatDTO> topAgendaEvents;
List<QuizStatDTO> quizStats;
@ -52,6 +59,7 @@ class StatsSummaryDTO {
appTypeDistribution: (json[r'appTypeDistribution'] is Map)
? (json[r'appTypeDistribution'] as Map).cast<String, int>()
: {},
voiceSessions: (json[r'voiceSessions'] as num?)?.toInt(),
topPois: PoiStatDTO.listFromJson(json[r'topPois']),
topAgendaEvents: AgendaEventStatDTO.listFromJson(json[r'topAgendaEvents']),
quizStats: QuizStatDTO.listFromJson(json[r'quizStats']),