Guide IA : libellés de canaux, activation depuis la carte, notifications topRight

La carte « Où le guide est disponible » affichait la valeur entière de l'enum
AppType (0, 1, 2…) au lieu d'un libellé. Réutilise les clés statsChannel*
déjà traduites en FR/EN/NL pour l'écran Statistiques.

L'activation de l'assistant par canal se réglait dans chaque onglet
d'application, alors que le champ vit sur ApplicationInstance et se résout
par (InstanceId, AppType). Le réglage passe dans la liste des canaux du
Guide IA, où tous les canaux sont visibles au même endroit ; le paramètre
showAssistant de AppConfigurationLinkScreen devient inutile.

Les notifications de cet écran passaient par ScaffoldMessenger (en bas),
contrairement au reste de l'application qui utilise showNotification
(toastification, topRight).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Fransolet 2026-08-14 15:30:14 +02:00
parent 9af4f8e2d8
commit a6463acce6
10 changed files with 89 additions and 88 deletions

View File

@ -25,10 +25,9 @@ import 'package:provider/provider.dart';
class AppConfigurationLinkScreen extends StatefulWidget {
final ApplicationInstanceDTO applicationInstanceDTO;
final bool showAssistant;
final String? configTitle;
final String? appUpdatedLabel;
AppConfigurationLinkScreen({Key? key, required this.applicationInstanceDTO, this.showAssistant = true, this.configTitle, this.appUpdatedLabel}) : super(key: key);
AppConfigurationLinkScreen({Key? key, required this.applicationInstanceDTO, this.configTitle, this.appUpdatedLabel}) : super(key: key);
@override
_AppConfigurationLinkScreenState createState() => _AppConfigurationLinkScreenState();
@ -335,45 +334,8 @@ class _AppConfigurationLinkScreenState extends State<AppConfigurationLinkScreen>
),
),
),
// Assistant IA visible uniquement si l'instance a la fonctionnalité
if (widget.showAssistant && managerAppContext.instanceDTO?.isAssistant == true)
Container(
width: elementWidth,
height: elementHeight,
child: Center(
child: StatefulBuilder(
builder: (context, localSetState) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(AppLocalizations.of(context)!.aiAssistantLabel, style: TextStyle(fontSize: 16)),
Switch(
activeThumbColor: kPrimaryColor,
inactiveThumbColor: kBodyTextColor,
inactiveTrackColor: kSecond,
hoverColor: kPrimaryColor.withValues(alpha: 0.2),
value: _applicationInstanceDTO.isAssistant ?? false,
onChanged: (bool newValue) async {
try {
_applicationInstanceDTO.isAssistant = newValue;
var applicationLink = await updateApplicationInstance(appContext, _applicationInstanceDTO);
if (applicationLink != null) {
localSetState(() {
_applicationInstanceDTO.isAssistant = applicationLink.isAssistant;
});
showNotification(kSuccess, kWhite, appUpdatedMsg, context, null);
}
} catch (e) {
showNotification(kError, kWhite, AppLocalizations.of(context)!.errorOccurred, context, null);
}
},
),
],
);
},
),
),
),
// L'activation de l'assistant par canal se règle dans l'onglet Guide IA,
// qui liste tous les canaux au même endroit.
],
),
),

View File

