blob: 657bf97230ffc0c6b9e8db6173ba3705cef3d34b [file]
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);
}