blob: 4f45bfa2d34d2f6cbbdabeb85e6a2ee617afd30d [file]
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',
};
}
}