neoagent 2.4.1-beta.30 → 2.4.1-beta.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  2. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  3. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +78 -56
  4. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  5. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +18 -17
  6. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +2 -1
  7. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  8. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +15 -11
  9. package/flutter_app/lib/main_account_settings.dart +1 -1
  10. package/flutter_app/lib/main_admin.dart +1 -1
  11. package/flutter_app/lib/main_app_shell.dart +190 -191
  12. package/flutter_app/lib/main_chat.dart +381 -228
  13. package/flutter_app/lib/main_devices.dart +1 -1
  14. package/flutter_app/lib/main_launcher.dart +1 -1
  15. package/flutter_app/lib/main_operations.dart +3 -3
  16. package/flutter_app/lib/main_shared.dart +336 -340
  17. package/flutter_app/lib/main_spacing.dart +4 -4
  18. package/flutter_app/lib/main_theme.dart +20 -14
  19. package/flutter_app/lib/src/theme/palette.dart +76 -34
  20. package/flutter_app/pubspec.lock +2 -2
  21. package/flutter_app/pubspec.yaml +1 -1
  22. package/package.json +1 -1
  23. package/server/public/.last_build_id +1 -1
  24. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  25. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  26. package/server/public/flutter_bootstrap.js +1 -1
  27. package/server/public/main.dart.js +60738 -60521
  28. package/server/services/ai/tools.js +51 -7
  29. package/server/services/messaging/formatting_guides.js +26 -1
  30. package/server/services/messaging/manager.js +5 -2
  31. package/server/services/tasks/runtime.js +182 -7
  32. package/server/services/tasks/task_repository.js +13 -0
@@ -119,6 +119,31 @@ class _AmbientBackdropState extends State<_AmbientBackdrop>
119
119
  }
120
120
  }
121
121
 
