Skip to content

Commit 1119ebe

Browse files
authored
Llm UI (#21)
* UI updates to integrate LLM chat * Updating with current progress * Ai chat panel in the left panel * Minor updates to ai chat panel * split out into separate ai panel file * ui color scheme updates * Fixed deletion bugs in ui * Cleanup of snack bar message * Fixing analysis issues * Adding unit tests for ai panel * Added import button for importing graphs * Fixed formatting issues * More formatting updates
1 parent f513a72 commit 1119ebe

27 files changed

+3149
-809
lines changed

ui/firebase.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"flutter":{"platforms":{"macos":{"default":{"projectId":"ui-proj-a684b","appId":"1:548317788995:ios:91bde8a7b93af4c7e00df6","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"ui-proj-a684b","configurations":{"macos":"1:548317788995:ios:91bde8a7b93af4c7e00df6","web":"1:548317788995:web:5c7568a1a8cccb7fe00df6","windows":"1:548317788995:web:b248d7f6143fbfe1e00df6"}}}}}}

ui/lib/ai_panel.dart

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
import 'dart:convert';
2+
import 'objects.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart';
5+
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
6+
import 'package:google_fonts/google_fonts.dart';
7+
8+
const double _aiPanelMinContentWidth = 260;
9+
const double _aiPanelMaxWidth = 400;
10+
11+
class AiChatPanel extends StatelessWidget {
12+
final bool show;
13+
final FirebaseProvider? provider;
14+
final String systemPrompt;
15+
final Graph? currentGraph;
16+
final void Function(Graph newGraph)? onResponse;
17+
final VoidCallback? onClose;
18+
19+
const AiChatPanel({
20+
super.key,
21+
required this.show,
22+
required this.provider,
23+
required this.systemPrompt,
24+
required this.currentGraph,
25+
this.onResponse,
26+
this.onClose,
27+
});
28+
29+
@override
30+
Widget build(BuildContext context) {
31+
return AnimatedContainer(
32+
duration: Duration(milliseconds: 300),
33+
width: show ? _aiPanelMaxWidth : 0,
34+
curve: Curves.easeInOut,
35+
child: Container(
36+
color: Colors.grey[800],
37+
child: LayoutBuilder(
38+
builder: (context, constraints) {
39+
if (constraints.maxWidth < _aiPanelMinContentWidth) {
40+
return const SizedBox.shrink();
41+
}
42+
return AnimatedOpacity(
43+
duration: Duration(milliseconds: 200),
44+
opacity: show ? 1.0 : 0.0,
45+
curve: Curves.easeInOut,
46+
child: Column(
47+
children: [
48+
Padding(
49+
padding: const EdgeInsets.symmetric(
50+
horizontal: 8.0,
51+
vertical: 4.0,
52+
),
53+
child: Row(
54+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
55+
children: [
56+
Expanded(
57+
child: Center(
58+
child: Text(
59+
'AI Assistant',
60+
style: TextStyle(
61+
fontSize: 15,
62+
fontWeight: FontWeight.bold,
63+
color: Colors.white,
64+
),
65+
),
66+
),
67+
),
68+
IconButton(
69+
icon: Icon(Icons.close, color: Colors.white70),
70+
onPressed: onClose,
71+
),
72+
],
73+
),
74+
),
75+
Expanded(
76+
child: provider == null
77+
? Center(child: Text('No AI provider'))
78+
: GraphAwareChatView(
79+
provider: provider!,
80+
systemPrompt: systemPrompt,
81+
currentGraph: currentGraph,
82+
onResponse: onResponse,
83+
),
84+
),
85+
],
86+
),
87+
);
88+
},
89+
),
90+
),
91+
);
92+
}
93+
}
94+
95+
class GraphAwareChatView extends StatefulWidget {
96+
final FirebaseProvider provider;
97+
final String systemPrompt;
98+
final Graph? currentGraph;
99+
final void Function(Graph newGraph)? onResponse;
100+
const GraphAwareChatView({
101+
super.key,
102+
required this.provider,
103+
required this.systemPrompt,
104+
this.currentGraph,
105+
this.onResponse,
106+
});
107+
108+
@override
109+
State<GraphAwareChatView> createState() => _GraphAwareChatViewState();
110+
}
111+
112+
class _GraphAwareChatViewState extends State<GraphAwareChatView> {
113+
String? _lastProcessedAiMsg;
114+
115+
@override
116+
void initState() {
117+
super.initState();
118+
widget.provider.addListener(_onProviderUpdate);
119+
}
120+
121+
@override
122+
void dispose() {
123+
widget.provider.removeListener(_onProviderUpdate);
124+
super.dispose();
125+
}
126+
127+
void _onProviderUpdate() {
128+
final history = widget.provider.history.toList();
129+
ChatMessage? lastAiMsg;
130+
for (var i = history.length - 1; i >= 0; i--) {
131+
final msg = history[i];
132+
if (!msg.origin.isUser && (msg.text?.trim().isNotEmpty ?? false)) {
133+
lastAiMsg = msg;
134+
break;
135+
}
136+
}
137+
if (lastAiMsg != null && lastAiMsg.text != _lastProcessedAiMsg) {
138+
try {
139+
final jsonMap = jsonDecode(lastAiMsg.text!);
140+
final newGraph = Graph.fromJson(jsonMap);
141+
_lastProcessedAiMsg = lastAiMsg.text;
142+
if (widget.onResponse != null) {
143+
widget.onResponse!(newGraph);
144+
}
145+
if (mounted) {
146+
ScaffoldMessenger.of(context).showSnackBar(
147+
const SnackBar(content: Text('Graph updated by AI Assistant!')),
148+
);
149+
}
150+
} catch (e) {
151+
if (lastAiMsg.text!.trim().startsWith('{')) {
152+
_lastProcessedAiMsg = lastAiMsg.text;
153+
if (mounted) {
154+
ScaffoldMessenger.of(context).showSnackBar(
155+
SnackBar(content: Text('Could not parse graph JSON: $e')),
156+
);
157+
}
158+
}
159+
}
160+
}
161+
}
162+
163+
String _buildUserPrompt(String userMessage, Graph? currentGraph) {
164+
if (currentGraph == null) return userMessage;
165+
final graphJson = jsonEncode(currentGraph.toJson());
166+
return '''Current graph JSON:
167+
$graphJson
168+
\nUser request:\n$userMessage''';
169+
}
170+
171+
@override
172+
Widget build(BuildContext context) {
173+
return LlmChatView(
174+
provider: widget.provider,
175+
style: darkChatViewStyle(),
176+
messageSender: (
177+
String userMessage, {
178+
required Iterable<Attachment> attachments,
179+
}) {
180+
final prompt = _buildUserPrompt(userMessage, widget.currentGraph);
181+
return widget.provider.sendMessageStream(
182+
prompt,
183+
attachments: attachments,
184+
);
185+
},
186+
enableAttachments: false,
187+
enableVoiceNotes: false,
188+
);
189+
}
190+
}
191+
192+
// Copyright 2024 The Flutter Authors. All rights reserved.
193+
// Use of this source code is governed by a BSD-style license that can be
194+
// found in the LICENSE file.
195+
196+
LlmChatViewStyle darkChatViewStyle() {
197+
final style = LlmChatViewStyle.defaultStyle();
198+
return LlmChatViewStyle(
199+
backgroundColor: _invertColor(style.backgroundColor),
200+
menuColor: Colors.grey.shade800,
201+
progressIndicatorColor: _invertColor(style.progressIndicatorColor),
202+
userMessageStyle: _darkUserMessageStyle(),
203+
llmMessageStyle: _darkLlmMessageStyle(),
204+
chatInputStyle: _darkChatInputStyle(),
205+
addButtonStyle: _darkActionButtonStyle(ActionButtonType.add),
206+
attachFileButtonStyle: _darkActionButtonStyle(ActionButtonType.attachFile),
207+
cameraButtonStyle: _darkActionButtonStyle(ActionButtonType.camera),
208+
stopButtonStyle: _darkActionButtonStyle(ActionButtonType.stop),
209+
recordButtonStyle: _darkActionButtonStyle(ActionButtonType.record),
210+
submitButtonStyle: _darkActionButtonStyle(ActionButtonType.submit),
211+
closeMenuButtonStyle: _darkActionButtonStyle(ActionButtonType.closeMenu),
212+
actionButtonBarDecoration: _invertDecoration(
213+
style.actionButtonBarDecoration,
214+
),
215+
fileAttachmentStyle: _darkFileAttachmentStyle(),
216+
suggestionStyle: _darkSuggestionStyle(),
217+
closeButtonStyle: _darkActionButtonStyle(ActionButtonType.close),
218+
cancelButtonStyle: _darkActionButtonStyle(ActionButtonType.cancel),
219+
copyButtonStyle: _darkActionButtonStyle(ActionButtonType.copy),
220+
editButtonStyle: _darkActionButtonStyle(ActionButtonType.edit),
221+
galleryButtonStyle: _darkActionButtonStyle(ActionButtonType.gallery),
222+
);
223+
}
224+
225+
UserMessageStyle _darkUserMessageStyle() {
226+
final style = UserMessageStyle.defaultStyle();
227+
return UserMessageStyle(
228+
textStyle: _invertTextStyle(style.textStyle),
229+
// inversion doesn't look great here
230+
// decoration: invertDecoration(style.decoration),
231+
decoration: (style.decoration! as BoxDecoration).copyWith(
232+
color: _greyBackground,
233+
),
234+
);
235+
}
236+
237+
LlmMessageStyle _darkLlmMessageStyle() {
238+
final style = LlmMessageStyle.defaultStyle();
239+
return LlmMessageStyle(
240+
icon: style.icon,
241+
iconColor: _invertColor(style.iconColor),
242+
// inversion doesn't look great here
243+
// iconDecoration: invertDecoration(style.iconDecoration),
244+
iconDecoration: BoxDecoration(
245+
color: _greyBackground,
246+
shape: BoxShape.circle,
247+
),
248+
markdownStyle: _invertMarkdownStyle(style.markdownStyle),
249+
decoration: _invertDecoration(style.decoration),
250+
);
251+
}
252+
253+
ChatInputStyle _darkChatInputStyle() {
254+
final style = ChatInputStyle.defaultStyle();
255+
return ChatInputStyle(
256+
decoration: _invertDecoration(style.decoration),
257+
textStyle: _invertTextStyle(style.textStyle),
258+
// inversion doesn't look great here
259+
// hintStyle: invertTextStyle(style.hintStyle),
260+
hintStyle: GoogleFonts.roboto(
261+
color: _greyBackground,
262+
fontSize: 14,
263+
fontWeight: FontWeight.w400,
264+
),
265+
hintText: style.hintText,
266+
backgroundColor: _invertColor(style.backgroundColor),
267+
);
268+
}
269+
270+
ActionButtonStyle _darkActionButtonStyle(ActionButtonType type) {
271+
final style = ActionButtonStyle.defaultStyle(type);
272+
return ActionButtonStyle(
273+
icon: style.icon,
274+
iconColor: _invertColor(style.iconColor),
275+
iconDecoration: switch (type) {
276+
ActionButtonType.add ||
277+
ActionButtonType.record ||
278+
ActionButtonType.stop =>
279+
BoxDecoration(
280+
color: _greyBackground,
281+
shape: BoxShape.circle,
282+
),
283+
_ => _invertDecoration(style.iconDecoration),
284+
},
285+
text: style.text,
286+
textStyle: _invertTextStyle(style.textStyle),
287+
);
288+
}
289+
290+
FileAttachmentStyle _darkFileAttachmentStyle() {
291+
final style = FileAttachmentStyle.defaultStyle();
292+
return FileAttachmentStyle(
293+
// inversion doesn't look great here
294+
// decoration: invertDecoration(style.decoration),
295+
decoration: ShapeDecoration(
296+
color: _greyBackground,
297+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
298+
),
299+
icon: style.icon,
300+
iconColor: _invertColor(style.iconColor),
301+
iconDecoration: _invertDecoration(style.iconDecoration),
302+
filenameStyle: _invertTextStyle(style.filenameStyle),
303+
// inversion doesn't look great here
304+
// filetypeStyle: invertTextStyle(style.filetypeStyle),
305+
filetypeStyle: style.filetypeStyle!.copyWith(color: Colors.black),
306+
);
307+
}
308+
309+
SuggestionStyle _darkSuggestionStyle() {
310+
final style = SuggestionStyle.defaultStyle();
311+
return SuggestionStyle(
312+
textStyle: _invertTextStyle(style.textStyle),
313+
decoration: BoxDecoration(
314+
color: _greyBackground,
315+
borderRadius: BorderRadius.all(Radius.circular(8)),
316+
),
317+
);
318+
}
319+
320+
const Color _greyBackground = Color(0xFF535353);
321+
322+
Color? _invertColor(Color? color) => color != null
323+
? Color.from(
324+
alpha: color.a,
325+
red: 1 - color.r,
326+
green: 1 - color.g,
327+
blue: 1 - color.b,
328+
)
329+
: null;
330+
331+
Decoration _invertDecoration(Decoration? decoration) => switch (decoration!) {
332+
final BoxDecoration d => d.copyWith(color: _invertColor(d.color)),
333+
final ShapeDecoration d => ShapeDecoration(
334+
color: _invertColor(d.color),
335+
shape: d.shape,
336+
shadows: d.shadows,
337+
image: d.image,
338+
gradient: d.gradient,
339+
),
340+
_ => decoration,
341+
};
342+
343+
TextStyle _invertTextStyle(TextStyle? style) =>
344+
style!.copyWith(color: _invertColor(style.color));
345+
346+
MarkdownStyleSheet? _invertMarkdownStyle(MarkdownStyleSheet? markdownStyle) =>
347+
markdownStyle?.copyWith(
348+
a: _invertTextStyle(markdownStyle.a),
349+
blockquote: _invertTextStyle(markdownStyle.blockquote),
350+
checkbox: _invertTextStyle(markdownStyle.checkbox),
351+
code: _invertTextStyle(markdownStyle.code),
352+
del: _invertTextStyle(markdownStyle.del),
353+
em: _invertTextStyle(markdownStyle.em),
354+
strong: _invertTextStyle(markdownStyle.strong),
355+
p: _invertTextStyle(markdownStyle.p),
356+
tableBody: _invertTextStyle(markdownStyle.tableBody),
357+
tableHead: _invertTextStyle(markdownStyle.tableHead),
358+
h1: _invertTextStyle(markdownStyle.h1),
359+
h2: _invertTextStyle(markdownStyle.h2),
360+
h3: _invertTextStyle(markdownStyle.h3),
361+
h4: _invertTextStyle(markdownStyle.h4),
362+
h5: _invertTextStyle(markdownStyle.h5),
363+
h6: _invertTextStyle(markdownStyle.h6),
364+
listBullet: _invertTextStyle(markdownStyle.listBullet),
365+
img: _invertTextStyle(markdownStyle.img),
366+
);

0 commit comments

Comments
 (0)