Socle visuel et écran Guide IA à deux onglets
Socle visuel (constants.dart)
- 11 rôles typographiques, 8 espacements, 5 rayons, paddings de carte et de page,
extraits du CSS des maquettes validées (DOCS/claude design/).
- Les couleurs restent celles de l'app : un écran neuf se fond dans manager-app,
il n'y ouvre pas une seconde palette. Seules les valeurs sans équivalent
existant sont reprises — remplissages discrets, bordures, gris atténué, ambre.
- Mesuré avant : 11 tailles de police différentes dans lib/Components/, de 9 à 25 px.
Écran Guide IA
- Coquille à deux onglets : Configuration / Ce que demandent vos visiteurs.
- Aperçu de conversation branché sur le vrai POST /api/AI/chat, pas une simulation.
- Carte « Ce que connaît votre guide » sur GET /api/Ai/knowledge/{id} : mesuré sur
l'index vectoriel, pas sur les tables de contenu — une section désactivée en est
purgée. Les « points d'intérêt » de la maquette deviennent « morceaux de contenu » :
les points d'une carte sont indexés dans le texte de leur SectionMap.
- Onglet des questions visiteurs alimenté par GET /api/Ai/insights/{id}. Une carte
sans données se masque au lieu d'afficher un cadre creux.
Client API
- AIApi était dans le client généré mais exposé nulle part dans client.dart, où
les 18 autres façades le sont. Câblé.
34 clés i18n FR/EN/NL. flutter analyze propre, flutter build web ✅.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
bb4d4fe2d5
commit
eb8e8497c8
@ -1,9 +1,13 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:manager_api_new/api.dart';
|
import 'package:manager_api_new/api.dart';
|
||||||
import 'package:manager_app/app_context.dart';
|
import 'package:manager_app/app_context.dart';
|
||||||
import 'package:manager_app/constants.dart';
|
import 'package:manager_app/constants.dart';
|
||||||
import 'package:manager_app/l10n/app_localizations.dart';
|
import 'package:manager_app/l10n/app_localizations.dart';
|
||||||
import 'package:manager_app/Models/managerContext.dart';
|
import 'package:manager_app/Models/managerContext.dart';
|
||||||
|
import 'package:manager_app/Screens/GuideIa/visitor_questions_tab.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
/// Voix Gemini TTS retenues. Voix et mot de réveil forment un couple indissociable :
|
/// Voix Gemini TTS retenues. Voix et mot de réveil forment un couple indissociable :
|
||||||
@ -27,6 +31,17 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
|||||||
InstanceDTO? _instance;
|
InstanceDTO? _instance;
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
bool _saving = false;
|
bool _saving = false;
|
||||||
|
bool _reindexing = false;
|
||||||
|
|
||||||
|
int _tab = 0;
|
||||||
|
|
||||||
|
final _previewController = TextEditingController();
|
||||||
|
final List<_PreviewTurn> _previewTurns = [];
|
||||||
|
bool _previewPending = false;
|
||||||
|
|
||||||
|
GuideIaInsights? _insights;
|
||||||
|
|
||||||
|
Map<String, dynamic>? _knowledge;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -38,6 +53,7 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_nameController.dispose();
|
_nameController.dispose();
|
||||||
_personaController.dispose();
|
_personaController.dispose();
|
||||||
|
_previewController.dispose();
|
||||||
for (final c in _fallbackControllers) {
|
for (final c in _fallbackControllers) {
|
||||||
c.dispose();
|
c.dispose();
|
||||||
}
|
}
|
||||||
@ -80,11 +96,48 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
|||||||
_instance = instance;
|
_instance = instance;
|
||||||
_loading = false;
|
_loading = false;
|
||||||
});
|
});
|
||||||
|
_loadKnowledge();
|
||||||
|
_loadInsights();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (mounted) setState(() => _loading = false);
|
if (mounted) setState(() => _loading = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Appel http direct, comme la relance d'indexation plus bas : un agrégat en lecture
|
||||||
|
/// seule ne justifie pas d'étendre `manager_api_new`, qui s'édite à la main.
|
||||||
|
/// Un échec est silencieux — la carte disparaît, elle n'affiche pas un chiffre faux.
|
||||||
|
Future<void> _loadKnowledge() async {
|
||||||
|
final ctx = _managerContext;
|
||||||
|
try {
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse('${ctx.host}/api/Ai/knowledge/${ctx.instanceId}'),
|
||||||
|
headers: {'Authorization': 'Bearer ${ctx.accessToken}'},
|
||||||
|
);
|
||||||
|
if (!mounted || response.statusCode != 200) return;
|
||||||
|
setState(() => _knowledge = jsonDecode(response.body) as Map<String, dynamic>);
|
||||||
|
} catch (_) {
|
||||||
|
// La carte reste masquée.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Même raison que `_loadKnowledge` pour l'appel direct. Un échec laisse `_insights`
|
||||||
|
/// nul, donc l'état vide — jamais un onglet à moitié rempli qui laisserait croire
|
||||||
|
/// que les visiteurs n'ont rien demandé.
|
||||||
|
Future<void> _loadInsights() async {
|
||||||
|
final ctx = _managerContext;
|
||||||
|
try {
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse('${ctx.host}/api/Ai/insights/${ctx.instanceId}'),
|
||||||
|
headers: {'Authorization': 'Bearer ${ctx.accessToken}'},
|
||||||
|
);
|
||||||
|
if (!mounted || response.statusCode != 200) return;
|
||||||
|
setState(() => _insights =
|
||||||
|
GuideIaInsights.fromJson(jsonDecode(response.body) as Map<String, dynamic>));
|
||||||
|
} catch (_) {
|
||||||
|
// L'onglet reste sur son état vide.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _save() async {
|
Future<void> _save() async {
|
||||||
final instance = _instance;
|
final instance = _instance;
|
||||||
if (instance == null) return;
|
if (instance == null) return;
|
||||||
@ -123,11 +176,125 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Appel direct plutôt que via le client généré : l'endpoint est réservé au support,
|
||||||
|
/// et `manager_api_new` s'édite à la main — on ne l'alourdit pas pour un bouton interne.
|
||||||
|
/// Textes en français en dur, pour la même raison : aucun client ne voit cet écran.
|
||||||
|
Future<void> _reindex() async {
|
||||||
|
final ctx = _managerContext;
|
||||||
|
final instanceId = ctx.instanceId;
|
||||||
|
if (instanceId == null) return;
|
||||||
|
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: const Text('Réindexer tout le contenu ?'),
|
||||||
|
content: const Text(
|
||||||
|
'Chaque section sera revectorisée, ce qui consomme du budget d\'embedding.\n\n'
|
||||||
|
'Inutile si le guide répond mal : neuf fois sur dix le contenu est trop maigre, '
|
||||||
|
'pas l\'index périmé. À réserver aux cas où l\'index est réellement incomplet.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(dialogContext, false), child: const Text('Annuler')),
|
||||||
|
TextButton(onPressed: () => Navigator.pop(dialogContext, true), child: const Text('Réindexer')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed != true) return;
|
||||||
|
|
||||||
|
setState(() => _reindexing = true);
|
||||||
|
try {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse('${ctx.host}/api/Ai/reindex/$instanceId'),
|
||||||
|
headers: {'Authorization': 'Bearer ${ctx.accessToken}'},
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
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,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text('Échec (${response.statusCode}) : ${response.body}'),
|
||||||
|
backgroundColor: Colors.redAccent,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Échec de la réindexation : $e'), backgroundColor: Colors.redAccent),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _reindexing = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
if (_loading) return const Center(child: CircularProgressIndicator());
|
if (_loading) return const Center(child: CircularProgressIndicator());
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_header(l),
|
||||||
|
const SizedBox(height: kSpace5),
|
||||||
|
_tabs(l),
|
||||||
|
const SizedBox(height: kSpace7),
|
||||||
|
Expanded(
|
||||||
|
child: _tab == 0
|
||||||
|
? _configurationTab(l)
|
||||||
|
: VisitorQuestionsTab(insights: _insights),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deux onglets, pas deux écrans : la configuration du guide et ce que les visiteurs
|
||||||
|
/// lui demandent sont les deux faces d'un même objet. Les séparer obligerait le client
|
||||||
|
/// à faire le lien lui-même entre une question sans réponse et le contenu à écrire.
|
||||||
|
Widget _tabs(AppLocalizations l) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: kLine))),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_tabButton(l.guideIaTabConfig, 0),
|
||||||
|
_tabButton(l.guideIaTabQuestions, 1),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _tabButton(String label, int index) {
|
||||||
|
final selected = _tab == index;
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => setState(() => _tab = index),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: selected ? kBrand : Colors.transparent,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: selected ? kInk : kInk3,
|
||||||
|
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _configurationTab(AppLocalizations l) {
|
||||||
final wide = MediaQuery.of(context).size.width > 1100;
|
final wide = MediaQuery.of(context).size.width > 1100;
|
||||||
|
|
||||||
final left = Column(
|
final left = Column(
|
||||||
@ -136,25 +303,30 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
|||||||
_usageCard(l),
|
_usageCard(l),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_channelsCard(l),
|
_channelsCard(l),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_identityCard(l),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
final right = Column(
|
final right = Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_identityCard(l),
|
_previewCard(l),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_voiceCard(l),
|
_voiceCard(l),
|
||||||
|
if (_knowledge != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_knowledgeCard(l),
|
||||||
|
],
|
||||||
|
if (_managerContext.role == UserRole.SuperAdmin) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_reindexCard(),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Column(
|
child: wide
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
? Row(
|
||||||
children: [
|
|
||||||
_header(l),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
if (wide)
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: left),
|
Expanded(child: left),
|
||||||
@ -162,12 +334,9 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
|||||||
Expanded(child: right),
|
Expanded(child: right),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else ...[
|
: Column(
|
||||||
left,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
const SizedBox(height: 16),
|
children: [left, const SizedBox(height: 16), right],
|
||||||
right,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -224,6 +393,34 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Visible pour le SuperAdmin seulement — c'est un outil de réparation, pas une
|
||||||
|
/// fonctionnalité. Exposé au client, il serait cliqué à chaque réponse décevante du guide,
|
||||||
|
/// pour un coût d'embedding complet à chaque fois et sans rien améliorer.
|
||||||
|
Widget _reindexCard() {
|
||||||
|
return _card(
|
||||||
|
title: 'Réindexer le contenu (support)',
|
||||||
|
subtitle: 'Visible par le support Unov uniquement.',
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Le rattrapage est automatique au passage à un plan avec IA. Ce bouton ne sert '
|
||||||
|
'que si l\'index est resté incomplet — un job d\'indexation en échec, par exemple.',
|
||||||
|
style: TextStyle(fontSize: 12, color: kBodyTextColor.withValues(alpha: 0.7)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _reindexing ? null : _reindex,
|
||||||
|
icon: _reindexing
|
||||||
|
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
|
: const Icon(Icons.refresh),
|
||||||
|
label: Text(_reindexing ? 'Lancement…' : 'Réindexer tout le contenu'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _usageCard(AppLocalizations l) {
|
Widget _usageCard(AppLocalizations l) {
|
||||||
final used = _instance?.aiTokensThisMonth ?? 0;
|
final used = _instance?.aiTokensThisMonth ?? 0;
|
||||||
final quota = _instance?.aiTokensPerMonth ?? 0;
|
final quota = _instance?.aiTokensPerMonth ?? 0;
|
||||||
@ -380,6 +577,208 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Interroge le guide réellement publié, pas une simulation : c'est le seul moyen de
|
||||||
|
/// vérifier que la personnalité saisie produit le ton attendu. D'où l'avertissement
|
||||||
|
/// quand des modifications ne sont pas encore enregistrées — sinon le client teste
|
||||||
|
/// l'ancienne configuration en croyant tester la nouvelle.
|
||||||
|
Future<void> _sendPreview() async {
|
||||||
|
final message = _previewController.text.trim();
|
||||||
|
if (message.isEmpty || _previewPending) return;
|
||||||
|
|
||||||
|
final ctx = _managerContext;
|
||||||
|
setState(() {
|
||||||
|
_previewTurns.add(_PreviewTurn(message, null));
|
||||||
|
_previewController.clear();
|
||||||
|
_previewPending = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await ctx.clientAPI!.aiApi!.aiChat(AiChatRequest(
|
||||||
|
message: message,
|
||||||
|
instanceId: ctx.instanceId,
|
||||||
|
appType: AppType.Web,
|
||||||
|
language: _editingLanguage,
|
||||||
|
));
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_previewTurns.last = _PreviewTurn(message, response?.reply ?? '');
|
||||||
|
_previewPending = false;
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_previewTurns.last = _PreviewTurn(message, null);
|
||||||
|
_previewPending = false;
|
||||||
|
});
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(AppLocalizations.of(context)!.guideIaPreviewError),
|
||||||
|
backgroundColor: Colors.redAccent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _previewCard(AppLocalizations l) {
|
||||||
|
return _card(
|
||||||
|
title: l.guideIaPreviewTitle,
|
||||||
|
subtitle: l.guideIaPreviewSub,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: kSurface2,
|
||||||
|
border: Border.all(color: kLine),
|
||||||
|
borderRadius: BorderRadius.circular(7),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (_previewTurns.isEmpty)
|
||||||
|
Text(l.guideIaPreviewUnsaved, style: kTextHint)
|
||||||
|
else
|
||||||
|
for (final turn in _previewTurns) ..._previewBubbles(turn),
|
||||||
|
if (_previewPending) ...[
|
||||||
|
const SizedBox(height: kSpace3),
|
||||||
|
const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: kSpace4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _previewController,
|
||||||
|
onSubmitted: (_) => _sendPreview(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
hintText: l.guideIaPreviewPlaceholder,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: kSpace3),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _previewPending ? null : _sendPreview,
|
||||||
|
icon: const Icon(Icons.send),
|
||||||
|
color: kBrand,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: kSpace4),
|
||||||
|
_info(l.guideIaPreviewSourcesInfo),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _previewBubbles(_PreviewTurn turn) {
|
||||||
|
return [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 9),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
||||||
|
constraints: const BoxConstraints(maxWidth: 340),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: kBrand,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(12),
|
||||||
|
topRight: Radius.circular(12),
|
||||||
|
bottomLeft: Radius.circular(12),
|
||||||
|
bottomRight: Radius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(turn.question, style: kTextBody.copyWith(color: kOnBrand)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (turn.answer != null)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 9),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
||||||
|
constraints: const BoxConstraints(maxWidth: 340),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: kSurface,
|
||||||
|
border: Border.all(color: kLine),
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(12),
|
||||||
|
topRight: Radius.circular(12),
|
||||||
|
bottomLeft: Radius.circular(4),
|
||||||
|
bottomRight: Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(turn.answer!, style: kTextBody),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mesuré sur l'index vectoriel, pas sur les tables de contenu : une section
|
||||||
|
/// désactivée est purgée de l'index, une section sans texte exploitable n'y entre pas.
|
||||||
|
/// Compter les sections publiées donnerait un chiffre plus flatteur et faux.
|
||||||
|
Widget _knowledgeCard(AppLocalizations l) {
|
||||||
|
final k = _knowledge!;
|
||||||
|
final languages = (k['languages'] as List?)?.cast<String>() ?? const <String>[];
|
||||||
|
final lastIndexed = DateTime.tryParse((k['lastIndexedAt'] ?? '').toString());
|
||||||
|
|
||||||
|
return _card(
|
||||||
|
title: l.guideIaKnowledgeTitle,
|
||||||
|
subtitle: l.guideIaKnowledgeSub,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_knowledgeRow(l.guideIaKnowledgeSections, '${k['indexedSections'] ?? 0}', first: true),
|
||||||
|
_knowledgeRow(l.guideIaKnowledgeChunks, '${k['chunks'] ?? 0}'),
|
||||||
|
_knowledgeRow(
|
||||||
|
l.guideIaKnowledgeLanguages,
|
||||||
|
languages.isEmpty ? '—' : languages.join(' · '),
|
||||||
|
),
|
||||||
|
_knowledgeRow(
|
||||||
|
l.guideIaKnowledgeLastIndexed,
|
||||||
|
lastIndexed == null ? '—' : _relativeTime(l, lastIndexed),
|
||||||
|
),
|
||||||
|
const SizedBox(height: kSpace3),
|
||||||
|
Text(l.guideIaKnowledgeHint, style: kTextHint),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _knowledgeRow(String label, String value, {bool first = false}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 7),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: first ? null : Border(top: BorderSide(color: kLineSoft)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(label, style: kTextBody)),
|
||||||
|
const SizedBox(width: kSpace4),
|
||||||
|
Text(value,
|
||||||
|
style: kTextBody.copyWith(
|
||||||
|
fontFamily: kMonoFamily, fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _relativeTime(AppLocalizations l, DateTime utc) {
|
||||||
|
final minutes = DateTime.now().toUtc().difference(utc).inMinutes;
|
||||||
|
if (minutes < 60) return l.guideIaKnowledgeMinutesAgo(minutes < 1 ? 1 : minutes);
|
||||||
|
if (minutes < 60 * 24) return l.guideIaKnowledgeHoursAgo(minutes ~/ 60);
|
||||||
|
return l.guideIaKnowledgeDaysAgo(minutes ~/ (60 * 24));
|
||||||
|
}
|
||||||
|
|
||||||
Widget _voiceCard(AppLocalizations l) {
|
Widget _voiceCard(AppLocalizations l) {
|
||||||
final wakeword = _voiceId == kGuideVoiceMarco ? 'Marco' : 'Viva';
|
final wakeword = _voiceId == kGuideVoiceMarco ? 'Marco' : 'Viva';
|
||||||
|
|
||||||
@ -451,3 +850,10 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PreviewTurn {
|
||||||
|
final String question;
|
||||||
|
final String? answer;
|
||||||
|
|
||||||
|
const _PreviewTurn(this.question, this.answer);
|
||||||
|
}
|
||||||
|
|||||||
309
lib/Screens/GuideIa/visitor_questions_tab.dart
Normal file
309
lib/Screens/GuideIa/visitor_questions_tab.dart
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:manager_app/constants.dart';
|
||||||
|
import 'package:manager_app/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
/// Forme des données attendue par l'onglet « Ce que demandent vos visiteurs ».
|
||||||
|
///
|
||||||
|
/// Écrite avant le backend, volontairement : c'est l'affichage qui décide de ce que
|
||||||
|
/// le job de regroupement doit produire, pas l'inverse. L'endpoint à écrire côté
|
||||||
|
/// manager-service doit rendre exactement cette structure, alimentée par la table
|
||||||
|
/// `VisitorQuestion` (déjà en base) et son job de regroupement en thèmes.
|
||||||
|
class GuideIaInsights {
|
||||||
|
final int questions;
|
||||||
|
final int unanswered;
|
||||||
|
final int themes;
|
||||||
|
final int languages;
|
||||||
|
final List<CountedLabel> unansweredQuestions;
|
||||||
|
final List<CountedLabel> topics;
|
||||||
|
final List<CountedLabel> questionLanguages;
|
||||||
|
final List<CountedLabel> citedContents;
|
||||||
|
|
||||||
|
const GuideIaInsights({
|
||||||
|
required this.questions,
|
||||||
|
required this.unanswered,
|
||||||
|
required this.themes,
|
||||||
|
required this.languages,
|
||||||
|
required this.unansweredQuestions,
|
||||||
|
required this.topics,
|
||||||
|
required this.questionLanguages,
|
||||||
|
required this.citedContents,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GuideIaInsights.fromJson(Map<String, dynamic> json) => GuideIaInsights(
|
||||||
|
questions: (json['questions'] as num?)?.toInt() ?? 0,
|
||||||
|
unanswered: (json['unanswered'] as num?)?.toInt() ?? 0,
|
||||||
|
themes: (json['themes'] as num?)?.toInt() ?? 0,
|
||||||
|
languages: (json['languages'] as num?)?.toInt() ?? 0,
|
||||||
|
unansweredQuestions: CountedLabel.listFrom(json['unansweredQuestions']),
|
||||||
|
topics: CountedLabel.listFrom(json['topics']),
|
||||||
|
questionLanguages: CountedLabel.listFrom(json['questionLanguages']),
|
||||||
|
citedContents: CountedLabel.listFrom(json['citedContents']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CountedLabel {
|
||||||
|
final String label;
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
const CountedLabel(this.label, this.count);
|
||||||
|
|
||||||
|
static List<CountedLabel> listFrom(dynamic raw) => (raw as List?)
|
||||||
|
?.map((e) => CountedLabel(
|
||||||
|
(e['label'] ?? '').toString(),
|
||||||
|
(e['count'] as num?)?.toInt() ?? 0,
|
||||||
|
))
|
||||||
|
.toList() ??
|
||||||
|
const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class VisitorQuestionsTab extends StatelessWidget {
|
||||||
|
final GuideIaInsights? insights;
|
||||||
|
|
||||||
|
const VisitorQuestionsTab({super.key, required this.insights});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
|
final data = insights;
|
||||||
|
if (data == null || data.questions == 0) return _empty(l);
|
||||||
|
|
||||||
|
final wide = MediaQuery.of(context).size.width > 1100;
|
||||||
|
|
||||||
|
// Une carte sans données n'est pas une carte vide à afficher : `topics` reste vide
|
||||||
|
// tant que le job de regroupement en thèmes n'a pas tourné, et un cadre avec un titre
|
||||||
|
// et rien dedans se lit comme une panne.
|
||||||
|
final columns = [
|
||||||
|
if (data.topics.isNotEmpty)
|
||||||
|
_barsCard(l.guideIaTopicsTitle, l.guideIaInsightsSubCount, data.topics),
|
||||||
|
if (data.questionLanguages.isNotEmpty)
|
||||||
|
_barsCard(l.guideIaQuestionLanguagesTitle, l.guideIaInsightsSubCount, data.questionLanguages),
|
||||||
|
if (data.citedContents.isNotEmpty)
|
||||||
|
_barsCard(l.guideIaCitedTitle, l.guideIaCitedSub, data.citedContents),
|
||||||
|
];
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_tiles(l, data),
|
||||||
|
const SizedBox(height: kSpace6),
|
||||||
|
if (data.unansweredQuestions.isNotEmpty) ...[
|
||||||
|
_gapCard(l, data),
|
||||||
|
const SizedBox(height: kSpace6),
|
||||||
|
],
|
||||||
|
if (wide)
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (final card in columns) ...[
|
||||||
|
Expanded(child: card),
|
||||||
|
if (card != columns.last) const SizedBox(width: kSpace6),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
for (final card in columns) ...[
|
||||||
|
card,
|
||||||
|
const SizedBox(height: kSpace6),
|
||||||
|
],
|
||||||
|
const SizedBox(height: kSpace6),
|
||||||
|
_privacyNote(l),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _empty(AppLocalizations l) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 64, horizontal: kSpace7),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.forum_outlined, size: 40, color: kInk3),
|
||||||
|
const SizedBox(height: kSpace4),
|
||||||
|
Text(l.guideIaInsightsEmpty, style: kTitleCard, textAlign: TextAlign.center),
|
||||||
|
const SizedBox(height: kSpace2),
|
||||||
|
SizedBox(
|
||||||
|
width: 460,
|
||||||
|
child: Text(l.guideIaInsightsEmptyHint, style: kSubtitleCard, textAlign: TextAlign.center),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _tiles(AppLocalizations l, GuideIaInsights data) {
|
||||||
|
final tiles = [
|
||||||
|
_Tile(l.guideIaKpiQuestions, data.questions.toString(), false),
|
||||||
|
_Tile(l.guideIaKpiUnanswered, data.unanswered.toString(), true),
|
||||||
|
_Tile(l.guideIaKpiThemes, data.themes.toString(), false),
|
||||||
|
_Tile(l.guideIaKpiLanguages, data.languages.toString(), false),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: kLine),
|
||||||
|
borderRadius: BorderRadius.circular(kRadiusCard),
|
||||||
|
color: kSurface,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
for (final tile in tiles)
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 14, 16, 15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: tile == tiles.first ? null : Border(left: BorderSide(color: kLine)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(tile.value,
|
||||||
|
style: kMetricMedium.copyWith(color: tile.warn ? kWarning : kInk)),
|
||||||
|
const SizedBox(height: kSpace1),
|
||||||
|
Text(tile.label.toUpperCase(), style: kOverline),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bordure ambre : c'est le seul bloc de l'écran qui appelle une action du client.
|
||||||
|
/// L'ambre n'est jamais réutilisé comme couleur de série dans les graphes.
|
||||||
|
Widget _gapCard(AppLocalizations l, GuideIaInsights data) {
|
||||||
|
return _shell(
|
||||||
|
borderColor: kWarning,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: const BoxDecoration(color: kWarning, shape: BoxShape.circle),
|
||||||
|
),
|
||||||
|
const SizedBox(width: kSpace3),
|
||||||
|
Expanded(child: Text(l.guideIaGapTitle, style: kTitleCard)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: kSpace1),
|
||||||
|
Text(l.guideIaGapSub(data.unanswered), style: kSubtitleCard),
|
||||||
|
const SizedBox(height: kSpace4),
|
||||||
|
for (final q in data.unansweredQuestions)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 9),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: q == data.unansweredQuestions.first
|
||||||
|
? null
|
||||||
|
: Border(top: BorderSide(color: kLineSoft)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(q.label, style: kTextBody)),
|
||||||
|
const SizedBox(width: kSpace4),
|
||||||
|
Text('${q.count} ×',
|
||||||
|
style: kTextSmall.copyWith(
|
||||||
|
fontFamily: kMonoFamily, color: kWarning)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Une seule mesure par carte, donc une seule teinte : c'est la longueur qui
|
||||||
|
/// porte l'information. Colorer chaque barre suggérerait une distinction qui n'existe pas.
|
||||||
|
Widget _barsCard(String title, String subtitle, List<CountedLabel> rows) {
|
||||||
|
final max = rows.isEmpty ? 1 : rows.map((r) => r.count).reduce((a, b) => a > b ? a : b);
|
||||||
|
|
||||||
|
return _shell(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(title, style: kTitleCard),
|
||||||
|
const SizedBox(height: kSpace1),
|
||||||
|
Text(subtitle, style: kSubtitleCard),
|
||||||
|
const SizedBox(height: kSpace5),
|
||||||
|
for (final row in rows)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 9),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 132,
|
||||||
|
child: Text(row.label, style: kTextSmall, overflow: TextOverflow.ellipsis),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 11),
|
||||||
|
Expanded(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(kRadiusTrack),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: max == 0 ? 0 : row.count / max,
|
||||||
|
minHeight: 9,
|
||||||
|
backgroundColor: kSurface3,
|
||||||
|
color: kBrand,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 11),
|
||||||
|
SizedBox(
|
||||||
|
width: 46,
|
||||||
|
child: Text(row.count.toString(),
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: kTextSmall.copyWith(fontFamily: kMonoFamily)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _privacyNote(AppLocalizations l) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: kSurface2,
|
||||||
|
border: Border.all(color: kLineSoft),
|
||||||
|
borderRadius: BorderRadius.circular(kRadiusInput),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.info_outline, size: 16, color: kBrand),
|
||||||
|
const SizedBox(width: 9),
|
||||||
|
Expanded(child: Text(l.guideIaInsightsPrivacy, style: kSubtitleCard.copyWith(color: kInk2))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _shell({required Widget child, Color? borderColor}) {
|
||||||
|
return Container(
|
||||||
|
padding: kCardPadding,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: kSurface,
|
||||||
|
border: Border.all(color: borderColor ?? kLine),
|
||||||
|
borderRadius: BorderRadius.circular(kRadiusCard),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Tile {
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final bool warn;
|
||||||
|
|
||||||
|
const _Tile(this.label, this.value, this.warn);
|
||||||
|
}
|
||||||
@ -207,6 +207,7 @@ Future<List<ResourceDTO?>?> create(ResourceDTO resourceDTO, List<File>? files, L
|
|||||||
print("Trying to create resource");
|
print("Trying to create resource");
|
||||||
// ADD LOADING
|
// ADD LOADING
|
||||||
resourceDTO.dateCreation = DateTime.now();
|
resourceDTO.dateCreation = DateTime.now();
|
||||||
|
resourceDTO.sizeBytes = platformFile.size;
|
||||||
ResourceDTO? newResource = await managerAppContext.clientAPI!.resourceApi!.resourceCreate(resourceDTO);
|
ResourceDTO? newResource = await managerAppContext.clientAPI!.resourceApi!.resourceCreate(resourceDTO);
|
||||||
print("created resource");
|
print("created resource");
|
||||||
print(newResource);
|
print(newResource);
|
||||||
|
|||||||
@ -59,6 +59,9 @@ class Client {
|
|||||||
OnboardingApi? _onboardingApi;
|
OnboardingApi? _onboardingApi;
|
||||||
OnboardingApi? get onboardingApi => _onboardingApi;
|
OnboardingApi? get onboardingApi => _onboardingApi;
|
||||||
|
|
||||||
|
AIApi? _aiApi;
|
||||||
|
AIApi? get aiApi => _aiApi;
|
||||||
|
|
||||||
Client(String path) {
|
Client(String path) {
|
||||||
_apiClient = ApiClient(basePath: path);
|
_apiClient = ApiClient(basePath: path);
|
||||||
//basePath: "https://192.168.31.140");
|
//basePath: "https://192.168.31.140");
|
||||||
@ -82,5 +85,6 @@ class Client {
|
|||||||
_notificationApi = NotificationApi(_apiClient);
|
_notificationApi = NotificationApi(_apiClient);
|
||||||
_subscriptionPlanApi = SubscriptionPlanApi(_apiClient);
|
_subscriptionPlanApi = SubscriptionPlanApi(_apiClient);
|
||||||
_onboardingApi = OnboardingApi(_apiClient);
|
_onboardingApi = OnboardingApi(_apiClient);
|
||||||
|
_aiApi = AIApi(_apiClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,122 @@ const kSectionLabelStyle = TextStyle(
|
|||||||
color: kTitleTextColor,
|
color: kTitleTextColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Socle visuel — l'échelle typographique et les espacements viennent du CSS des
|
||||||
|
// maquettes validées (DOCS/claude design/guide-ia-screen.html et
|
||||||
|
// statistics-screen.html, tokens identiques dans les deux).
|
||||||
|
//
|
||||||
|
// Les couleurs, elles, restent celles de l'app : un écran neuf doit se fondre
|
||||||
|
// dans manager-app, pas y ouvrir une seconde palette. Seules les valeurs sans
|
||||||
|
// équivalent existant sont reprises des maquettes — les remplissages discrets,
|
||||||
|
// les bordures, le gris atténué et l'ambre d'avertissement.
|
||||||
|
// Pas de thème sombre : l'app n'en a pas.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const kGround = kBackgroundColor;
|
||||||
|
const kSurface = kWhite;
|
||||||
|
const kSurface2 = Color(0xFFF3F5F8);
|
||||||
|
const kSurface3 = Color(0xFFE7EBF1);
|
||||||
|
|
||||||
|
const kInk = kTitleTextColor;
|
||||||
|
const kInk2 = kBodyTextColor;
|
||||||
|
const kInk3 = Color(0xFF7C8B9A);
|
||||||
|
|
||||||
|
const kLine = Color(0xFFD6DDE5);
|
||||||
|
const kLineSoft = Color(0xFFE6EBF1);
|
||||||
|
|
||||||
|
const kBrand = kPrimaryColor;
|
||||||
|
const kBrand2 = Color(0xFF3C6C90);
|
||||||
|
const kBrandSoft = kSecond;
|
||||||
|
const kOnBrand = kWhite;
|
||||||
|
|
||||||
|
const kWarning = Color(0xFF9A6608);
|
||||||
|
const kSerious = kError;
|
||||||
|
const kGood = kSuccess;
|
||||||
|
|
||||||
|
const kMonoFamily = 'monospace';
|
||||||
|
|
||||||
|
const kSpace1 = 4.0;
|
||||||
|
const kSpace2 = 6.0;
|
||||||
|
const kSpace3 = 8.0;
|
||||||
|
const kSpace4 = 12.0;
|
||||||
|
const kSpace5 = 16.0;
|
||||||
|
const kSpace6 = 18.0;
|
||||||
|
const kSpace7 = 24.0;
|
||||||
|
const kSpace8 = 32.0;
|
||||||
|
|
||||||
|
const kRadiusInput = 5.0;
|
||||||
|
const kRadiusCard = 8.0;
|
||||||
|
const kRadiusShell = 10.0;
|
||||||
|
const kRadiusTrack = 4.0;
|
||||||
|
const kRadiusPill = 999.0;
|
||||||
|
|
||||||
|
const kCardPadding = EdgeInsets.fromLTRB(18, 17, 18, 19);
|
||||||
|
const kPagePadding = EdgeInsets.fromLTRB(28, 24, 28, 30);
|
||||||
|
|
||||||
|
const kCardShadow = [kDefaultShadow];
|
||||||
|
|
||||||
|
const kTitleScreen = TextStyle(
|
||||||
|
fontSize: 23,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: -0.41,
|
||||||
|
color: kInk,
|
||||||
|
);
|
||||||
|
const kTitleCard = TextStyle(
|
||||||
|
fontSize: 14.5,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: kInk,
|
||||||
|
);
|
||||||
|
const kSubtitleCard = TextStyle(
|
||||||
|
fontSize: 12.5,
|
||||||
|
color: kInk3,
|
||||||
|
);
|
||||||
|
const kTextBody = TextStyle(
|
||||||
|
fontSize: 13.5,
|
||||||
|
color: kInk,
|
||||||
|
);
|
||||||
|
const kTextSmall = TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: kInk2,
|
||||||
|
);
|
||||||
|
const kLabelField = TextStyle(
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: kInk2,
|
||||||
|
);
|
||||||
|
const kTextHint = TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: kInk3,
|
||||||
|
);
|
||||||
|
const kOverline = TextStyle(
|
||||||
|
fontSize: 11.5,
|
||||||
|
letterSpacing: 0.69,
|
||||||
|
color: kInk3,
|
||||||
|
);
|
||||||
|
const kOverlineMono = TextStyle(
|
||||||
|
fontFamily: kMonoFamily,
|
||||||
|
fontSize: 10.5,
|
||||||
|
letterSpacing: 0.95,
|
||||||
|
color: kInk3,
|
||||||
|
);
|
||||||
|
const kMetricLarge = TextStyle(
|
||||||
|
fontFamily: kMonoFamily,
|
||||||
|
fontSize: 29,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: -0.73,
|
||||||
|
height: 1,
|
||||||
|
color: kInk,
|
||||||
|
fontFeatures: [FontFeature.tabularFigures()],
|
||||||
|
);
|
||||||
|
const kMetricMedium = TextStyle(
|
||||||
|
fontFamily: kMonoFamily,
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: -0.52,
|
||||||
|
color: kInk,
|
||||||
|
fontFeatures: [FontFeature.tabularFigures()],
|
||||||
|
);
|
||||||
|
|
||||||
const List<String> section_types = ["Map", "Slider", "Video", "Web", "Menu", "Quiz", "Article", "PDF", "Game", "Agenda", "Weather", "Event", "Parcours"];
|
const List<String> section_types = ["Map", "Slider", "Video", "Web", "Menu", "Quiz", "Article", "PDF", "Game", "Agenda", "Weather", "Event", "Parcours"];
|
||||||
const List<String> map_types = ["none", "normal", "satellite", "terrain", "hybrid"];
|
const List<String> map_types = ["none", "normal", "satellite", "terrain", "hybrid"];
|
||||||
const List<String> languages = ["FR", "NL", "EN", "DE", "IT", "ES", "CN", "PL", "AR", "UK"];
|
const List<String> languages = ["FR", "NL", "EN", "DE", "IT", "ES", "CN", "PL", "AR", "UK"];
|
||||||
|
|||||||
@ -88,6 +88,39 @@
|
|||||||
"guideIaSave": "Save",
|
"guideIaSave": "Save",
|
||||||
"guideIaSaved": "Guide saved",
|
"guideIaSaved": "Guide saved",
|
||||||
"guideIaSaveError": "Saving failed. Please try again.",
|
"guideIaSaveError": "Saving failed. Please try again.",
|
||||||
|
"guideIaTabConfig": "Configuration",
|
||||||
|
"guideIaTabQuestions": "What your visitors ask",
|
||||||
|
"guideIaPreviewTitle": "Try your guide",
|
||||||
|
"guideIaPreviewSub": "Test the tone and the answers without reaching for a phone",
|
||||||
|
"guideIaPreviewPlaceholder": "Ask a question, the way a visitor would",
|
||||||
|
"guideIaPreviewSources": "Where does this answer come from?",
|
||||||
|
"guideIaPreviewSourcesInfo": "This source indicator only appears on this screen. Your visitors see the answer alone.",
|
||||||
|
"guideIaPreviewError": "The guide did not answer. Try again in a moment.",
|
||||||
|
"guideIaPreviewUnsaved": "Save your changes before testing them: the preview queries the guide as published.",
|
||||||
|
"guideIaKnowledgeTitle": "What your guide knows",
|
||||||
|
"guideIaKnowledgeSub": "Measured on what is actually indexed, not on what is published",
|
||||||
|
"guideIaKnowledgeSections": "Indexed sections",
|
||||||
|
"guideIaKnowledgeChunks": "Content chunks",
|
||||||
|
"guideIaKnowledgeLanguages": "Languages covered",
|
||||||
|
"guideIaKnowledgeLastIndexed": "Last indexed",
|
||||||
|
"guideIaKnowledgeHint": "An unpublished section drops out of the guide's answers within the minute.",
|
||||||
|
"guideIaKnowledgeMinutesAgo": "{count} min ago",
|
||||||
|
"guideIaKnowledgeHoursAgo": "{count} h ago",
|
||||||
|
"guideIaKnowledgeDaysAgo": "{count} d ago",
|
||||||
|
"guideIaKpiQuestions": "Questions this month",
|
||||||
|
"guideIaKpiUnanswered": "Unanswered",
|
||||||
|
"guideIaKpiThemes": "Themes",
|
||||||
|
"guideIaKpiLanguages": "Languages",
|
||||||
|
"guideIaGapTitle": "What your visitors look for and don't find",
|
||||||
|
"guideIaGapSub": "{count} questions found no answer in your content. Add it and your guide will answer from the next visit on.",
|
||||||
|
"guideIaTopicsTitle": "Question topics",
|
||||||
|
"guideIaQuestionLanguagesTitle": "Question languages",
|
||||||
|
"guideIaInsightsSubCount": "Number of questions over the period",
|
||||||
|
"guideIaCitedTitle": "Most used content",
|
||||||
|
"guideIaCitedSub": "Sections cited in answers",
|
||||||
|
"guideIaInsightsEmpty": "No questions yet",
|
||||||
|
"guideIaInsightsEmptyHint": "As soon as your visitors start asking the guide, their questions will be grouped here by topic — and the ones left unanswered will tell you what to add to your content.",
|
||||||
|
"guideIaInsightsPrivacy": "Questions are kept for 90 days, then deleted. Only the topic groupings are kept beyond that. No data identifying a visitor is recorded.",
|
||||||
"menuNotifications": "Notifications",
|
"menuNotifications": "Notifications",
|
||||||
"menuUsers": "Users",
|
"menuUsers": "Users",
|
||||||
"menuApiKeys": "API Keys",
|
"menuApiKeys": "API Keys",
|
||||||
|
|||||||
@ -88,6 +88,39 @@
|
|||||||
"guideIaSave": "Enregistrer",
|
"guideIaSave": "Enregistrer",
|
||||||
"guideIaSaved": "Guide enregistré",
|
"guideIaSaved": "Guide enregistré",
|
||||||
"guideIaSaveError": "L'enregistrement a échoué. Réessayez.",
|
"guideIaSaveError": "L'enregistrement a échoué. Réessayez.",
|
||||||
|
"guideIaTabConfig": "Configuration",
|
||||||
|
"guideIaTabQuestions": "Ce que demandent vos visiteurs",
|
||||||
|
"guideIaPreviewTitle": "Essayer votre guide",
|
||||||
|
"guideIaPreviewSub": "Testez le ton et les réponses sans passer par un téléphone",
|
||||||
|
"guideIaPreviewPlaceholder": "Posez une question, comme le ferait un visiteur",
|
||||||
|
"guideIaPreviewSources": "D'où vient cette réponse ?",
|
||||||
|
"guideIaPreviewSourcesInfo": "Cet indicateur de sources n'apparaît que sur cet écran. Vos visiteurs, eux, ne voient que la réponse.",
|
||||||
|
"guideIaPreviewError": "Le guide n'a pas répondu. Réessayez dans un instant.",
|
||||||
|
"guideIaPreviewUnsaved": "Enregistrez vos modifications pour les essayer : l'aperçu interroge le guide tel qu'il est publié.",
|
||||||
|
"guideIaKnowledgeTitle": "Ce que connaît votre guide",
|
||||||
|
"guideIaKnowledgeSub": "Mesuré sur ce qui est réellement indexé, pas sur ce qui est publié",
|
||||||
|
"guideIaKnowledgeSections": "Sections indexées",
|
||||||
|
"guideIaKnowledgeChunks": "Morceaux de contenu",
|
||||||
|
"guideIaKnowledgeLanguages": "Langues couvertes",
|
||||||
|
"guideIaKnowledgeLastIndexed": "Dernière indexation",
|
||||||
|
"guideIaKnowledgeHint": "Une section dépubliée disparaît des réponses du guide dans la minute.",
|
||||||
|
"guideIaKnowledgeMinutesAgo": "il y a {count} min",
|
||||||
|
"guideIaKnowledgeHoursAgo": "il y a {count} h",
|
||||||
|
"guideIaKnowledgeDaysAgo": "il y a {count} j",
|
||||||
|
"guideIaKpiQuestions": "Questions ce mois",
|
||||||
|
"guideIaKpiUnanswered": "Sans réponse",
|
||||||
|
"guideIaKpiThemes": "Thèmes",
|
||||||
|
"guideIaKpiLanguages": "Langues",
|
||||||
|
"guideIaGapTitle": "Ce que vos visiteurs cherchent sans le trouver",
|
||||||
|
"guideIaGapSub": "{count} questions n'ont trouvé aucune réponse dans vos contenus. Ajoutez-les et votre guide saura répondre dès la prochaine visite.",
|
||||||
|
"guideIaTopicsTitle": "Sujets des questions",
|
||||||
|
"guideIaQuestionLanguagesTitle": "Langues des questions",
|
||||||
|
"guideIaInsightsSubCount": "Nombre de questions sur la période",
|
||||||
|
"guideIaCitedTitle": "Contenus les plus utilisés",
|
||||||
|
"guideIaCitedSub": "Sections citées dans les réponses",
|
||||||
|
"guideIaInsightsEmpty": "Aucune question pour l'instant",
|
||||||
|
"guideIaInsightsEmptyHint": "Dès que vos visiteurs interrogeront le guide, leurs questions seront regroupées ici par sujet — et celles restées sans réponse vous diront quoi ajouter à vos contenus.",
|
||||||
|
"guideIaInsightsPrivacy": "Les questions sont conservées 90 jours puis supprimées. Seuls les regroupements par sujet sont gardés au-delà. Aucune donnée permettant d'identifier un visiteur n'est enregistrée.",
|
||||||
"menuNotifications": "Notifications",
|
"menuNotifications": "Notifications",
|
||||||
"menuUsers": "Utilisateurs",
|
"menuUsers": "Utilisateurs",
|
||||||
"menuApiKeys": "Clés API",
|
"menuApiKeys": "Clés API",
|
||||||
|
|||||||
@ -598,6 +598,204 @@ abstract class AppLocalizations {
|
|||||||
/// **'L\'enregistrement a échoué. Réessayez.'**
|
/// **'L\'enregistrement a échoué. Réessayez.'**
|
||||||
String get guideIaSaveError;
|
String get guideIaSaveError;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaTabConfig.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Configuration'**
|
||||||
|
String get guideIaTabConfig;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaTabQuestions.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Ce que demandent vos visiteurs'**
|
||||||
|
String get guideIaTabQuestions;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaPreviewTitle.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Essayer votre guide'**
|
||||||
|
String get guideIaPreviewTitle;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaPreviewSub.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Testez le ton et les réponses sans passer par un téléphone'**
|
||||||
|
String get guideIaPreviewSub;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaPreviewPlaceholder.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Posez une question, comme le ferait un visiteur'**
|
||||||
|
String get guideIaPreviewPlaceholder;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaPreviewSources.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'D\'où vient cette réponse ?'**
|
||||||
|
String get guideIaPreviewSources;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaPreviewSourcesInfo.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Cet indicateur de sources n\'apparaît que sur cet écran. Vos visiteurs, eux, ne voient que la réponse.'**
|
||||||
|
String get guideIaPreviewSourcesInfo;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaPreviewError.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Le guide n\'a pas répondu. Réessayez dans un instant.'**
|
||||||
|
String get guideIaPreviewError;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaPreviewUnsaved.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Enregistrez vos modifications pour les essayer : l\'aperçu interroge le guide tel qu\'il est publié.'**
|
||||||
|
String get guideIaPreviewUnsaved;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKnowledgeTitle.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Ce que connaît votre guide'**
|
||||||
|
String get guideIaKnowledgeTitle;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKnowledgeSub.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Mesuré sur ce qui est réellement indexé, pas sur ce qui est publié'**
|
||||||
|
String get guideIaKnowledgeSub;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKnowledgeSections.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Sections indexées'**
|
||||||
|
String get guideIaKnowledgeSections;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKnowledgeChunks.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Morceaux de contenu'**
|
||||||
|
String get guideIaKnowledgeChunks;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKnowledgeLanguages.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Langues couvertes'**
|
||||||
|
String get guideIaKnowledgeLanguages;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKnowledgeLastIndexed.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Dernière indexation'**
|
||||||
|
String get guideIaKnowledgeLastIndexed;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKnowledgeHint.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Une section dépubliée disparaît des réponses du guide dans la minute.'**
|
||||||
|
String get guideIaKnowledgeHint;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKnowledgeMinutesAgo.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'il y a {count} min'**
|
||||||
|
String guideIaKnowledgeMinutesAgo(Object count);
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKnowledgeHoursAgo.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'il y a {count} h'**
|
||||||
|
String guideIaKnowledgeHoursAgo(Object count);
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKnowledgeDaysAgo.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'il y a {count} j'**
|
||||||
|
String guideIaKnowledgeDaysAgo(Object count);
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKpiQuestions.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Questions ce mois'**
|
||||||
|
String get guideIaKpiQuestions;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKpiUnanswered.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Sans réponse'**
|
||||||
|
String get guideIaKpiUnanswered;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKpiThemes.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Thèmes'**
|
||||||
|
String get guideIaKpiThemes;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaKpiLanguages.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Langues'**
|
||||||
|
String get guideIaKpiLanguages;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaGapTitle.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Ce que vos visiteurs cherchent sans le trouver'**
|
||||||
|
String get guideIaGapTitle;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaGapSub.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'{count} questions n\'ont trouvé aucune réponse dans vos contenus. Ajoutez-les et votre guide saura répondre dès la prochaine visite.'**
|
||||||
|
String guideIaGapSub(Object count);
|
||||||
|
|
||||||
|
/// No description provided for @guideIaTopicsTitle.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Sujets des questions'**
|
||||||
|
String get guideIaTopicsTitle;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaQuestionLanguagesTitle.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Langues des questions'**
|
||||||
|
String get guideIaQuestionLanguagesTitle;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaInsightsSubCount.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Nombre de questions sur la période'**
|
||||||
|
String get guideIaInsightsSubCount;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaCitedTitle.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Contenus les plus utilisés'**
|
||||||
|
String get guideIaCitedTitle;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaCitedSub.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Sections citées dans les réponses'**
|
||||||
|
String get guideIaCitedSub;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaInsightsEmpty.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Aucune question pour l\'instant'**
|
||||||
|
String get guideIaInsightsEmpty;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaInsightsEmptyHint.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Dès que vos visiteurs interrogeront le guide, leurs questions seront regroupées ici par sujet — et celles restées sans réponse vous diront quoi ajouter à vos contenus.'**
|
||||||
|
String get guideIaInsightsEmptyHint;
|
||||||
|
|
||||||
|
/// No description provided for @guideIaInsightsPrivacy.
|
||||||
|
///
|
||||||
|
/// In fr, this message translates to:
|
||||||
|
/// **'Les questions sont conservées 90 jours puis supprimées. Seuls les regroupements par sujet sont gardés au-delà. Aucune donnée permettant d\'identifier un visiteur n\'est enregistrée.'**
|
||||||
|
String get guideIaInsightsPrivacy;
|
||||||
|
|
||||||
/// No description provided for @menuNotifications.
|
/// No description provided for @menuNotifications.
|
||||||
///
|
///
|
||||||
/// In fr, this message translates to:
|
/// In fr, this message translates to:
|
||||||
|
|||||||
@ -282,6 +282,122 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get guideIaSaveError => 'Saving failed. Please try again.';
|
String get guideIaSaveError => 'Saving failed. Please try again.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaTabConfig => 'Configuration';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaTabQuestions => 'What your visitors ask';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewTitle => 'Try your guide';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewSub =>
|
||||||
|
'Test the tone and the answers without reaching for a phone';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewPlaceholder =>
|
||||||
|
'Ask a question, the way a visitor would';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewSources => 'Where does this answer come from?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewSourcesInfo =>
|
||||||
|
'This source indicator only appears on this screen. Your visitors see the answer alone.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewError =>
|
||||||
|
'The guide did not answer. Try again in a moment.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewUnsaved =>
|
||||||
|
'Save your changes before testing them: the preview queries the guide as published.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeTitle => 'What your guide knows';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeSub =>
|
||||||
|
'Measured on what is actually indexed, not on what is published';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeSections => 'Indexed sections';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeChunks => 'Content chunks';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeLanguages => 'Languages covered';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeLastIndexed => 'Last indexed';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeHint =>
|
||||||
|
'An unpublished section drops out of the guide\'s answers within the minute.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String guideIaKnowledgeMinutesAgo(Object count) {
|
||||||
|
return '$count min ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String guideIaKnowledgeHoursAgo(Object count) {
|
||||||
|
return '$count h ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String guideIaKnowledgeDaysAgo(Object count) {
|
||||||
|
return '$count d ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKpiQuestions => 'Questions this month';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKpiUnanswered => 'Unanswered';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKpiThemes => 'Themes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKpiLanguages => 'Languages';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaGapTitle => 'What your visitors look for and don\'t find';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String guideIaGapSub(Object count) {
|
||||||
|
return '$count questions found no answer in your content. Add it and your guide will answer from the next visit on.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaTopicsTitle => 'Question topics';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaQuestionLanguagesTitle => 'Question languages';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaInsightsSubCount => 'Number of questions over the period';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaCitedTitle => 'Most used content';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaCitedSub => 'Sections cited in answers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaInsightsEmpty => 'No questions yet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaInsightsEmptyHint =>
|
||||||
|
'As soon as your visitors start asking the guide, their questions will be grouped here by topic — and the ones left unanswered will tell you what to add to your content.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaInsightsPrivacy =>
|
||||||
|
'Questions are kept for 90 days, then deleted. Only the topic groupings are kept beyond that. No data identifying a visitor is recorded.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get menuNotifications => 'Notifications';
|
String get menuNotifications => 'Notifications';
|
||||||
|
|
||||||
|
|||||||
@ -286,6 +286,123 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get guideIaSaveError => 'L\'enregistrement a échoué. Réessayez.';
|
String get guideIaSaveError => 'L\'enregistrement a échoué. Réessayez.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaTabConfig => 'Configuration';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaTabQuestions => 'Ce que demandent vos visiteurs';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewTitle => 'Essayer votre guide';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewSub =>
|
||||||
|
'Testez le ton et les réponses sans passer par un téléphone';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewPlaceholder =>
|
||||||
|
'Posez une question, comme le ferait un visiteur';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewSources => 'D\'où vient cette réponse ?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewSourcesInfo =>
|
||||||
|
'Cet indicateur de sources n\'apparaît que sur cet écran. Vos visiteurs, eux, ne voient que la réponse.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewError =>
|
||||||
|
'Le guide n\'a pas répondu. Réessayez dans un instant.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewUnsaved =>
|
||||||
|
'Enregistrez vos modifications pour les essayer : l\'aperçu interroge le guide tel qu\'il est publié.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeTitle => 'Ce que connaît votre guide';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeSub =>
|
||||||
|
'Mesuré sur ce qui est réellement indexé, pas sur ce qui est publié';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeSections => 'Sections indexées';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeChunks => 'Morceaux de contenu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeLanguages => 'Langues couvertes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeLastIndexed => 'Dernière indexation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeHint =>
|
||||||
|
'Une section dépubliée disparaît des réponses du guide dans la minute.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String guideIaKnowledgeMinutesAgo(Object count) {
|
||||||
|
return 'il y a $count min';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String guideIaKnowledgeHoursAgo(Object count) {
|
||||||
|
return 'il y a $count h';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String guideIaKnowledgeDaysAgo(Object count) {
|
||||||
|
return 'il y a $count j';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKpiQuestions => 'Questions ce mois';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKpiUnanswered => 'Sans réponse';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKpiThemes => 'Thèmes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKpiLanguages => 'Langues';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaGapTitle =>
|
||||||
|
'Ce que vos visiteurs cherchent sans le trouver';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String guideIaGapSub(Object count) {
|
||||||
|
return '$count questions n\'ont trouvé aucune réponse dans vos contenus. Ajoutez-les et votre guide saura répondre dès la prochaine visite.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaTopicsTitle => 'Sujets des questions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaQuestionLanguagesTitle => 'Langues des questions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaInsightsSubCount => 'Nombre de questions sur la période';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaCitedTitle => 'Contenus les plus utilisés';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaCitedSub => 'Sections citées dans les réponses';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaInsightsEmpty => 'Aucune question pour l\'instant';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaInsightsEmptyHint =>
|
||||||
|
'Dès que vos visiteurs interrogeront le guide, leurs questions seront regroupées ici par sujet — et celles restées sans réponse vous diront quoi ajouter à vos contenus.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaInsightsPrivacy =>
|
||||||
|
'Les questions sont conservées 90 jours puis supprimées. Seuls les regroupements par sujet sont gardés au-delà. Aucune donnée permettant d\'identifier un visiteur n\'est enregistrée.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get menuNotifications => 'Notifications';
|
String get menuNotifications => 'Notifications';
|
||||||
|
|
||||||
|
|||||||
@ -286,6 +286,122 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get guideIaSaveError => 'Opslaan is mislukt. Probeer opnieuw.';
|
String get guideIaSaveError => 'Opslaan is mislukt. Probeer opnieuw.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaTabConfig => 'Configuratie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaTabQuestions => 'Wat uw bezoekers vragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewTitle => 'Uw gids uitproberen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewSub =>
|
||||||
|
'Test de toon en de antwoorden zonder een telefoon te nemen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewPlaceholder =>
|
||||||
|
'Stel een vraag, zoals een bezoeker dat zou doen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewSources => 'Waar komt dit antwoord vandaan?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewSourcesInfo =>
|
||||||
|
'Deze bronvermelding verschijnt alleen op dit scherm. Uw bezoekers zien enkel het antwoord.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewError =>
|
||||||
|
'De gids heeft niet geantwoord. Probeer het zo meteen opnieuw.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaPreviewUnsaved =>
|
||||||
|
'Bewaar uw wijzigingen om ze te testen: het voorbeeld bevraagt de gids zoals hij gepubliceerd is.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeTitle => 'Wat uw gids kent';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeSub =>
|
||||||
|
'Gemeten op wat werkelijk geïndexeerd is, niet op wat gepubliceerd is';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeSections => 'Geïndexeerde secties';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeChunks => 'Inhoudsfragmenten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeLanguages => 'Gedekte talen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeLastIndexed => 'Laatste indexering';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKnowledgeHint =>
|
||||||
|
'Een gedepubliceerde sectie verdwijnt binnen de minuut uit de antwoorden van de gids.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String guideIaKnowledgeMinutesAgo(Object count) {
|
||||||
|
return '$count min geleden';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String guideIaKnowledgeHoursAgo(Object count) {
|
||||||
|
return '$count u geleden';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String guideIaKnowledgeDaysAgo(Object count) {
|
||||||
|
return '$count d geleden';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKpiQuestions => 'Vragen deze maand';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKpiUnanswered => 'Zonder antwoord';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKpiThemes => 'Thema\'s';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaKpiLanguages => 'Talen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaGapTitle => 'Wat uw bezoekers zoeken zonder het te vinden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String guideIaGapSub(Object count) {
|
||||||
|
return '$count vragen vonden geen antwoord in uw inhoud. Voeg ze toe en uw gids antwoordt vanaf het volgende bezoek.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaTopicsTitle => 'Onderwerpen van de vragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaQuestionLanguagesTitle => 'Talen van de vragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaInsightsSubCount => 'Aantal vragen in de periode';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaCitedTitle => 'Meest gebruikte inhoud';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaCitedSub => 'Secties die in de antwoorden worden geciteerd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaInsightsEmpty => 'Nog geen vragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaInsightsEmptyHint =>
|
||||||
|
'Zodra uw bezoekers de gids bevragen, worden hun vragen hier per onderwerp gegroepeerd — en de onbeantwoorde vragen tonen u wat u aan uw inhoud moet toevoegen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guideIaInsightsPrivacy =>
|
||||||
|
'Vragen worden 90 dagen bewaard en daarna verwijderd. Alleen de groeperingen per onderwerp blijven langer bewaard. Er worden geen gegevens opgeslagen waarmee een bezoeker kan worden geïdentificeerd.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get menuNotifications => 'Meldingen';
|
String get menuNotifications => 'Meldingen';
|
||||||
|
|
||||||
|
|||||||
@ -88,6 +88,39 @@
|
|||||||
"guideIaSave": "Opslaan",
|
"guideIaSave": "Opslaan",
|
||||||
"guideIaSaved": "Gids opgeslagen",
|
"guideIaSaved": "Gids opgeslagen",
|
||||||
"guideIaSaveError": "Opslaan is mislukt. Probeer opnieuw.",
|
"guideIaSaveError": "Opslaan is mislukt. Probeer opnieuw.",
|
||||||
|
"guideIaTabConfig": "Configuratie",
|
||||||
|
"guideIaTabQuestions": "Wat uw bezoekers vragen",
|
||||||
|
"guideIaPreviewTitle": "Uw gids uitproberen",
|
||||||
|
"guideIaPreviewSub": "Test de toon en de antwoorden zonder een telefoon te nemen",
|
||||||
|
"guideIaPreviewPlaceholder": "Stel een vraag, zoals een bezoeker dat zou doen",
|
||||||
|
"guideIaPreviewSources": "Waar komt dit antwoord vandaan?",
|
||||||
|
"guideIaPreviewSourcesInfo": "Deze bronvermelding verschijnt alleen op dit scherm. Uw bezoekers zien enkel het antwoord.",
|
||||||
|
"guideIaPreviewError": "De gids heeft niet geantwoord. Probeer het zo meteen opnieuw.",
|
||||||
|
"guideIaPreviewUnsaved": "Bewaar uw wijzigingen om ze te testen: het voorbeeld bevraagt de gids zoals hij gepubliceerd is.",
|
||||||
|
"guideIaKnowledgeTitle": "Wat uw gids kent",
|
||||||
|
"guideIaKnowledgeSub": "Gemeten op wat werkelijk geïndexeerd is, niet op wat gepubliceerd is",
|
||||||
|
"guideIaKnowledgeSections": "Geïndexeerde secties",
|
||||||
|
"guideIaKnowledgeChunks": "Inhoudsfragmenten",
|
||||||
|
"guideIaKnowledgeLanguages": "Gedekte talen",
|
||||||
|
"guideIaKnowledgeLastIndexed": "Laatste indexering",
|
||||||
|
"guideIaKnowledgeHint": "Een gedepubliceerde sectie verdwijnt binnen de minuut uit de antwoorden van de gids.",
|
||||||
|
"guideIaKnowledgeMinutesAgo": "{count} min geleden",
|
||||||
|
"guideIaKnowledgeHoursAgo": "{count} u geleden",
|
||||||
|
"guideIaKnowledgeDaysAgo": "{count} d geleden",
|
||||||
|
"guideIaKpiQuestions": "Vragen deze maand",
|
||||||
|
"guideIaKpiUnanswered": "Zonder antwoord",
|
||||||
|
"guideIaKpiThemes": "Thema's",
|
||||||
|
"guideIaKpiLanguages": "Talen",
|
||||||
|
"guideIaGapTitle": "Wat uw bezoekers zoeken zonder het te vinden",
|
||||||
|
"guideIaGapSub": "{count} vragen vonden geen antwoord in uw inhoud. Voeg ze toe en uw gids antwoordt vanaf het volgende bezoek.",
|
||||||
|
"guideIaTopicsTitle": "Onderwerpen van de vragen",
|
||||||
|
"guideIaQuestionLanguagesTitle": "Talen van de vragen",
|
||||||
|
"guideIaInsightsSubCount": "Aantal vragen in de periode",
|
||||||
|
"guideIaCitedTitle": "Meest gebruikte inhoud",
|
||||||
|
"guideIaCitedSub": "Secties die in de antwoorden worden geciteerd",
|
||||||
|
"guideIaInsightsEmpty": "Nog geen vragen",
|
||||||
|
"guideIaInsightsEmptyHint": "Zodra uw bezoekers de gids bevragen, worden hun vragen hier per onderwerp gegroepeerd — en de onbeantwoorde vragen tonen u wat u aan uw inhoud moet toevoegen.",
|
||||||
|
"guideIaInsightsPrivacy": "Vragen worden 90 dagen bewaard en daarna verwijderd. Alleen de groeperingen per onderwerp blijven langer bewaard. Er worden geen gegevens opgeslagen waarmee een bezoeker kan worden geïdentificeerd.",
|
||||||
"menuNotifications": "Meldingen",
|
"menuNotifications": "Meldingen",
|
||||||
"menuUsers": "Gebruikers",
|
"menuUsers": "Gebruikers",
|
||||||
"menuApiKeys": "API-sleutels",
|
"menuApiKeys": "API-sleutels",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user