From 9a46b443b637d10fb4e0a1c867fca78034bf64c3 Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Fri, 13 Mar 2026 15:09:23 +0100 Subject: [PATCH] Add stats and ia assistant (to be tested!) --- android/app/build.gradle | 8 +- android/gradle.properties | 5 +- lib/Components/assistant_chat_view.dart | 325 ++++++++++++++++++ lib/Models/AssistantResponse.dart | 56 +++ lib/Models/tabletContext.dart | 3 + lib/Screens/Agenda/agenda_view.dart | 5 + lib/Screens/MainView/main_view.dart | 75 +++- lib/Screens/MainView/section_page_detail.dart | 30 ++ lib/Screens/Map/google_map_view.dart | 8 + lib/Screens/Menu/menu_view.dart | 5 + lib/Screens/Puzzle/puzzle_view.dart | 7 + lib/Screens/Quizz/quizz_view.dart | 6 + lib/Services/assistantService.dart | 43 +++ lib/Services/statisticsService.dart | 52 +++ lib/client.dart | 8 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 +- .../ephemeral/Flutter-Generated.xcconfig | 12 + .../ephemeral/flutter_export_environment.sh | 13 + pubspec.yaml | 2 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 21 files changed, 665 insertions(+), 6 deletions(-) create mode 100644 lib/Components/assistant_chat_view.dart create mode 100644 lib/Models/AssistantResponse.dart create mode 100644 lib/Services/assistantService.dart create mode 100644 lib/Services/statisticsService.dart create mode 100644 macos/Flutter/ephemeral/Flutter-Generated.xcconfig create mode 100644 macos/Flutter/ephemeral/flutter_export_environment.sh diff --git a/android/app/build.gradle b/android/app/build.gradle index f288817..2600b7a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -42,8 +42,8 @@ apply plugin: 'com.google.gms.google-services'*/ android { namespace "be.unov.myinfomate.tablet" - - compileSdkVersion 34 + compileSdkVersion 36 + ndkVersion "27.0.12077973" compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -77,6 +77,10 @@ android { versionCode flutterVersionCode.toInteger() versionName flutterVersionName multiDexEnabled true + + ndk { + abiFilters "arm64-v8a" + } } signingConfigs { diff --git a/android/gradle.properties b/android/gradle.properties index a8c8d76..a6c1a05 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -2,4 +2,7 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true android.enableR8=true -SDK_REGISTRY_TOKEN=sk.eyJ1IjoidGZyYW5zb2xldCIsImEiOiJjbHRpcTF1d28wYmxmMmxwNjRleXpodGY0In0.qr-jo1vAb08bL3WRDEB4pw \ No newline at end of file +SDK_REGISTRY_TOKEN=sk.eyJ1IjoidGZyYW5zb2xldCIsImEiOiJjbHRpcTF1d28wYmxmMmxwNjRleXpodGY0In0.qr-jo1vAb08bL3WRDEB4pw +android.bundle.enableUncompressedNativeLibs=false +android.experimental.enable16kApk=true +android.useNewNativePlugin=true \ No newline at end of file diff --git a/lib/Components/assistant_chat_view.dart b/lib/Components/assistant_chat_view.dart new file mode 100644 index 0000000..fbccc3a --- /dev/null +++ b/lib/Components/assistant_chat_view.dart @@ -0,0 +1,325 @@ +import 'package:flutter/material.dart'; +import 'package:tablet_app/Models/AssistantResponse.dart'; +import 'package:tablet_app/Models/tabletContext.dart'; +import 'package:tablet_app/Services/assistantService.dart'; +import 'package:tablet_app/constants.dart'; + +class AssistantChatView extends StatefulWidget { + final TabletAppContext tabletAppContext; + final String? configurationId; + final void Function(String sectionId, String sectionTitle)? onNavigateToSection; + + const AssistantChatView({ + Key? key, + required this.tabletAppContext, + this.configurationId, + this.onNavigateToSection, + }) : super(key: key); + + @override + State createState() => _AssistantChatViewState(); +} + +class _AssistantChatViewState extends State { + late AssistantService _assistantService; + final TextEditingController _controller = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + final List _bubbles = []; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _assistantService = AssistantService(tabletAppContext: widget.tabletAppContext); + } + + Future _send() async { + final text = _controller.text.trim(); + if (text.isEmpty || _isLoading) return; + + _controller.clear(); + setState(() { + _bubbles.add(_ChatBubble(text: text, isUser: true)); + _isLoading = true; + }); + _scrollToBottom(); + + try { + final response = await _assistantService.chat( + message: text, + configurationId: widget.configurationId, + ); + setState(() { + _bubbles.add(_AssistantMessage( + response: response, + onNavigate: widget.onNavigateToSection, + )); + }); + } catch (_) { + setState(() { + _bubbles.add(_ChatBubble(text: "Une erreur est survenue, réessayez.", isUser: false)); + }); + } finally { + setState(() => _isLoading = false); + _scrollToBottom(); + } + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + @override + Widget build(BuildContext context) { + return Container( + width: 480, + height: double.infinity, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.horizontal(left: Radius.circular(20)), + boxShadow: [ + BoxShadow(color: Colors.black26, blurRadius: 12, offset: Offset(-4, 0)), + ], + ), + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + decoration: BoxDecoration( + color: kMainGrey, + borderRadius: const BorderRadius.only(topLeft: Radius.circular(20)), + ), + child: Row( + children: [ + const Icon(Icons.chat_bubble_outline, color: Colors.white, size: 22), + const SizedBox(width: 10), + const Expanded( + child: Text( + "Assistant", + style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w600), + ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + // Messages + Expanded( + child: _bubbles.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Bonjour ! Posez-moi vos questions sur cette visite.", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey[500], fontSize: 18), + ), + ), + ) + : ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + itemCount: _bubbles.length, + itemBuilder: (_, i) => _bubbles[i], + ), + ), + // Loading + if (_isLoading) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Row( + children: [ + SizedBox( + width: 22, height: 22, + child: CircularProgressIndicator(strokeWidth: 2, color: kMainGrey), + ), + const SizedBox(width: 10), + Text("...", style: TextStyle(color: Colors.grey[400], fontSize: 16)), + ], + ), + ), + // Input + Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + textCapitalization: TextCapitalization.sentences, + style: const TextStyle(fontSize: 16), + decoration: InputDecoration( + hintText: "Votre question...", + filled: true, + fillColor: Colors.grey[100], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), + ), + onSubmitted: (_) => _send(), + ), + ), + const SizedBox(width: 10), + CircleAvatar( + radius: 26, + backgroundColor: kMainGrey, + child: IconButton( + icon: const Icon(Icons.send, color: Colors.white, size: 20), + onPressed: _send, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _ChatBubble extends StatelessWidget { + final String text; + final bool isUser; + + const _ChatBubble({required this.text, required this.isUser}); + + @override + Widget build(BuildContext context) { + return Align( + alignment: isUser ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.65), + decoration: BoxDecoration( + color: isUser ? kMainGrey : Colors.grey[100], + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(18), + topRight: const Radius.circular(18), + bottomLeft: isUser ? const Radius.circular(18) : const Radius.circular(4), + bottomRight: isUser ? const Radius.circular(4) : const Radius.circular(18), + ), + ), + child: Text( + text, + style: TextStyle( + color: isUser ? Colors.white : kSecondGrey, + fontSize: 15, + ), + ), + ), + ); + } +} + +class _AssistantMessage extends StatelessWidget { + final AssistantResponse response; + final void Function(String sectionId, String sectionTitle)? onNavigate; + + const _AssistantMessage({required this.response, this.onNavigate}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Text bubble + _ChatBubble(text: response.reply, isUser: false), + + // Cards + if (response.cards != null && response.cards!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 6, left: 4, right: 32), + child: Column( + children: response.cards! + .map((card) => _AiCardWidget(card: card)) + .toList(), + ), + ), + + // Navigation button + if (response.navigation != null && onNavigate != null) + Padding( + padding: const EdgeInsets.only(top: 8, left: 4), + child: ElevatedButton.icon( + onPressed: () => onNavigate!( + response.navigation!.sectionId, + response.navigation!.sectionTitle, + ), + icon: const Icon(Icons.arrow_forward, size: 18), + label: Text(response.navigation!.sectionTitle), + style: ElevatedButton.styleFrom( + backgroundColor: kMainGrey, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + ), + ), + ), + ], + ), + ); + } +} + +class _AiCardWidget extends StatelessWidget { + final AiCard card; + + const _AiCardWidget({required this.card}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[200]!), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2)), + ], + ), + child: Row( + children: [ + if (card.icon != null) ...[ + Text(card.icon!, style: const TextStyle(fontSize: 20)), + const SizedBox(width: 10), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + card.title, + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, color: kSecondGrey), + ), + if (card.subtitle.isNotEmpty) + Text( + card.subtitle, + style: TextStyle(fontSize: 12, color: Colors.grey[500]), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/Models/AssistantResponse.dart b/lib/Models/AssistantResponse.dart new file mode 100644 index 0000000..1d470f6 --- /dev/null +++ b/lib/Models/AssistantResponse.dart @@ -0,0 +1,56 @@ +class AiCard { + final String title; + final String subtitle; + final String? icon; + + const AiCard({required this.title, required this.subtitle, this.icon}); + + factory AiCard.fromJson(Map json) => AiCard( + title: json['title'] as String? ?? '', + subtitle: json['subtitle'] as String? ?? '', + icon: json['icon'] as String?, + ); +} + +class AssistantNavigationAction { + final String sectionId; + final String sectionTitle; + final String sectionType; + + const AssistantNavigationAction({ + required this.sectionId, + required this.sectionTitle, + required this.sectionType, + }); + + factory AssistantNavigationAction.fromJson(Map json) => + AssistantNavigationAction( + sectionId: json['sectionId'] as String? ?? '', + sectionTitle: json['sectionTitle'] as String? ?? '', + sectionType: json['sectionType'] as String? ?? '', + ); +} + +class AssistantResponse { + final String reply; + final List? cards; + final AssistantNavigationAction? navigation; + + const AssistantResponse({ + required this.reply, + this.cards, + this.navigation, + }); + + factory AssistantResponse.fromJson(Map json) => + AssistantResponse( + reply: json['reply'] as String? ?? '', + cards: (json['cards'] as List?) + ?.map((e) => AiCard.fromJson(e as Map)) + .toList(), + navigation: json['navigation'] != null + ? AssistantNavigationAction.fromJson( + json['navigation'] as Map) + : null, + ); +} diff --git a/lib/Models/tabletContext.dart b/lib/Models/tabletContext.dart index 48f508b..fc994ae 100644 --- a/lib/Models/tabletContext.dart +++ b/lib/Models/tabletContext.dart @@ -3,6 +3,7 @@ import 'package:manager_api_new/api.dart'; //import 'package:mqtt_client/mqtt_browser_client.dart'; import 'package:mqtt_client/mqtt_server_client.dart'; import 'package:tablet_app/client.dart'; +import 'package:tablet_app/Services/statisticsService.dart'; import 'dart:convert'; @@ -18,6 +19,8 @@ class TabletAppContext with ChangeNotifier{ String? instanceId; Size? puzzleSize; String? localPath; + ApplicationInstanceDTO? applicationInstanceDTO; + StatisticsService? statisticsService; TabletAppContext({this.id, this.deviceId, this.host, this.configuration, this.language, this.instanceId, this.clientAPI}); diff --git a/lib/Screens/Agenda/agenda_view.dart b/lib/Screens/Agenda/agenda_view.dart index e9d7a43..5887498 100644 --- a/lib/Screens/Agenda/agenda_view.dart +++ b/lib/Screens/Agenda/agenda_view.dart @@ -150,6 +150,11 @@ class _AgendaView extends State { return GestureDetector( onTap: () { print("${eventAgenda.name}"); + (Provider.of(context, listen: false).getContext() as TabletAppContext) + .statisticsService?.track( + VisitEventType.agendaEventTap, + metadata: {'eventId': eventAgenda.name, 'eventTitle': eventAgenda.name}, + ); showDialog( context: context, builder: (BuildContext context) { diff --git a/lib/Screens/MainView/main_view.dart b/lib/Screens/MainView/main_view.dart index a70dc94..7562f97 100644 --- a/lib/Screens/MainView/main_view.dart +++ b/lib/Screens/MainView/main_view.dart @@ -15,6 +15,7 @@ import 'package:manager_api_new/api.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; +import 'package:tablet_app/Services/statisticsService.dart'; import 'package:tablet_app/Components/loading_common.dart'; import 'package:tablet_app/Helpers/DatabaseHelper.dart'; import 'package:tablet_app/Helpers/ImageCustomProvider.dart'; @@ -42,6 +43,7 @@ import 'package:http/http.dart' as http; import 'package:image/image.dart' as IMG; import 'dart:ui' as ui; +import 'package:tablet_app/Components/assistant_chat_view.dart'; import '../Quizz/quizz_view.dart'; import 'language_selection.dart'; @@ -194,7 +196,49 @@ class _MainViewWidget extends State { } ), ) - ) + ), + if (tabletAppContext.applicationInstanceDTO?.isAssistant == true) + Positioned( + bottom: 24, + right: 24, + child: FloatingActionButton.extended( + backgroundColor: kMainGrey, + icon: const Icon(Icons.chat_bubble_outline, color: Colors.white), + label: const Text("Assistant", style: TextStyle(color: Colors.white, fontSize: 16)), + onPressed: () { + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: 'Assistant', + barrierColor: Colors.black45, + transitionDuration: const Duration(milliseconds: 250), + pageBuilder: (dialogContext, __, ___) => Align( + alignment: Alignment.centerRight, + child: AssistantChatView( + tabletAppContext: tabletAppContext, + configurationId: tabletAppContext.configuration?.id, + onNavigateToSection: (sectionId, sectionTitle) { + Navigator.of(dialogContext).pop(); + final index = sectionsLocal?.indexWhere((s) => s.id == sectionId) ?? -1; + if (index >= 0) { + Navigator.push(context, MaterialPageRoute( + builder: (_) => SectionPageDetail( + configurationDTO: configurationDTO, + sectionDTO: sectionsLocal![index], + textColor: textColor, + isImageBackground: isImageBackground, + elementToShow: getContent(tabletAppContext, sectionsLocal![index], isImageBackground, rawSectionsData[index]), + isFromMenu: false, + ), + )); + } + }, + ), + ), + ); + }, + ), + ) /*if(configurationDTO.weatherCity != null && configurationDTO.weatherCity!.length > 2 && configurationDTO.weatherResult != null) Positioned( bottom: 0, @@ -277,6 +321,35 @@ class _MainViewWidget extends State { await getCurrentConfiguration(appContext); + // Load applicationInstanceDTO to check if assistant/statistics are enabled + try { + if (tabletAppContext.instanceId != null) { + final instanceDTO = await tabletAppContext.clientAPI!.instanceApi!.instanceGetDetail(tabletAppContext.instanceId!); + if (instanceDTO != null && instanceDTO.applicationInstanceDTOs != null) { + final tabletInstance = instanceDTO.applicationInstanceDTOs! + .where((a) => a.appType == AppType.Tablet) + .firstOrNull; + if (tabletInstance != null) { + if (instanceDTO.isAssistant == true) { + tabletAppContext.applicationInstanceDTO = tabletInstance; + } + if (tabletInstance.isStatistic == true) { + tabletAppContext.statisticsService = StatisticsService( + clientAPI: tabletAppContext.clientAPI!, + instanceId: tabletAppContext.instanceId, + configurationId: tabletAppContext.configuration?.id, + appType: 'Tablet', + language: tabletAppContext.language, + ); + } + } + } + appContext.setContext(tabletAppContext); + } + } catch (e) { + print('Failed to load applicationInstanceDTO: $e'); + } + print(sectionsLocal); if(isInit) { diff --git a/lib/Screens/MainView/section_page_detail.dart b/lib/Screens/MainView/section_page_detail.dart index 05a6a76..21917fc 100644 --- a/lib/Screens/MainView/section_page_detail.dart +++ b/lib/Screens/MainView/section_page_detail.dart @@ -18,6 +18,7 @@ import 'package:tablet_app/Screens/Map/geo_point_filter.dart'; import 'package:tablet_app/Screens/Map/map_context.dart'; import 'package:html/parser.dart' show parse; import 'package:tablet_app/Screens/Menu/menu_view.dart'; +import 'package:tablet_app/Services/statisticsService.dart'; import 'package:tablet_app/app_context.dart'; import 'package:tablet_app/constants.dart'; @@ -46,6 +47,35 @@ class SectionPageDetail extends StatefulWidget { class _SectionPageDetailState extends State { bool init = false; + DateTime? _sectionOpenTime; + StatisticsService? _statisticsService; + + @override + void initState() { + super.initState(); + _sectionOpenTime = DateTime.now(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final tabletAppContext = Provider.of(context, listen: false).getContext() as TabletAppContext; + _statisticsService = tabletAppContext.statisticsService; + _statisticsService?.track( + VisitEventType.sectionView, + sectionId: widget.sectionDTO.id, + ); + }); + } + + @override + void dispose() { + final duration = _sectionOpenTime != null + ? DateTime.now().difference(_sectionOpenTime!).inSeconds + : null; + _statisticsService?.track( + VisitEventType.sectionLeave, + sectionId: widget.sectionDTO.id, + durationSeconds: duration, + ); + super.dispose(); + } @override Widget build(BuildContext context) { diff --git a/lib/Screens/Map/google_map_view.dart b/lib/Screens/Map/google_map_view.dart index e52e066..db3c0b4 100644 --- a/lib/Screens/Map/google_map_view.dart +++ b/lib/Screens/Map/google_map_view.dart @@ -69,6 +69,14 @@ class _GoogleMapViewState extends State { mapContext.setSelectedPoint(point); //mapContext.setSelectedPointForNavigate(point); //}); + (Provider.of(context, listen: false).getContext() as TabletAppContext) + .statisticsService?.track( + VisitEventType.mapPoiTap, + metadata: { + 'geoPointId': point.id, + 'geoPointTitle': parse(textSansHTML.body!.text).documentElement!.text, + }, + ); }, infoWindow: InfoWindow.noText)); } diff --git a/lib/Screens/Menu/menu_view.dart b/lib/Screens/Menu/menu_view.dart index 560545c..46bbc58 100644 --- a/lib/Screens/Menu/menu_view.dart +++ b/lib/Screens/Menu/menu_view.dart @@ -69,6 +69,11 @@ class _MenuView extends State { SectionDTO section = subSections[index]; var rawSectionData = rawSubSectionsData[index]; + tabletAppContext.statisticsService?.track( + VisitEventType.menuItemTap, + metadata: {'targetSectionId': section.id, 'menuItemTitle': section.title?.where((t) => t.language == tabletAppContext.language).firstOrNull?.value}, + ); + setState(() { //selectedSection = section; //selectedSection = menuDTO.sections![index]; diff --git a/lib/Screens/Puzzle/puzzle_view.dart b/lib/Screens/Puzzle/puzzle_view.dart index 8fb30cd..e711d8a 100644 --- a/lib/Screens/Puzzle/puzzle_view.dart +++ b/lib/Screens/Puzzle/puzzle_view.dart @@ -28,6 +28,7 @@ class _PuzzleView extends State { int allInPlaceCount = 0; bool isFinished = false; + DateTime? _puzzleStartTime; GlobalKey _widgetKey = GlobalKey(); Size? realWidgetSize; List pieces = []; @@ -40,6 +41,7 @@ class _PuzzleView extends State { puzzleDTO = widget.section; puzzleDTO.rows = puzzleDTO.rows != null ? puzzleDTO.rows : 3; puzzleDTO.cols = puzzleDTO.cols != null ? puzzleDTO.cols : 3; + _puzzleStartTime = DateTime.now(); WidgetsBinding.instance.addPostFrameCallback((_) { Size size = MediaQuery.of(context).size; @@ -191,6 +193,11 @@ class _PuzzleView extends State { Size size = MediaQuery.of(context).size; final appContext = Provider.of(context, listen: false); TabletAppContext tabletAppContext = appContext.getContext(); + final duration = _puzzleStartTime != null ? DateTime.now().difference(_puzzleStartTime!).inSeconds : 0; + tabletAppContext.statisticsService?.track( + VisitEventType.gameComplete, + metadata: {'gameType': 'Puzzle', 'durationSeconds': duration}, + ); TranslationAndResourceDTO? messageFin = puzzleDTO.messageFin != null && puzzleDTO.messageFin!.length > 0 ? puzzleDTO.messageFin!.where((message) => message.language!.toUpperCase() == tabletAppContext.language!.toUpperCase()).firstOrNull : null; if(messageFin != null) { diff --git a/lib/Screens/Quizz/quizz_view.dart b/lib/Screens/Quizz/quizz_view.dart index 5627223..16de175 100644 --- a/lib/Screens/Quizz/quizz_view.dart +++ b/lib/Screens/Quizz/quizz_view.dart @@ -407,6 +407,12 @@ class _QuizzView extends State { { showResult = true; _controllerCenter!.play(); // TODO Maybe show only confetti on super score .. + final goodResponses = _questionsSubDTO.where((q) => q.chosen == q.responsesSubDTO!.indexWhere((r) => r.isGood!)).length; + (Provider.of(context, listen: false).getContext() as TabletAppContext) + .statisticsService?.track( + VisitEventType.quizComplete, + metadata: {'score': goodResponses, 'totalQuestions': _questionsSubDTO.length}, + ); } else { sliderController!.nextPage(duration: new Duration(milliseconds: 650), curve: Curves.fastOutSlowIn); } diff --git a/lib/Services/assistantService.dart b/lib/Services/assistantService.dart new file mode 100644 index 0000000..9eb765d --- /dev/null +++ b/lib/Services/assistantService.dart @@ -0,0 +1,43 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:tablet_app/Models/tabletContext.dart'; +import 'package:tablet_app/Models/AssistantResponse.dart'; + +class AssistantService { + final TabletAppContext tabletAppContext; + final List> _history = []; + + AssistantService({required this.tabletAppContext}); + + Future chat({required String message, String? configurationId}) async { + final host = tabletAppContext.host ?? ''; + final instanceId = tabletAppContext.instanceId ?? ''; + final language = tabletAppContext.language ?? 'FR'; + + _history.add({'role': 'user', 'content': message}); + + final body = { + 'message': message, + 'instanceId': instanceId, + 'appType': 'Tablet', + 'configurationId': configurationId, + 'language': language, + 'history': _history.take(10).toList(), + }; + + final response = await http.post( + Uri.parse('$host/api/ai/chat'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(body), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + final result = AssistantResponse.fromJson(data); + _history.add({'role': 'assistant', 'content': result.reply}); + return result; + } else { + throw Exception('Assistant error: ${response.statusCode}'); + } + } +} diff --git a/lib/Services/statisticsService.dart b/lib/Services/statisticsService.dart new file mode 100644 index 0000000..8f869b3 --- /dev/null +++ b/lib/Services/statisticsService.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:manager_api_new/api.dart'; +import 'package:tablet_app/client.dart'; + +class StatisticsService { + final Client clientAPI; + final String? instanceId; + final String? configurationId; + final String? appType; // "Mobile" or "Tablet" + final String? language; + final String sessionId; + + StatisticsService({ + required this.clientAPI, + required this.instanceId, + required this.configurationId, + this.appType = 'Tablet', + this.language, + }) : sessionId = _generateSessionId(); + + static String _generateSessionId() { + final rand = Random(); + final bytes = List.generate(16, (_) => rand.nextInt(256)); + return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + } + + Future track( + String eventType, { + String? sectionId, + int? durationSeconds, + Map? metadata, + }) async { + try { + await clientAPI.statsApi!.statsTrackEvent(VisitEventDTO( + instanceId: instanceId ?? '', + configurationId: configurationId, + sectionId: sectionId, + sessionId: sessionId, + eventType: eventType, + appType: appType, + language: language, + durationSeconds: durationSeconds, + metadata: metadata != null ? jsonEncode(metadata) : null, + timestamp: DateTime.now(), + )); + } catch (_) { + // fire-and-forget — never block the UI on stats errors + } + } +} diff --git a/lib/client.dart b/lib/client.dart index fab7b41..01da5c5 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -26,6 +26,12 @@ class Client { DeviceApi? _deviceApi; DeviceApi? get deviceApi => _deviceApi; + ApplicationInstanceApi? _applicationInstanceApi; + ApplicationInstanceApi? get applicationInstanceApi => _applicationInstanceApi; + + StatsApi? _statsApi; + StatsApi? get statsApi => _statsApi; + Client(String path) { _apiClient = ApiClient( basePath: path); // "http://192.168.31.96" @@ -37,5 +43,7 @@ class Client { _sectionApi = SectionApi(_apiClient); _resourceApi = ResourceApi(_apiClient); _deviceApi = DeviceApi(_apiClient); + _applicationInstanceApi = ApplicationInstanceApi(_apiClient); + _statsApi = StatsApi(_apiClient); } } \ No newline at end of file diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a7a5897..10eaa66 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -14,10 +14,11 @@ import just_audio import package_info_plus import path_provider_foundation import shared_preferences_foundation -import sqflite +import sqflite_darwin import url_launcher_macos import video_player_avfoundation import wakelock_plus +import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) @@ -33,4 +34,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) + WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) } diff --git a/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/macos/Flutter/ephemeral/Flutter-Generated.xcconfig new file mode 100644 index 0000000..6333a27 --- /dev/null +++ b/macos/Flutter/ephemeral/Flutter-Generated.xcconfig @@ -0,0 +1,12 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=C:\PROJ\flutter +FLUTTER_APPLICATION_PATH=C:\Users\ThomasFransolet\Documents\Documents\Perso\GITEA\tablet-app +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=2.1.5 +FLUTTER_BUILD_NUMBER=26 +FLUTTER_CLI_BUILD_MODE=debug +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=true +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/macos/Flutter/ephemeral/flutter_export_environment.sh b/macos/Flutter/ephemeral/flutter_export_environment.sh new file mode 100644 index 0000000..48fa81f --- /dev/null +++ b/macos/Flutter/ephemeral/flutter_export_environment.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=C:\PROJ\flutter" +export "FLUTTER_APPLICATION_PATH=C:\Users\ThomasFransolet\Documents\Documents\Perso\GITEA\tablet-app" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=2.1.5" +export "FLUTTER_BUILD_NUMBER=26" +export "FLUTTER_CLI_BUILD_MODE=debug" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/pubspec.yaml b/pubspec.yaml index b435e82..d086e87 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -80,7 +80,7 @@ dependencies: #win32: ^4.1.2 #archive: ^3.6.1 manager_api_new: - path: manager_api_new + path: ../manager-app/manager_api_new dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index f7fb082..941d79d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -16,6 +17,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); FirebaseStoragePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseStoragePluginCApi")); + FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d17f009..6da9426 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_core firebase_storage + flutter_inappwebview_windows permission_handler_windows url_launcher_windows )