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.
122 lines
3.9 KiB
Dart
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();
|
|
}
|
|
}
|