Still some AI integration issues
This commit is contained in:
@@ -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<GeminiAnimatedBorder> createState() => _GeminiAnimatedBorderState();
|
||||
}
|
||||
|
||||
class _GeminiAnimatedBorderState extends State<GeminiAnimatedBorder>
|
||||
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<GeminiAnimatedTextField> createState() =>
|
||||
_GeminiAnimatedTextFieldState();
|
||||
}
|
||||
|
||||
class _GeminiAnimatedTextFieldState extends State<GeminiAnimatedTextField>
|
||||
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;
|
||||
}
|
||||
@@ -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<GeminiButton> createState() => _GeminiButtonState();
|
||||
}
|
||||
|
||||
class _GeminiButtonState extends State<GeminiButton> {
|
||||
bool _isProcessing = false;
|
||||
late final AiService _aiService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_aiService = AiService();
|
||||
}
|
||||
|
||||
Future<void> _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),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user