| import 'package:jaspr/dom.dart'; |
| import 'package:jaspr/jaspr.dart'; |
| |
| import '../../../utils/styles.dart'; |
| import '../../shared/components/image_preview_dialog.dart'; |
| import '../../shared/components/image_thumbnail.dart'; |
| import '../../shared/models/prompt.dart'; |
| import 'file_chip.dart'; |
| import 'widget_chip.dart'; |
| |
| /// A widget that displays a user or assistant chat message bubble. |
| class AgentPromptCard extends StatefulComponent { |
| const AgentPromptCard({required this.prompt, super.key}); |
| |
| final Prompt prompt; |
| |
| @override |
| State<AgentPromptCard> createState() => _AgentPromptCardState(); |
| |
| @css |
| static List<StyleRule> get styles => [ |
| css('.agent-prompt-card').styles( |
| display: .flex, |
| width: 100.percent, |
| padding: Padding.symmetric(vertical: 8.px, horizontal: 12.px), |
| margin: Margin.zero, |
| boxSizing: .borderBox, |
| border: Border.all(color: colorBorder, width: 1.px), |
| radius: .circular(8.px), |
| flexDirection: .column, |
| color: colorOnSurface, |
| fontSize: 13.px, |
| lineHeight: 18.px, |
| backgroundColor: colorContainer, |
| raw: {'word-break': 'break-word'}, |
| ), |
| css('.agent-prompt-body').styles( |
| display: .block, |
| whiteSpace: .preWrap, |
| ), |
| css('.agent-prompt-attachments-strip').styles( |
| display: .flex, |
| margin: Margin.only(top: 8.px), |
| flexWrap: .wrap, |
| gap: Gap.all(8.px), |
| ), |
| css('.diagnostic-chip-container').styles( |
| display: .inline, |
| ), |
| css('.diagnostic-severity-badge').styles( |
| display: .inlineFlex, |
| width: 16.px, |
| height: 16.px, |
| radius: .circular(999.px), |
| userSelect: .none, |
| justifyContent: .center, |
| alignItems: .center, |
| fontSize: 10.px, |
| fontWeight: .w700, |
| raw: {'vertical-align': 'bottom'}, |
| ), |
| css('.diagnostic-severity-badge.error').styles( |
| color: colorOnSurface, |
| backgroundColor: colorError.withOpacity(0.2), |
| ), |
| css('.diagnostic-severity-badge.warning').styles( |
| color: colorOnSurface, |
| backgroundColor: colorWarning.withOpacity(0.2), |
| ), |
| css('.diagnostic-severity-badge.info, .diagnostic-severity-badge.hint').styles( |
| color: colorOnSurface, |
| backgroundColor: colorInfo.withOpacity(0.2), |
| ), |
| css('.diagnostic-message').styles( |
| color: colorOnSurface, |
| fontSize: 13.px, |
| ), |
| ]; |
| } |
| |
| class _AgentPromptCardState extends State<AgentPromptCard> { |
| ImagePromptPart? _previewPart; |
| |
| @override |
| Component build(BuildContext context) { |
| final imageParts = component.prompt.parts.whereType<ImagePromptPart>().toList(); |
| final otherParts = component.prompt.parts.where((part) => part is! ImagePromptPart).toList(); |
| |
| return div(classes: 'agent-prompt-card', [ |
| div(classes: 'agent-prompt-body', [ |
| for (final part in otherParts) |
| switch (part) { |
| TextPromptPart(:final text) => .text(text), |
| FilePromptPart part => FileChip(part: part), |
| WidgetSnapshotPromptPart part => WidgetChip(part: part), |
| DiagnosticPromptPart(:final path, :final line, :final message, :final severity) => span( |
| classes: 'diagnostic-chip-container', |
| [ |
| span( |
| classes: 'diagnostic-severity-badge $severity', |
| [.text(_getSeverityIcon(severity))], |
| ), |
| FileChip( |
| part: FilePromptPart( |
| path: path, |
| lineFrom: line, |
| lineTo: line, |
| text: '', |
| ), |
| ), |
| span( |
| classes: 'diagnostic-message', |
| [.text(': $message')], |
| ), |
| ], |
| ), |
| _ => const Component.text(''), |
| }, |
| ]), |
| if (imageParts.isNotEmpty) |
| div(classes: 'agent-prompt-attachments-strip', [ |
| for (final part in imageParts) |
| ImageThumbnail( |
| imageUrl: part.dataUrl, |
| fileName: part.fileName, |
| onTap: () { |
| setState(() { |
| _previewPart = part; |
| }); |
| }, |
| ), |
| ]), |
| if (_previewPart case final part?) |
| ImagePreviewDialog( |
| imageUrl: part.dataUrl, |
| fileName: part.fileName, |
| onClose: () { |
| setState(() { |
| _previewPart = null; |
| }); |
| }, |
| ), |
| ]); |
| } |
| |
| String _getSeverityIcon(String severity) { |
| return switch (severity) { |
| 'error' => 'E', |
| 'warning' => 'W', |
| 'info' => 'I', |
| 'hint' => 'H', |
| _ => 'E', |
| }; |
| } |
| } |