Add stats and ia assistant (to be tested!)
This commit is contained in:
parent
1fddd6363f
commit
9a46b443b6
@ -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 {
|
||||
|
||||
@ -3,3 +3,6 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.enableR8=true
|
||||
SDK_REGISTRY_TOKEN=sk.eyJ1IjoidGZyYW5zb2xldCIsImEiOiJjbHRpcTF1d28wYmxmMmxwNjRleXpodGY0In0.qr-jo1vAb08bL3WRDEB4pw
|
||||
android.bundle.enableUncompressedNativeLibs=false
|
||||
android.experimental.enable16kApk=true
|
||||
android.useNewNativePlugin=true
|
||||
325
lib/Components/assistant_chat_view.dart
Normal file
325
lib/Components/assistant_chat_view.dart
Normal file
@ -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<AssistantChatView> createState() => _AssistantChatViewState();
|
||||
}
|
||||
|
||||
class _AssistantChatViewState extends State<AssistantChatView> {
|
||||
late AssistantService _assistantService;
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final List<Widget> _bubbles = [];
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_assistantService = AssistantService(tabletAppContext: widget.tabletAppContext);
|
||||
}
|
||||
|
||||
Future<void> _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]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/Models/AssistantResponse.dart
Normal file
56
lib/Models/AssistantResponse.dart
Normal file
@ -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<String, dynamic> 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<String, dynamic> json) =>
|
||||
AssistantNavigationAction(
|
||||
sectionId: json['sectionId'] as String? ?? '',
|
||||
sectionTitle: json['sectionTitle'] as String? ?? '',
|
||||
sectionType: json['sectionType'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
class AssistantResponse {
|
||||
final String reply;
|
||||
final List<AiCard>? cards;
|
||||
final AssistantNavigationAction? navigation;
|
||||
|
||||
const AssistantResponse({
|
||||
required this.reply,
|
||||
this.cards,
|
||||
this.navigation,
|
||||
});
|
||||
|
||||
factory AssistantResponse.fromJson(Map<String, dynamic> json) =>
|
||||
AssistantResponse(
|
||||
reply: json['reply'] as String? ?? '',
|
||||
cards: (json['cards'] as List<dynamic>?)
|
||||
?.map((e) => AiCard.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
navigation: json['navigation'] != null
|
||||
? AssistantNavigationAction.fromJson(
|
||||
json['navigation'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
@ -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});
|
||||
|
||||
|
||||
@ -150,6 +150,11 @@ class _AgendaView extends State<AgendaView> {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
print("${eventAgenda.name}");
|
||||
(Provider.of<AppContext>(context, listen: false).getContext() as TabletAppContext)
|
||||
.statisticsService?.track(
|
||||
VisitEventType.agendaEventTap,
|
||||
metadata: {'eventId': eventAgenda.name, 'eventTitle': eventAgenda.name},
|
||||
);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
|
||||
@ -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,6 +196,48 @@ class _MainViewWidget extends State<MainViewWidget> {
|
||||
}
|
||||
),
|
||||
)
|
||||
),
|
||||
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(
|
||||
@ -277,6 +321,35 @@ class _MainViewWidget extends State<MainViewWidget> {
|
||||
|
||||
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) {
|
||||
|
||||
@ -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<SectionPageDetail> {
|
||||
bool init = false;
|
||||
DateTime? _sectionOpenTime;
|
||||
StatisticsService? _statisticsService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sectionOpenTime = DateTime.now();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final tabletAppContext = Provider.of<AppContext>(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) {
|
||||
|
||||
@ -69,6 +69,14 @@ class _GoogleMapViewState extends State<GoogleMapView> {
|
||||
mapContext.setSelectedPoint(point);
|
||||
//mapContext.setSelectedPointForNavigate(point);
|
||||
//});
|
||||
(Provider.of<AppContext>(context, listen: false).getContext() as TabletAppContext)
|
||||
.statisticsService?.track(
|
||||
VisitEventType.mapPoiTap,
|
||||
metadata: {
|
||||
'geoPointId': point.id,
|
||||
'geoPointTitle': parse(textSansHTML.body!.text).documentElement!.text,
|
||||
},
|
||||
);
|
||||
},
|
||||
infoWindow: InfoWindow.noText));
|
||||
}
|
||||
|
||||
@ -69,6 +69,11 @@ class _MenuView extends State<MenuView> {
|
||||
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];
|
||||
|
||||
@ -28,6 +28,7 @@ class _PuzzleView extends State<PuzzleView> {
|
||||
|
||||
int allInPlaceCount = 0;
|
||||
bool isFinished = false;
|
||||
DateTime? _puzzleStartTime;
|
||||
GlobalKey _widgetKey = GlobalKey();
|
||||
Size? realWidgetSize;
|
||||
List<Widget> pieces = [];
|
||||
@ -40,6 +41,7 @@ class _PuzzleView extends State<PuzzleView> {
|
||||
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<PuzzleView> {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
final appContext = Provider.of<AppContext>(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) {
|
||||
|
||||
@ -407,6 +407,12 @@ class _QuizzView extends State<QuizzView> {
|
||||
{
|
||||
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<AppContext>(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);
|
||||
}
|
||||
|
||||
43
lib/Services/assistantService.dart
Normal file
43
lib/Services/assistantService.dart
Normal file
@ -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<Map<String, String>> _history = [];
|
||||
|
||||
AssistantService({required this.tabletAppContext});
|
||||
|
||||
Future<AssistantResponse> 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<String, dynamic>;
|
||||
final result = AssistantResponse.fromJson(data);
|
||||
_history.add({'role': 'assistant', 'content': result.reply});
|
||||
return result;
|
||||
} else {
|
||||
throw Exception('Assistant error: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
}
|
||||
52
lib/Services/statisticsService.dart
Normal file
52
lib/Services/statisticsService.dart
Normal file
@ -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<int>.generate(16, (_) => rand.nextInt(256));
|
||||
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
|
||||
Future<void> track(
|
||||
String eventType, {
|
||||
String? sectionId,
|
||||
int? durationSeconds,
|
||||
Map<String, dynamic>? 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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"))
|
||||
}
|
||||
|
||||
12
macos/Flutter/ephemeral/Flutter-Generated.xcconfig
Normal file
12
macos/Flutter/ephemeral/Flutter-Generated.xcconfig
Normal file
@ -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
|
||||
13
macos/Flutter/ephemeral/flutter_export_environment.sh
Normal file
13
macos/Flutter/ephemeral/flutter_export_environment.sh
Normal file
@ -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"
|
||||
@ -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:
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||
#include <firebase_storage/firebase_storage_plugin_c_api.h>
|
||||
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
firebase_core
|
||||
firebase_storage
|
||||
flutter_inappwebview_windows
|
||||
permission_handler_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user