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:http/http.dart' as http;
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:manager_app/app_context.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/GuideIa/visitor_questions_tab.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// 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;
|
||||
bool _loading = true;
|
||||
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
|
||||
void initState() {
|
||||
@ -38,6 +53,7 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_personaController.dispose();
|
||||
_previewController.dispose();
|
||||
for (final c in _fallbackControllers) {
|
||||
c.dispose();
|
||||
}
|
||||
@ -80,11 +96,48 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
||||
_instance = instance;
|
||||
_loading = false;
|
||||
});
|
||||
_loadKnowledge();
|
||||
_loadInsights();
|
||||
} catch (_) {
|
||||
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 {
|
||||
final instance = _instance;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
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 left = Column(
|
||||
@ -136,25 +303,30 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
||||
_usageCard(l),
|
||||
const SizedBox(height: 16),
|
||||
_channelsCard(l),
|
||||
const SizedBox(height: 16),
|
||||
_identityCard(l),
|
||||
],
|
||||
);
|
||||
final right = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_identityCard(l),
|
||||
_previewCard(l),
|
||||
const SizedBox(height: 16),
|
||||
_voiceCard(l),
|
||||
if (_knowledge != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
_knowledgeCard(l),
|
||||
],
|
||||
if (_managerContext.role == UserRole.SuperAdmin) ...[
|
||||
const SizedBox(height: 16),
|
||||
_reindexCard(),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_header(l),
|
||||
const SizedBox(height: 20),
|
||||
if (wide)
|
||||
Row(
|
||||
child: wide
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: left),
|
||||
@ -162,13 +334,10 @@ class _GuideIaScreenState extends State<GuideIaScreen> {
|
||||
Expanded(child: right),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
left,
|
||||
const SizedBox(height: 16),
|
||||
right,
|
||||
],
|
||||
],
|
||||
),
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [left, const SizedBox(height: 16), 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) {
|
||||
final used = _instance?.aiTokensThisMonth ?? 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) {
|
||||
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");
|
||||
// ADD LOADING
|
||||
resourceDTO.dateCreation = DateTime.now();
|
||||
resourceDTO.sizeBytes = platformFile.size;
|
||||
ResourceDTO? newResource = await managerAppContext.clientAPI!.resourceApi!.resourceCreate(resourceDTO);
|
||||
print("created resource");
|
||||
print(newResource);
|
||||
|
||||
@ -59,6 +59,9 @@ class Client {
|
||||
OnboardingApi? _onboardingApi;
|
||||
OnboardingApi? get onboardingApi => _onboardingApi;
|
||||
|
||||
AIApi? _aiApi;
|
||||
AIApi? get aiApi => _aiApi;
|
||||
|
||||
Client(String path) {
|
||||
_apiClient = ApiClient(basePath: path);
|
||||
//basePath: "https://192.168.31.140");
|
||||
@ -82,5 +85,6 @@ class Client {
|
||||
_notificationApi = NotificationApi(_apiClient);
|
||||
_subscriptionPlanApi = SubscriptionPlanApi(_apiClient);
|
||||
_onboardingApi = OnboardingApi(_apiClient);
|
||||
_aiApi = AIApi(_apiClient);
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,6 +44,122 @@ const kSectionLabelStyle = TextStyle(
|
||||
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> map_types = ["none", "normal", "satellite", "terrain", "hybrid"];
|
||||
const List<String> languages = ["FR", "NL", "EN", "DE", "IT", "ES", "CN", "PL", "AR", "UK"];
|
||||
|
||||
@ -88,6 +88,39 @@
|
||||
"guideIaSave": "Save",
|
||||
"guideIaSaved": "Guide saved",
|
||||
"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",
|
||||
"menuUsers": "Users",
|
||||
"menuApiKeys": "API Keys",
|
||||
|
||||
@ -88,6 +88,39 @@
|
||||
"guideIaSave": "Enregistrer",
|
||||
"guideIaSaved": "Guide enregistré",
|
||||
"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",
|
||||
"menuUsers": "Utilisateurs",
|
||||
"menuApiKeys": "Clés API",
|
||||
|
||||
@ -598,6 +598,204 @@ abstract class AppLocalizations {
|
||||
/// **'L\'enregistrement a échoué. Réessayez.'**
|
||||
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.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
|
||||
@ -282,6 +282,122 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get menuNotifications => 'Notifications';
|
||||
|
||||
|
||||
@ -286,6 +286,123 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get menuNotifications => 'Notifications';
|
||||
|
||||
|
||||
@ -286,6 +286,122 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get menuNotifications => 'Meldingen';
|
||||
|
||||
|
||||
@ -88,6 +88,39 @@
|
||||
"guideIaSave": "Opslaan",
|
||||
"guideIaSaved": "Gids opgeslagen",
|
||||
"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",
|
||||
"menuUsers": "Gebruikers",
|
||||
"menuApiKeys": "API-sleutels",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user