diff --git a/lib/helpers/services/gemini_service.dart b/lib/helpers/services/gemini_service.dart deleted file mode 100644 index dcaf907..0000000 --- a/lib/helpers/services/gemini_service.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:firebase_ai/firebase_ai.dart'; - -class GeminiService { - static late GenerativeModel _model; - - /// Initializes Gemini Developer API - static Future initialize() async { - _model = FirebaseAI.googleAI().generativeModel( - model: 'gemini-2.5-flash', - ); - } - - /// Generate text from a text prompt - static Future generateText(String prompt) async { - try { - final content = [Content.text(prompt)]; - - final response = await _model.generateContent(content); - - return response.text ?? "No response text found."; - } catch (e) { - return "Error: $e"; - } - } - - /// Stream text response (optional) - static Stream streamText(String prompt) async* { - final content = [Content.text(prompt)]; - - final stream = _model.generateContentStream(content); - - await for (final chunk in stream) { - if (chunk.text != null) yield chunk.text!; - } - } -} diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index 832cf42..dff0b48 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -20,7 +20,6 @@ import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/model/attendance/attendence_action_button.dart'; import 'package:on_field_work/model/attendance/log_details_view.dart'; import 'package:on_field_work/view/layouts/layout.dart'; -import 'package:on_field_work/view/dashboard/gemini_chat_card.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @@ -604,11 +603,6 @@ Widget build(BuildContext context) { ], ), ), - - // Floating Gemini assistant overlay - GeminiFloatingAssistant( - dashboardController: dashboardController, - ), ], ), ), diff --git a/lib/view/dashboard/gemini_chat_card.dart b/lib/view/dashboard/gemini_chat_card.dart deleted file mode 100644 index be05c70..0000000 --- a/lib/view/dashboard/gemini_chat_card.dart +++ /dev/null @@ -1,574 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:flutter_lucide/flutter_lucide.dart'; -import 'package:get/get.dart'; -import 'package:on_field_work/controller/dashboard/dashboard_controller.dart'; -import 'package:on_field_work/helpers/services/gemini_service.dart'; -import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; -import 'package:on_field_work/helpers/widgets/my_spacing.dart'; -import 'package:on_field_work/helpers/widgets/my_text.dart'; - -/// Floating Gemini assistant for mobile dashboard. -/// Shows a circular FAB, which expands into a small chat panel from bottom-right. -class GeminiFloatingAssistant extends StatefulWidget { - final DashboardController dashboardController; - - const GeminiFloatingAssistant({ - super.key, - required this.dashboardController, - }); - - @override - State createState() => - _GeminiFloatingAssistantState(); -} - -class _GeminiFloatingAssistantState extends State - with SingleTickerProviderStateMixin, UIMixin { - // State - final TextEditingController _promptController = TextEditingController(); - final RxList> _chatHistory = - >[].obs; - final RxBool _isLoading = false.obs; - final RxBool _isAnalyzingData = false.obs; - final RxString _error = ''.obs; - final RxBool _isOpen = false.obs; - - final ScrollController _scrollController = ScrollController(); - - late final AnimationController _animController; - late final Animation _scaleAnimation; - late final Animation _opacityAnimation; - - @override - void initState() { - super.initState(); - _animController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 280), - ); - _scaleAnimation = CurvedAnimation( - parent: _animController, - curve: Curves.fastOutSlowIn, - ); - _opacityAnimation = CurvedAnimation( - parent: _animController, - curve: Curves.easeInOut, - ); - - // Add initial welcome message - _chatHistory.add({ - 'sender': 'ai', - 'message': - 'Hello! I am your AI assistant. How can I help you today? You can ask general questions or use the chart button below to analyze your monthly expenses.', - }); - } - - // Scroll helper - void _scrollToBottom() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent, - duration: const Duration(milliseconds: 250), - curve: Curves.easeOut, - ); - } - }); - } - - // Toggle FAB / panel - void _toggleOpen() { - _error.value = ''; - final newValue = !_isOpen.value; - _isOpen.value = newValue; - if (newValue) { - _animController.forward(); - } else { - _animController.reverse(); - } - } - - // Chat bubble - Widget _buildResponseBubble(String text, bool isAI) { - if (text.isEmpty) return const SizedBox.shrink(); - - final Color backgroundColor = - isAI ? Colors.blue.shade50 : contentTheme.primary.withOpacity(0.9); - final Color textColor = isAI ? Colors.black87 : Colors.white; - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - isAI ? MainAxisAlignment.start : MainAxisAlignment.end, - children: [ - if (isAI) ...[ - const Icon(LucideIcons.bot, color: Colors.blue, size: 20), - MySpacing.width(8), - ], - Flexible( - child: Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.only( - topLeft: const Radius.circular(14), - topRight: const Radius.circular(14), - bottomLeft: - isAI ? Radius.zero : const Radius.circular(14), - bottomRight: - isAI ? const Radius.circular(14) : Radius.zero, - ), - ), - child: MyText.bodySmall( - text, - color: textColor, - height: 1.4, - ), - ), - ), - if (!isAI) ...[ - MySpacing.width(8), - const Icon(LucideIcons.user, color: Colors.black54, size: 18), - ], - ], - ), - ); - } - - // Prompt submit - Future _handlePromptSubmission() async { - final promptText = _promptController.text.trim(); - if (promptText.isEmpty || _isLoading.value || _isAnalyzingData.value) { - return; - } - - _chatHistory.add({'sender': 'user', 'message': promptText}); - _promptController.clear(); - _scrollToBottom(); - - _isLoading.value = true; - _error.value = ''; - - // Placeholder - _chatHistory.add({'sender': 'ai', 'message': 'Thinking...'}); - _scrollToBottom(); - final placeholderIndex = _chatHistory.length - 1; - - try { - // NOTE: Assuming GeminiService.generateText handles the API call - final result = await GeminiService.generateText(promptText); - if (placeholderIndex >= 0 && - _chatHistory.length > placeholderIndex) { - _chatHistory[placeholderIndex] = { - 'sender': 'ai', - 'message': result, - }; - } - _scrollToBottom(); - } catch (e, stack) { - // ignore: avoid_print - print("Gemini Error: $e\n$stack"); - _error.value = - "An error occurred. Failed to get response: ${e.toString().split(':').first}"; - if (placeholderIndex >= 0 && - _chatHistory.length > placeholderIndex) { - _chatHistory.removeAt(placeholderIndex); - } - } finally { - _isLoading.value = false; - } - } - - // Expense analysis - Future _handleDataAnalysis() async { - if (_isLoading.value || _isAnalyzingData.value) return; - - _isAnalyzingData.value = true; - _error.value = ''; - - const String analysisPrompt = 'Analyze my monthly expense overview.'; - _chatHistory.add({'sender': 'user', 'message': analysisPrompt}); - _chatHistory - .add({'sender': 'ai', 'message': 'Analyzing monthly expenses...'}); - _scrollToBottom(); - final placeholderIndex = _chatHistory.length - 1; - - try { - await widget.dashboardController.fetchMonthlyExpenses(); - // NOTE: Assuming monthlyExpenseList contains a list of objects that have a .toJson() method - final rawDataList = widget.dashboardController.monthlyExpenseList; - - if (rawDataList.isEmpty) { - _error.value = "No monthly expense data found for analysis."; - if (placeholderIndex >= 0 && - _chatHistory.length > placeholderIndex) { - _chatHistory.removeAt(placeholderIndex); - } - _isAnalyzingData.value = false; - return; - } - - final rawDataJson = - jsonEncode(rawDataList.map((e) => e.toJson()).toList()); - - final prompt = - "Analyze the following monthly expense data and provide a concise " - "summary, key trends (like increasing/decreasing expenses), and " - "highlight the highest expense months. The data is a list of " - "monthly expense summaries over time in JSON format:\n\n$rawDataJson"; - - final result = await GeminiService.generateText(prompt); - - if (placeholderIndex >= 0 && - _chatHistory.length > placeholderIndex) { - _chatHistory[placeholderIndex] = { - 'sender': 'ai', - 'message': result, - }; - } - _scrollToBottom(); - } catch (e, stack) { - // ignore: avoid_print - print("Gemini Data Analysis Error: $e\n$stack"); - _error.value = - "Failed to analyze data. An error occurred: ${e.toString().split(':').first}"; - if (placeholderIndex >= 0 && - _chatHistory.length > placeholderIndex) { - _chatHistory.removeAt(placeholderIndex); - } - } finally { - _isAnalyzingData.value = false; - } - } - - // Small floating chat panel (mobile-friendly) - Widget _buildFloatingPanel(BuildContext context) { - final media = MediaQuery.of(context).size; - final double panelWidth = media.width * 0.9; - final double panelHeight = media.height * 0.45; - - return FadeTransition( - opacity: _opacityAnimation, - child: ScaleTransition( - scale: _scaleAnimation, - alignment: Alignment.bottomRight, - child: Align( - alignment: Alignment.bottomRight, - child: Container( - width: panelWidth, - height: panelHeight, - margin: const EdgeInsets.only(right: 16, bottom: 80), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(18), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.18), - blurRadius: 18, - offset: const Offset(0, 8), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header - Row( - children: [ - Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: contentTheme.primary.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon( - LucideIcons.bot, - size: 18, - color: Colors.blue, - ), - ), - MySpacing.width(8), - Expanded( - child: MyText.titleSmall( - "Gemini AI Assistant", - fontWeight: 700, - overflow: TextOverflow.ellipsis, - ), - ), - IconButton( - splashRadius: 18, - iconSize: 18, - onPressed: _toggleOpen, - icon: const Icon(Icons.close, color: Colors.black54), - ), - ], - ), - MySpacing.height(4), - MyText.labelSmall( - "Ask questions or analyze expenses.", - color: Colors.grey.shade600, - ), - MySpacing.height(8), - - // Error - Obx( - () => _error.value.isNotEmpty - ? Container( - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.only(bottom: 8), - decoration: BoxDecoration( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(8), - border: - Border.all(color: Colors.red.shade200), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon( - LucideIcons.badge_alert, - color: Colors.red, - size: 16, - ), - MySpacing.width(6), - Expanded( - child: MyText.bodySmall( - _error.value, - color: Colors.red.shade800, - ), - ), - ], - ), - ) - : const SizedBox.shrink(), - ), - - // Chat list - Expanded( - child: Obx( - () { - if (_chatHistory.isEmpty) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - LucideIcons.bot, - size: 40, - color: Colors.blue.shade300, - ), - MySpacing.height(6), - Text( - "Ask me about your data!", - style: TextStyle( - color: Colors.grey.shade500, - ), - ), - ], - ), - ); - } - return ListView.builder( - controller: _scrollController, - itemCount: _chatHistory.length, - padding: - const EdgeInsets.symmetric(vertical: 4), - itemBuilder: (context, index) { - final message = _chatHistory[index]; - return _buildResponseBubble( - message['message'] ?? '', - message['sender'] == 'ai', - ); - }, - ); - }, - ), - ), - MySpacing.height(6), - - // Bottom row: Analyze + input - Row( - children: [ - Obx( - () => IconButton( - splashRadius: 18, - onPressed: (_isAnalyzingData.value || - _isLoading.value) - ? null - : _handleDataAnalysis, - iconSize: 22, - icon: _isAnalyzingData.value - ? SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: contentTheme.primary, - ), - ) - : const Icon( - LucideIcons.chart_line, - color: Colors.blue, - ), - tooltip: 'Analyze expenses', - ), - ), - Expanded( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(999), - border: Border.all( - color: Colors.grey.shade300, - ), - ), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _promptController, - minLines: 1, - maxLines: 3, - keyboardType: TextInputType.multiline, - decoration: InputDecoration( - hintText: "Ask Gemini…", - border: InputBorder.none, - isDense: true, - hintStyle: TextStyle( - color: Colors.grey.shade500, - fontSize: 13, - ), - ), - onSubmitted: (_) => - _handlePromptSubmission(), - enabled: !_isAnalyzingData.value && !_isLoading.value, - ), - ), - Obx( - () => IconButton( - splashRadius: 18, - iconSize: 18, - onPressed: (_isLoading.value || - _isAnalyzingData.value || - _promptController.text - .trim() - .isEmpty) - ? null - : _handlePromptSubmission, - icon: _isLoading.value && _promptController.text.trim().isNotEmpty // Show loading only for text submission - ? SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: contentTheme.primary, - ), - ) - : Icon( - LucideIcons.send, - color: _promptController.text - .trim() - .isEmpty - ? Colors.grey.shade400 - : contentTheme.primary, - ), - ), - ), - ], - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ); - } - - // FAB - Widget _buildFab() { - return Obx( - () => AnimatedScale( - scale: _isOpen.value ? 0.0 : 1.0, - duration: const Duration(milliseconds: 200), - curve: Curves.easeOut, - child: FloatingActionButton.extended( - onPressed: _toggleOpen, - heroTag: 'gemini_fab', - backgroundColor: contentTheme.primary, - icon: const Icon(LucideIcons.bot, color: Colors.white), - label: const Text( - 'Ask Me Anything', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - ), - ); - } - - // Dim background when open - Full screen scrim - Widget _buildScrim() { - return Obx( - () => _isOpen.value - ? FadeTransition( - opacity: _opacityAnimation, - child: GestureDetector( - onTap: _toggleOpen, - // Use Positioned.fill to guarantee it covers the entire Stack - child: const Positioned.fill( - child: ColoredBox( - color: Colors.black38, // Use Colors.black38 for 38% opacity, slightly stronger than 0.25 - ), - ), - ), - ) - : const SizedBox.shrink(), - ); - } - - @override - Widget build(BuildContext context) { - // Wrap the Stack in SizedBox.expand to force the widget to take the full space - // of its parent, which is typically the whole screen area in a dashboard. - return SizedBox.expand( - child: Stack( - children: [ - // 1. Scrim (Background layer when open) - Now using Positioned.fill for reliability - _buildScrim(), - - // 2. Panel (Middle layer when open) - _buildFloatingPanel(context), - - // 3. FAB (Top layer when closed) - Positioned( - bottom: 16, - right: 16, - child: _buildFab(), - ), - ], - ), - ); - } - - @override - void dispose() { - _promptController.dispose(); - _scrollController.dispose(); - _animController.dispose(); - super.dispose(); - } -} \ No newline at end of file