AI Subject, Description and Action taken support
This commit is contained in:
parent
b5449f7842
commit
c123c09233
|
|
@ -334,7 +334,7 @@ final tasksProvider = StreamProvider<List<Task>>((ref) {
|
||||||
.cast<Map<String, dynamic>>()
|
.cast<Map<String, dynamic>>()
|
||||||
.map(Task.fromMap)
|
.map(Task.fromMap)
|
||||||
.toList();
|
.toList();
|
||||||
final hash = tasks.fold('', (h, t) => '$h${t.id}');
|
final hash = tasks.fold('', (h, t) => '$h${t.hashCode}');
|
||||||
if (!controller.isClosed && hash != lastResultHash) {
|
if (!controller.isClosed && hash != lastResultHash) {
|
||||||
lastResultHash = hash;
|
lastResultHash = hash;
|
||||||
controller.add(tasks); // emit immediately – no debounce
|
controller.add(tasks); // emit immediately – no debounce
|
||||||
|
|
@ -365,7 +365,7 @@ final tasksProvider = StreamProvider<List<Task>>((ref) {
|
||||||
})
|
})
|
||||||
.listen(
|
.listen(
|
||||||
(tasks) {
|
(tasks) {
|
||||||
final hash = tasks.fold('', (h, t) => '$h${t.id}');
|
final hash = tasks.fold('', (h, t) => '$h${t.hashCode}');
|
||||||
if (hash != lastResultHash) {
|
if (hash != lastResultHash) {
|
||||||
lastResultHash = hash;
|
lastResultHash = hash;
|
||||||
emitDebounced(tasks);
|
emitDebounced(tasks);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// ignore_for_file: use_build_context_synchronously
|
// ignore_for_file: use_build_context_synchronously
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../models/profile.dart';
|
import '../../models/profile.dart';
|
||||||
|
|
@ -12,7 +13,6 @@ import '../../models/office.dart';
|
||||||
import '../../providers/notifications_provider.dart';
|
import '../../providers/notifications_provider.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||||
import '../../providers/services_provider.dart';
|
import '../../providers/services_provider.dart';
|
||||||
|
|
@ -99,6 +99,10 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
// Rich text editor for Action taken
|
// Rich text editor for Action taken
|
||||||
quill.QuillController? _actionController;
|
quill.QuillController? _actionController;
|
||||||
Timer? _actionDebounce;
|
Timer? _actionDebounce;
|
||||||
|
|
||||||
|
/// Tracks the last plain-text snapshot so the addListener callback can
|
||||||
|
/// distinguish real edits from cursor / selection / focus notifications.
|
||||||
|
String _actionLastPlain = '';
|
||||||
late final FocusNode _actionFocusNode;
|
late final FocusNode _actionFocusNode;
|
||||||
late final ScrollController _actionScrollController;
|
late final ScrollController _actionScrollController;
|
||||||
Timer? _requestedDebounce;
|
Timer? _requestedDebounce;
|
||||||
|
|
@ -320,18 +324,33 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
_actionController = quill.QuillController.basic();
|
_actionController = quill.QuillController.basic();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach auto-save listener for action taken (debounced)
|
// Snapshot current content so the listener can skip non-edit
|
||||||
|
// notifications (cursor moves, selection changes, focus events).
|
||||||
|
_actionLastPlain =
|
||||||
|
_actionController?.document.toPlainText().trim() ?? '';
|
||||||
|
|
||||||
|
// Attach auto-save listener for action taken (debounced).
|
||||||
|
// QuillController fires notifications on every cursor /
|
||||||
|
// selection / formatting change — not just text edits. We
|
||||||
|
// compare plaintext to avoid endlessly resetting the timer.
|
||||||
_actionController?.addListener(() {
|
_actionController?.addListener(() {
|
||||||
|
final currentPlain =
|
||||||
|
_actionController?.document.toPlainText().trim() ?? '';
|
||||||
|
if (currentPlain == _actionLastPlain) return; // no text change
|
||||||
|
_actionLastPlain = currentPlain;
|
||||||
|
|
||||||
_actionDebounce?.cancel();
|
_actionDebounce?.cancel();
|
||||||
_actionDebounce = Timer(
|
_actionDebounce = Timer(
|
||||||
const Duration(milliseconds: 700),
|
const Duration(milliseconds: 700),
|
||||||
() async {
|
() async {
|
||||||
final plain =
|
if (!mounted) return;
|
||||||
_actionController?.document.toPlainText().trim() ?? '';
|
final plain = currentPlain;
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_actionSaving = true;
|
setState(() {
|
||||||
_actionSaved = false;
|
_actionSaving = true;
|
||||||
});
|
_actionSaved = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final deltaJson = jsonEncode(
|
final deltaJson = jsonEncode(
|
||||||
_actionController?.document.toDelta().toJson(),
|
_actionController?.document.toDelta().toJson(),
|
||||||
|
|
@ -339,15 +358,19 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
await ref
|
await ref
|
||||||
.read(tasksControllerProvider)
|
.read(tasksControllerProvider)
|
||||||
.updateTask(taskId: task.id, actionTaken: deltaJson);
|
.updateTask(taskId: task.id, actionTaken: deltaJson);
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_actionSaved = plain.isNotEmpty;
|
setState(() {
|
||||||
});
|
_actionSaved = plain.isNotEmpty;
|
||||||
} catch (_) {
|
});
|
||||||
// ignore
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[TasQ] action-taken auto-save error: $e');
|
||||||
} finally {
|
} finally {
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_actionSaving = false;
|
setState(() {
|
||||||
});
|
_actionSaving = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
if (_actionSaved) {
|
if (_actionSaved) {
|
||||||
Future.delayed(const Duration(seconds: 2), () {
|
Future.delayed(const Duration(seconds: 2), () {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -3442,42 +3465,17 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
];
|
];
|
||||||
String? selectedOffice = task.officeId;
|
String? selectedOffice = task.officeId;
|
||||||
|
|
||||||
// ---- Title-field AI-button debounce state ----
|
// ---- Title-field AI-button visibility ----
|
||||||
// We listen to titleCtrl directly instead of using TypeAheadFormField's
|
// The button is hidden by default and shown only after the user pauses
|
||||||
// onChanged, because TypeAheadFormField fires onChanged unpredictably
|
// manual typing (700 ms debounce). We can NOT rely on onChanged or
|
||||||
// (on focus, on overlay teardown, on suggestion selection) and we have
|
// controller listeners because TypeAheadFormField fires those
|
||||||
// no reliable way to distinguish those from real user keystrokes.
|
// unpredictably (on focus, overlay teardown, suggestion selection).
|
||||||
|
// Instead we wrap the field in a KeyboardListener and only react to
|
||||||
|
// actual key events.
|
||||||
var showTitleGemini = false;
|
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 titleDeepSeek = false;
|
||||||
var descDeepSeek = false;
|
var descDeepSeek = false;
|
||||||
Timer? titleTypingTimer;
|
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 {
|
try {
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
|
|
@ -3488,7 +3486,6 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
var descProcessing = false;
|
var descProcessing = false;
|
||||||
return StatefulBuilder(
|
return StatefulBuilder(
|
||||||
builder: (context, setDialogState) {
|
builder: (context, setDialogState) {
|
||||||
titleSetState = setDialogState; // wire listener → setDialogState
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
shape: dialogShape,
|
shape: dialogShape,
|
||||||
title: const Text('Edit Task'),
|
title: const Text('Edit Task'),
|
||||||
|
|
@ -3502,44 +3499,69 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
child: GeminiAnimatedBorder(
|
child: GeminiAnimatedBorder(
|
||||||
isProcessing: titleProcessing,
|
isProcessing: titleProcessing,
|
||||||
useDeepSeekColors: titleDeepSeek,
|
useDeepSeekColors: titleDeepSeek,
|
||||||
child: TypeAheadFormField<String>(
|
child: KeyboardListener(
|
||||||
textFieldConfiguration: TextFieldConfiguration(
|
focusNode: FocusNode(),
|
||||||
controller: titleCtrl,
|
onKeyEvent: (event) {
|
||||||
enabled: !saving,
|
// Only react to actual key-down (typing).
|
||||||
decoration: const InputDecoration(
|
if (event is! KeyDownEvent &&
|
||||||
labelText: 'Title',
|
event is! KeyRepeatEvent) {
|
||||||
),
|
return;
|
||||||
// Debounce is handled by titleCtrl.addListener
|
}
|
||||||
// (declared above showDialog) to avoid
|
// Skip modifier-only keys.
|
||||||
// TypeAheadFormField's unpredictable onChanged
|
if (event.character == null ||
|
||||||
// behaviour (fires on focus, overlay teardown,
|
event.character!.isEmpty) {
|
||||||
// and suggestion selection).
|
return;
|
||||||
),
|
}
|
||||||
suggestionsCallback: (pattern) async {
|
titleTypingTimer?.cancel();
|
||||||
return SubjectSuggestionEngine.suggest(
|
if (showTitleGemini) {
|
||||||
existingSubjects: existingSubjects,
|
setDialogState(
|
||||||
query: pattern,
|
() => showTitleGemini = false,
|
||||||
limit: 8,
|
);
|
||||||
|
}
|
||||||
|
titleTypingTimer = Timer(
|
||||||
|
const Duration(milliseconds: 700),
|
||||||
|
() {
|
||||||
|
if (titleCtrl.text.trim().isNotEmpty) {
|
||||||
|
setDialogState(
|
||||||
|
() => showTitleGemini = true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemBuilder: (context, suggestion) => ListTile(
|
child: TypeAheadFormField<String>(
|
||||||
dense: true,
|
textFieldConfiguration:
|
||||||
title: Text(suggestion),
|
TextFieldConfiguration(
|
||||||
),
|
controller: titleCtrl,
|
||||||
onSuggestionSelected: (suggestion) {
|
enabled: !saving,
|
||||||
// Suppress the titleCtrl listener while we
|
decoration: const InputDecoration(
|
||||||
// set text programmatically so it doesn't
|
labelText: 'Title',
|
||||||
// start the debounce timer.
|
),
|
||||||
titleSuppressListener = true;
|
),
|
||||||
titleTypingTimer?.cancel();
|
suggestionsCallback: (pattern) async {
|
||||||
titleCtrl
|
return SubjectSuggestionEngine.suggest(
|
||||||
..text = suggestion
|
existingSubjects: existingSubjects,
|
||||||
..selection = TextSelection.collapsed(
|
query: pattern,
|
||||||
offset: suggestion.length,
|
limit: 8,
|
||||||
);
|
);
|
||||||
titleSuppressListener = false;
|
},
|
||||||
setDialogState(() => showTitleGemini = false);
|
itemBuilder: (context, suggestion) =>
|
||||||
},
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(suggestion),
|
||||||
|
),
|
||||||
|
onSuggestionSelected: (suggestion) {
|
||||||
|
titleTypingTimer?.cancel();
|
||||||
|
titleCtrl
|
||||||
|
..text = suggestion
|
||||||
|
..selection = TextSelection.collapsed(
|
||||||
|
offset: suggestion.length,
|
||||||
|
);
|
||||||
|
setDialogState(
|
||||||
|
() => showTitleGemini = false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -3549,14 +3571,11 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
GeminiButton(
|
GeminiButton(
|
||||||
textController: titleCtrl,
|
textController: titleCtrl,
|
||||||
onTextUpdated: (updatedText) {
|
onTextUpdated: (updatedText) {
|
||||||
// Suppress the ctrl listener so the AI-improved
|
titleTypingTimer?.cancel();
|
||||||
// text doesn't restart the show-button debounce.
|
|
||||||
titleSuppressListener = true;
|
|
||||||
setDialogState(() {
|
setDialogState(() {
|
||||||
titleCtrl.text = updatedText;
|
titleCtrl.text = updatedText;
|
||||||
|
showTitleGemini = false;
|
||||||
});
|
});
|
||||||
titleSuppressListener = false;
|
|
||||||
setDialogState(() => showTitleGemini = false);
|
|
||||||
},
|
},
|
||||||
onProcessingStateChanged: (isProcessing) {
|
onProcessingStateChanged: (isProcessing) {
|
||||||
setDialogState(() {
|
setDialogState(() {
|
||||||
|
|
@ -3730,7 +3749,6 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
titleCtrl.removeListener(titleListener);
|
|
||||||
titleTypingTimer?.cancel();
|
titleTypingTimer?.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3770,10 +3788,12 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
'professional English. Return ONLY the improved text, no explanations:',
|
'professional English. Return ONLY the improved text, no explanations:',
|
||||||
);
|
);
|
||||||
|
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_actionSaving = true;
|
setState(() {
|
||||||
_actionProcessing = true;
|
_actionSaving = true;
|
||||||
});
|
_actionProcessing = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final aiService = AiService();
|
final aiService = AiService();
|
||||||
final improvedText = await aiService.enhanceText(
|
final improvedText = await aiService.enhanceText(
|
||||||
|
|
@ -3794,6 +3814,9 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
.updateTask(taskId: widget.taskId, actionTaken: deltaJson);
|
.updateTask(taskId: widget.taskId, actionTaken: deltaJson);
|
||||||
|
|
||||||
if (_actionController != null) {
|
if (_actionController != null) {
|
||||||
|
// Update the snapshot BEFORE replaceText so the listener sees no
|
||||||
|
// content change and skips the redundant auto-save.
|
||||||
|
_actionLastPlain = trimmed;
|
||||||
final docLen = _actionController!.document.length;
|
final docLen = _actionController!.document.length;
|
||||||
_actionController!.replaceText(
|
_actionController!.replaceText(
|
||||||
0,
|
0,
|
||||||
|
|
@ -3801,7 +3824,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
trimmed,
|
trimmed,
|
||||||
TextSelection.collapsed(offset: trimmed.length),
|
TextSelection.collapsed(offset: trimmed.length),
|
||||||
);
|
);
|
||||||
// Cancel the auto-save listener triggered by replaceText.
|
// Cancel any debounce that may have slipped through.
|
||||||
_actionDebounce?.cancel();
|
_actionDebounce?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3817,10 +3840,12 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||||
).showSnackBar(SnackBar(content: Text('Error: $e')));
|
).showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_actionSaving = false;
|
setState(() {
|
||||||
_actionProcessing = false;
|
_actionSaving = false;
|
||||||
});
|
_actionProcessing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
import 'package:tasq/utils/app_time.dart';
|
import 'package:tasq/utils/app_time.dart';
|
||||||
|
|
@ -582,6 +585,9 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||||
String? requestTypeOther;
|
String? requestTypeOther;
|
||||||
String? selectedRequestCategory;
|
String? selectedRequestCategory;
|
||||||
|
|
||||||
|
var showTitleGemini = false;
|
||||||
|
Timer? titleTypingTimer;
|
||||||
|
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) {
|
builder: (dialogContext) {
|
||||||
|
|
@ -607,52 +613,84 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||||
child: GeminiAnimatedBorder(
|
child: GeminiAnimatedBorder(
|
||||||
isProcessing: titleProcessing,
|
isProcessing: titleProcessing,
|
||||||
useDeepSeekColors: titleDeepSeek,
|
useDeepSeekColors: titleDeepSeek,
|
||||||
child: TypeAheadFormField<String>(
|
child: KeyboardListener(
|
||||||
textFieldConfiguration: TextFieldConfiguration(
|
focusNode: FocusNode(),
|
||||||
controller: titleController,
|
onKeyEvent: (event) {
|
||||||
decoration: const InputDecoration(
|
if (event is! KeyDownEvent &&
|
||||||
labelText: 'Task title',
|
event is! KeyRepeatEvent) {
|
||||||
),
|
return;
|
||||||
enabled: !saving,
|
}
|
||||||
),
|
if (event.character == null ||
|
||||||
suggestionsCallback: (pattern) async {
|
event.character!.isEmpty) {
|
||||||
return SubjectSuggestionEngine.suggest(
|
return;
|
||||||
existingSubjects: existingSubjects,
|
}
|
||||||
query: pattern,
|
titleTypingTimer?.cancel();
|
||||||
limit: 8,
|
if (showTitleGemini) {
|
||||||
|
setState(() => showTitleGemini = false);
|
||||||
|
}
|
||||||
|
titleTypingTimer = Timer(
|
||||||
|
const Duration(milliseconds: 700),
|
||||||
|
() {
|
||||||
|
if (titleController.text
|
||||||
|
.trim()
|
||||||
|
.isNotEmpty) {
|
||||||
|
setState(() => showTitleGemini = true);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemBuilder: (context, suggestion) => ListTile(
|
child: TypeAheadFormField<String>(
|
||||||
dense: true,
|
textFieldConfiguration: TextFieldConfiguration(
|
||||||
title: Text(suggestion),
|
controller: titleController,
|
||||||
),
|
decoration: const InputDecoration(
|
||||||
onSuggestionSelected: (suggestion) {
|
labelText: 'Task title',
|
||||||
titleController
|
),
|
||||||
..text = suggestion
|
enabled: !saving,
|
||||||
..selection = TextSelection.collapsed(
|
),
|
||||||
offset: suggestion.length,
|
suggestionsCallback: (pattern) async {
|
||||||
|
return SubjectSuggestionEngine.suggest(
|
||||||
|
existingSubjects: existingSubjects,
|
||||||
|
query: pattern,
|
||||||
|
limit: 8,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
itemBuilder: (context, suggestion) => ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(suggestion),
|
||||||
|
),
|
||||||
|
onSuggestionSelected: (suggestion) {
|
||||||
|
titleTypingTimer?.cancel();
|
||||||
|
titleController
|
||||||
|
..text = suggestion
|
||||||
|
..selection = TextSelection.collapsed(
|
||||||
|
offset: suggestion.length,
|
||||||
|
);
|
||||||
|
setState(() => showTitleGemini = false);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GeminiButton(
|
if (showTitleGemini)
|
||||||
textController: titleController,
|
GeminiButton(
|
||||||
onTextUpdated: (updatedText) {
|
textController: titleController,
|
||||||
setState(() {
|
onTextUpdated: (updatedText) {
|
||||||
titleController.text = updatedText;
|
titleTypingTimer?.cancel();
|
||||||
});
|
setState(() {
|
||||||
},
|
titleController.text = updatedText;
|
||||||
onProcessingStateChanged: (isProcessing) {
|
showTitleGemini = false;
|
||||||
setState(() {
|
});
|
||||||
titleProcessing = isProcessing;
|
},
|
||||||
});
|
onProcessingStateChanged: (isProcessing) {
|
||||||
},
|
setState(() {
|
||||||
onProviderChanged: (isDeepSeek) {
|
titleProcessing = isProcessing;
|
||||||
setState(() => titleDeepSeek = isDeepSeek);
|
});
|
||||||
},
|
},
|
||||||
tooltip: 'Improve task title with Gemini',
|
onProviderChanged: (isDeepSeek) {
|
||||||
),
|
setState(() => titleDeepSeek = isDeepSeek);
|
||||||
|
},
|
||||||
|
tooltip: 'Improve task title with Gemini',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ class _GeminiButtonState extends State<GeminiButton> {
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: Image.asset(
|
: Image.asset(
|
||||||
'assets/gemini_icon.png',
|
'gemini_icon.png',
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
errorBuilder: (context, error, stackTrace) =>
|
errorBuilder: (context, error, stackTrace) =>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user