Skip to content

Commit 2fff032

Browse files
authored
feat(ui): AI components (#2058)
* feat(ui): ai assistant components * chore: update CHANGELOG.md * chore: fix lints * test: add tests * chore: fix lints
1 parent 13c2e87 commit 2fff032

File tree

9 files changed

+807
-21
lines changed

9 files changed

+807
-21
lines changed

packages/stream_chat_flutter/CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
## Upcoming
22

3+
✅ Added
4+
5+
- Added several new widgets to enhance the AI assistant features.
6+
- `StreamingMessageView` to show AI assistant messages with streaming animation.
7+
- `AITypingIndicatorView` to show AI typing indicator.
8+
39
🐞 Fixed
410

5-
- [[#2030]](https://github.com/GetStream/stream-chat-flutter/issues/2030) Fixed `video_thumbnail`
6-
Namespace not specified.
11+
- [[#2030]](https://github.com/GetStream/stream-chat-flutter/issues/2030) Fixed `video_thumbnail` Namespace not specified.
712

813
## 8.2.0
914

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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+
key: ValueKey(index),
122+
index: index,
123+
size: size,
124+
color: color,
125+
),
126+
),
127+
].insertBetween(const SizedBox(width: 4)),
128+
);
129+
}
130+
}
131+
132+
class _AnimatedDot extends StatefulWidget {
133+
const _AnimatedDot({
134+
super.key,
135+
required this.index,
136+
this.size = 8,
137+
this.color = Colors.black,
138+
});
139+
140+
final int index;
141+
final double size;
142+
final Color color;
143+
144+
@override
145+
State<_AnimatedDot> createState() => _AnimatedDotState();
146+
}
147+
148+
class _AnimatedDotState extends State<_AnimatedDot>
149+
with SingleTickerProviderStateMixin<_AnimatedDot> {
150+
late final AnimationController _repeatingController;
151+
152+
@override
153+
void initState() {
154+
super.initState();
155+
_repeatingController = AnimationController(
156+
vsync: this,
157+
duration: const Duration(milliseconds: 800),
158+
)..addStatusListener(
159+
(status) {
160+
if (status == AnimationStatus.completed) {
161+
if (mounted) _repeatingController.reverse();
162+
} else if (status == AnimationStatus.dismissed) {
163+
if (mounted) _repeatingController.forward();
164+
}
165+
},
166+
);
167+
168+
Future.delayed(
169+
Duration(milliseconds: 200 * widget.index),
170+
() {
171+
if (mounted) _repeatingController.forward();
172+
},
173+
);
174+
}
175+
176+
@override
177+
void dispose() {
178+
_repeatingController.dispose();
179+
super.dispose();
180+
}
181+
182+
@override
183+
Widget build(BuildContext context) {
184+
final animation = CurvedAnimation(
185+
parent: _repeatingController,
186+
curve: Curves.easeInOut,
187+
);
188+
189+
return ScaleTransition(
190+
scale: Tween<double>(begin: 0.5, end: 1).animate(animation),
191+
child: FadeTransition(
192+
opacity: Tween<double>(begin: 0.3, end: 1).animate(animation),
193+
child: Container(
194+
width: widget.size,
195+
height: widget.size,
196+
decoration: BoxDecoration(
197+
color: widget.color,
198+
shape: BoxShape.circle,
199+
),
200+
),
201+
),
202+
);
203+
}
204+
}

0 commit comments

Comments
 (0)