122
+ class _ControlSurfaceBackdrop extends StatelessWidget {
123
+ const _ControlSurfaceBackdrop({required this.child});
124
+
125
+ final Widget child;
126
+
127
+ @override
128
+ Widget build(BuildContext context) {
129
+ return DecoratedBox(
130
+ decoration: BoxDecoration(
131
+ gradient: LinearGradient(
132
+ colors: <Color>[
133
+ _bgPrimary,
134
+ Color.lerp(_bgPrimary, _accentAlt, 0.025)!,
135
+ Color.lerp(_bgPrimary, _accent, 0.02)!,
136
+ ],
137
+ stops: const <double>[0, 0.58, 1],
138
+ begin: const Alignment(-0.95, -1),
139
+ end: const Alignment(0.96, 1),
140
+ ),
141
+ ),
142
+ child: child,
143
+ );
144
+ }
145
+ }
146
+
122
147
  class _AuroraFieldPainter extends CustomPainter {
123
148
  const _AuroraFieldPainter({
124
149
  required this.progress,
@@ -393,7 +418,6 @@ List<AppSection> _mainSections(NeoAgentController controller) {
393
418
  return <AppSection>[
394
419
  AppSection.chat,
395
420
  AppSection.recordings,
396
- AppSection.runs,
397
421
  AppSection.devices,
398
422
  AppSection.tasks,
399
423
  AppSection.widgets,
@@ -401,6 +425,7 @@ List<AppSection> _mainSections(NeoAgentController controller) {
401
425
  AppSection.memory,
402
426
  if (controller.showHealthSection) AppSection.health,
403
427
  AppSection.settings,
428
+ AppSection.runs,
404
429
  AppSection.agents,
405
430
  AppSection.messaging,
406
431
  ];
@@ -425,11 +450,19 @@ List<Widget> _buildSidebarItems(
425
450
  continue;
426
451
  }
427
452
 
453
+ final defaultSection = sections.first;
454
+ final parentSection = group == SidebarGroup.settings
455
+ ? AppSection.settings
456
+ : defaultSection;
457
+ final childSections = group == SidebarGroup.settings
458
+ ? sections.where((section) => section != parentSection).toList()
459
+ : sections;
460
+ final hasChildren =
461
+ childSections.length > 1 ||
462
+ (group == SidebarGroup.settings && childSections.isNotEmpty);
428
463
  final active =
429
464
  selectedSidebarSection &&
430
- controller.selectedSection.sidebarSection.group == group;
431
- final defaultSection = sections.first;
432
- final hasChildren = sections.length > 1;
465
+ controller.selectedSection.sidebarSection == parentSection;
433
466
  final expanded = expandedGroup == group;
434
467
 
435
468
  widgets.add(
@@ -441,12 +474,14 @@ List<Widget> _buildSidebarItems(
441
474
  ? Icon(
442
475
  expanded ? Icons.expand_less : Icons.expand_more,
443
476
  size: 16,
444
- color: active ? _accent : _textMuted,
477
+ color: active ? _textSecondary : _textMuted,
445
478
  )
446
479
  : null,
447
480
  onTap: hasChildren
448
- ? () => onToggleGroup(group)
449
- : () => onSelect(defaultSection),
481
+ ? group == SidebarGroup.settings
482
+ ? () => onSelect(parentSection)
483
+ : () => onToggleGroup(group)
484
+ : () => onSelect(parentSection),
450
485
  ),
451
486
  );
452
487
 
@@ -454,19 +489,23 @@ List<Widget> _buildSidebarItems(
454
489
  continue;
455
490
  }
456
491
 
457
- for (final section in sections) {
458
- widgets.add(
459
- _SidebarButton(
460
- label: section.label,
461
- icon: section.icon,
462
- active: controller.selectedSection.sidebarSection == section,
463
- indent: 18,
464
- iconSize: 16,
465
- fontSize: 12,
466
- onTap: () => onSelect(section),
467
- ),
468
- );
469
- }
492
+ widgets.add(
493
+ _SidebarSubnav(
494
+ children: childSections
495
+ .map(
496
+ (section) => _SidebarButton(
497
+ label: section.label,
498
+ icon: section.icon,
499
+ active: controller.selectedSection.sidebarSection == section,
500
+ iconSize: 16,
501
+ fontSize: 13,
502
+ compact: true,
503
+ onTap: () => onSelect(section),
504
+ ),
505
+ )
506
+ .toList(growable: false),
507
+ ),
508
+ );
470
509
  }
471
510
  return widgets;
472
511
  }
@@ -1067,14 +1106,14 @@ class _PulseHaloState extends State<_PulseHalo>
1067
1106
  }
1068
1107
  }
1069
1108
 
1070
- class _SidebarButton extends StatelessWidget {
1109
+ class _SidebarButton extends StatefulWidget {
1071
1110
  const _SidebarButton({
1072
1111
  required this.label,
1073
1112
  required this.icon,
1074
1113
  this.active = false,
1075
- this.indent = 0,
1076
1114
  this.iconSize = 18,
1077
1115
  this.fontSize = 13,
1116
+ this.compact = false,
1078
1117
  this.trailing,
1079
1118
  required this.onTap,
1080
1119
  });
@@ -1082,114 +1121,106 @@ class _SidebarButton extends StatelessWidget {
1082
1121
  final String label;
1083
1122
  final IconData icon;
1084
1123
  final bool active;
1085
- final double indent;
1086
1124
  final double iconSize;
1087
1125
  final double fontSize;
1126
+ final bool compact;
1088
1127
  final Widget? trailing;
1089
1128
  final VoidCallback? onTap;
1090
1129
 
1130
+ @override
1131
+ State<_SidebarButton> createState() => _SidebarButtonState();
1132
+ }
1133
+
1134
+ class _SidebarButtonState extends State<_SidebarButton> {
1135
+ bool _hovering = false;
1136
+
1091
1137
  @override
1092
1138
  Widget build(BuildContext context) {
1139
+ final active = widget.active;
1140
+ final BoxDecoration decoration = active
1141
+ ? BoxDecoration(
1142
+ color: _bgCard,
1143
+ borderRadius: BorderRadius.circular(9),
1144
+ border: Border.all(color: _border),
1145
+ boxShadow: <BoxShadow>[
1146
+ BoxShadow(
1147
+ color: Colors.black.withValues(alpha: 0.05),
1148
+ blurRadius: 10,
1149
+ offset: const Offset(0, 2),
1150
+ ),
1151
+ ],
1152
+ )
1153
+ : BoxDecoration(
1154
+ color: _hovering
1155
+ ? _bgTertiary.withValues(alpha: 0.72)
1156
+ : Colors.transparent,
1157
+ borderRadius: BorderRadius.circular(9),
1158
+ );
1093
1159
  return Padding(
1094
- padding: const EdgeInsets.only(bottom: 6),
1095
- child: _HoverLift(
1096
- active: active,
1097
- child: _GlassSurface(
1098
- borderRadius: BorderRadius.circular(18),
1099
- blurSigma: active ? 22 : 18,
1100
- fillColor: active
1101
- ? _accentMuted.withValues(alpha: 0.36)
1102
- : _bgCard.withValues(alpha: 0.22),
1103
- borderColor: active
1104
- ? _accent.withValues(alpha: 0.45)
1105
- : Colors.white.withValues(alpha: 0.04),
1106
- boxShadow: active
1107
- ? <BoxShadow>[
1108
- BoxShadow(
1109
- color: _accent.withValues(alpha: 0.16),
1110
- blurRadius: 24,
1111
- offset: const Offset(0, 10),
1112
- ),
1113
- ]
1114
- : null,
1115
- child: Material(
1116
- color: Colors.transparent,
1117
- child: InkWell(
1118
- borderRadius: BorderRadius.circular(18),
1119
- onTap: onTap,
1160
+ padding: const EdgeInsets.only(bottom: 2),
1161
+ child: MouseRegion(
1162
+ onEnter: (_) => setState(() => _hovering = true),
1163
+ onExit: (_) => setState(() => _hovering = false),
1164
+ child: Material(
1165
+ color: Colors.transparent,
1166
+ child: InkWell(
1167
+ borderRadius: BorderRadius.circular(9),
1168
+ onTap: widget.onTap,
1169
+ child: AnimatedContainer(
1170
+ duration: const Duration(milliseconds: 180),
1171
+ curve: Curves.easeOutCubic,
1172
+ width: double.infinity,
1173
+ padding: EdgeInsets.fromLTRB(
1174
+ 12,
1175
+ widget.compact ? 8 : 10,
1176
+ 12,
1177
+ widget.compact ? 8 : 10,
1178
+ ),
1179
+ decoration: decoration,
1120
1180
  child: Stack(
1181
+ alignment: Alignment.centerLeft,
1121
1182
  children: <Widget>[
1122
1183
  if (active)
1123
- Positioned.fill(
1124
- child: IgnorePointer(
1125
- child: DecoratedBox(
1126
- decoration: BoxDecoration(
1127
- borderRadius: BorderRadius.circular(18),
1128
- gradient: LinearGradient(
1129
- colors: <Color>[
1130
- Colors.white.withValues(alpha: 0.08),
1131
- Colors.transparent,
1132
- _accentAlt.withValues(alpha: 0.08),
1133
- ],
1134
- stops: const <double>[0, 0.44, 1],
1135
- begin: Alignment.topLeft,
1136
- end: Alignment.bottomRight,
1137
- ),
1184
+ Positioned(
1185
+ left: -12,
1186
+ child: Container(
1187
+ width: 3,
1188
+ height: 18,
1189
+ decoration: BoxDecoration(
1190
+ borderRadius: BorderRadius.circular(999),
1191
+ gradient: LinearGradient(
1192
+ colors: <Color>[_accentAlt, _accent],
1193
+ begin: Alignment.topCenter,
1194
+ end: Alignment.bottomCenter,
1138
1195
  ),
1139
1196
  ),
1140
1197
  ),
1141
1198
  ),
1142
- Container(
1143
- width: double.infinity,
1144
- padding: EdgeInsets.fromLTRB(12 + indent, 12, 12, 12),
1145
- child: Row(
1146
- children: <Widget>[
1147
- AnimatedContainer(
1148
- duration: const Duration(milliseconds: 220),
1149
- curve: Curves.easeOutCubic,
1150
- width: active ? 6 : 3,
1151
- height: active ? 26 : 16,
1152
- margin: EdgeInsets.only(right: active ? 10 : 13),
1153
- decoration: BoxDecoration(
1154
- gradient: LinearGradient(
1155
- colors: <Color>[
1156
- active
1157
- ? _accentHover
1158
- : _textMuted.withValues(alpha: 0.35),
1159
- active
1160
- ? _accentAlt.withValues(alpha: 0.95)
1161
- : _textMuted.withValues(alpha: 0.08),
1162
- ],
1163
- begin: Alignment.topCenter,
1164
- end: Alignment.bottomCenter,
1165
- ),
1166
- borderRadius: BorderRadius.circular(999),
1167
- ),
1168
- ),
1169
- Icon(
1170
- icon,
1171
- size: iconSize,
1172
- color: active ? _accentHover : _textSecondary,
1173
- ),
1174
- const SizedBox(width: 10),
1175
- Expanded(
1176
- child: Text(
1177
- label,
1178
- style: TextStyle(
1179
- fontSize: fontSize,
1180
- fontWeight: active
1181
- ? FontWeight.w800
1182
- : FontWeight.w600,
1183
- color: active ? _textPrimary : _textSecondary,
1184
- ),
1199
+ Row(
1200
+ children: <Widget>[
1201
+ Icon(
1202
+ widget.icon,
1203
+ size: widget.iconSize,
1204
+ color: active ? _accent : _textMuted,
1205
+ ),
1206
+ const SizedBox(width: 11),
1207
+ Expanded(
1208
+ child: Text(
1209
+ widget.label,
1210
+ maxLines: 1,
1211
+ overflow: TextOverflow.ellipsis,
1212
+ style: GoogleFonts.geist(
1213
+ fontSize: widget.fontSize,
1214
+ fontWeight: FontWeight.w600,
1215
+ color: active ? _textPrimary : _textSecondary,
1185
1216
  ),
1186
1217
  ),
1187
- if (trailing != null) ...<Widget>[
1188
- const SizedBox(width: 8),
1189
- trailing!,
1190
- ],
1218
+ ),
1219
+ if (widget.trailing != null) ...<Widget>[
1220
+ const SizedBox(width: 8),
1221
+ widget.trailing!,
1191
1222
  ],
1192
- ),
1223
+ ],
1193
1224
  ),
1194
1225
  ],
1195
1226
  ),
@@ -1201,38 +1232,20 @@ class _SidebarButton extends StatelessWidget {
1201
1232
  }
1202
1233
  }
1203
1234
 
1204
- class _HoverLift extends StatefulWidget {
1205
- const _HoverLift({required this.child, this.active = false});
1206
-
1207
- final Widget child;
1208
- final bool active;
1209
-
1210
- @override
1211
- State<_HoverLift> createState() => _HoverLiftState();
1212
- }
1235
+ class _SidebarSubnav extends StatelessWidget {
1236
+ const _SidebarSubnav({required this.children});
1213
1237
 
1214
- class _HoverLiftState extends State<_HoverLift> {
1215
- bool _hovering = false;
1238
+ final List<Widget> children;
1216
1239
 
1217
1240
  @override
1218
1241
  Widget build(BuildContext context) {
1219
- final lifted = widget.active || _hovering;
1220
- return MouseRegion(
1221
- onEnter: (_) => setState(() => _hovering = true),
1222
- onExit: (_) => setState(() => _hovering = false),
1223
- child: AnimatedScale(
1224
- duration: const Duration(milliseconds: 180),
1225
- curve: Curves.easeOutCubic,
1226
- scale: lifted ? 1.012 : 1,
1227
- child: AnimatedSlide(
1228
- duration: const Duration(milliseconds: 180),
1229
- curve: Curves.easeOutCubic,
1230
- offset: _hovering && !widget.active
1231
- ? const Offset(0.01, 0)
1232
- : Offset.zero,
1233
- child: widget.child,
1234
- ),
1242
+ return Container(
1243
+ margin: const EdgeInsets.fromLTRB(14, 2, 0, 2),
1244
+ padding: const EdgeInsets.only(left: 12),
1245
+ decoration: BoxDecoration(
1246
+ border: Border(left: BorderSide(color: _border)),
1235
1247
  ),
1248
+ child: Column(children: children),
1236
1249
  );
1237
1250
  }
1238
1251
  }
@@ -1252,21 +1265,21 @@ class _SidebarIconButton extends StatelessWidget {
1252
1265
  Widget build(BuildContext context) {
1253
1266
  return Tooltip(
1254
1267
  message: tooltip,
1255
- child: _GlassSurface(
1256
- borderRadius: BorderRadius.circular(999),
1257
- blurSigma: 18,
1258
- fillColor: _bgCard.withValues(alpha: 0.3),
1259
- child: Material(
1260
- color: Colors.transparent,
1261
- shape: const CircleBorder(),
1262
- child: InkWell(
1263
- customBorder: const CircleBorder(),
1264
- onTap: onTap,
1265
- child: SizedBox(
1266
- width: 38,
1267
- height: 38,
1268
- child: Icon(icon, size: 17, color: _textSecondary),
1268
+ child: Material(
1269
+ color: Colors.transparent,
1270
+ borderRadius: BorderRadius.circular(9),
1271
+ child: InkWell(
1272
+ borderRadius: BorderRadius.circular(9),
1273
+ onTap: onTap,
1274
+ child: Container(
1275
+ width: 32,
1276
+ height: 32,
1277
+ decoration: BoxDecoration(
1278
+ borderRadius: BorderRadius.circular(9),
1279
+ color: _bgSecondary.withValues(alpha: 0.01),
1280
+ border: Border.all(color: Colors.transparent),
1269
1281
  ),
1282
+ child: Icon(icon, size: 17, color: _textSecondary),
1270
1283
  ),
1271
1284
  ),
1272
1285
  ),
@@ -1379,7 +1392,7 @@ class _BrandLockup extends StatelessWidget {
1379
1392
  final titleColor = Theme.of(context).colorScheme.onSurface;
1380
1393
  final title = Text(
1381
1394
  'NeoAgent',
1382
- style: GoogleFonts.spaceGrotesk(
1395
+ style: GoogleFonts.geist(
1383
1396
  fontSize: titleFontSize,
1384
1397
  fontWeight: FontWeight.w700,
1385
1398
  color: titleColor,
@@ -1500,186 +1513,180 @@ class _ChatBubble extends StatelessWidget {
1500
1513
  const SizedBox(width: 12),
1501
1514
  ],
1502
1515
  Flexible(
1503
- child: AnimatedContainer(
1504
- duration: const Duration(milliseconds: 220),
1505
- curve: Curves.easeOutCubic,
1506
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 11),
1507
- decoration: BoxDecoration(
1508
- gradient: isUser
1509
- ? LinearGradient(
1510
- colors: <Color>[_accent, _accentAlt],
1511
- begin: Alignment.topLeft,
1512
- end: Alignment.bottomRight,
1513
- )
1514
- : LinearGradient(
1515
- colors: <Color>[
1516
- _bgCard,
1517
- _bgSecondary.withValues(alpha: 0.94),
1518
- ],
1519
- begin: Alignment.topLeft,
1520
- end: Alignment.bottomRight,
1521
- ),
1522
- borderRadius: BorderRadius.only(
1523
- topLeft: const Radius.circular(14),
1524
- topRight: const Radius.circular(14),
1525
- bottomLeft: Radius.circular(isUser ? 14 : 4),
1526
- bottomRight: Radius.circular(isUser ? 4 : 14),
1527
- ),
1528
- border: Border.all(
1529
- color: isUser
1530
- ? Colors.white.withValues(alpha: 0.18)
1531
- : _borderLight.withValues(alpha: 0.72),
1516
+ child: ConstrainedBox(
1517
+ constraints: const BoxConstraints(maxWidth: 672),
1518
+ child: AnimatedContainer(
1519
+ duration: const Duration(milliseconds: 220),
1520
+ curve: Curves.easeOutCubic,
1521
+ padding: const EdgeInsets.symmetric(
1522
+ horizontal: 20,
1523
+ vertical: 16,
1532
1524
  ),
1533
- boxShadow: <BoxShadow>[
1534
- if (isUser)
1525
+ decoration: BoxDecoration(
1526
+ gradient: isUser
1527
+ ? LinearGradient(
1528
+ colors: <Color>[
1529
+ _accentAlt,
1530
+ Color.lerp(_accent, _accentAlt, 0.28)!,
1531
+ ],
1532
+ begin: Alignment.topLeft,
1533
+ end: Alignment.bottomRight,
1534
+ )
1535
+ : null,
1536
+ color: isUser ? null : _bgCard,
1537
+ borderRadius: BorderRadius.only(
1538
+ topLeft: Radius.circular(isUser ? 21 : 8),
1539
+ topRight: Radius.circular(isUser ? 8 : 21),
1540
+ bottomLeft: const Radius.circular(21),
1541
+ bottomRight: const Radius.circular(21),
1542
+ ),
1543
+ border: Border.all(
1544
+ color: isUser ? Colors.transparent : _border,
1545
+ ),
1546
+ boxShadow: <BoxShadow>[
1535
1547
  BoxShadow(
1536
- color: _accentAlt.withValues(alpha: 0.30),
1537
- blurRadius: 12,
1548
+ color: Colors.black.withValues(alpha: 0.05),
1549
+ blurRadius: 10,
1538
1550
  offset: const Offset(0, 2),
1539
1551
  ),
1540
- if (!isUser)
1541
- BoxShadow(
1542
- color: Colors.black.withValues(alpha: 0.08),
1543
- blurRadius: 16,
1544
- offset: const Offset(0, 8),
1545
- ),
1546
- ],
1547
- ),
1548
- child: Column(
1549
- crossAxisAlignment: isUser
1550
- ? CrossAxisAlignment.end
1551
- : CrossAxisAlignment.start,
1552
- children: <Widget>[
1553
- if (!isUser && entry.platformTag != null)
1554
- Padding(
1555
- padding: const EdgeInsets.only(bottom: 8),
1556
- child: _StatusPill(
1557
- label: entry.platformTag!,
1558
- color: entry.platform == 'live' ? _info : _warning,
1559
- ),
1560
- ),
1561
- MarkdownBody(
1562
- data: entry.content,
1563
- selectable: false,
1564
- styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context))
1565
- .copyWith(
1566
- p: Theme.of(context).textTheme.bodyMedium?.copyWith(
1567
- color: isUser ? Colors.white : _textPrimary,
1568
- height: 1.65,
1569
- ),
1570
- code: Theme.of(context).textTheme.bodyMedium
1571
- ?.copyWith(
1572
- fontFamily:
1573
- GoogleFonts.jetBrainsMono().fontFamily,
1574
- backgroundColor: _bgPrimary,
1575
- color: isUser ? Colors.white : _textPrimary,
1576
- ),
1577
- blockquoteDecoration: BoxDecoration(
1578
- borderRadius: BorderRadius.circular(14),
1579
- color: const Color(0x22000000),
1580
- ),
1552
+ ],
1553
+ ),
1554
+ child: Column(
1555
+ crossAxisAlignment: isUser
1556
+ ? CrossAxisAlignment.end
1557
+ : CrossAxisAlignment.start,
1558
+ children: <Widget>[
1559
+ if (!isUser && entry.platformTag != null)
1560
+ Padding(
1561
+ padding: const EdgeInsets.only(bottom: 8),
1562
+ child: _StatusPill(
1563
+ label: entry.platformTag!,
1564
+ color: entry.platform == 'live' ? _info : _warning,
1581
1565
  ),
1582
- ),
1583
- if (sharedAttachments.isNotEmpty) ...<Widget>[
1584
- const SizedBox(height: 10),
1585
- Wrap(
1586
- spacing: 8,
1587
- runSpacing: 8,
1588
- children: sharedAttachments
1589
- .map((attachment) {
1590
- final icon =
1591
- attachment.mimeType.toLowerCase().startsWith(
1592
- 'video/',
1593
- )
1594
- ? Icons.videocam_outlined
1595
- : attachment.mimeType.toLowerCase().startsWith(
1596
- 'image/',
1597
- )
1598
- ? Icons.image_outlined
1599
- : attachment.mimeType.toLowerCase().startsWith(
1600
- 'audio/',
1601
- )
1602
- ? Icons.audiotrack_outlined
1603
- : Icons.attach_file_rounded;
1604
- return Container(
1605
- padding: const EdgeInsets.symmetric(
1606
- horizontal: 10,
1607
- vertical: 7,
1608
- ),
1609
- decoration: BoxDecoration(
1610
- color: isUser
1611
- ? const Color(0x1FFFFFFF)
1612
- : _bgSecondary,
1613
- borderRadius: BorderRadius.circular(999),
1614
- border: Border.all(
1615
- color: isUser
1616
- ? const Color(0x40FFFFFF)
1617
- : _border,
1566
+ ),
1567
+ MarkdownBody(
1568
+ data: entry.content,
1569
+ selectable: false,
1570
+ styleSheet:
1571
+ MarkdownStyleSheet.fromTheme(
1572
+ Theme.of(context),
1573
+ ).copyWith(
1574
+ p: Theme.of(context).textTheme.bodyMedium?.copyWith(
1575
+ color: isUser ? Colors.white : _textPrimary,
1576
+ fontSize: 14.5,
1577
+ height: 1.62,
1578
+ ),
1579
+ code: Theme.of(context).textTheme.bodyMedium
1580
+ ?.copyWith(
1581
+ fontFamily:
1582
+ GoogleFonts.geistMono().fontFamily,
1583
+ backgroundColor: _bgPrimary,
1584
+ color: isUser ? Colors.white : _textPrimary,
1618
1585
  ),
1619
- ),
1620
- child: Row(
1621
- mainAxisSize: MainAxisSize.min,
1622
- children: <Widget>[
1623
- Icon(
1624
- icon,
1625
- size: 14,
1626
- color: isUser
1627
- ? Colors.white
1628
- : _textSecondary,
1629
- ),
1630
- const SizedBox(width: 6),
1631
- ConstrainedBox(
1632
- constraints: const BoxConstraints(
1633
- maxWidth: 180,
1634
- ),
1635
- child: Text(
1636
- attachment.name,
1637
- maxLines: 1,
1638
- overflow: TextOverflow.ellipsis,
1639
- style: TextStyle(
1640
- color: isUser
1641
- ? Colors.white
1642
- : _textPrimary,
1643
- fontSize: 12,
1644
- ),
1645
- ),
1646
- ),
1647
- ],
1648
- ),
1649
- );
1650
- })
1651
- .toList(growable: false),
1586
+ blockquoteDecoration: BoxDecoration(
1587
+ borderRadius: BorderRadius.circular(14),
1588
+ color: const Color(0x22000000),
1589
+ ),
1590
+ ),
1652
1591
  ),
1653
- ],
1654
- if (!isUser &&
1655
- entry.runId?.trim().isNotEmpty == true) ...<Widget>[
1656
- const SizedBox(height: 12),
1657
- _MessageRunPreview(
1658
- runId: entry.runId!.trim(),
1659
- onLoadRunDetail: onLoadRunDetail,
1592
+ if (sharedAttachments.isNotEmpty) ...<Widget>[
1593
+ const SizedBox(height: 10),
1594
+ _SharedAttachmentChipWrap(
1595
+ attachments: sharedAttachments,
1596
+ isUser: isUser,
1597
+ ),
1598
+ ],
1599
+ if (!isUser &&
1600
+ entry.runId?.trim().isNotEmpty == true) ...<Widget>[
1601
+ const SizedBox(height: 12),
1602
+ _MessageRunPreview(
1603
+ runId: entry.runId!.trim(),
1604
+ onLoadRunDetail: onLoadRunDetail,
1605
+ ),
1606
+ ],
1607
+ const SizedBox(height: 10),
1608
+ Text(
1609
+ entry.createdAtLabel,
1610
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
1611
+ color: isUser
1612
+ ? const Color(0xCCFFFFFF)
1613
+ : _textSecondary,
1614
+ ),
1660
1615
  ),
1661
1616
  ],
1662
- const SizedBox(height: 10),
1663
- Text(
1664
- entry.createdAtLabel,
1665
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
1666
- color: isUser ? const Color(0xCCFFFFFF) : _textSecondary,
1667
- ),
1668
- ),
1669
- ],
1617
+ ),
1670
1618
  ),
1671
1619
  ),
1672
1620
  ),
1673
- if (isUser) ...<Widget>[
1674
- const SizedBox(width: 12),
1675
- const _MessageAvatar(assistant: false),
1676
- ],
1677
1621
  ],
1678
1622
  ),
1679
1623
  );
1680
1624
  }
1681
1625
  }
1682
1626
 
1627
+ class _SharedAttachmentChipWrap extends StatelessWidget {
1628
+ const _SharedAttachmentChipWrap({
1629
+ required this.attachments,
1630
+ required this.isUser,
1631
+ });
1632
+
1633
+ final List<SharedChatAttachment> attachments;
1634
+ final bool isUser;
1635
+
1636
+ @override
1637
+ Widget build(BuildContext context) {
1638
+ return Wrap(
1639
+ spacing: 8,
1640
+ runSpacing: 8,
1641
+ children: attachments
1642
+ .map((attachment) {
1643
+ final mimeType = attachment.mimeType.toLowerCase();
1644
+ final icon = mimeType.startsWith('video/')
1645
+ ? Icons.videocam_outlined
1646
+ : mimeType.startsWith('image/')
1647
+ ? Icons.image_outlined
1648
+ : mimeType.startsWith('audio/')
1649
+ ? Icons.audiotrack_outlined
1650
+ : Icons.attach_file_rounded;
1651
+ return Container(
1652
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
1653
+ decoration: BoxDecoration(
1654
+ color: isUser ? const Color(0x1FFFFFFF) : _bgSecondary,
1655
+ borderRadius: BorderRadius.circular(999),
1656
+ border: Border.all(
1657
+ color: isUser ? const Color(0x40FFFFFF) : _border,
1658
+ ),
1659
+ ),
1660
+ child: Row(
1661
+ mainAxisSize: MainAxisSize.min,
1662
+ children: <Widget>[
1663
+ Icon(
1664
+ icon,
1665
+ size: 14,
1666
+ color: isUser ? Colors.white : _textSecondary,
1667
+ ),
1668
+ const SizedBox(width: 6),
1669
+ ConstrainedBox(
1670
+ constraints: const BoxConstraints(maxWidth: 180),
1671
+ child: Text(
1672
+ attachment.name,
1673
+ maxLines: 1,
1674
+ overflow: TextOverflow.ellipsis,
1675
+ style: TextStyle(
1676
+ color: isUser ? Colors.white : _textPrimary,
1677
+ fontSize: 12,
1678
+ ),
1679
+ ),
1680
+ ),
1681
+ ],
1682
+ ),
1683
+ );
1684
+ })
1685
+ .toList(growable: false),
1686
+ );
1687
+ }
1688
+ }
1689
+
1683
1690
  class _MessageRunPreview extends StatefulWidget {
1684
1691
  const _MessageRunPreview({
1685
1692
  required this.runId,
@@ -1926,33 +1933,22 @@ class _MessageAvatar extends StatelessWidget {
1926
1933
  @override
1927
1934
  Widget build(BuildContext context) {
1928
1935
  return Container(
1929
- width: 30,
1930
- height: 30,
1936
+ width: assistant ? 34 : 30,
1937
+ height: assistant ? 34 : 30,
1931
1938
  decoration: BoxDecoration(
1932
- borderRadius: BorderRadius.circular(8),
1933
- gradient: assistant
1934
- ? LinearGradient(
1935
- colors: <Color>[_accentHover, _accent, _accentAlt],
1936
- begin: Alignment.topLeft,
1937
- end: Alignment.bottomRight,
1938
- )
1939
- : null,
1939
+ borderRadius: BorderRadius.circular(assistant ? 10 : 999),
1940
1940
  color: assistant ? null : _bgTertiary,
1941
- boxShadow: assistant
1942
- ? <BoxShadow>[
1943
- BoxShadow(
1944
- color: _accentAlt.withValues(alpha: 0.42),
1945
- blurRadius: 14,
1946
- offset: const Offset(0, 2),
1947
- ),
1948
- ]
1949
- : null,
1950
- ),
1951
- child: Icon(
1952
- assistant ? Icons.auto_awesome : Icons.person,
1953
- size: 16,
1954
- color: assistant ? Colors.white : _textSecondary,
1941
+ border: assistant ? null : Border.all(color: _borderLight),
1955
1942
  ),
1943
+ clipBehavior: Clip.antiAlias,
1944
+ child: assistant
1945
+ ? Image.asset(
1946
+ MediaQuery.platformBrightnessOf(context) == Brightness.dark
1947
+ ? 'assets/branding/app_icon_1024.png'
1948
+ : 'assets/branding/app_icon_light_1024.png',
1949
+ filterQuality: FilterQuality.high,
1950
+ )
1951
+ : Icon(Icons.person, size: 16, color: _textSecondary),
1956
1952
  );
1957
1953
  }
1958
1954
  }