refactor: followups design
This commit is contained in:
@@ -603,7 +603,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
|
|
||||||
if (hasStatusTimeline) ...[
|
if (hasStatusTimeline) ...[
|
||||||
StatusHistoryTimeline(updates: visibleStatusHistory),
|
StatusHistoryTimeline(updates: visibleStatusHistory),
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.xs),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Tool calls are rendered inline via segmented content
|
// Tool calls are rendered inline via segmented content
|
||||||
@@ -1279,151 +1279,100 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AssistantResponseSection extends StatelessWidget {
|
class StatusHistoryTimeline extends StatefulWidget {
|
||||||
const _AssistantResponseSection({
|
|
||||||
required this.title,
|
|
||||||
required this.child,
|
|
||||||
this.icon,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final Widget child;
|
|
||||||
final IconData? icon;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = context.conduitTheme;
|
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
if (icon != null) ...[
|
|
||||||
Icon(icon, size: 16, color: theme.buttonPrimary),
|
|
||||||
const SizedBox(width: Spacing.xs),
|
|
||||||
],
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.textSecondary,
|
|
||||||
fontSize: AppTypography.bodySmall,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
letterSpacing: 0.15,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: Spacing.xs),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(Spacing.sm),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.cardBackground,
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.card),
|
|
||||||
border: Border.all(
|
|
||||||
color: theme.cardBorder.withValues(alpha: 0.6),
|
|
||||||
width: BorderWidth.thin,
|
|
||||||
),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
|
||||||
blurRadius: 16,
|
|
||||||
offset: const Offset(0, 6),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AssistantSuggestionChip extends StatelessWidget {
|
|
||||||
const _AssistantSuggestionChip({
|
|
||||||
required this.label,
|
|
||||||
this.icon,
|
|
||||||
this.onPressed,
|
|
||||||
this.enabled = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String label;
|
|
||||||
final IconData? icon;
|
|
||||||
final VoidCallback? onPressed;
|
|
||||||
final bool enabled;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = context.conduitTheme;
|
|
||||||
final effectiveOnPressed = enabled ? onPressed : null;
|
|
||||||
final iconColor = enabled
|
|
||||||
? theme.textSecondary
|
|
||||||
: theme.textSecondary.withValues(alpha: 0.5);
|
|
||||||
|
|
||||||
final background = theme.cardBackground.withValues(
|
|
||||||
alpha: enabled ? 0.95 : 0.85,
|
|
||||||
);
|
|
||||||
final borderColor = theme.cardBorder.withValues(
|
|
||||||
alpha: enabled ? 0.6 : 0.35,
|
|
||||||
);
|
|
||||||
|
|
||||||
return RawChip(
|
|
||||||
avatar: icon != null ? Icon(icon, size: 16, color: iconColor) : null,
|
|
||||||
label: Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(
|
|
||||||
color: enabled ? theme.textPrimary : theme.textSecondary,
|
|
||||||
fontSize: AppTypography.labelMedium,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
letterSpacing: 0.2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: effectiveOnPressed,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: Spacing.sm,
|
|
||||||
vertical: Spacing.xxs,
|
|
||||||
),
|
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
backgroundColor: background,
|
|
||||||
disabledColor: background,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.pill),
|
|
||||||
side: BorderSide(color: borderColor, width: BorderWidth.thin),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StatusHistoryTimeline extends StatelessWidget {
|
|
||||||
const StatusHistoryTimeline({super.key, required this.updates});
|
const StatusHistoryTimeline({super.key, required this.updates});
|
||||||
|
|
||||||
final List<ChatStatusUpdate> updates;
|
final List<ChatStatusUpdate> updates;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatusHistoryTimeline> createState() => _StatusHistoryTimelineState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatusHistoryTimelineState extends State<StatusHistoryTimeline> {
|
||||||
|
bool _isExpanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (updates.isEmpty) {
|
if (widget.updates.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _AssistantResponseSection(
|
final theme = context.conduitTheme;
|
||||||
title: 'Status updates',
|
final hasMultipleUpdates = widget.updates.length > 1;
|
||||||
icon: Icons.sync_alt,
|
final finalUpdate = widget.updates.last;
|
||||||
child: Column(
|
final previousUpdates = widget.updates.sublist(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
0,
|
||||||
children: [
|
widget.updates.length - 1,
|
||||||
const SizedBox(height: Spacing.xs),
|
);
|
||||||
for (var index = 0; index < updates.length; index++)
|
|
||||||
Padding(
|
return Column(
|
||||||
padding: EdgeInsets.only(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
bottom: index == updates.length - 1 ? 0 : Spacing.xs,
|
children: [
|
||||||
|
// Animated container for previous updates
|
||||||
|
AnimatedCrossFade(
|
||||||
|
firstChild: const SizedBox.shrink(),
|
||||||
|
secondChild: previousUpdates.isNotEmpty
|
||||||
|
? Column(
|
||||||
|
children: [
|
||||||
|
...previousUpdates.map(
|
||||||
|
(update) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: Spacing.xs),
|
||||||
|
child: _StatusHistoryEntry(update: update),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
crossFadeState: _isExpanded
|
||||||
|
? CrossFadeState.showSecond
|
||||||
|
: CrossFadeState.showFirst,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Always show the final update
|
||||||
|
_StatusHistoryEntry(update: finalUpdate),
|
||||||
|
|
||||||
|
// Show expand/collapse button if there are multiple updates
|
||||||
|
if (hasMultipleUpdates)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: Spacing.xxs),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_isExpanded = !_isExpanded;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.xxs,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_isExpanded ? Icons.expand_less : Icons.expand_more,
|
||||||
|
size: 12,
|
||||||
|
color: theme.textSecondary.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_isExpanded
|
||||||
|
? 'Show less'
|
||||||
|
: 'Show ${previousUpdates.length} earlier step${previousUpdates.length == 1 ? '' : 's'}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppTypography.labelSmall,
|
||||||
|
color: theme.textSecondary.withValues(alpha: 0.6),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: _StatusHistoryEntry(update: updates[index]),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1440,17 +1389,11 @@ class _StatusHistoryEntry extends StatelessWidget {
|
|||||||
if (update.done == true) {
|
if (update.done == true) {
|
||||||
return theme.success;
|
return theme.success;
|
||||||
}
|
}
|
||||||
return theme.textSecondary;
|
return theme.textSecondary.withValues(alpha: 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
IconData _indicatorIcon() {
|
IconData _indicatorIcon() {
|
||||||
if (update.done == false) {
|
return Icons.circle;
|
||||||
return Icons.timelapse;
|
|
||||||
}
|
|
||||||
if (update.done == true) {
|
|
||||||
return Icons.check_circle;
|
|
||||||
}
|
|
||||||
return Icons.radio_button_unchecked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1470,28 +1413,19 @@ class _StatusHistoryEntry extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Padding(
|
||||||
width: double.infinity,
|
padding: const EdgeInsets.symmetric(vertical: Spacing.xxs),
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: Spacing.sm,
|
|
||||||
vertical: Spacing.sm,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.cardBackground.withValues(alpha: 0.92),
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
||||||
border: Border.all(
|
|
||||||
color: theme.cardBorder.withValues(alpha: 0.5),
|
|
||||||
width: BorderWidth.thin,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(_indicatorIcon(), size: 16, color: indicatorColor),
|
Container(
|
||||||
const SizedBox(width: Spacing.sm),
|
margin: const EdgeInsets.only(top: 2),
|
||||||
|
child: Icon(_indicatorIcon(), size: 12, color: indicatorColor),
|
||||||
|
),
|
||||||
|
const SizedBox(width: Spacing.xs),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -1501,133 +1435,139 @@ class _StatusHistoryEntry extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: AppTypography.bodySmall,
|
fontSize: AppTypography.bodySmall,
|
||||||
color: theme.textSecondary,
|
color: theme.textSecondary,
|
||||||
fontWeight: update.done == true
|
fontWeight: FontWeight.w500,
|
||||||
? FontWeight.w600
|
height: 1.3,
|
||||||
: FontWeight.w500,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (update.count != null)
|
if (update.count != null)
|
||||||
Padding(
|
Text(
|
||||||
padding: const EdgeInsets.only(top: Spacing.xxs),
|
update.count == 1
|
||||||
child: Text(
|
? '• Retrieved 1 source'
|
||||||
update.count == 1
|
: '• Retrieved ${update.count} sources',
|
||||||
? 'Retrieved 1 source'
|
style: TextStyle(
|
||||||
: 'Retrieved ${update.count} sources',
|
color: theme.textSecondary.withValues(alpha: 0.8),
|
||||||
style: TextStyle(
|
fontSize: AppTypography.labelSmall,
|
||||||
color: theme.textSecondary,
|
|
||||||
fontSize: AppTypography.labelSmall,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (timestamp != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: Spacing.xxs),
|
|
||||||
child: Text(
|
|
||||||
_formatTimestamp(timestamp),
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.textSecondary.withValues(alpha: 0.8),
|
|
||||||
fontSize: AppTypography.labelSmall,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (timestamp != null)
|
||||||
|
Text(
|
||||||
|
_formatTimestamp(timestamp),
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.textSecondary.withValues(alpha: 0.6),
|
||||||
|
fontSize: AppTypography.labelSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (queries.isNotEmpty)
|
if (queries.isNotEmpty ||
|
||||||
|
update.urls.isNotEmpty ||
|
||||||
|
update.items.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: Spacing.xs),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: Spacing.sm),
|
padding: const EdgeInsets.only(left: 16),
|
||||||
child: Wrap(
|
|
||||||
spacing: Spacing.xs,
|
|
||||||
runSpacing: Spacing.xs,
|
|
||||||
children: queries.map((query) {
|
|
||||||
return _AssistantSuggestionChip(
|
|
||||||
label: query,
|
|
||||||
icon: Icons.search,
|
|
||||||
onPressed: () {
|
|
||||||
_launchUri(
|
|
||||||
'https://www.google.com/search?q=${Uri.encodeComponent(query)}',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (update.urls.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: Spacing.sm),
|
|
||||||
child: Wrap(
|
|
||||||
spacing: Spacing.xs,
|
|
||||||
runSpacing: Spacing.xs,
|
|
||||||
children: update.urls.map((url) {
|
|
||||||
final host = Uri.tryParse(url)?.host ?? 'Link';
|
|
||||||
return _AssistantSuggestionChip(
|
|
||||||
label: host,
|
|
||||||
icon: Icons.open_in_new,
|
|
||||||
onPressed: () => _launchUri(url),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (update.items.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: Spacing.sm),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: update.items.map((item) {
|
children: [
|
||||||
final title = item.title?.isNotEmpty == true
|
if (queries.isNotEmpty)
|
||||||
? item.title!
|
_buildMinimalLinks(
|
||||||
: item.link ?? 'Result';
|
context,
|
||||||
return Padding(
|
queries
|
||||||
padding: const EdgeInsets.only(bottom: Spacing.xs),
|
.map(
|
||||||
child: InkWell(
|
(query) => _MinimalLinkData(
|
||||||
onTap: item.link != null
|
label: query,
|
||||||
? () => _launchUri(item.link!)
|
icon: Icons.search,
|
||||||
: null,
|
onTap: () => _launchUri(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
|
'https://www.google.com/search?q=${Uri.encodeComponent(query)}',
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: Spacing.xxs,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.link,
|
|
||||||
size: 16,
|
|
||||||
color: theme.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: Spacing.xs),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
color: item.link != null
|
|
||||||
? theme.buttonPrimary
|
|
||||||
: theme.textSecondary,
|
|
||||||
decoration: item.link != null
|
|
||||||
? TextDecoration.underline
|
|
||||||
: TextDecoration.none,
|
|
||||||
fontSize: AppTypography.bodySmall,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
)
|
||||||
),
|
.toList(),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
if (update.urls.isNotEmpty)
|
||||||
}).toList(),
|
_buildMinimalLinks(
|
||||||
|
context,
|
||||||
|
update.urls.map((url) {
|
||||||
|
final host = Uri.tryParse(url)?.host ?? 'Link';
|
||||||
|
return _MinimalLinkData(
|
||||||
|
label: host,
|
||||||
|
icon: Icons.open_in_new,
|
||||||
|
onTap: () => _launchUri(url),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
if (update.items.isNotEmpty)
|
||||||
|
_buildMinimalLinks(
|
||||||
|
context,
|
||||||
|
update.items.map((item) {
|
||||||
|
final title = item.title?.isNotEmpty == true
|
||||||
|
? item.title!
|
||||||
|
: item.link ?? 'Result';
|
||||||
|
return _MinimalLinkData(
|
||||||
|
label: title,
|
||||||
|
icon: Icons.link,
|
||||||
|
onTap: item.link != null
|
||||||
|
? () => _launchUri(item.link!)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildMinimalLinks(
|
||||||
|
BuildContext context,
|
||||||
|
List<_MinimalLinkData> links,
|
||||||
|
) {
|
||||||
|
final theme = context.conduitTheme;
|
||||||
|
return Wrap(
|
||||||
|
spacing: Spacing.sm,
|
||||||
|
runSpacing: Spacing.xxs,
|
||||||
|
children: links.map((link) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: link.onTap,
|
||||||
|
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.xxs,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(link.icon, size: 10, color: theme.buttonPrimary),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
link.label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.buttonPrimary,
|
||||||
|
fontSize: AppTypography.labelSmall,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationColor: theme.buttonPrimary.withValues(
|
||||||
|
alpha: 0.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String _formatTimestamp(DateTime timestamp) {
|
String _formatTimestamp(DateTime timestamp) {
|
||||||
final local = timestamp.toLocal();
|
final local = timestamp.toLocal();
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
@@ -1643,6 +1583,14 @@ class _StatusHistoryEntry extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _MinimalLinkData {
|
||||||
|
const _MinimalLinkData({required this.label, required this.icon, this.onTap});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final IconData icon;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
}
|
||||||
|
|
||||||
class CodeExecutionListView extends StatelessWidget {
|
class CodeExecutionListView extends StatelessWidget {
|
||||||
const CodeExecutionListView({super.key, required this.executions});
|
const CodeExecutionListView({super.key, required this.executions});
|
||||||
|
|
||||||
@@ -1901,6 +1849,7 @@ class FollowUpSuggestionBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final theme = context.conduitTheme;
|
||||||
final trimmedSuggestions = suggestions
|
final trimmedSuggestions = suggestions
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.where((s) => s.isNotEmpty)
|
.where((s) => s.isNotEmpty)
|
||||||
@@ -1910,26 +1859,108 @@ class FollowUpSuggestionBar extends StatelessWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _AssistantResponseSection(
|
return Column(
|
||||||
title: 'Suggested next steps',
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
icon: Icons.auto_awesome,
|
children: [
|
||||||
child: Column(
|
// Subtle header
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: Spacing.xs),
|
Icon(
|
||||||
Wrap(
|
Icons.lightbulb_outline,
|
||||||
spacing: Spacing.xs,
|
size: 14,
|
||||||
runSpacing: Spacing.xs,
|
color: theme.textSecondary.withValues(alpha: 0.8),
|
||||||
children: [
|
),
|
||||||
for (final suggestion in trimmedSuggestions)
|
const SizedBox(width: Spacing.xxs),
|
||||||
_AssistantSuggestionChip(
|
Text(
|
||||||
label: suggestion,
|
'Continue with',
|
||||||
onPressed: isBusy ? null : () => onSelected(suggestion),
|
style: TextStyle(
|
||||||
enabled: !isBusy,
|
fontSize: AppTypography.labelSmall,
|
||||||
),
|
color: theme.textSecondary.withValues(alpha: 0.8),
|
||||||
],
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: Spacing.xs),
|
||||||
|
Wrap(
|
||||||
|
spacing: Spacing.sm,
|
||||||
|
runSpacing: Spacing.xs,
|
||||||
|
children: [
|
||||||
|
for (final suggestion in trimmedSuggestions)
|
||||||
|
_MinimalFollowUpButton(
|
||||||
|
label: suggestion,
|
||||||
|
onPressed: isBusy ? null : () => onSelected(suggestion),
|
||||||
|
enabled: !isBusy,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MinimalFollowUpButton extends StatelessWidget {
|
||||||
|
const _MinimalFollowUpButton({
|
||||||
|
required this.label,
|
||||||
|
this.onPressed,
|
||||||
|
this.enabled = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = context.conduitTheme;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: enabled ? onPressed : null,
|
||||||
|
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.sm,
|
||||||
|
vertical: Spacing.xs,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: enabled
|
||||||
|
? theme.surfaceContainer.withValues(alpha: 0.3)
|
||||||
|
: theme.surfaceContainer.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
|
||||||
|
border: Border.all(
|
||||||
|
color: enabled
|
||||||
|
? theme.buttonPrimary.withValues(alpha: 0.2)
|
||||||
|
: theme.dividerColor.withValues(alpha: 0.3),
|
||||||
|
width: 1,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_forward,
|
||||||
|
size: 12,
|
||||||
|
color: enabled
|
||||||
|
? theme.buttonPrimary.withValues(alpha: 0.8)
|
||||||
|
: theme.textSecondary.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
const SizedBox(width: Spacing.xxs),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: enabled
|
||||||
|
? theme.buttonPrimary
|
||||||
|
: theme.textSecondary.withValues(alpha: 0.5),
|
||||||
|
fontSize: AppTypography.bodySmall,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ class _ChatActionButtonState extends ConsumerState<ChatActionButton> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = context.conduitTheme;
|
final theme = context.conduitTheme;
|
||||||
final hapticEnabled = ref.read(hapticEnabledProvider);
|
final hapticEnabled = ref.read(hapticEnabledProvider);
|
||||||
final radius =
|
final radius = BorderRadius.circular(AppBorderRadius.circular);
|
||||||
widget.borderRadius ?? BorderRadius.circular(AppBorderRadius.lg);
|
|
||||||
final overlay = theme.buttonPrimary.withValues(alpha: 0.08);
|
final overlay = theme.buttonPrimary.withValues(alpha: 0.08);
|
||||||
|
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
@@ -42,7 +41,7 @@ class _ChatActionButtonState extends ConsumerState<ChatActionButton> {
|
|||||||
button: true,
|
button: true,
|
||||||
label: widget.label,
|
label: widget.label,
|
||||||
child: AnimatedScale(
|
child: AnimatedScale(
|
||||||
scale: _pressed ? 0.98 : 1.0,
|
scale: _pressed ? 0.95 : 1.0,
|
||||||
duration: const Duration(milliseconds: 120),
|
duration: const Duration(milliseconds: 120),
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
child: Material(
|
child: Material(
|
||||||
@@ -62,6 +61,8 @@ class _ChatActionButtonState extends ConsumerState<ChatActionButton> {
|
|||||||
widget.onTap!();
|
widget.onTap!();
|
||||||
},
|
},
|
||||||
child: Ink(
|
child: Ink(
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.textPrimary.withValues(alpha: 0.04),
|
color: theme.textPrimary.withValues(alpha: 0.04),
|
||||||
borderRadius: radius,
|
borderRadius: radius,
|
||||||
@@ -70,28 +71,10 @@ class _ChatActionButtonState extends ConsumerState<ChatActionButton> {
|
|||||||
width: BorderWidth.regular,
|
width: BorderWidth.regular,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Icon(
|
||||||
padding: widget.padding,
|
widget.icon,
|
||||||
child: Row(
|
size: IconSize.sm,
|
||||||
mainAxisSize: MainAxisSize.min,
|
color: theme.textPrimary.withValues(alpha: 0.8),
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
widget.icon,
|
|
||||||
size: IconSize.sm,
|
|
||||||
color: theme.textPrimary.withValues(alpha: 0.8),
|
|
||||||
),
|
|
||||||
const SizedBox(width: Spacing.xs),
|
|
||||||
Text(
|
|
||||||
widget.label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: AppTypography.labelMedium,
|
|
||||||
color: theme.textPrimary.withValues(alpha: 0.8),
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
letterSpacing: 0.2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user