@ -153,7 +153,6 @@ class WebAppScreen extends StatelessWidget {
Expanded(
child: AppConfigurationLinkScreen(
applicationInstanceDTO: applicationInstanceDTO,
showAssistant: false,
configTitle: 'Contenu de l\'application web',
appUpdatedLabel: 'Application web mise à jour',
),

View File

@ -4,9 +4,11 @@ import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:manager_api_new/api.dart';
import 'package:manager_app/app_context.dart';
import 'package:manager_app/Components/message_notification.dart';
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/Screens/GuideIa/visitor_questions_tab.dart';
import 'package:provider/provider.dart';
@ -33,6 +35,9 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
bool _saving = false;
bool _reindexing = false;
/// Ids des canaux dont l'activation est en cours — chaque ligne bascule indépendamment.
final Set<String?> _togglingChannels = {};
int _tab = 0;
final _previewController = TextEditingController();
@ -66,8 +71,9 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
super.dispose();
}
ManagerAppContext get _managerContext =>
Provider.of<AppContext>(context, listen: false).getContext() as ManagerAppContext;
AppContext get _appContext => Provider.of<AppContext>(context, listen: false);
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.
@ -196,14 +202,10 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
try {
await _managerContext.clientAPI!.instanceApi!.instanceUpdateinstance(instance);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.guideIaSaved), backgroundColor: kPrimaryColor),
);
showNotification(kSuccess, kWhite, l.guideIaSaved, context, null);
} catch (_) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.guideIaSaveError), backgroundColor: Colors.redAccent),
);
showNotification(kError, kWhite, l.guideIaSaveError, context, null);
} finally {
if (mounted) setState(() => _saving = false);
}
@ -244,22 +246,19 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
if (response.statusCode == 202) {
final sections = (jsonDecode(response.body)['sectionsQueued'] as num?)?.toInt() ?? 0;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Réindexation lancée — $sections section(s) en file. '
'Le détail par section part dans les logs.'),
backgroundColor: kPrimaryColor,
));
showNotification(
kSuccess,
kWhite,
'Réindexation lancée — $sections section(s) en file. '
'Le détail par section part dans les logs.',
context,
null);
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Échec (${response.statusCode}) : ${response.body}'),
backgroundColor: Colors.redAccent,
));
showNotification(kError, kWhite, 'Échec (${response.statusCode}) : ${response.body}', context, null);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Échec de la réindexation : $e'), backgroundColor: Colors.redAccent),
);
showNotification(kError, kWhite, 'Échec de la réindexation : $e', context, null);
} finally {
if (mounted) setState(() => _reindexing = false);
}
@ -531,6 +530,44 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
);
}
String _channelLabel(AppLocalizations l, AppType? channel) {
switch (channel?.value) {
case 0:
return l.statsChannelMobile;
case 1:
return l.statsChannelTablet;
case 2:
return l.statsChannelWeb;
case 3:
return l.statsChannelVR;
case 4:
return l.statsChannelVoice;
default:
return '';
}
}
Future<void> _toggleChannel(ApplicationInstanceDTO app, AppLocalizations l) async {
final previous = app.isAssistant;
setState(() {
app.isAssistant = !(previous ?? false);
_togglingChannels.add(app.id);
});
try {
final updated = await updateApplicationInstance(_appContext, app);
if (!mounted) return;
setState(() => app.isAssistant = updated?.isAssistant ?? app.isAssistant);
showNotification(kSuccess, kWhite, l.guideIaSaved, context, null);
} catch (_) {
if (!mounted) return;
setState(() => app.isAssistant = previous);
showNotification(kError, kWhite, l.guideIaSaveError, context, null);
} finally {
if (mounted) setState(() => _togglingChannels.remove(app.id));
}
}
Widget _channelsCard(AppLocalizations l) {
final apps = _instance?.applicationInstanceDTOs ?? [];
@ -540,19 +577,27 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
child: Column(
children: apps.map((app) {
final on = app.isAssistant == true;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 7),
child: Row(
children: [
Icon(on ? Icons.check_circle : Icons.remove_circle_outline,
size: 18, color: on ? kPrimaryColor : kSecond),
const SizedBox(width: 10),
Expanded(
child: Text((app.appType?.value ?? '').toString(),
style: const TextStyle(fontSize: 14))),
Text(on ? l.guideIaChannelOn : l.guideIaChannelOff,
style: TextStyle(fontSize: 12.5, color: kBodyTextColor.withValues(alpha: 0.7))),
],
final busy = _togglingChannels.contains(app.id);
return InkWell(
onTap: busy ? null : () => _toggleChannel(app, l),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 7),
child: Row(
children: [
if (busy)
const SizedBox(
width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2))
else
Icon(on ? Icons.check_circle : Icons.remove_circle_outline,
size: 18, color: on ? kPrimaryColor : kSecond),
const SizedBox(width: 10),
Expanded(
child: Text(_channelLabel(l, app.appType),
style: const TextStyle(fontSize: 14))),
Text(on ? l.guideIaChannelOn : l.guideIaChannelOff,
style: TextStyle(fontSize: 12.5, color: kBodyTextColor.withValues(alpha: 0.7))),
],
),
),
);
}).toList(),
@ -690,12 +735,7 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
_previewTurns.last = _PreviewTurn(message, null);
_previewPending = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.guideIaPreviewError),
backgroundColor: Colors.redAccent,
),
);
showNotification(kError, kWhite, AppLocalizations.of(context)!.guideIaPreviewError, context, null);
}
}

