mymuseum-visitapp/lib/Services/assistantService.dart
Thomas Fransolet af44927d4a Assistant IA aligné sur le web + parcours guidés + centrage de carte
Assistant
  AssistantChatSheet.dart (+252), Helpers/assistantSuggestions.dart,
  Services/assistantService.dart — même comportement et mêmes suggestions que
  visitapp-web, dérivées du contenu réel.

Parcours guidés
  guided_path_content_progression_page (+72), guided_path_map_progression_page (+39),
  parcours_page : alignés sur les 3 questions de progression qui remplacent les
  9 booléens côté manager-app.

Carte
  Helpers/mapCenter.dart + les trois vues (flutter_map, google_map, map_box) et
  marker_view : centrage et icônes honorés.

Fix du build
  guided_step_challenge.dart:83 — typage explicite. Avec le // @dart=2.18 de
  manager_api_new côté manager-app, c'est ce qui débloque flutter build apk.

flutter build apk .
Reste ouvert : M3 — meterZoneGPS toujours ignoré, une constante en dur à 100 m
remplace le rayon configuré par section.
2026-08-09 22:15:26 +02:00

122 lines
3.9 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:manager_api_new/api.dart';
import 'package:mymuseum_visitapp/Models/visitContext.dart';
import 'package:mymuseum_visitapp/Models/AssistantResponse.dart';
/// Levée quand l'instance a épuisé son quota IA du mois (HTTP 429).
///
/// Distincte d'une panne : le visiteur ne doit pas être invité à réessayer, ça
/// le ferait boucler sur un mur. L'appelant affiche un message neutre, sans
/// jamais mentionner le motif — c'est une affaire entre le client et nous.
class AssistantUnavailableException implements Exception {
const AssistantUnavailableException();
}
class AssistantService {
final VisitAppContext visitAppContext;
/// Nombre maximum de messages conservés dans l'historique envoyé au backend.
final int maxHistory;
/// Durée d'inactivité après laquelle l'historique est automatiquement vidé.
/// null = pas de vidage automatique.
final Duration? inactivityTimeout;
final List<AiChatMessage> _history = [];
Timer? _inactivityTimer;
AssistantService({
required this.visitAppContext,
this.maxHistory = 10,
this.inactivityTimeout = const Duration(minutes: 5),
});
Future<AssistantResponse> chat({
required String message,
String? configurationId,
bool isVoice = false,
}) => chatWithAppType(message: message, configurationId: configurationId, isVoice: isVoice);
Future<AssistantResponse> chatWithAppType({
required String message,
String? configurationId,
AppType appType = AppType.Mobile,
bool isVoice = false,
}) async {
_resetInactivityTimer();
final request = AiChatRequest(
message: message,
instanceId: visitAppContext.instanceId,
appType: appType,
configurationId: configurationId,
language: visitAppContext.language?.toUpperCase() ?? 'FR',
history: List.from(_history),
isVoice: isVoice,
);
final AiChatResponse? response;
try {
response = await visitAppContext.clientAPI.aiApi!.aiChat(request);
} on ApiException catch (e) {
if (e.code == 429) throw const AssistantUnavailableException();
rethrow;
}
if (response == null) {
throw Exception('Empty response from assistant');
}
debugPrint("AI raw response: reply='${response.reply}' navigation.sectionId='${response.navigation?.sectionId}' navigation.sectionTitle='${response.navigation?.sectionTitle}' navigation.sectionType='${response.navigation?.sectionType}' cards=${response.cards?.length}");
final result = AssistantResponse(
reply: response.reply ?? '',
cards: response.cards
?.map((c) => AiCard(
title: c.title ?? '',
subtitle: c.subtitle ?? '',
icon: c.icon,
))
.toList(),
navigation: response.navigation != null
? AssistantNavigationAction(
sectionId: response.navigation!.sectionId ?? '',
sectionTitle: response.navigation!.sectionTitle ?? '',
sectionType: response.navigation!.sectionType ?? '',
imageUrl: response.navigation!.imageUrl,
)
: null,
expectsReply: response.expectsReply ?? true,
);
_history.add(AiChatMessage(role: 'user', content: message));
_history.add(AiChatMessage(role: 'assistant', content: result.reply));
// Cap local — inutile de garder plus que maxHistory côté client
if (_history.length > maxHistory) {
_history.removeRange(0, _history.length - maxHistory);
}
return result;
}
void _resetInactivityTimer() {
if (inactivityTimeout == null) return;
_inactivityTimer?.cancel();
_inactivityTimer = Timer(inactivityTimeout!, () {
debugPrint('[AssistantService] Inactivity timeout — clearing history');
clearHistory();
});
}
void clearHistory() {
_history.clear();
_inactivityTimer?.cancel();
}
void dispose() {
_inactivityTimer?.cancel();
}
}