Guide IA : traduire les formulations de repli à l'enregistrement
L'infobulle promettait que ces phrases seraient traduites « comme le reste de vos contenus », mais rien ne les traduisait jamais : BuildFallbackInstruction filtre sur la langue du visiteur et, ne trouvant rien, laisse le modèle improviser son refus. Les formulations du client ne servaient qu'en français. L'enregistrement traduit désormais vers les langues déclarées sur les canaux, en n'appelant l'IA que pour ce qui en a besoin : les langues sans traduction, et toutes les langues si une phrase source a changé depuis le chargement. Une sauvegarde qui ne touche pas aux formulations ne consomme donc pas de jetons. Les langues visées sont celles des canaux, pas les dix supportées, pour la même raison. Un échec de traduction n'empêche pas l'enregistrement : le reste est sauvé, les traductions précédentes sont conservées et l'écran le signale. Effacer toutes les formulations efface aussi leurs traductions, devenues sans objet. Corrige au passage le mot de réveil affiché : le modèle OpenWakeWord est « hey_viva » / « hey_marco », le visiteur doit donc dire « Hey Viva », pas « Viva ». Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7407335371
commit
810374abc2
@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart' show listEquals;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:manager_api_new/api.dart';
|
||||
@ -9,6 +10,7 @@ import 'package:manager_app/constants.dart';
|
||||
import 'package:manager_app/l10n/app_localizations.dart';
|
||||
import 'package:manager_app/Models/managerContext.dart';
|
||||
import 'package:manager_app/Screens/Applications/app_configuration_link_screen.dart';
|
||||
import 'package:manager_app/Services/ai_translate_service.dart';
|
||||
import 'package:manager_app/Screens/GuideIa/visitor_questions_tab.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@ -38,6 +40,10 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
||||
/// Ids des canaux dont l'activation est en cours — chaque ligne bascule indépendamment.
|
||||
final Set<String?> _togglingChannels = {};
|
||||
|
||||
/// Formulations de repli telles que chargées, pour repérer une modification :
|
||||
/// une traduction ne vaut plus rien dès que sa phrase source change.
|
||||
List<String> _loadedFallbacks = [];
|
||||
|
||||
int _tab = 0;
|
||||
|
||||
final _previewController = TextEditingController();
|
||||
@ -76,9 +82,18 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
||||
ManagerAppContext get _managerContext => _appContext.getContext() as ManagerAppContext;
|
||||
|
||||
/// Langue dans laquelle le gestionnaire saisit ses messages de repli.
|
||||
/// Les autres langues sont produites par la traduction automatique, comme le reste du contenu.
|
||||
/// Les autres langues sont produites par le bouton de traduction, comme le reste du contenu.
|
||||
String get _editingLanguage => 'FR';
|
||||
|
||||
/// Langues réellement servies aux visiteurs : l'union de celles déclarées par les canaux.
|
||||
/// Traduire vers les dix langues supportées gaspillerait le quota IA du client.
|
||||
List<String> get _visitorLanguages => (_instance?.applicationInstanceDTOs ?? [])
|
||||
.expand<String>((app) => app.languages ?? const [])
|
||||
.map((lang) => lang.toUpperCase())
|
||||
.where((lang) => lang != _editingLanguage)
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
Future<void> _load() async {
|
||||
final ctx = _managerContext;
|
||||
final instanceId = ctx.instanceId;
|
||||
@ -104,6 +119,7 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
||||
if (_fallbackControllers.isEmpty) {
|
||||
_fallbackControllers.add(TextEditingController());
|
||||
}
|
||||
_loadedFallbacks = _fallbackControllers.map((c) => c.text.trim()).where((t) => t.isNotEmpty).toList();
|
||||
|
||||
setState(() {
|
||||
_instance = instance;
|
||||
@ -176,6 +192,44 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Traduit les formulations de repli vers les langues des visiteurs. Sans elles,
|
||||
/// `BuildFallbackInstruction` côté serveur ne trouve rien dans la langue du visiteur
|
||||
/// et laisse le modèle improviser son refus — les phrases du client sont perdues.
|
||||
/// Traduit les formulations de repli vers les langues des visiteurs, en ne rappelant
|
||||
/// l'IA que pour les langues qui en ont besoin : celles qui n'ont encore rien, et
|
||||
/// toutes les autres si une phrase source a changé depuis le chargement.
|
||||
/// Renvoie null si l'appel a échoué, pour que l'enregistrement le signale.
|
||||
Future<List<TranslationDTO>?> _translateFallbacks(List<String> sources, List<TranslationDTO> existing) async {
|
||||
final ctx = _managerContext;
|
||||
final sourcesChanged = !listEquals(sources, _loadedFallbacks);
|
||||
|
||||
final targets = _visitorLanguages
|
||||
.where((lang) => sourcesChanged || !existing.any((m) => m.language == lang))
|
||||
.toList();
|
||||
|
||||
if (sources.isEmpty || targets.isEmpty) return const [];
|
||||
|
||||
// Une requête par formulation : l'endpoint traduit un texte vers N langues.
|
||||
final translated = <TranslationDTO>[];
|
||||
for (final source in sources) {
|
||||
final result = await AiTranslateService.translate(
|
||||
host: ctx.host!,
|
||||
accessToken: ctx.accessToken!,
|
||||
instanceId: ctx.instanceId!,
|
||||
text: source,
|
||||
sourceLang: _editingLanguage,
|
||||
targetLangs: targets,
|
||||
);
|
||||
for (final lang in targets) {
|
||||
final value = result[lang]?.trim();
|
||||
if (value != null && value.isNotEmpty) {
|
||||
translated.add(TranslationDTO(language: lang, value: value));
|
||||
}
|
||||
}
|
||||
}
|
||||
return translated;
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
final instance = _instance;
|
||||
if (instance == null) return;
|
||||
@ -183,26 +237,46 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
||||
|
||||
setState(() => _saving = true);
|
||||
|
||||
// Les messages des autres langues sont préservés tels quels : on ne réécrit que la langue d'édition.
|
||||
final otherLanguages = (instance.guideFallbackMessages)
|
||||
.where((m) => m.language != _editingLanguage)
|
||||
.toList();
|
||||
final edited = _fallbackControllers
|
||||
.map((c) => c.text.trim())
|
||||
.where((t) => t.isNotEmpty)
|
||||
.map((t) => TranslationDTO(language: _editingLanguage, value: t))
|
||||
.toList();
|
||||
final sources = _fallbackControllers.map((c) => c.text.trim()).where((t) => t.isNotEmpty).toList();
|
||||
final existing = instance.guideFallbackMessages.where((m) => m.language != _editingLanguage).toList();
|
||||
|
||||
// Sans traduction dans la langue du visiteur, le serveur laisse le modèle improviser
|
||||
// son refus : les formulations du client seraient perdues hors du français.
|
||||
List<TranslationDTO>? translated;
|
||||
String? translationError;
|
||||
try {
|
||||
translated = await _translateFallbacks(sources, existing);
|
||||
} on AiTranslateException catch (e) {
|
||||
translationError = e.message;
|
||||
} catch (_) {
|
||||
translationError = l.guideIaFallbackTranslateError;
|
||||
}
|
||||
if (!mounted) return;
|
||||
|
||||
// Une traduction produite remplace celle de sa langue ; les autres restent en place.
|
||||
// Plus aucune formulation source : les traductions n'ont plus d'objet et disparaissent.
|
||||
final refreshedLanguages = (translated ?? []).map((m) => m.language).toSet();
|
||||
final preserved = sources.isEmpty
|
||||
? <TranslationDTO>[]
|
||||
: existing.where((m) => !refreshedLanguages.contains(m.language)).toList();
|
||||
final edited = sources.map((t) => TranslationDTO(language: _editingLanguage, value: t)).toList();
|
||||
|
||||
instance.guideName = _nameController.text.trim();
|
||||
instance.guidePersonaPrompt = _personaController.text.trim();
|
||||
instance.guideVoiceId = _voiceId;
|
||||
instance.isVisitorQuestionCollectionEnabled = _collectionEnabled;
|
||||
instance.guideFallbackMessages = [...otherLanguages, ...edited];
|
||||
instance.guideFallbackMessages = [...preserved, ...edited, ...(translated ?? [])];
|
||||
|
||||
try {
|
||||
await _managerContext.clientAPI!.instanceApi!.instanceUpdateinstance(instance);
|
||||
if (!mounted) return;
|
||||
_loadedFallbacks = List.of(sources);
|
||||
// Le reste est bien enregistré : l'échec de traduction se signale sans masquer ça.
|
||||
if (translationError != null) {
|
||||
showNotification(Colors.orange, kWhite, l.guideIaSavedTranslationFailed(translationError), context, null);
|
||||
} else {
|
||||
showNotification(kSuccess, kWhite, l.guideIaSaved, context, null);
|
||||
}
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
showNotification(kError, kWhite, l.guideIaSaveError, context, null);
|
||||
@ -900,7 +974,7 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
||||
}
|
||||
|
||||
Widget _voiceCard(AppLocalizations l) {
|
||||
final wakeword = _voiceId == kGuideVoiceMarco ? 'Marco' : 'Viva';
|
||||
final wakeword = _voiceId == kGuideVoiceMarco ? 'Hey Marco' : 'Hey Viva';
|
||||
|
||||
return _card(
|
||||
title: l.guideIaVoiceTitle,
|
||||
@ -944,7 +1018,7 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
||||
const SizedBox(height: 2),
|
||||
Text(description, style: TextStyle(fontSize: 12.5, color: kBodyTextColor)),
|
||||
const SizedBox(height: 8),
|
||||
Text(l.guideIaVoiceWakeword(name),
|
||||
Text(l.guideIaVoiceWakeword('Hey $name'),
|
||||
style: TextStyle(fontSize: 11.5, color: kBodyTextColor.withValues(alpha: 0.7))),
|
||||
],
|
||||
),
|
||||
|
||||
@ -80,7 +80,16 @@
|
||||
"guideIaFallbackLabel": "When the guide doesn't know",
|
||||
"guideIaFallbackHint": "The guide picks one at random. With a single sentence it repeats it word for word, and that shows immediately.",
|
||||
"guideIaFallbackAdd": "Add a wording",
|
||||
"guideIaFallbackTranslated": "Visitors see these sentences: they will be translated into their language, like the rest of your content.",
|
||||
"guideIaFallbackTranslated": "Visitors see these sentences: on save, they are translated into your channels' languages.",
|
||||
"guideIaFallbackTranslateError": "AI translation failed",
|
||||
"guideIaSavedTranslationFailed": "Saved, but translating the wordings failed: {reason}",
|
||||
"@guideIaSavedTranslationFailed": {
|
||||
"placeholders": {
|
||||
"reason": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"guideIaVoiceTitle": "Your guide out loud",
|
||||
"guideIaVoiceSub": "Visitors ask out loud and hear the answer — in the app, or in their connected glasses.",
|
||||
"guideIaVoiceFemale": "Female voice, warm",
|
||||
|
||||
@ -80,7 +80,16 @@
|
||||
"guideIaFallbackLabel": "Quand le guide ne sait pas répondre",
|
||||
"guideIaFallbackHint": "Le guide en choisit une au hasard. Avec une seule phrase, il la répète à l'identique et ça se remarque tout de suite.",
|
||||
"guideIaFallbackAdd": "Ajouter une formulation",
|
||||
"guideIaFallbackTranslated": "Ces phrases sont vues par vos visiteurs : elles seront traduites dans leur langue, comme le reste de vos contenus.",
|
||||
"guideIaFallbackTranslated": "Ces phrases sont vues par vos visiteurs : à l'enregistrement, elles sont traduites dans les langues de vos canaux.",
|
||||
"guideIaFallbackTranslateError": "Erreur lors de la traduction IA",
|
||||
"guideIaSavedTranslationFailed": "Enregistré, mais la traduction des formulations a échoué : {reason}",
|
||||
"@guideIaSavedTranslationFailed": {
|
||||
"placeholders": {
|
||||
"reason": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"guideIaVoiceTitle": "Votre guide à voix haute",
|
||||
"guideIaVoiceSub": "Le visiteur pose sa question à voix haute et entend la réponse — dans l'application, ou dans ses lunettes connectées.",
|
||||
"guideIaVoiceFemale": "Voix féminine, chaleureuse",
|
||||
|
||||
@ -553,9 +553,21 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @guideIaFallbackTranslated.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Ces phrases sont vues par vos visiteurs : elles seront traduites dans leur langue, comme le reste de vos contenus.'**
|
||||
/// **'Ces phrases sont vues par vos visiteurs : à l\'enregistrement, elles sont traduites dans les langues de vos canaux.'**
|
||||
String get guideIaFallbackTranslated;
|
||||
|
||||
/// No description provided for @guideIaFallbackTranslateError.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Erreur lors de la traduction IA'**
|
||||
String get guideIaFallbackTranslateError;
|
||||
|
||||
/// No description provided for @guideIaSavedTranslationFailed.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Enregistré, mais la traduction des formulations a échoué : {reason}'**
|
||||
String guideIaSavedTranslationFailed(String reason);
|
||||
|
||||
/// No description provided for @guideIaVoiceTitle.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
|
||||
@ -255,7 +255,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get guideIaFallbackTranslated =>
|
||||
'Visitors see these sentences: they will be translated into their language, like the rest of your content.';
|
||||
'Visitors see these sentences: on save, they are translated into your channels\' languages.';
|
||||
|
||||
@override
|
||||
String get guideIaFallbackTranslateError => 'AI translation failed';
|
||||
|
||||
@override
|
||||
String guideIaSavedTranslationFailed(String reason) {
|
||||
return 'Saved, but translating the wordings failed: $reason';
|
||||
}
|
||||
|
||||
@override
|
||||
String get guideIaVoiceTitle => 'Your guide out loud';
|
||||
|
||||
@ -259,7 +259,15 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get guideIaFallbackTranslated =>
|
||||
'Ces phrases sont vues par vos visiteurs : elles seront traduites dans leur langue, comme le reste de vos contenus.';
|
||||
'Ces phrases sont vues par vos visiteurs : à l\'enregistrement, elles sont traduites dans les langues de vos canaux.';
|
||||
|
||||
@override
|
||||
String get guideIaFallbackTranslateError => 'Erreur lors de la traduction IA';
|
||||
|
||||
@override
|
||||
String guideIaSavedTranslationFailed(String reason) {
|
||||
return 'Enregistré, mais la traduction des formulations a échoué : $reason';
|
||||
}
|
||||
|
||||
@override
|
||||
String get guideIaVoiceTitle => 'Votre guide à voix haute';
|
||||
|
||||
@ -259,7 +259,15 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get guideIaFallbackTranslated =>
|
||||
'Bezoekers zien deze zinnen: ze worden vertaald naar hun taal, net als de rest van uw inhoud.';
|
||||
'Bezoekers zien deze zinnen: bij het opslaan worden ze vertaald naar de talen van uw kanalen.';
|
||||
|
||||
@override
|
||||
String get guideIaFallbackTranslateError => 'Fout bij de AI-vertaling';
|
||||
|
||||
@override
|
||||
String guideIaSavedTranslationFailed(String reason) {
|
||||
return 'Opgeslagen, maar het vertalen van de formuleringen is mislukt: $reason';
|
||||
}
|
||||
|
||||
@override
|
||||
String get guideIaVoiceTitle => 'Uw gids hardop';
|
||||
|
||||
@ -80,7 +80,16 @@
|
||||
"guideIaFallbackLabel": "Wanneer de gids het niet weet",
|
||||
"guideIaFallbackHint": "De gids kiest er willekeurig een. Met één zin herhaalt hij die letterlijk, en dat valt meteen op.",
|
||||
"guideIaFallbackAdd": "Formulering toevoegen",
|
||||
"guideIaFallbackTranslated": "Bezoekers zien deze zinnen: ze worden vertaald naar hun taal, net als de rest van uw inhoud.",
|
||||
"guideIaFallbackTranslated": "Bezoekers zien deze zinnen: bij het opslaan worden ze vertaald naar de talen van uw kanalen.",
|
||||
"guideIaFallbackTranslateError": "Fout bij de AI-vertaling",
|
||||
"guideIaSavedTranslationFailed": "Opgeslagen, maar het vertalen van de formuleringen is mislukt: {reason}",
|
||||
"@guideIaSavedTranslationFailed": {
|
||||
"placeholders": {
|
||||
"reason": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"guideIaVoiceTitle": "Uw gids hardop",
|
||||
"guideIaVoiceSub": "De bezoeker stelt zijn vraag hardop en hoort het antwoord — in de app of in zijn verbonden bril.",
|
||||
"guideIaVoiceFemale": "Vrouwelijke stem, warm",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user