manager-app/lib/Services/ai_translate_service.dart
Thomas Fransolet bb4d4fe2d5 Écran Statistiques refondu + Guide IA + onboarding (auth, abonnement) + i18n FR/EN/NL
Statistiques — refonte complète, aucun changement backend
  statistics_screen.dart réécrit (+1832) : barres horizontales monochromes à la place
  des barres verticales tronquées et des deux anneaux, barre de filtres unique avec les
  volumes par canal, règle mono-canal, 4 KPI portant chacun leur variation, bandeau
  « à retenir », courbe en aire avec bandes de week-end. La période précédente s'obtient
  en rappelant le même endpoint.
  statistics_report.dart : export PDF généré côté client (paquet pdf Dart), il partage
  les valeurs calculées de l'écran — un chiffre ne peut pas diverger entre l'écran et le
  document envoyé à la commune. Deux puces du sommaire promettaient des données
  inexistantes (parcours terminés, questions au guide IA), retirées.
  ⚠️ Jamais ouvert dans un navigateur. Cases de test : test-plan.md §8bis / §8ter.

Guide IA
  Screens/GuideIa/guide_ia_screen.dart — onglet Configuration. Menu conditionné à
  isAssistant, le même drapeau que la garde d'AiController. L'onglet « Ce que demandent
  vos visiteurs » n'est pas dans ce commit : le schéma backend est prêt, l'UI non.

Onboarding self-service
  Screens/Auth/ (mot de passe oublié, définition du mot de passe),
  Screens/Billing/subscription_screen.dart, ai_quota_hint.dart.
  ⚠️ Aucun parcours joué de bout en bout — test-plan.md §18.

Parcours guidés
  progression_mode.dart : 9 booléens sur 3 niveaux remplacés par 3 questions.
  Popups GuidedPath / GuidedStep / QuizQuestion mises à jour en conséquence.

Client API (manager_api_new) — édité À LA MAIN, ne pas relancer la génération
  onboarding_api.dart, authentication_api.dart (+80), instance_dto (champs Guide*),
  guided_step / quiz_question_guided_step (flags morts retirés).
  Le // @dart=2.18 manquant dans onboarding_api.dart cassait les 3 apps Flutter d'un
  coup — corrigé ici.

i18n : ~180 clés par langue (FR/EN/NL) + fichiers générés.
Tests : progression_mode_test, statistics_report_test (le second a attrapé deux
plantages qui seraient sortis au premier clic).

flutter build web . flutter analyze : 68 erreurs, toutes dans les fichiers modèle
orphelins de manager_api_new — dette connue, pas une régression, ces fichiers ne sont
pas dans le graphe de compilation.
2026-08-09 22:14:37 +02:00

73 lines
2.4 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
/// Erreur remontée par l'API de traduction IA, en conservant le message du backend
/// (ex. "Quota IA mensuel dépassé") pour pouvoir l'afficher tel quel à l'utilisateur.
class AiTranslateException implements Exception {
final String message;
final int statusCode;
AiTranslateException(this.message, this.statusCode);
/// true si l'échec est dû à un quota atteint (mensuel ou période d'essai)
bool get isQuotaExceeded => statusCode == 429;
@override
String toString() => message;
}
class AiTranslateService {
static Future<Map<String, String>> translate({
required String host,
required String accessToken,
required String instanceId,
required String text,
required String sourceLang,
required List<String> targetLangs,
}) async {
final uri = Uri.parse('$host/api/Ai/translate?instanceId=$instanceId');
final response = await http.post(
uri,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $accessToken',
},
body: jsonEncode({
'text': text,
'sourceLang': sourceLang,
'targetLangs': targetLangs,
}),
);
if (response.statusCode != 200) {
throw AiTranslateException(_extractMessage(response), response.statusCode);
}
final data = jsonDecode(utf8.decode(response.bodyBytes));
final translations = data['translations'] as Map<String, dynamic>;
return translations.map((k, v) => MapEntry(k, v.toString()));
}
/// Le backend renvoie ses messages d'erreur en texte via StatusCode(429, "..."),
/// ce qui donne une chaîne JSON. On décode en UTF-8 explicitement : le package http
/// retombe sur latin1 quand le charset n'est pas précisé, ce qui casse les accents.
static String _extractMessage(http.Response response) {
final body = utf8.decode(response.bodyBytes, allowMalformed: true).trim();
if (body.isNotEmpty) {
try {
final decoded = jsonDecode(body);
if (decoded is String && decoded.trim().isNotEmpty) return decoded.trim();
if (decoded is Map && decoded['title'] is String) return decoded['title'] as String;
} catch (_) {
return body;
}
}
if (response.statusCode == 403) {
return "L'assistant IA n'est pas activé pour cette instance";
}
return 'Erreur lors de la traduction IA (${response.statusCode})';
}
}