1
- import 'dart:async' ;
2
-
1
+ import 'package:collection/collection.dart' ;
3
2
import 'package:firebase_app_check/firebase_app_check.dart' ;
4
3
import 'package:firebase_core/firebase_core.dart' ;
5
4
import 'package:flutter/material.dart' ;
6
5
7
6
import 'firebase_options.dart' ;
8
7
import 'src/ai_client/ai_client.dart' ;
8
+ import 'src/chat_message.dart' ;
9
9
import 'src/dynamic_ui.dart' ;
10
+ import 'src/ui_models.dart' ;
10
11
import 'src/ui_server.dart' ;
11
12
12
13
void main () async {
@@ -88,12 +89,11 @@ class GenUIHomePage extends StatefulWidget {
88
89
}
89
90
90
91
class _GenUIHomePageState extends State <GenUIHomePage > {
91
- final _updateController = StreamController <Map <String , Object ?>>.broadcast ();
92
- Map <String , Object ?>? _uiDefinition;
92
+ final _chatHistory = < ChatMessage > [];
93
93
String _connectionStatus = 'Initializing...' ;
94
- Key _uiKey = UniqueKey ();
95
94
final _promptController = TextEditingController ();
96
95
late final ServerConnection _serverConnection;
96
+ final ScrollController _scrollController = ScrollController ();
97
97
98
98
@override
99
99
void initState () {
@@ -102,29 +102,58 @@ class _GenUIHomePageState extends State<GenUIHomePage> {
102
102
onSetUi: (definition) {
103
103
if (! mounted) return ;
104
104
setState (() {
105
- _uiDefinition = definition;
106
- _uiKey = UniqueKey ();
105
+ final surfaceId = definition['surfaceId' ] as String ? ;
106
+ _chatHistory.add (UiResponse (
107
+ definition: definition,
108
+ surfaceId: surfaceId,
109
+ ));
107
110
});
111
+ WidgetsBinding .instance.addPostFrameCallback ((_) => _scrollToBottom ());
108
112
},
109
113
onUpdateUi: (updates) {
110
114
if (! mounted) return ;
111
- for (final update in updates) {
112
- _updateController.add (update);
113
- }
115
+ setState (() {
116
+ for (final update in updates) {
117
+ final uiUpdate = UiDefinition .fromMap (update);
118
+ final oldResponse =
119
+ _chatHistory.whereType <UiResponse >().firstWhereOrNull (
120
+ (response) => response.surfaceId == uiUpdate.surfaceId,
121
+ );
122
+ if (oldResponse != null ) {
123
+ final index = _chatHistory.indexOf (oldResponse);
124
+ _chatHistory[index] =
125
+ UiResponse (definition: update, surfaceId: uiUpdate.surfaceId);
126
+ }
127
+ }
128
+ });
129
+ },
130
+ onDeleteUi: (surfaceId) {
131
+ if (! mounted) return ;
132
+ setState (() {
133
+ _chatHistory.removeWhere ((message) =>
134
+ message is UiResponse && message.surfaceId == surfaceId);
135
+ });
136
+ },
137
+ onTextResponse: (text) {
138
+ if (! mounted) return ;
139
+ setState (() {
140
+ _chatHistory.add (TextResponse (text: text));
141
+ });
114
142
},
115
143
onError: (message) {
116
144
if (! mounted) return ;
117
145
setState (() {
146
+ _chatHistory.add (SystemMessage (text: 'Error: $message ' ));
118
147
_connectionStatus = 'Error: $message ' ;
119
- _uiDefinition = null ;
120
148
});
121
149
},
122
150
onStatusUpdate: (status) {
123
151
if (! mounted) return ;
124
152
setState (() {
125
153
_connectionStatus = status;
126
- if (status != 'Server started.' ) {
127
- _uiDefinition = null ;
154
+ if (status == 'Server started.' && _chatHistory.isEmpty) {
155
+ _chatHistory.add (const SystemMessage (
156
+ text: 'I can create UIs. What should I make for you?' ));
128
157
}
129
158
});
130
159
},
@@ -136,11 +165,17 @@ class _GenUIHomePageState extends State<GenUIHomePage> {
136
165
}
137
166
}
138
167
168
+ void _scrollToBottom () {
169
+ if (_scrollController.hasClients) {
170
+ _scrollController.jumpTo (_scrollController.position.maxScrollExtent);
171
+ }
172
+ }
173
+
139
174
@override
140
175
void dispose () {
141
- _updateController.close ();
142
176
_serverConnection.dispose ();
143
177
_promptController.dispose ();
178
+ _scrollController.dispose ();
144
179
super .dispose ();
145
180
}
146
181
@@ -150,14 +185,20 @@ class _GenUIHomePageState extends State<GenUIHomePage> {
150
185
151
186
void _sendPrompt () {
152
187
final prompt = _promptController.text;
153
- _serverConnection.sendPrompt (prompt);
154
188
if (prompt.isNotEmpty) {
189
+ setState (() {
190
+ _chatHistory.add (UserPrompt (text: prompt));
191
+ });
192
+ _serverConnection.sendPrompt (prompt);
155
193
_promptController.clear ();
194
+ WidgetsBinding .instance.addPostFrameCallback ((_) => _scrollToBottom ());
156
195
}
157
196
}
158
197
159
198
@override
160
199
Widget build (BuildContext context) {
200
+ final showProgressIndicator = _connectionStatus == 'Generating UI...' ||
201
+ _connectionStatus == 'Starting server...' ;
161
202
return Scaffold (
162
203
appBar: AppBar (
163
204
backgroundColor: Theme .of (context).colorScheme.inversePrimary,
@@ -168,6 +209,73 @@ class _GenUIHomePageState extends State<GenUIHomePage> {
168
209
constraints: const BoxConstraints (maxWidth: 1000 ),
169
210
child: Column (
170
211
children: [
212
+ Expanded (
213
+ child: _chatHistory.isEmpty
214
+ ? Center (
215
+ child: Column (
216
+ mainAxisAlignment: MainAxisAlignment .center,
217
+ children: [
218
+ if (showProgressIndicator)
219
+ const CircularProgressIndicator (),
220
+ const SizedBox (height: 16 ),
221
+ Text (_connectionStatus),
222
+ ],
223
+ ),
224
+ )
225
+ : ListView .builder (
226
+ controller: _scrollController,
227
+ itemCount: _chatHistory.length,
228
+ itemBuilder: (context, index) {
229
+ final message = _chatHistory[index];
230
+ return switch (message) {
231
+ SystemMessage () => Card (
232
+ elevation: 2.0 ,
233
+ margin: const EdgeInsets .symmetric (
234
+ horizontal: 8.0 , vertical: 4.0 ),
235
+ child: ListTile (
236
+ title: Text (message.text),
237
+ leading: const Icon (Icons .smart_toy_outlined),
238
+ ),
239
+ ),
240
+ TextResponse () => Card (
241
+ elevation: 2.0 ,
242
+ margin: const EdgeInsets .symmetric (
243
+ horizontal: 8.0 , vertical: 4.0 ),
244
+ child: ListTile (
245
+ title: Text (message.text),
246
+ leading: const Icon (Icons .smart_toy_outlined),
247
+ ),
248
+ ),
249
+ UserPrompt () => Card (
250
+ elevation: 2.0 ,
251
+ margin: const EdgeInsets .symmetric (
252
+ horizontal: 8.0 , vertical: 4.0 ),
253
+ child: ListTile (
254
+ title: Text (
255
+ message.text,
256
+ textAlign: TextAlign .right,
257
+ ),
258
+ trailing: const Icon (Icons .person),
259
+ ),
260
+ ),
261
+ UiResponse () => Card (
262
+ elevation: 2.0 ,
263
+ margin: const EdgeInsets .symmetric (
264
+ horizontal: 8.0 , vertical: 4.0 ),
265
+ child: Padding (
266
+ padding: const EdgeInsets .all (16.0 ),
267
+ child: DynamicUi (
268
+ key: message.uiKey,
269
+ surfaceId: message.surfaceId,
270
+ definition: message.definition,
271
+ onEvent: _handleUiEvent,
272
+ ),
273
+ ),
274
+ ),
275
+ };
276
+ },
277
+ ),
278
+ ),
171
279
Padding (
172
280
padding: const EdgeInsets .all (8.0 ),
173
281
child: Row (
@@ -188,29 +296,18 @@ class _GenUIHomePageState extends State<GenUIHomePage> {
188
296
],
189
297
),
190
298
),
191
- Expanded (
192
- child: _uiDefinition == null
193
- ? Center (
194
- child: Column (
195
- mainAxisAlignment: MainAxisAlignment .center,
196
- children: [
197
- if (_connectionStatus == 'Generating UI...' )
198
- const CircularProgressIndicator (),
199
- const SizedBox (height: 16 ),
200
- Text (_connectionStatus),
201
- ],
202
- ),
203
- )
204
- : Padding (
205
- padding: const EdgeInsets .all (16.0 ),
206
- child: DynamicUi (
207
- key: _uiKey,
208
- definition: _uiDefinition! ,
209
- updateStream: _updateController.stream,
210
- onEvent: _handleUiEvent,
211
- ),
212
- ),
213
- ),
299
+ if (showProgressIndicator)
300
+ const Padding (
301
+ padding: EdgeInsets .all (8.0 ),
302
+ child: Row (
303
+ mainAxisAlignment: MainAxisAlignment .center,
304
+ children: [
305
+ CircularProgressIndicator (),
306
+ SizedBox (width: 16 ),
307
+ Text ('Generating UI...' ),
308
+ ],
309
+ ),
310
+ ),
214
311
],
215
312
),
216
313
),
0 commit comments