1
0
mirror of https://github.com/flutter/samples.git synced 2026-05-09 08:27:19 +00:00

Adds ai_recipe_generation sample (#2242)

Adding the demo app from my I/O talk. Because AI.

## Pre-launch Checklist

- [x] I read the [Flutter Style Guide] _recently_, and have followed its
advice.
- [x] I signed the [CLA].
- [x] I read the [Contributors Guide].
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] All existing and new tests are passing.

---------

Co-authored-by: Brett Morgan <brett.morgan@gmail.com>
This commit is contained in:
Eric Windmill
2024-05-14 11:41:20 -04:00
committed by GitHub
parent 8575261d37
commit be52906894
171 changed files with 8626 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../theme.dart';
class AppInfoDialog extends StatelessWidget {
const AppInfoDialog({super.key});
Widget bulletRow(String text, {IconData? icon}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon ?? Symbols.label_important_outline),
const SizedBox(
width: 10,
),
Expanded(
child: Text(
text,
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
MarketplaceTheme.defaultBorderRadius,
),
),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MarketplaceTheme.borderColor),
),
padding: const EdgeInsets.all(MarketplaceTheme.spacing4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Use the form on this screen to ask Cat Chef to make a recipe for you.",
style: MarketplaceTheme.heading3,
),
const SizedBox(
height: MarketplaceTheme.spacing4,
),
bulletRow(
"Add images of ingredients you have, like a picture of the inside of your fridge or pantry.",
icon: Symbols.looks_one,
),
const SizedBox(
height: MarketplaceTheme.spacing7,
),
bulletRow(
"Choose what kind of food you're in the mood for, and what staple ingredients you have that might not be pictured.",
icon: Symbols.looks_two,
),
const SizedBox(
height: MarketplaceTheme.spacing7,
),
bulletRow(
"In the text box at the bottom, add any additional context that you'd like. \nFor example, you could say \"I'm in a hurry! Make sure the recipe doesn't take longer than 30 minutes to make.\"",
icon: Symbols.looks_3,
),
const SizedBox(
height: MarketplaceTheme.spacing7,
),
bulletRow(
"Submit the prompt, and Chef Noodle will give you a recipe!",
icon: Symbols.looks_4,
),
const SizedBox(
height: MarketplaceTheme.spacing4,
),
Text(
"Steps 1, 2 and 3 are optional. More information will provide better results.",
style: MarketplaceTheme.label,
),
const SizedBox(height: MarketplaceTheme.spacing4),
TextButton.icon(
icon: const Icon(
Symbols.close,
color: Colors.black87,
),
label: Text(
'Close',
style: MarketplaceTheme.dossierParagraph,
),
onPressed: () {
Navigator.pop(context);
},
style: ButtonStyle(
shape: WidgetStateProperty.resolveWith(
(states) {
return const RoundedRectangleBorder(
side: BorderSide(color: Colors.black26),
borderRadius: BorderRadius.all(
Radius.circular(MarketplaceTheme.defaultBorderRadius),
),
);
},
),
textStyle: WidgetStateTextStyle.resolveWith(
(states) {
return MarketplaceTheme.dossierParagraph
.copyWith(color: Colors.black45);
},
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../theme.dart';
import '../../../widgets/prompt_image_widget.dart';
import '../prompt_model.dart';
class FullPromptDialog extends StatelessWidget {
const FullPromptDialog({super.key, required this.promptData});
final PromptData promptData;
Widget bulletRow(String text, {IconData? icon}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon ?? Symbols.label_important_outline),
const SizedBox(
width: 10,
),
Expanded(
child: Text(
text,
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Dialog.fullscreen(
child: SingleChildScrollView(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MarketplaceTheme.borderColor),
),
padding: const EdgeInsets.all(MarketplaceTheme.spacing4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"This is the full prompt that will be sent to Google's Gemini model.",
style: MarketplaceTheme.heading3,
),
const SizedBox(height: MarketplaceTheme.spacing4),
if (promptData.images.isNotEmpty)
Container(
height: 100,
decoration: const BoxDecoration(
border: Border.symmetric(
horizontal: BorderSide(
color: MarketplaceTheme.borderColor,
),
),
),
child: ListView(
scrollDirection: Axis.horizontal,
children: [
for (var image in promptData.images)
Padding(
padding: const EdgeInsets.all(8.0),
child: PromptImage(
file: image,
),
),
],
),
),
const SizedBox(height: MarketplaceTheme.spacing4),
bulletRow(promptData.textInput),
if (promptData.additionalTextInputs.isNotEmpty)
...promptData.additionalTextInputs.map((i) => bulletRow(i)),
const SizedBox(height: MarketplaceTheme.spacing4),
TextButton.icon(
icon: const Icon(
Symbols.close,
color: Colors.black87,
),
label: Text(
'Close',
style: MarketplaceTheme.dossierParagraph,
),
onPressed: () {
Navigator.pop(context);
},
style: ButtonStyle(
shape: WidgetStateProperty.resolveWith(
(states) {
return const RoundedRectangleBorder(
side: BorderSide(color: Colors.black26),
borderRadius: BorderRadius.all(
Radius.circular(MarketplaceTheme.defaultBorderRadius),
),
);
},
),
textStyle: WidgetStateTextStyle.resolveWith(
(states) {
return MarketplaceTheme.dossierParagraph
.copyWith(color: Colors.black45);
},
),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,278 @@
import 'package:ai_recipe_generation/widgets/highlight_border_on_hover_widget.dart';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import '../../../main.dart';
import '../../../theme.dart';
import '../../../util/device_info.dart';
import '../../../widgets/add_image_widget.dart';
import '../../../widgets/prompt_image_widget.dart';
import '../prompt_view_model.dart';
class AddImageToPromptWidget extends StatefulWidget {
const AddImageToPromptWidget({
super.key,
this.width = 100,
this.height = 100,
});
final double width;
final double height;
@override
State<AddImageToPromptWidget> createState() => _AddImageToPromptWidgetState();
}
class _AddImageToPromptWidgetState extends State<AddImageToPromptWidget> {
final ImagePicker picker = ImagePicker();
late CameraController _controller;
late Future<void> _initializeControllerFuture;
bool flashOn = false;
@override
void initState() {
super.initState();
if (DeviceInfo.isPhysicalDeviceWithCamera(deviceInfo)) {
_controller = CameraController(
camera,
ResolutionPreset.medium,
);
_initializeControllerFuture = _controller.initialize();
}
}
Future<XFile> _showCamera() async {
final image = await showGeneralDialog<XFile?>(
context: context,
transitionBuilder: (context, animation, secondaryAnimation, child) {
return AnimatedOpacity(
opacity: animation.value,
duration: const Duration(milliseconds: 100),
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) {
return Dialog.fullscreen(
insetAnimationDuration: const Duration(seconds: 1),
child: FutureBuilder(
future: _initializeControllerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If the Future is complete, display the preview.
return CameraView(
controller: _controller,
initializeControllerFuture: _initializeControllerFuture,
);
} else {
// Otherwise, display a loading indicator.
return const Center(child: CircularProgressIndicator());
}
},
),
);
},
);
if (image != null) {
return image;
} else {
throw "failed to take image";
}
}
Future<XFile> _pickImage() async {
final image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
return image;
} else {
throw "failed to take image";
}
}
Future<XFile> _addImage() async {
if (DeviceInfo.isPhysicalDeviceWithCamera(deviceInfo)) {
return await _showCamera();
} else {
return await _pickImage();
}
}
@override
Widget build(BuildContext context) {
final viewModel = context.watch<PromptViewModel>();
return HighlightBorderOnHoverWidget(
borderRadius: BorderRadius.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
left: MarketplaceTheme.spacing7,
top: MarketplaceTheme.spacing7,
),
child: Text(
'I have these ingredients:',
style: MarketplaceTheme.dossierParagraph,
),
),
SizedBox(
height: widget.height,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
Padding(
padding: const EdgeInsets.all(MarketplaceTheme.spacing7),
child: AddImage(
width: widget.width,
height: widget.height,
onTap: () async {
final image = await _addImage();
viewModel.addImage(image);
}),
),
for (var image in viewModel.userPrompt.images)
Padding(
padding: const EdgeInsets.all(MarketplaceTheme.spacing7),
child: PromptImage(
width: widget.width,
file: image,
onTapIcon: () => viewModel.removeImage(image),
),
),
],
),
),
],
),
);
}
}
class CameraView extends StatefulWidget {
final CameraController controller;
final Future initializeControllerFuture;
const CameraView(
{super.key,
required this.controller,
required this.initializeControllerFuture});
@override
State<CameraView> createState() => _CameraViewState();
}
class _CameraViewState extends State<CameraView> {
bool flashOn = false;
@override
Widget build(BuildContext context) {
CameraController controller = widget.controller;
return Stack(
children: [
Center(
child: AspectRatio(
aspectRatio: 9 / 14,
child: ClipRect(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
height: controller.value.previewSize!.width,
width: controller.value.previewSize!.height,
child: Center(
child: CameraPreview(
controller,
// child: ElevatedButton(
// child: Text('Button'),
// onPressed: () {},
// ),
),
),
),
),
),
),
),
Positioned(
top: 0,
left: 0,
right: 0,
height: 89.5,
child: Container(
color: Colors.black.withOpacity(.7),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding:
const EdgeInsets.only(left: MarketplaceTheme.spacing4),
child: IconButton(
icon: Icon(
flashOn ? Symbols.flash_on : Symbols.flash_off,
size: 40,
color: flashOn ? Colors.yellowAccent : Colors.white,
),
onPressed: () {
controller.setFlashMode(
flashOn ? FlashMode.off : FlashMode.always);
setState(() {
flashOn = !flashOn;
});
},
),
),
Padding(
padding:
const EdgeInsets.only(right: MarketplaceTheme.spacing4),
child: IconButton(
icon: const Icon(
Symbols.cancel,
color: Colors.white,
size: 40,
),
onPressed: () async {
Navigator.of(context).pop();
},
),
),
],
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
height: 150,
child: Container(
color: Colors.black.withOpacity(.7),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(
Symbols.camera,
color: Colors.white,
size: 70,
),
onPressed: () async {
try {
await widget.initializeControllerFuture;
final image = await controller.takePicture();
if (!context.mounted) return;
Navigator.of(context).pop(image);
} catch (e) {
rethrow;
}
},
),
],
),
),
)
],
);
}
}