From b5449f78420e9e39ed8ac5c80cc4419242064af9 Mon Sep 17 00:00:00 2001 From: Marc Rejohn Castillano Date: Tue, 3 Mar 2026 23:35:07 +0800 Subject: [PATCH] Still some AI integration issues --- assets/gemini.png | Bin 0 -> 19333 bytes gemini-input-glow.png | Bin 0 -> 84736 bytes lib/screens/tasks/task_detail_screen.dart | 1342 +++++++++++-------- lib/screens/tasks/tasks_list_screen.dart | 121 +- lib/services/ai_service.dart | 202 +++ lib/services/gemini_service.dart | 144 ++ lib/utils/subject_suggestions.dart | 237 ++++ lib/widgets/gemini_animated_text_field.dart | 280 ++++ lib/widgets/gemini_button.dart | 120 ++ pubspec.lock | 10 +- pubspec.yaml | 2 + 11 files changed, 1893 insertions(+), 565 deletions(-) create mode 100644 assets/gemini.png create mode 100644 gemini-input-glow.png create mode 100644 lib/services/ai_service.dart create mode 100644 lib/services/gemini_service.dart create mode 100644 lib/utils/subject_suggestions.dart create mode 100644 lib/widgets/gemini_animated_text_field.dart create mode 100644 lib/widgets/gemini_button.dart diff --git a/assets/gemini.png b/assets/gemini.png new file mode 100644 index 0000000000000000000000000000000000000000..e539633a2de4b789b171867a03cda24b97e36bcc GIT binary patch literal 19333 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 literal 0 HcmV?d00001 diff --git a/gemini-input-glow.png b/gemini-input-glow.png new file mode 100644 index 0000000000000000000000000000000000000000..42c3afc47e662113c1d8b4a0181aad986aac37b1 GIT binary patch literal 84736 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 bool _categorySaved = false; bool _actionSaving = false; bool _actionSaved = false; + bool _actionProcessing = false; late final AnimationController _saveAnimController; late final Animation _savePulse; static const List _statusOptions = [ @@ -1579,334 +1583,55 @@ class _TaskDetailScreenState extends ConsumerState crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('Action taken'), + Row( + children: [ + const Text('Action taken'), + const Spacer(), + IconButton( + tooltip: + 'Improve action taken with Gemini', + icon: Image.asset( + 'assets/gemini_icon.png', + width: 24, + height: 24, + errorBuilder: + (context, error, stackTrace) { + return const Icon( + Icons.auto_awesome, + ); + }, + ), + onPressed: () => + _processActionTakenWithGemini( + context, + ref, + ), + ), + ], + ), const SizedBox(height: 6), // Toolbar + editor with inline save indicator - Container( - height: isWide ? 260 : 220, - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of( - context, - ).colorScheme.outline, + GeminiAnimatedBorder( + isProcessing: _actionProcessing, + child: Container( + height: isWide ? 260 : 220, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of( + context, + ).colorScheme.outline, + ), + borderRadius: BorderRadius.circular( + 8, + ), ), - borderRadius: BorderRadius.circular( - 8, - ), - ), - child: Stack( - children: [ - Column( - children: [ - isWide - ? Row( - children: [ - IconButton( - tooltip: 'Bold', - icon: const Icon( - Icons.format_bold, - ), - onPressed: () => - _actionController - ?.formatSelection( - quill - .Attribute - .bold, - ), - ), - IconButton( - tooltip: 'Italic', - icon: const Icon( - Icons - .format_italic, - ), - onPressed: () => - _actionController - ?.formatSelection( - quill - .Attribute - .italic, - ), - ), - IconButton( - tooltip: - 'Underline', - icon: const Icon( - Icons - .format_underlined, - ), - onPressed: () => - _actionController - ?.formatSelection( - quill - .Attribute - .underline, - ), - ), - IconButton( - tooltip: - 'Bullet list', - icon: const Icon( - Icons - .format_list_bulleted, - ), - onPressed: () => - _actionController - ?.formatSelection( - quill - .Attribute - .ul, - ), - ), - IconButton( - tooltip: - 'Numbered list', - icon: const Icon( - Icons - .format_list_numbered, - ), - onPressed: () => - _actionController - ?.formatSelection( - quill - .Attribute - .ol, - ), - ), - const SizedBox( - width: 8, - ), - IconButton( - tooltip: - 'Heading 2', - icon: const Icon( - Icons.format_size, - ), - onPressed: () => - _actionController - ?.formatSelection( - quill - .Attribute - .h2, - ), - ), - IconButton( - tooltip: - 'Heading 3', - icon: const Icon( - Icons.format_size, - size: 18, - ), - onPressed: () => - _actionController - ?.formatSelection( - quill - .Attribute - .h3, - ), - ), - IconButton( - tooltip: 'Undo', - icon: const Icon( - Icons.undo, - ), - onPressed: () => - _actionController - ?.undo(), - ), - IconButton( - tooltip: 'Redo', - icon: const Icon( - Icons.redo, - ), - onPressed: () => - _actionController - ?.redo(), - ), - IconButton( - tooltip: - 'Insert link', - icon: const Icon( - Icons.link, - ), - onPressed: () async { - final urlCtrl = - TextEditingController(); - final res = await showDialog( - context: - context, - builder: (ctx) => AlertDialog( - title: const Text( - 'Insert link', - ), - content: TextField( - controller: - urlCtrl, - decoration: - const InputDecoration( - hintText: - 'https://', - ), - ), - actions: [ - TextButton( - onPressed: () => - Navigator.of( - ctx, - ).pop(), - child: const Text( - 'Cancel', - ), - ), - TextButton( - onPressed: () => - Navigator.of( - ctx, - ).pop( - urlCtrl.text.trim(), - ), - child: const Text( - 'Insert', - ), - ), - ], - ), - ); - if (res == null || - res.isEmpty) { - return; - } - final sel = - _actionController - ?.selection ?? - const TextSelection.collapsed( - offset: 0, - ); - final start = sel - .baseOffset; - final end = sel - .extentOffset; - if (!sel.isCollapsed && - end > start) { - final len = - end - start; - try { - _actionController - ?.document - .delete( - start, - len, - ); - } catch (_) {} - _actionController - ?.document - .insert( - start, - res, - ); - } else { - _actionController - ?.document - .insert( - start, - res, - ); - } - }, - ), - IconButton( - tooltip: - 'Insert image', - icon: const Icon( - Icons.image, - ), - onPressed: () async { - try { - final r = await FilePicker - .platform - .pickFiles( - withData: - true, - type: FileType - .image, - ); - if (r == null || - r - .files - .isEmpty) { - return; - } - final file = r - .files - .first; - final bytes = - file.bytes; - if (bytes == - null) { - return; - } - final ext = - file.extension ?? - 'png'; - String? url; - try { - url = await ref - .read( - tasksControllerProvider, - ) - .uploadActionImage( - taskId: - task.id, - bytes: - bytes, - extension: - ext, - ); - } catch (e) { - showErrorSnackBar( - context, - 'Upload error: $e', - ); - return; - } - if (url == - null) { - showErrorSnackBar( - context, - 'Image upload failed (no URL returned)', - ); - return; - } - final trimmedUrl = - url.trim(); - final idx = - _actionController - ?.selection - .baseOffset ?? - 0; - // ignore: avoid_print - print( - 'inserting image embed idx=$idx url=$trimmedUrl', - ); - _actionController - ?.document - .insert( - idx, - quill - .BlockEmbed.image( - trimmedUrl, - ), - ); - } catch (_) {} - }, - ), - ], - ) - : SingleChildScrollView( - scrollDirection: - Axis.horizontal, - child: Row( + child: Stack( + children: [ + Column( + children: [ + isWide + ? Row( children: [ IconButton( tooltip: 'Bold', @@ -2213,76 +1938,395 @@ class _TaskDetailScreenState extends ConsumerState }, ), ], + ) + : SingleChildScrollView( + scrollDirection: + Axis.horizontal, + child: Row( + children: [ + IconButton( + tooltip: 'Bold', + icon: const Icon( + Icons + .format_bold, + ), + onPressed: () => + _actionController?.formatSelection( + quill + .Attribute + .bold, + ), + ), + IconButton( + tooltip: + 'Italic', + icon: const Icon( + Icons + .format_italic, + ), + onPressed: () => + _actionController?.formatSelection( + quill + .Attribute + .italic, + ), + ), + IconButton( + tooltip: + 'Underline', + icon: const Icon( + Icons + .format_underlined, + ), + onPressed: () => + _actionController?.formatSelection( + quill + .Attribute + .underline, + ), + ), + IconButton( + tooltip: + 'Bullet list', + icon: const Icon( + Icons + .format_list_bulleted, + ), + onPressed: () => + _actionController + ?.formatSelection( + quill + .Attribute + .ul, + ), + ), + IconButton( + tooltip: + 'Numbered list', + icon: const Icon( + Icons + .format_list_numbered, + ), + onPressed: () => + _actionController + ?.formatSelection( + quill + .Attribute + .ol, + ), + ), + const SizedBox( + width: 8, + ), + IconButton( + tooltip: + 'Heading 2', + icon: const Icon( + Icons + .format_size, + ), + onPressed: () => + _actionController + ?.formatSelection( + quill + .Attribute + .h2, + ), + ), + IconButton( + tooltip: + 'Heading 3', + icon: const Icon( + Icons + .format_size, + size: 18, + ), + onPressed: () => + _actionController + ?.formatSelection( + quill + .Attribute + .h3, + ), + ), + IconButton( + tooltip: 'Undo', + icon: + const Icon( + Icons + .undo, + ), + onPressed: () => + _actionController + ?.undo(), + ), + IconButton( + tooltip: 'Redo', + icon: + const Icon( + Icons + .redo, + ), + onPressed: () => + _actionController + ?.redo(), + ), + IconButton( + tooltip: + 'Insert link', + icon: + const Icon( + Icons + .link, + ), + onPressed: () async { + final urlCtrl = + TextEditingController(); + final res = await showDialog( + context: + context, + builder: (ctx) => AlertDialog( + title: const Text( + 'Insert link', + ), + content: TextField( + controller: + urlCtrl, + decoration: const InputDecoration( + hintText: + 'https://', + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of( + ctx, + ).pop(), + child: const Text( + 'Cancel', + ), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop( + urlCtrl.text.trim(), + ), + child: const Text( + 'Insert', + ), + ), + ], + ), + ); + if (res == + null || + res.isEmpty) { + return; + } + final sel = + _actionController + ?.selection ?? + const TextSelection.collapsed( + offset: + 0, + ); + final start = + sel.baseOffset; + final end = sel + .extentOffset; + if (!sel.isCollapsed && + end > + start) { + final len = + end - + start; + try { + _actionController + ?.document + .delete( + start, + len, + ); + } catch ( + _ + ) {} + _actionController + ?.document + .insert( + start, + res, + ); + } else { + _actionController + ?.document + .insert( + start, + res, + ); + } + }, + ), + IconButton( + tooltip: + 'Insert image', + icon: const Icon( + Icons.image, + ), + onPressed: () async { + try { + final r = await FilePicker + .platform + .pickFiles( + withData: + true, + type: + FileType.image, + ); + if (r == + null || + r + .files + .isEmpty) { + return; + } + final file = r + .files + .first; + final bytes = + file.bytes; + if (bytes == + null) { + return; + } + final ext = + file.extension ?? + 'png'; + String? url; + try { + url = await ref + .read( + tasksControllerProvider, + ) + .uploadActionImage( + taskId: + task.id, + bytes: + bytes, + extension: + ext, + ); + } catch ( + e + ) { + showErrorSnackBar( + context, + 'Upload error: $e', + ); + return; + } + if (url == + null) { + showErrorSnackBar( + context, + 'Image upload failed (no URL returned)', + ); + return; + } + final trimmedUrl = + url.trim(); + final idx = + _actionController + ?.selection + .baseOffset ?? + 0; + // ignore: avoid_print + print( + 'inserting image embed idx=$idx url=$trimmedUrl', + ); + _actionController + ?.document + .insert( + idx, + quill + .BlockEmbed.image( + trimmedUrl, + ), + ); + } catch (_) {} + }, + ), + ], + ), + ), + Expanded( + child: MouseRegion( + cursor: SystemMouseCursors + .text, + child: quill.QuillEditor.basic( + controller: + _actionController!, + focusNode: + _actionFocusNode, + scrollController: + _actionScrollController, + config: quill.QuillEditorConfig( + embedBuilders: const [ + _ImageEmbedBuilder(), + ], + scrollable: true, + padding: + EdgeInsets.zero, ), - ), - Expanded( - child: MouseRegion( - cursor: - SystemMouseCursors.text, - child: quill.QuillEditor.basic( - controller: - _actionController!, - focusNode: - _actionFocusNode, - scrollController: - _actionScrollController, - config: quill.QuillEditorConfig( - embedBuilders: const [ - _ImageEmbedBuilder(), - ], - scrollable: true, - padding: - EdgeInsets.zero, ), ), ), - ), - ], - ), - Positioned( - right: 6, - bottom: 6, - child: _actionSaving - ? SizedBox( - width: 20, - height: 20, - child: ScaleTransition( - scale: _savePulse, - child: const Icon( - Icons.save, - size: 16, - ), - ), - ) - : _actionSaved - ? SizedBox( - width: 20, - height: 20, - child: Stack( - alignment: - Alignment.center, - children: const [ - Icon( + ], + ), + Positioned( + right: 6, + bottom: 6, + child: _actionSaving + ? SizedBox( + width: 20, + height: 20, + child: ScaleTransition( + scale: _savePulse, + child: const Icon( Icons.save, size: 16, - color: Colors.green, ), - Positioned( - right: -2, - bottom: -2, - child: Icon( - Icons.check, - size: 10, + ), + ) + : _actionSaved + ? SizedBox( + width: 20, + height: 20, + child: Stack( + alignment: + Alignment.center, + children: const [ + Icon( + Icons.save, + size: 16, color: - Colors.white, + Colors.green, ), - ), - ], - ), - ) - : const SizedBox.shrink(), - ), - ], + Positioned( + right: -2, + bottom: -2, + child: Icon( + Icons.check, + size: 10, + color: Colors + .white, + ), + ), + ], + ), + ) + : const SizedBox.shrink(), + ), + ], + ), ), ), ], @@ -3398,156 +3442,386 @@ class _TaskDetailScreenState extends ConsumerState ]; String? selectedOffice = task.officeId; - await showDialog( - context: context, - builder: (dialogContext) { - var saving = false; - return StatefulBuilder( - builder: (context, setDialogState) { - return AlertDialog( - shape: dialogShape, - title: const Text('Edit Task'), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TypeAheadFormField( - textFieldConfiguration: TextFieldConfiguration( - controller: titleCtrl, - enabled: !saving, - decoration: const InputDecoration(labelText: 'Title'), - ), - suggestionsCallback: (pattern) async { - return SubjectSuggestionEngine.suggest( - existingSubjects: existingSubjects, - query: pattern, - limit: 8, - ); - }, - itemBuilder: (context, suggestion) => - ListTile(dense: true, title: Text(suggestion)), - onSuggestionSelected: (suggestion) { - titleCtrl - ..text = suggestion - ..selection = TextSelection.collapsed( - offset: suggestion.length, - ); - }, - ), - const SizedBox(height: 8), - TextField( - controller: descCtrl, - enabled: !saving, - decoration: const InputDecoration( - labelText: 'Description', - ), - maxLines: 4, - ), - const SizedBox(height: 8), - Consumer( - builder: (dialogContext, dialogRef, _) { - final officesAsync = dialogRef.watch( - officesOnceProvider, - ); - return officesAsync.when( - data: (offices) { - final officesSorted = List.from(offices) - ..sort( - (a, b) => a.name.toLowerCase().compareTo( - b.name.toLowerCase(), - ), - ); - return DropdownButtonFormField( - initialValue: selectedOffice, - decoration: const InputDecoration( - labelText: 'Office', - ), - items: [ - const DropdownMenuItem( - value: null, - child: Text('Unassigned'), - ), - for (final o in officesSorted) - DropdownMenuItem( - value: o.id, - child: Text(o.name), - ), - ], - onChanged: saving - ? null - : (v) => setDialogState( - () => selectedOffice = v, - ), - ); - }, - loading: () => const Padding( - padding: EdgeInsets.symmetric(vertical: 12), - child: LinearProgressIndicator(), - ), - error: (error, _) => const SizedBox.shrink(), - ); - }, - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: saving - ? null - : () => Navigator.of(dialogContext).pop(), - child: const Text('Cancel'), - ), - ElevatedButton( - onPressed: saving - ? null - : () async { - final title = - SubjectSuggestionEngine.normalizeDisplay( - titleCtrl.text.trim(), - ); - final desc = descCtrl.text.trim(); - setDialogState(() => saving = true); - try { - await ref - .read(tasksControllerProvider) - .updateTaskFields( - taskId: task.id, - title: title.isEmpty ? null : title, - description: desc.isEmpty ? null : desc, - officeId: selectedOffice, - ); - ref.invalidate(tasksProvider); - ref.invalidate(taskByIdProvider(task.id)); - if (!mounted) return; - Navigator.of(dialogContext).pop(); - showSuccessSnackBar(context, 'Task updated'); - } catch (e) { - if (!mounted) return; - showErrorSnackBar( - context, - 'Failed to update task: $e', - ); - } finally { - if (dialogContext.mounted) { - setDialogState(() => saving = false); - } - } - }, - child: saving - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Save'), - ), - ], - ); - }, + // ---- Title-field AI-button debounce state ---- + // We listen to titleCtrl directly instead of using TypeAheadFormField's + // onChanged, because TypeAheadFormField fires onChanged unpredictably + // (on focus, on overlay teardown, on suggestion selection) and we have + // no reliable way to distinguish those from real user keystrokes. + var showTitleGemini = false; + var titleSuppressListener = + false; // true while we set text programmatically + // Tracks the last known text so the listener can detect actual changes + // (vs spurious notifications from attaching the controller to a TextField). + var titleLastText = titleCtrl.text; + var titleDeepSeek = false; + var descDeepSeek = false; + Timer? titleTypingTimer; + // Assigned by StatefulBuilder on every build so the listener can call it. + void Function(VoidCallback)? titleSetState; + + void titleListener() { + if (titleSuppressListener) return; + // Only react when the text actually changed — the controller fires + // notifications on attach/focus/selection changes too. + if (titleCtrl.text == titleLastText) return; + titleLastText = titleCtrl.text; + titleTypingTimer?.cancel(); + // Hide immediately. + titleSetState?.call(() => showTitleGemini = false); + // Show after the user pauses typing. + if (titleCtrl.text.isNotEmpty) { + titleTypingTimer = Timer( + const Duration(milliseconds: 700), + () => titleSetState?.call(() => showTitleGemini = true), ); - }, + } + } + + titleCtrl.addListener(titleListener); + + try { + await showDialog( + context: context, + builder: (dialogContext) { + var saving = false; + var titleProcessing = false; + var descProcessing = false; + return StatefulBuilder( + builder: (context, setDialogState) { + titleSetState = setDialogState; // wire listener → setDialogState + return AlertDialog( + shape: dialogShape, + title: const Text('Edit Task'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: GeminiAnimatedBorder( + isProcessing: titleProcessing, + useDeepSeekColors: titleDeepSeek, + child: TypeAheadFormField( + textFieldConfiguration: TextFieldConfiguration( + controller: titleCtrl, + enabled: !saving, + decoration: const InputDecoration( + labelText: 'Title', + ), + // Debounce is handled by titleCtrl.addListener + // (declared above showDialog) to avoid + // TypeAheadFormField's unpredictable onChanged + // behaviour (fires on focus, overlay teardown, + // and suggestion selection). + ), + suggestionsCallback: (pattern) async { + return SubjectSuggestionEngine.suggest( + existingSubjects: existingSubjects, + query: pattern, + limit: 8, + ); + }, + itemBuilder: (context, suggestion) => ListTile( + dense: true, + title: Text(suggestion), + ), + onSuggestionSelected: (suggestion) { + // Suppress the titleCtrl listener while we + // set text programmatically so it doesn't + // start the debounce timer. + titleSuppressListener = true; + titleTypingTimer?.cancel(); + titleCtrl + ..text = suggestion + ..selection = TextSelection.collapsed( + offset: suggestion.length, + ); + titleSuppressListener = false; + setDialogState(() => showTitleGemini = false); + }, + ), + ), + ), + // Show Gemini button only after the user pauses + // typing and has not selected a suggestion. + if (showTitleGemini) + GeminiButton( + textController: titleCtrl, + onTextUpdated: (updatedText) { + // Suppress the ctrl listener so the AI-improved + // text doesn't restart the show-button debounce. + titleSuppressListener = true; + setDialogState(() { + titleCtrl.text = updatedText; + }); + titleSuppressListener = false; + setDialogState(() => showTitleGemini = false); + }, + onProcessingStateChanged: (isProcessing) { + setDialogState(() { + titleProcessing = isProcessing; + }); + }, + onProviderChanged: (isDeepSeek) { + setDialogState( + () => titleDeepSeek = isDeepSeek, + ); + }, + tooltip: 'Improve task title with Gemini', + promptBuilder: (_) => + 'Fix the spelling and grammar of this IT ' + 'helpdesk ticket subject. Make it concise, ' + 'clear, and professional in English. ' + 'Return ONLY the corrected subject, ' + 'no explanations:', + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: GeminiAnimatedTextField( + controller: descCtrl, + enabled: !saving, + labelText: 'Description', + maxLines: 4, + isProcessing: descProcessing, + useDeepSeekColors: descDeepSeek, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: GeminiButton( + textController: descCtrl, + onTextUpdated: (updatedText) { + setDialogState(() { + descCtrl.text = updatedText; + }); + }, + onProcessingStateChanged: (isProcessing) { + setDialogState(() { + descProcessing = isProcessing; + }); + }, + onProviderChanged: (isDeepSeek) { + setDialogState(() => descDeepSeek = isDeepSeek); + }, + tooltip: 'Improve description with Gemini', + promptBuilder: (_) { + final subject = titleCtrl.text.trim(); + final hint = subject.isNotEmpty + ? 'about "$subject" ' + : ''; + return 'Improve this IT helpdesk ticket ' + 'description ${hint}for clarity and ' + 'professionalism. Fix grammar and translate ' + 'to English. Return ONLY the improved ' + 'description, no explanations:'; + }, + ), + ), + ], + ), + const SizedBox(height: 8), + Consumer( + builder: (dialogContext, dialogRef, _) { + final officesAsync = dialogRef.watch( + officesOnceProvider, + ); + return officesAsync.when( + data: (offices) { + final officesSorted = List.from(offices) + ..sort( + (a, b) => a.name.toLowerCase().compareTo( + b.name.toLowerCase(), + ), + ); + return DropdownButtonFormField( + initialValue: selectedOffice, + decoration: const InputDecoration( + labelText: 'Office', + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('Unassigned'), + ), + for (final o in officesSorted) + DropdownMenuItem( + value: o.id, + child: Text(o.name), + ), + ], + onChanged: saving + ? null + : (v) => setDialogState( + () => selectedOffice = v, + ), + ); + }, + loading: () => const Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: LinearProgressIndicator(), + ), + error: (error, _) => const SizedBox.shrink(), + ); + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: saving + ? null + : () => Navigator.of(dialogContext).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: saving + ? null + : () async { + final title = + SubjectSuggestionEngine.normalizeDisplay( + titleCtrl.text.trim(), + ); + final desc = descCtrl.text.trim(); + setDialogState(() => saving = true); + try { + await ref + .read(tasksControllerProvider) + .updateTaskFields( + taskId: task.id, + title: title.isEmpty ? null : title, + description: desc.isEmpty ? null : desc, + officeId: selectedOffice, + ); + ref.invalidate(tasksProvider); + ref.invalidate(taskByIdProvider(task.id)); + if (!mounted) return; + Navigator.of(dialogContext).pop(); + showSuccessSnackBar(context, 'Task updated'); + } catch (e) { + if (!mounted) return; + showErrorSnackBar( + context, + 'Failed to update task: $e', + ); + } finally { + if (dialogContext.mounted) { + setDialogState(() => saving = false); + } + } + }, + child: saving + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Save'), + ), + ], + ); + }, + ); + }, + ); + } finally { + titleCtrl.removeListener(titleListener); + titleTypingTimer?.cancel(); + } + } + + /// Directly enhances the Action Taken field with Gemini — no dialog, + /// no language detection. Builds a context-aware prompt from the task's + /// title and description so the model understands what is being addressed. + Future _processActionTakenWithGemini( + BuildContext context, + WidgetRef ref, + ) async { + final plainText = _actionController?.document.toPlainText().trim() ?? ''; + if (plainText.isEmpty) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please enter some action taken text first'), + ), + ); + return; + } + + // Use task title/description as context for a richer prompt. + final task = ref.read(taskByIdProvider(widget.taskId)); + final subject = task?.title.trim() ?? ''; + final description = task?.description.trim() ?? ''; + + final hint = StringBuffer( + 'This is the action taken / workaround for an IT helpdesk ticket', ); + if (subject.isNotEmpty) hint.write(' about "$subject"'); + if (description.isNotEmpty) { + hint.write('. Ticket description: "$description"'); + } + hint.write( + '. Fix spelling and grammar, improve clarity, and translate to ' + 'professional English. Return ONLY the improved text, no explanations:', + ); + + setState(() { + _actionSaving = true; + _actionProcessing = true; + }); + try { + final aiService = AiService(); + final improvedText = await aiService.enhanceText( + plainText, + promptInstruction: hint.toString(), + ); + + final trimmed = improvedText.trim(); + // Build delta JSON directly — [{"insert": "text\n"}] — to avoid any + // race between replaceText() and reading toDelta() immediately after. + final deltaJson = jsonEncode([ + {'insert': '$trimmed\n'}, + ]); + + _actionDebounce?.cancel(); + await ref + .read(tasksControllerProvider) + .updateTask(taskId: widget.taskId, actionTaken: deltaJson); + + if (_actionController != null) { + final docLen = _actionController!.document.length; + _actionController!.replaceText( + 0, + docLen - 1, + trimmed, + TextSelection.collapsed(offset: trimmed.length), + ); + // Cancel the auto-save listener triggered by replaceText. + _actionDebounce?.cancel(); + } + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Action taken improved successfully')), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error: $e'))); + } + } finally { + setState(() { + _actionSaving = false; + _actionProcessing = false; + }); + } } bool _canAssignStaff(String role) { diff --git a/lib/screens/tasks/tasks_list_screen.dart b/lib/screens/tasks/tasks_list_screen.dart index 5c3f0ecf..add2271d 100644 --- a/lib/screens/tasks/tasks_list_screen.dart +++ b/lib/screens/tasks/tasks_list_screen.dart @@ -25,6 +25,8 @@ import '../../widgets/typing_dots.dart'; import '../../theme/app_surfaces.dart'; import '../../utils/snackbar.dart'; import '../../utils/subject_suggestions.dart'; +import '../../widgets/gemini_button.dart'; +import '../../widgets/gemini_animated_text_field.dart'; // request metadata options used in task creation/editing dialogs const List _requestTypeOptions = [ @@ -584,6 +586,10 @@ class _TasksListScreenState extends ConsumerState context: context, builder: (dialogContext) { bool saving = false; + bool titleProcessing = false; + bool descProcessing = false; + bool titleDeepSeek = false; + bool descDeepSeek = false; final officesAsync = ref.watch(officesProvider); return StatefulBuilder( builder: (context, setState) { @@ -595,39 +601,94 @@ class _TasksListScreenState extends ConsumerState child: Column( mainAxisSize: MainAxisSize.min, children: [ - TypeAheadFormField( - textFieldConfiguration: TextFieldConfiguration( - controller: titleController, - decoration: const InputDecoration( - labelText: 'Task title', + Row( + children: [ + Expanded( + child: GeminiAnimatedBorder( + isProcessing: titleProcessing, + useDeepSeekColors: titleDeepSeek, + child: TypeAheadFormField( + textFieldConfiguration: TextFieldConfiguration( + controller: titleController, + decoration: const InputDecoration( + labelText: 'Task title', + ), + enabled: !saving, + ), + suggestionsCallback: (pattern) async { + return SubjectSuggestionEngine.suggest( + existingSubjects: existingSubjects, + query: pattern, + limit: 8, + ); + }, + itemBuilder: (context, suggestion) => ListTile( + dense: true, + title: Text(suggestion), + ), + onSuggestionSelected: (suggestion) { + titleController + ..text = suggestion + ..selection = TextSelection.collapsed( + offset: suggestion.length, + ); + }, + ), + ), ), - enabled: !saving, - ), - suggestionsCallback: (pattern) async { - return SubjectSuggestionEngine.suggest( - existingSubjects: existingSubjects, - query: pattern, - limit: 8, - ); - }, - itemBuilder: (context, suggestion) => - ListTile(dense: true, title: Text(suggestion)), - onSuggestionSelected: (suggestion) { - titleController - ..text = suggestion - ..selection = TextSelection.collapsed( - offset: suggestion.length, - ); - }, + GeminiButton( + textController: titleController, + onTextUpdated: (updatedText) { + setState(() { + titleController.text = updatedText; + }); + }, + onProcessingStateChanged: (isProcessing) { + setState(() { + titleProcessing = isProcessing; + }); + }, + onProviderChanged: (isDeepSeek) { + setState(() => titleDeepSeek = isDeepSeek); + }, + tooltip: 'Improve task title with Gemini', + ), + ], ), const SizedBox(height: 12), - TextField( - controller: descriptionController, - decoration: const InputDecoration( - labelText: 'Description', - ), - maxLines: 3, - enabled: !saving, + Row( + children: [ + Expanded( + child: GeminiAnimatedTextField( + controller: descriptionController, + labelText: 'Description', + maxLines: 3, + enabled: !saving, + isProcessing: descProcessing, + useDeepSeekColors: descDeepSeek, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: GeminiButton( + textController: descriptionController, + onTextUpdated: (updatedText) { + setState(() { + descriptionController.text = updatedText; + }); + }, + onProcessingStateChanged: (isProcessing) { + setState(() { + descProcessing = isProcessing; + }); + }, + onProviderChanged: (isDeepSeek) { + setState(() => descDeepSeek = isDeepSeek); + }, + tooltip: 'Improve description with Gemini', + ), + ), + ], ), const SizedBox(height: 12), officesAsync.when( diff --git a/lib/services/ai_service.dart b/lib/services/ai_service.dart new file mode 100644 index 00000000..c05646d7 --- /dev/null +++ b/lib/services/ai_service.dart @@ -0,0 +1,202 @@ +import 'dart:convert'; + +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:google_generative_ai/google_generative_ai.dart'; +import 'package:http/http.dart' as http; + +/// Unified AI text-enhancement service. +/// +/// Tries Gemini (free-tier flash/lite models) first, with automatic +/// 429-retry across all discovered models. If every Gemini model fails +/// (quota exhausted or any unrecoverable error) it seamlessly falls back +/// to the DeepSeek API. +/// +/// Usage: +/// ```dart +/// final result = await AiService().enhanceText( +/// myText, +/// promptInstruction: 'Fix grammar and translate to English …', +/// ); +/// ``` +class AiService { + static final AiService _instance = AiService._internal(); + factory AiService() => _instance; + + late final String _geminiApiKey; + late final String _deepseekApiKey; + + /// Cached Gemini model IDs (flash / lite, generateContent-capable). + List _geminiModels = []; + + AiService._internal() { + final gKey = dotenv.env['GEMINI_API_KEY']; + if (gKey == null || gKey.isEmpty) { + throw Exception('GEMINI_API_KEY not found in .env'); + } + _geminiApiKey = gKey; + + final dsKey = dotenv.env['DEEPSEEK_API_KEY']; + if (dsKey == null || dsKey.isEmpty) { + throw Exception('DEEPSEEK_API_KEY not found in .env'); + } + _deepseekApiKey = dsKey; + } + + // --------------------------------------------------------------------------- + // Public API + // --------------------------------------------------------------------------- + + /// Fixes spelling / grammar, improves clarity, and translates [text] to + /// professional English. + /// + /// Supply [promptInstruction] to give the model field-specific context + /// (e.g. "This is an IT helpdesk ticket subject …"). If omitted a + /// sensible generic instruction is used. + /// + /// Order of preference: + /// 1. Gemini flash / lite models (auto-retry on 429 across all models) + /// 2. DeepSeek `deepseek-chat` (fallback on total Gemini failure) + /// + /// Throws only if **both** providers fail. + /// [onFallbackToDeepSeek] is called (from the same isolate) just before + /// switching to the DeepSeek provider, so callers can update UI accordingly. + /// + /// This method never throws — if both providers fail it returns [text] unchanged. + Future enhanceText( + String text, { + String? promptInstruction, + void Function()? onFallbackToDeepSeek, + }) async { + if (text.trim().isEmpty) return text; + + final instruction = + promptInstruction ?? + 'Fix spelling and grammar, improve clarity, and translate to ' + 'professional English. Return ONLY the improved text, ' + 'no explanations:'; + final prompt = '$instruction\n\n"$text"'; + + // --- 1. Try Gemini --- + try { + return await _geminiGenerate(prompt, fallback: text); + } catch (_) { + // All Gemini models failed — fall through to DeepSeek. + onFallbackToDeepSeek?.call(); + } + + // --- 2. Fallback: DeepSeek --- + try { + return await _deepseekGenerate(prompt, fallback: text); + } catch (_) { + // Both providers failed — return original text unchanged. + return text; + } + } + + // --------------------------------------------------------------------------- + // Gemini + // --------------------------------------------------------------------------- + + Future> _getGeminiModels() async { + if (_geminiModels.isNotEmpty) return _geminiModels; + + try { + final uri = Uri.parse( + 'https://generativelanguage.googleapis.com/v1beta/models' + '?key=$_geminiApiKey', + ); + final res = await http.get(uri); + if (res.statusCode == 200) { + final data = jsonDecode(res.body) as Map; + final rawModels = (data['models'] as List?) ?? []; + final discovered = []; + for (final m in rawModels) { + final fullName = m['name'] as String? ?? ''; + final lower = fullName.toLowerCase(); + final methods = + (m['supportedGenerationMethods'] as List?) ?? []; + if (methods.contains('generateContent') && + (lower.contains('flash') || lower.contains('lite'))) { + final id = fullName.startsWith('models/') + ? fullName.substring('models/'.length) + : fullName; + discovered.add(id); + } + } + discovered.sort((a, b) => b.compareTo(a)); + _geminiModels = discovered; + } + } catch (_) { + // Fall through to hard-coded list. + } + + if (_geminiModels.isEmpty) { + _geminiModels = [ + 'gemini-2.5-flash-lite', + 'gemini-2.5-flash', + 'gemini-2.0-flash', + 'gemini-1.5-flash', + ]; + } + + return _geminiModels; + } + + Future _geminiGenerate( + String prompt, { + required String fallback, + }) async { + final models = await _getGeminiModels(); + + Object? lastError; + for (final modelId in models) { + try { + final model = GenerativeModel(model: modelId, apiKey: _geminiApiKey); + final response = await model.generateContent([Content.text(prompt)]); + return response.text ?? fallback; + } catch (e) { + lastError = e; + // Try the next model regardless of error type. + } + } + + throw Exception('All Gemini models failed. Last error: $lastError'); + } + + // --------------------------------------------------------------------------- + // DeepSeek (OpenAI-compatible REST) + // --------------------------------------------------------------------------- + + Future _deepseekGenerate( + String prompt, { + required String fallback, + }) async { + const url = 'https://api.deepseek.com/chat/completions'; + final body = jsonEncode({ + 'model': 'deepseek-chat', + 'messages': [ + {'role': 'user', 'content': prompt}, + ], + }); + + final res = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $_deepseekApiKey', + }, + body: body, + ); + + if (res.statusCode == 200) { + final data = jsonDecode(res.body) as Map; + final choices = data['choices'] as List?; + final content = choices?.firstOrNull?['message']?['content'] as String?; + return content?.trim() ?? fallback; + } + + throw Exception( + 'DeepSeek request failed (HTTP ${res.statusCode}): ${res.body}', + ); + } +} diff --git a/lib/services/gemini_service.dart b/lib/services/gemini_service.dart new file mode 100644 index 00000000..e80fb62e --- /dev/null +++ b/lib/services/gemini_service.dart @@ -0,0 +1,144 @@ +import 'dart:convert'; + +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:google_generative_ai/google_generative_ai.dart'; +import 'package:http/http.dart' as http; + +class GeminiService { + static final GeminiService _instance = GeminiService._internal(); + + late final String _apiKey; + + /// Cache of valid model IDs (flash/lite, supporting generateContent). + List _validModels = []; + + factory GeminiService() => _instance; + + GeminiService._internal() { + final apiKey = dotenv.env['GEMINI_API_KEY']; + if (apiKey == null || apiKey.isEmpty) { + throw Exception('GEMINI_API_KEY not found in .env file'); + } + _apiKey = apiKey; + } + + // --------------------------------------------------------------------------- + // Model discovery + // --------------------------------------------------------------------------- + + /// Queries the Gemini REST API for available models and caches those that + /// - support `generateContent`, AND + /// - contain "flash" or "lite" in their name (free-tier / fast models). + /// + /// Returns a stable cached list on subsequent calls. + Future> _getValidModels() async { + if (_validModels.isNotEmpty) return _validModels; + + try { + final uri = Uri.parse( + 'https://generativelanguage.googleapis.com/v1beta/models?key=$_apiKey', + ); + final response = await http.get(uri); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + final rawModels = (data['models'] as List?) ?? []; + + final discovered = []; + for (final m in rawModels) { + final fullName = (m['name'] as String? ?? ''); + final lower = fullName.toLowerCase(); + final methods = + (m['supportedGenerationMethods'] as List?) ?? []; + + if (methods.contains('generateContent') && + (lower.contains('flash') || lower.contains('lite'))) { + // Strip the "models/" prefix so it can be passed directly to + // GenerativeModel(model: ...). + final id = fullName.startsWith('models/') + ? fullName.substring('models/'.length) + : fullName; + discovered.add(id); + } + } + + // Sort descending so newer/more-capable models are tried first. + discovered.sort((a, b) => b.compareTo(a)); + _validModels = discovered; + } + } catch (_) { + // Fall back to hard-coded list of known free-tier models below. + } + + // If discovery failed or returned nothing, use safe known fallbacks. + if (_validModels.isEmpty) { + _validModels = [ + 'gemini-2.5-flash-lite', + 'gemini-2.5-flash', + 'gemini-2.0-flash', + 'gemini-1.5-flash', + ]; + } + + return _validModels; + } + + // --------------------------------------------------------------------------- + // Public API + // --------------------------------------------------------------------------- + + /// Fixes spelling/grammar, improves clarity, and translates [text] to + /// professional English. + /// + /// Provide a custom [promptInstruction] to give Gemini field-specific + /// context (subject, description, action-taken). If omitted a sensible + /// default is used. + /// + /// Automatically retries with the next available model on 429 / quota + /// errors to minimise wasted quota calls. + Future enhanceText(String text, {String? promptInstruction}) async { + if (text.trim().isEmpty) return text; + final instruction = + promptInstruction ?? + 'Fix spelling and grammar, improve clarity, and translate to ' + 'professional English. Return ONLY the improved text, no explanations:'; + final prompt = '$instruction\n\n"$text"'; + return _generateWithRetry(prompt, fallback: text); + } + + // --------------------------------------------------------------------------- + // Internal helpers + // --------------------------------------------------------------------------- + + /// Sends [prompt] to Gemini, retrying across all valid models on 429 errors. + /// Returns the model response on success, or throws if all models fail. + Future _generateWithRetry( + String prompt, { + required String fallback, + }) async { + final models = await _getValidModels(); + + for (int i = 0; i < models.length; i++) { + try { + final model = GenerativeModel(model: models[i], apiKey: _apiKey); + final response = await model.generateContent([Content.text(prompt)]); + return response.text ?? fallback; + } catch (e) { + final msg = e.toString().toLowerCase(); + final is429 = + msg.contains('429') || + msg.contains('quota') || + msg.contains('resource_exhausted'); + + if (!is429 || i == models.length - 1) { + // Non-quota error or last model — give up. + throw Exception('Gemini request failed: $e'); + } + // Quota hit — try the next model. + } + } + + // Should not reach here, but safety fallback. + throw Exception('All Gemini models exhausted'); + } +} diff --git a/lib/utils/subject_suggestions.dart b/lib/utils/subject_suggestions.dart new file mode 100644 index 00000000..5dcd7d1e --- /dev/null +++ b/lib/utils/subject_suggestions.dart @@ -0,0 +1,237 @@ +import 'dart:math' as math; + +class SubjectSuggestionEngine { + const SubjectSuggestionEngine._(); + + static List suggest({ + required Iterable existingSubjects, + required String query, + int limit = 8, + }) { + final statsByKey = {}; + + for (final raw in existingSubjects) { + final cleaned = normalizeDisplay(raw); + if (cleaned.isEmpty) { + continue; + } + final key = normalizeKey(cleaned); + if (key.isEmpty) { + continue; + } + final stats = statsByKey.putIfAbsent( + key, + () => _SubjectStats(display: cleaned), + ); + stats.count += 1; + if (_isBetterDisplay(cleaned, stats.display)) { + stats.display = cleaned; + } + } + + if (statsByKey.isEmpty) { + return const []; + } + + final cleanedQuery = normalizeDisplay(query); + final queryKey = normalizeKey(cleanedQuery); + + final scored = + statsByKey.entries + .map((entry) { + final value = entry.value; + final score = _score( + candidateKey: entry.key, + candidateDisplay: value.display, + count: value.count, + queryKey: queryKey, + ); + return _ScoredSubject(subject: value.display, score: score); + }) + .where((entry) => entry.score > 0) + .toList() + ..sort((a, b) { + final byScore = b.score.compareTo(a.score); + if (byScore != 0) { + return byScore; + } + return a.subject.toLowerCase().compareTo(b.subject.toLowerCase()); + }); + + return scored.take(limit).map((entry) => entry.subject).toList(); + } + + static String normalizeDisplay(String input) { + final trimmed = input.trim(); + if (trimmed.isEmpty) { + return ''; + } + + final compactWhitespace = trimmed.replaceAll(RegExp(r'\s+'), ' '); + final punctuationSpacing = compactWhitespace + .replaceAll(RegExp(r'\s+([,.;:!?])'), r'$1') + .replaceAll(RegExp(r'([,.;:!?])(\S)'), r'$1 $2') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + + final words = punctuationSpacing.split(' '); + final correctedWords = words.map(_correctWord).toList(growable: false); + final sentence = correctedWords.join(' ').trim(); + + if (sentence.isEmpty) { + return ''; + } + + return sentence[0].toUpperCase() + sentence.substring(1); + } + + static String normalizeKey(String input) { + final lowered = input.toLowerCase(); + return lowered + .replaceAll(RegExp(r'[^a-z0-9\s]'), ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + } + + static double _score({ + required String candidateKey, + required String candidateDisplay, + required int count, + required String queryKey, + }) { + final popularity = math.log(count + 1) * 0.1; + + if (queryKey.isEmpty) { + return 0.5 + popularity; + } + + final startsWith = candidateKey.startsWith(queryKey) ? 1.2 : 0.0; + final contains = + !candidateKey.startsWith(queryKey) && candidateKey.contains(queryKey) + ? 0.5 + : 0.0; + + final vectorSimilarity = _cosineSimilarity( + _tokenVector(candidateKey), + _tokenVector(queryKey), + ); + + final displayLower = candidateDisplay.toLowerCase(); + final queryLower = queryKey.toLowerCase(); + final editLikeBoost = displayLower.contains(queryLower) ? 0.25 : 0.0; + + return (vectorSimilarity * 2.0) + + startsWith + + contains + + editLikeBoost + + popularity; + } + + static Map _tokenVector(String input) { + final tokens = input + .split(' ') + .where((token) => token.isNotEmpty) + .toList(growable: false); + final vector = {}; + for (final token in tokens) { + vector[token] = (vector[token] ?? 0) + 1; + } + return vector; + } + + static double _cosineSimilarity(Map a, Map b) { + if (a.isEmpty || b.isEmpty) { + return 0; + } + + var dot = 0.0; + var normA = 0.0; + var normB = 0.0; + + for (final entry in a.entries) { + final av = entry.value.toDouble(); + normA += av * av; + final bv = b[entry.key]?.toDouble() ?? 0.0; + dot += av * bv; + } + + for (final entry in b.entries) { + final bv = entry.value.toDouble(); + normB += bv * bv; + } + + final denominator = math.sqrt(normA) * math.sqrt(normB); + if (denominator == 0) { + return 0; + } + + return dot / denominator; + } + + static String _correctWord(String rawWord) { + if (rawWord.isEmpty) { + return rawWord; + } + + final punctuationMatch = RegExp( + r'^([^a-zA-Z0-9]*)(.*?)([^a-zA-Z0-9]*)$', + ).firstMatch(rawWord); + if (punctuationMatch == null) { + return rawWord; + } + + final leading = punctuationMatch.group(1) ?? ''; + final core = punctuationMatch.group(2) ?? ''; + final trailing = punctuationMatch.group(3) ?? ''; + + if (core.isEmpty) { + return rawWord; + } + + final isAcronym = core.length > 1 && core == core.toUpperCase(); + final correctedCore = isAcronym + ? core + : core[0].toUpperCase() + core.substring(1).toLowerCase(); + + return '$leading$correctedCore$trailing'; + } + + static bool _isBetterDisplay(String candidate, String current) { + if (candidate == current) { + return false; + } + + final candidatePenalty = _displayPenalty(candidate); + final currentPenalty = _displayPenalty(current); + if (candidatePenalty != currentPenalty) { + return candidatePenalty < currentPenalty; + } + + return candidate.length < current.length; + } + + static int _displayPenalty(String value) { + var penalty = 0; + if (value.contains(RegExp(r'\s{2,}'))) { + penalty += 2; + } + if (value == value.toUpperCase()) { + penalty += 1; + } + return penalty; + } +} + +class _SubjectStats { + _SubjectStats({required this.display}); + + String display; + int count = 0; +} + +class _ScoredSubject { + _ScoredSubject({required this.subject, required this.score}); + + final String subject; + final double score; +} diff --git a/lib/widgets/gemini_animated_text_field.dart b/lib/widgets/gemini_animated_text_field.dart new file mode 100644 index 00000000..134a155b --- /dev/null +++ b/lib/widgets/gemini_animated_text_field.dart @@ -0,0 +1,280 @@ +import 'dart:math' as math; +import 'package:flutter/material.dart'; + +// How far the glow bleeds outside the child's bounds on each side. +const _kGlowSpread = 12.0; + +/// Wraps any widget with a soft outward Gemini-colored glow that animates +/// while [isProcessing] is true and disappears when false. +/// The glow paints *outside* the child's bounds without affecting layout. +class GeminiAnimatedBorder extends StatefulWidget { + final Widget child; + final bool isProcessing; + + /// Must match the border-radius of the wrapped widget so the glow follows its shape. + final double borderRadius; + + /// When true the glow uses DeepSeek blue tones instead of Gemini colours. + final bool useDeepSeekColors; + + const GeminiAnimatedBorder({ + super.key, + required this.child, + required this.isProcessing, + this.borderRadius = 8, + this.useDeepSeekColors = false, + }); + + @override + State createState() => _GeminiAnimatedBorderState(); +} + +class _GeminiAnimatedBorderState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + ); + if (widget.isProcessing) _controller.repeat(); + } + + @override + void didUpdateWidget(GeminiAnimatedBorder old) { + super.didUpdateWidget(old); + if (widget.isProcessing && !old.isProcessing) { + _controller.repeat(); + } else if (!widget.isProcessing && old.isProcessing) { + _controller.stop(); + _controller.reset(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!widget.isProcessing) return widget.child; + + return AnimatedBuilder( + animation: _controller, + child: widget.child, + builder: (context, child) { + return Stack( + clipBehavior: Clip.none, + children: [ + // Glow layer positioned to bleed outside the child on all sides. + Positioned( + left: -_kGlowSpread, + right: -_kGlowSpread, + top: -_kGlowSpread, + bottom: -_kGlowSpread, + child: CustomPaint( + painter: _GeminiGlowPainter( + rotation: _controller.value, + borderRadius: widget.borderRadius, + glowSpread: _kGlowSpread, + deepSeekMode: widget.useDeepSeekColors, + ), + ), + ), + child!, + ], + ); + }, + ); + } +} + +/// Wraps a TextField with a rotating gradient border that animates with Gemini colors +/// when processing is active. Shows normal border appearance when not processing. +class GeminiAnimatedTextField extends StatefulWidget { + final TextEditingController controller; + final String? labelText; + final int? maxLines; + final bool enabled; + final bool isProcessing; + final InputDecoration? decoration; + + /// When true the glow uses DeepSeek blue tones instead of Gemini colours. + final bool useDeepSeekColors; + + const GeminiAnimatedTextField({ + super.key, + required this.controller, + this.labelText, + this.maxLines, + this.enabled = true, + this.isProcessing = false, + this.decoration, + this.useDeepSeekColors = false, + }); + + @override + State createState() => + _GeminiAnimatedTextFieldState(); +} + +class _GeminiAnimatedTextFieldState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + ); + if (widget.isProcessing) _controller.repeat(); + } + + @override + void didUpdateWidget(GeminiAnimatedTextField oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isProcessing && !oldWidget.isProcessing) { + _controller.repeat(); + } else if (!widget.isProcessing && oldWidget.isProcessing) { + _controller.stop(); + _controller.reset(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final field = TextField( + controller: widget.controller, + enabled: widget.enabled && !widget.isProcessing, + maxLines: widget.maxLines, + decoration: + widget.decoration ?? InputDecoration(labelText: widget.labelText), + ); + + if (!widget.isProcessing) return field; + + return AnimatedBuilder( + animation: _controller, + child: field, + builder: (context, child) { + return Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + left: -_kGlowSpread, + right: -_kGlowSpread, + top: -_kGlowSpread, + bottom: -_kGlowSpread, + child: CustomPaint( + painter: _GeminiGlowPainter( + rotation: _controller.value, + borderRadius: 4, + glowSpread: _kGlowSpread, + deepSeekMode: widget.useDeepSeekColors, + ), + ), + ), + child!, + ], + ); + }, + ); + } +} + +/// Paints a soft outward glow using layered blurred strokes. +/// The [size] passed to [paint] is LARGER than the child by [glowSpread] on +/// every side, so the rrect is inset by [glowSpread] to sit exactly on the +/// child's border, and the blur bleeds outward. +class _GeminiGlowPainter extends CustomPainter { + final double rotation; + final double borderRadius; + final double glowSpread; + final bool deepSeekMode; + + // Gemini brand colors — closed loop for a seamless sweep. + static const _geminiColors = [ + Color(0xFF4285F4), // Blue + Color(0xFFEA4335), // Red + Color(0xFFFBBC04), // Yellow + Color(0xFF34A853), // Green + Color(0xFF4285F4), // Blue (close loop) + ]; + static const _geminiStops = [0.0, 0.25, 0.5, 0.75, 1.0]; + + // DeepSeek brand colors — pure blue closed loop. + static const _deepSeekColors = [ + Color(0xFF4D9BFF), // Sky blue + Color(0xFF1A56DB), // Deep blue + Color(0xFF00CFFF), // Cyan + Color(0xFF4D9BFF), // Sky blue (close loop) + ]; + static const _deepSeekStops = [0.0, 0.33, 0.66, 1.0]; + + const _GeminiGlowPainter({ + required this.rotation, + required this.borderRadius, + required this.glowSpread, + this.deepSeekMode = false, + }); + + @override + void paint(Canvas canvas, Size size) { + // The rect that coincides with the actual child's outline. + final childRect = Rect.fromLTWH( + glowSpread, + glowSpread, + size.width - glowSpread * 2, + size.height - glowSpread * 2, + ); + final rrect = RRect.fromRectAndRadius( + childRect, + Radius.circular(borderRadius), + ); + + final colors = deepSeekMode ? _deepSeekColors : _geminiColors; + final stops = deepSeekMode ? _deepSeekStops : _geminiStops; + final shader = SweepGradient( + colors: colors, + stops: stops, + transform: GradientRotation(rotation * 2 * math.pi), + ).createShader(childRect); + + // Outer glow — wide stroke + strong blur spreads the color outward. + canvas.drawRRect( + rrect, + Paint() + ..shader = shader + ..style = PaintingStyle.stroke + ..strokeWidth = glowSpread * 1.6 + ..maskFilter = MaskFilter.blur(BlurStyle.normal, glowSpread), + ); + + // Inner glow — narrower, less blurred for a crisper halo near the border. + canvas.drawRRect( + rrect, + Paint() + ..shader = shader + ..style = PaintingStyle.stroke + ..strokeWidth = glowSpread * 0.7 + ..maskFilter = MaskFilter.blur(BlurStyle.normal, glowSpread * 0.4), + ); + } + + @override + bool shouldRepaint(_GeminiGlowPainter old) => + old.rotation != rotation || old.deepSeekMode != deepSeekMode; +} diff --git a/lib/widgets/gemini_button.dart b/lib/widgets/gemini_button.dart new file mode 100644 index 00000000..5af1a44e --- /dev/null +++ b/lib/widgets/gemini_button.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import '../services/ai_service.dart'; + +typedef TextUpdateCallback = void Function(String updatedText); +typedef ProcessingStateCallback = void Function(bool isProcessing); + +/// An AI icon button that immediately enhances the text in [textController] +/// when pressed — no dialog, no language detection. +/// +/// Provide [promptBuilder] to give the AI field-specific context. It receives +/// the current (trimmed) text and must return the prompt instruction string. +class GeminiButton extends StatefulWidget { + final TextEditingController textController; + final TextUpdateCallback onTextUpdated; + final ProcessingStateCallback? onProcessingStateChanged; + final String? tooltip; + + /// Optional callback that builds the Gemini prompt instruction from the + /// current field text (called at press time, so captures live context). + final String Function(String text)? promptBuilder; + + /// Called when the active AI provider changes. + /// [isDeepSeek] is true once Gemini fails and DeepSeek takes over. + /// Called with false again when processing finishes. + final void Function(bool isDeepSeek)? onProviderChanged; + + const GeminiButton({ + super.key, + required this.textController, + required this.onTextUpdated, + this.onProcessingStateChanged, + this.tooltip, + this.promptBuilder, + this.onProviderChanged, + }); + + @override + State createState() => _GeminiButtonState(); +} + +class _GeminiButtonState extends State { + bool _isProcessing = false; + late final AiService _aiService; + + @override + void initState() { + super.initState(); + _aiService = AiService(); + } + + Future _enhance(BuildContext context) async { + final text = widget.textController.text.trim(); + if (text.isEmpty) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please enter some text first')), + ); + return; + } + + setState(() { + _isProcessing = true; + }); + widget.onProcessingStateChanged?.call(true); + widget.onProviderChanged?.call(false); + try { + final instruction = widget.promptBuilder?.call(text); + final improvedText = await _aiService.enhanceText( + text, + promptInstruction: instruction, + onFallbackToDeepSeek: () { + if (mounted) { + widget.onProviderChanged?.call(true); + } + }, + ); + final trimmed = improvedText.trim(); + widget.textController.text = trimmed; + widget.onTextUpdated(trimmed); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Text improved successfully')), + ); + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error: $e'))); + } finally { + if (mounted) { + setState(() { + _isProcessing = false; + }); + } + widget.onProcessingStateChanged?.call(false); + widget.onProviderChanged?.call(false); + } + } + + @override + Widget build(BuildContext context) { + return IconButton( + tooltip: widget.tooltip ?? 'Improve with Gemini', + icon: _isProcessing + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Image.asset( + 'assets/gemini_icon.png', + width: 24, + height: 24, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.auto_awesome), + ), + onPressed: _isProcessing ? null : () => _enhance(context), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 8e3ad6bb..d161b2fb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -709,6 +709,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.3.3" + google_generative_ai: + dependency: "direct main" + description: + name: google_generative_ai + sha256: "71f613d0247968992ad87a0eb21650a566869757442ba55a31a81be6746e0d1f" + url: "https://pub.dev" + source: hosted + version: "0.4.7" gotrue: dependency: transitive description: @@ -742,7 +750,7 @@ packages: source: hosted version: "0.15.6" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" diff --git a/pubspec.yaml b/pubspec.yaml index 6d7bdd45..e55feab5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,8 @@ dependencies: uuid: ^4.1.0 skeletonizer: ^2.1.3 fl_chart: ^0.70.2 + google_generative_ai: ^0.4.0 + http: ^1.2.0 dev_dependencies: flutter_test: