| import '../utils/widget_tree_helper.dart'; |
| |
| /// Holds a MIME type and data URL pair for media prompt parts. |
| typedef MediaInfo = ({String mimeType, String url}); |
| |
| class Prompt { |
| const Prompt(this.parts); |
| |
| factory Prompt.fromString(String s) { |
| return Prompt([TextPromptPart(s)]); |
| } |
| |
| final List<PromptPart> parts; |
| |
| bool get isEmpty => |
| parts.isEmpty || |
| parts.every( |
| (e) => switch (e) { |
| TextPromptPart(:final text) => text.isEmpty, |
| ImagePromptPart() => false, |
| _ => false, |
| }, |
| ); |
| |
| String toMessage() { |
| return parts.map((e) => e.toMessage()).join(); |
| } |
| } |
| |
| sealed class PromptPart { |
| const PromptPart(); |
| |
| String toMessage(); |
| |
| /// Returns structured media info (MIME type + data URL) if this part |
| /// represents inline media. Returns `null` for text-only parts. |
| MediaInfo? toMediaInfo(); |
| } |
| |
| class TextPromptPart extends PromptPart { |
| const TextPromptPart(this.text); |
| |
| final String text; |
| |
| @override |
| String toMessage() { |
| return text; |
| } |
| |
| @override |
| MediaInfo? toMediaInfo() => null; |
| } |
| |
| class FilePromptPart extends PromptPart { |
| const FilePromptPart({ |
| required this.path, |
| required this.lineFrom, |
| required this.lineTo, |
| required this.text, |
| }); |
| |
| final String path; |
| final int lineFrom; |
| final int lineTo; |
| final String text; |
| |
| @override |
| String toMessage() { |
| final line = lineFrom == lineTo ? '$lineFrom' : '$lineFrom-$lineTo'; |
| return '<file path="$path" line="$line">$text</file>'; |
| } |
| |
| @override |
| MediaInfo? toMediaInfo() => null; |
| } |
| |
| class WidgetSnapshotPromptPart extends PromptPart { |
| const WidgetSnapshotPromptPart({ |
| required this.widgetName, |
| required this.screenshotBase64, |
| required this.summaryTree, |
| this.properties = const {}, |
| }); |
| |
| final String widgetName; |
| final String screenshotBase64; |
| final Map<String, dynamic> summaryTree; |
| final Map<String, String> properties; |
| |
| @override |
| String toMessage() { |
| const helper = WidgetTreeHelper(); |
| final xmlTree = helper.nodeToXml(summaryTree, ' '); |
| final sb = StringBuffer(); |
| sb.writeln('<$widgetName>'); |
| if (properties.isNotEmpty) { |
| sb.writeln(' <properties>'); |
| for (final entry in properties.entries) { |
| sb.writeln(' ${entry.key}: ${entry.value}'); |
| } |
| sb.writeln(' </properties>'); |
| } |
| sb.writeln(' <summary_tree>'); |
| sb.write(xmlTree); |
| sb.writeln(' </summary_tree>'); |
| sb.write('</$widgetName>'); |
| return sb.toString(); |
| } |
| |
| @override |
| MediaInfo? toMediaInfo() => (mimeType: 'image/png', url: 'data:image/png;base64,$screenshotBase64'); |
| } |
| |
| class DiagnosticPromptPart extends PromptPart { |
| const DiagnosticPromptPart({ |
| required this.path, |
| required this.line, |
| required this.message, |
| required this.severity, |
| }); |
| |
| final String path; |
| final int line; |
| final String message; |
| final String severity; |
| |
| @override |
| String toMessage() { |
| return '<diagnostic path="$path" line="$line" severity="$severity">$message</diagnostic>'; |
| } |
| |
| @override |
| MediaInfo? toMediaInfo() => null; |
| } |
| |
| /// A prompt part representing a user-uploaded image. |
| class ImagePromptPart extends PromptPart { |
| const ImagePromptPart({ |
| required this.fileName, |
| required this.mimeType, |
| required this.base64Data, |
| }); |
| |
| /// Display name of the uploaded file. |
| final String fileName; |
| |
| /// MIME type, e.g. `image/png`, `image/jpeg`. |
| final String mimeType; |
| |
| /// Base64-encoded image data. |
| final String base64Data; |
| |
| /// The full data URL for this image (`data:<mimeType>;base64,<data>`). |
| String get dataUrl => 'data:$mimeType;base64,$base64Data'; |
| |
| /// Accepted MIME types for Gemini 2.5 image input. |
| static const Set<String> supportedMimeTypes = { |
| 'image/png', |
| 'image/jpeg', |
| 'image/webp', |
| 'image/heic', |
| 'image/heif', |
| }; |
| |
| /// File extensions accepted in the file picker. |
| static const String acceptAttribute = '.png,.jpg,.jpeg,.webp,.heic,.heif'; |
| |
| @override |
| // Intentionally empty – image data is sent as a MediaPart via |
| // toMediaInfo(), not as inline text in the prompt. |
| String toMessage() { |
| return ''; |
| } |
| |
| @override |
| MediaInfo? toMediaInfo() => (mimeType: mimeType, url: dataUrl); |
| } |