View File

@ -62,7 +62,7 @@
"guideIaUsageQuestions": "{count} questions asked",
"guideIaUsageNoQuota": "No cap set on your plan.",
"guideIaChannelsTitle": "Where the guide is available",
"guideIaChannelsSub": "Enabled per channel, from each application's settings.",
"guideIaChannelsSub": "Click a channel to enable or disable the guide there.",
"guideIaChannelOn": "Enabled",
"guideIaChannelOff": "Disabled",
"guideIaIdentityTitle": "Guide identity",

View File

@ -62,7 +62,7 @@
"guideIaUsageQuestions": "{count} questions posées",
"guideIaUsageNoQuota": "Aucun plafond défini sur votre offre.",
"guideIaChannelsTitle": "Où le guide est disponible",
"guideIaChannelsSub": "Activable canal par canal, depuis la configuration de chaque application.",
"guideIaChannelsSub": "Cliquez sur un canal pour y activer ou désactiver le guide.",
"guideIaChannelOn": "Activé",
"guideIaChannelOff": "Désactivé",
"guideIaIdentityTitle": "Identité du guide",

View File

@ -445,7 +445,7 @@ abstract class AppLocalizations {
/// No description provided for @guideIaChannelsSub.
///
/// In fr, this message translates to:
/// **'Activable canal par canal, depuis la configuration de chaque application.'**
/// **'Cliquez sur un canal pour y activer ou désactiver le guide.'**
String get guideIaChannelsSub;
/// No description provided for @guideIaChannelOn.

View File

@ -195,7 +195,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get guideIaChannelsSub =>
'Enabled per channel, from each application\'s settings.';
'Click a channel to enable or disable the guide there.';
@override
String get guideIaChannelOn => 'Enabled';

View File

@ -198,7 +198,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get guideIaChannelsSub =>
'Activable canal par canal, depuis la configuration de chaque application.';
'Cliquez sur un canal pour y activer ou désactiver le guide.';
@override
String get guideIaChannelOn => 'Activé';

View File

@ -198,7 +198,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get guideIaChannelsSub =>
'Per kanaal in te schakelen, vanuit de instellingen van elke toepassing.';
'Klik op een kanaal om de gids daar in of uit te schakelen.';
@override
String get guideIaChannelOn => 'Ingeschakeld';

View File

@ -62,7 +62,7 @@
"guideIaUsageQuestions": "{count} gestelde vragen",
"guideIaUsageNoQuota": "Geen limiet ingesteld op uw abonnement.",
"guideIaChannelsTitle": "Waar de gids beschikbaar is",
"guideIaChannelsSub": "Per kanaal in te schakelen, vanuit de instellingen van elke toepassing.",
"guideIaChannelsSub": "Klik op een kanaal om de gids daar in of uit te schakelen.",
"guideIaChannelOn": "Ingeschakeld",
"guideIaChannelOff": "Uitgeschakeld",
"guideIaIdentityTitle": "Identiteit van de gids",