Skip to content

Commit a3c69e4

Browse files
committed
feat(ui): ai assistant components
1 parent 5e7d3e6 commit a3c69e4

File tree

6 files changed

+657
-19
lines changed

6 files changed

+657
-19
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:stream_chat_flutter/src/utils/extensions.dart';
3+
4+
/// {@template aiTypingIndicatorView}
5+
/// A widget that displays a typing indicator for the AI.
6+
///
7+
/// This widget is used to indicate the various states of the AI such as
8+
/// [AI_STATE_THINKING], [AI_STATE_CHECKING_SOURCES] etc.
9+
///
10+
/// The widget displays a text and a series of animated dots.
11+
///
12+
/// ```dart
13+
/// AITypingIndicatorView(
14+
/// text: 'AI is thinking',
15+
/// );
16+
/// ```
17+
///
18+
/// see also:
19+
/// - [AnimatedDots] which is used to display the animated dots.
20+
/// {@endtemplate}
21+
class AITypingIndicatorView extends StatelessWidget {
22+
/// {@macro aiTypingIndicatorView}
23+
const AITypingIndicatorView({
24+
super.key,
25+
required this.text,
26+
this.textStyle,
27+
this.dotColor,
28+
this.dotCount = 3,
29+
this.dotSize = 8,
30+
});
31+
32+
/// The text to display in the widget.
33+
///
34+
/// Typically this is the state of the AI such as "AI is thinking",
35+
final String text;
36+
37+
/// The style to use for the text.
38+
///
39+
/// If not provided, the default text style is used.
40+
final TextStyle? textStyle;
41+
42+
/// The color of the animated dots displayed next to the text.
43+
///
44+
/// If not provided, the color of the [textStyle] is used if available or
45+
/// [Colors.black] is used.
46+
final Color? dotColor;
47+
48+
/// The number of animated dots to display next to the text.
49+
///
50+
/// Defaults to 3.
51+
final int dotCount;
52+
53+
/// The size of the animated dots displayed next to the text.
54+
///
55+
/// Defaults to 8.
56+
final double dotSize;
57+
58+
@override
59+
Widget build(BuildContext context) {
60+
return Row(
61+
mainAxisSize: MainAxisSize.min,
62+
children: [
63+
Text(text, style: textStyle),
64+
const SizedBox(width: 8),
65+
AnimatedDots(
66+
size: dotSize,
67+
count: dotCount,
68+
color: dotColor ?? textStyle?.color ?? Colors.black,
69+
),
70+
],
71+
);
72+
}
73+
}
74+
75+
/// {@template animatedDots}
76+
/// A widget that displays a series of animated dots.
77+
///
78+
/// The dots are animated to scale up and down in size and fade in and out in
79+
/// opacity.
80+
///
81+
/// The widget is typically used to indicate that someone is typing.
82+
/// {@endtemplate}
83+
class AnimatedDots extends StatelessWidget {
84+
/// {@macro animatedDots}
85+
const AnimatedDots({
86+
super.key,
87+
this.count = 3,
88+
this.size = 8,
89+
this.spacing = 4,
90+
this.color = Colors.black,
91+
});
92+
93+
/// The number of dots to display.
94+
///
95+
/// Defaults to 3.
96+
final int count;
97+
98+
/// The size of each dot.
99+
///
100+
/// Defaults to 8.
101+
final double size;
102+
103+
/// The spacing between each dot.
104+
///
105+
/// Defaults to 4.
106+
final double spacing;
107+
108+
/// The color of the dots.
109+
///
110+
/// Defaults to [Colors.black].
111+
final Color color;
112+
113+
@override
114+
Widget build(BuildContext context) {
115+
return Row(
116+
mainAxisSize: MainAxisSize.min,
117+
children: <Widget>[
118+
...List.generate(
119+
count,
120+
(index) => _AnimatedDot(
121+
index: index,
122+
size: size,
123+
color: color,
124+
),
125+
),
126+
].insertBetween(const SizedBox(width: 4)),
127+
);
128+
}
129+
}
130+
131+
class _AnimatedDot extends StatefulWidget {
132+
const _AnimatedDot({
133+
super.key,
134+
required this.index,
135+
this.size = 8,
136+
this.color = Colors.black,
137+
});
138+
139+
final int index;
140+
final double size;
141+
final Color color;
142+
143+
@override
144+
State<_AnimatedDot> createState() => _AnimatedDotState();
145+
}
146+
147+
class _AnimatedDotState extends State<_AnimatedDot>
148+
with SingleTickerProviderStateMixin<_AnimatedDot> {
149+
late final AnimationController _repeatingController;
150+
151+
@override
152+
void initState() {
153+
super.initState();
154+
_repeatingController = AnimationController(
155+
vsync: this,
156+
duration: const Duration(milliseconds: 800),
157+
)..addStatusListener(
158+
(status) {
159+
if (status == AnimationStatus.completed) {
160+
if (mounted) _repeatingController.reverse();
161+
} else if (status == AnimationStatus.dismissed) {
162+
if (mounted) _repeatingController.forward();
163+
}
164+
},
165+
);
166+
167+
Future.delayed(
168+
Duration(milliseconds: 200 * widget.index),
169+
() {
170+
if (mounted) _repeatingController.forward();
171+
},
172+
);
173+
}
174+
175+
@override
176+
void dispose() {
177+
_repeatingController.dispose();
178+
super.dispose();
179+
}
180+
181+
@override
182+
Widget build(BuildContext context) {
183+
final animation = CurvedAnimation(
184+
parent: _repeatingController,
185+
curve: Curves.easeInOut,
186+
);
187+
188+
return ScaleTransition(
189+
scale: Tween<double>(begin: 0.5, end: 1).animate(animation),
190+
child: FadeTransition(
191+
opacity: Tween<double>(begin: 0.3, end: 1).animate(animation),
192+
child: Container(
193+
width: widget.size,
194+
height: widget.size,
195+
decoration: BoxDecoration(
196+
color: widget.color,
197+
shape: BoxShape.circle,
198+
),
199+
),
200+
),
201+
);
202+
}
203+
}

0 commit comments

Comments
 (0)