1. परिचय
इस कोडलैब में, Flutter, Firebase AI Logic, और नए genui पैकेज का इस्तेमाल करके, टास्क की सूची वाला ऐप्लिकेशन बनाया जाएगा. इस वर्कशॉप में, आपको टेक्स्ट पर आधारित चैट ऐप्लिकेशन बनाने से शुरुआत करनी होगी. इसके बाद, आपको इसे GenUI की मदद से अपग्रेड करना होगा, ताकि एजेंट को अपना यूज़र इंटरफ़ेस (यूआई) बनाने की सुविधा मिल सके. आखिर में, आपको अपनी पसंद के मुताबिक इंटरैक्टिव यूआई कॉम्पोनेंट बनाना होगा, जिसे आप और एजेंट सीधे तौर पर इस्तेमाल कर सकें.

आपको क्या करना होगा
- Flutter और Firebase AI Logic का इस्तेमाल करके, बुनियादी चैट इंटरफ़ेस बनाना
- एआई की मदद से काम करने वाले प्लैटफ़ॉर्म जनरेट करने के लिए,
genuiपैकेज इंटिग्रेट करें - प्रोग्रेस बार जोड़ें, ताकि यह पता चल सके कि ऐप्लिकेशन, एजेंट से जवाब मिलने का इंतज़ार कब कर रहा है
- नाम वाला कोई सर्फ़ेस बनाएं और उसे यूज़र इंटरफ़ेस (यूआई) में किसी खास जगह पर दिखाएं.
- कस्टम GenUI कैटलॉग कॉम्पोनेंट बनाएं. इससे आपको यह कंट्रोल मिलता है कि टास्क कैसे दिखाए जाएं
आपको किन चीज़ों की ज़रूरत होगी
- कोई वेब ब्राउज़र, जैसे कि Chrome
- डिवाइस पर इंस्टॉल किया गया Flutter SDK
- Firebase CLI इंस्टॉल और कॉन्फ़िगर किया गया हो
यह कोडलैब, Flutter के इंटरमीडिएट डेवलपर के लिए है.
2. शुरू करने से पहले
Flutter प्रोजेक्ट सेट अप करना
अपना टर्मिनल खोलें और नया प्रोजेक्ट बनाने के लिए, flutter create चलाएं:
flutter create intro_to_genui
cd intro_to_genui
अपने Flutter प्रोजेक्ट में ज़रूरी डिपेंडेंसी जोड़ें:
flutter pub add firebase_core firebase_ai genui json_schema_builder
आपका फ़ाइनल dependencies सेक्शन ऐसा दिखना चाहिए (वर्शन नंबर में थोड़ा अंतर हो सकता है):
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
firebase_core: ^4.5.0
firebase_ai: ^3.9.0
genui: ^0.8.0
json_schema_builder: ^0.1.3
सभी पैकेज डाउनलोड करने के लिए, flutter pub get चलाएं.
एपीआई और Firebase चालू करना
firebase_ai पैकेज का इस्तेमाल करने के लिए, आपको सबसे पहले अपने प्रोजेक्ट में Firebase AI Logic को चालू करना होगा.
- Firebase कंसोल में Firebase AI Logic पर जाएं.
- निर्देशों के साथ वर्कफ़्लो शुरू करने के लिए, शुरू करें पर क्लिक करें.
- अपने प्रोजेक्ट को सेट अप करने के लिए, स्क्रीन पर दिए गए निर्देशों का पालन करें.
ज़्यादा जानकारी के लिए, Flutter ऐप्लिकेशन में Firebase जोड़ने से जुड़े निर्देश देखें.
एपीआई चालू होने के बाद, FlutterFire CLI का इस्तेमाल करके अपने Flutter ऐप्लिकेशन में Firebase शुरू करें:
flutterfire configure
अपना Firebase प्रोजेक्ट चुनें. इसके बाद, टारगेट किए गए प्लैटफ़ॉर्म (जैसे, Android, iOS, वेब) के लिए इसे कॉन्फ़िगर करने के लिए दिए गए निर्देशों का पालन करें. इस कोडलैब को पूरा करने के लिए, आपकी मशीन पर सिर्फ़ Flutter SDK और Chrome इंस्टॉल होना चाहिए. हालांकि, यह ऐप्लिकेशन अन्य प्लैटफ़ॉर्म पर भी काम करेगा.
3. चैट इंटरफ़ेस का बुनियादी ढांचा तैयार करना
जनरेटिव यूज़र इंटरफ़ेस (यूआई) की सुविधा को लागू करने से पहले, आपके ऐप्लिकेशन में बुनियादी सुविधाएं होनी चाहिए. जैसे, टेक्स्ट पर आधारित चैट ऐप्लिकेशन, जो Firebase के एआई लॉजिक पर काम करता हो. जल्दी से शुरू करने के लिए, चैट इंटरफ़ेस के पूरे सेटअप को कॉपी करके चिपकाएं.

मैसेज बबल विजेट बनाना
उपयोगकर्ता और एजेंट के टेक्स्ट मैसेज दिखाने के लिए, आपके ऐप्लिकेशन को विजेट की ज़रूरत होती है. lib/message_bubble.dart नाम की नई फ़ाइल बनाएं और इसमें यह क्लास जोड़ें:
import 'package:flutter/material.dart';
class MessageBubble extends StatelessWidget {
final String text;
final bool isUser;
const MessageBubble({super.key, required this.text, required this.isUser});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final bubbleColor = isUser
? colorScheme.primary
: colorScheme.surfaceContainerHighest;
final textColor = isUser
? colorScheme.onPrimary
: colorScheme.onSurfaceVariant;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 8.0),
child: Column(
crossAxisAlignment: isUser
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: isUser
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12.0,
),
decoration: BoxDecoration(
color: bubbleColor,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(20),
topRight: const Radius.circular(20),
bottomLeft: Radius.circular(isUser ? 20 : 0),
bottomRight: Radius.circular(isUser ? 0 : 20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(20),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
gradient: isUser
? LinearGradient(
colors: [
colorScheme.primary,
colorScheme.primary.withAlpha(200),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
),
child: Text(
text,
style: theme.textTheme.bodyLarge?.copyWith(
color: textColor,
height: 1.3,
),
),
),
),
],
),
const SizedBox(height: 2),
],
),
);
}
}
MessageBubble एक StatelessWidget है, जो एक चैट मैसेज दिखाता है. इसका इस्तेमाल इस कोडलैब में बाद में किया जाएगा, ताकि आपको और एजेंट को मैसेज दिखाए जा सकें. हालांकि, यह ज़्यादातर सिर्फ़ एक शानदार Text विजेट है.
main.dart में Chat यूज़र इंटरफ़ेस (यूआई) लागू करना
lib/main.dart के पूरे कॉन्टेंट को, चैटबॉट को लागू करने के इस पूरे टेक्स्ट से बदलें:
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:intro_to_genui/message_bubble.dart';
import 'firebase_options.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Just Today',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
sealed class ConversationItem {}
class TextItem extends ConversationItem {
final String text;
final bool isUser;
TextItem({required this.text, this.isUser = false});
}
class _MyHomePageState extends State<MyHomePage> {
final List<ConversationItem> _items = [];
final _textController = TextEditingController();
final _scrollController = ScrollController();
late final ChatSession _chatSession;
@override
void initState() {
super.initState();
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-3-flash-preview',
);
_chatSession = model.startChat();
_chatSession.sendMessage(Content.text(systemInstruction));
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
@override
void dispose() {
_textController.dispose();
_scrollController.dispose();
super.dispose();
}
Future<void> _addMessage() async {
final text = _textController.text;
if (text.trim().isEmpty) {
return;
}
_textController.clear();
setState(() {
_items.add(TextItem(text: text, isUser: true));
});
_scrollToBottom();
final response = await _chatSession.sendMessage(Content.text(text));
if (response.text?.isNotEmpty ?? false) {
setState(() {
_items.add(TextItem(text: response.text!, isUser: false));
});
_scrollToBottom();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Just Today'),
),
body: Column(
children: [
Expanded(
child: ListView(
controller: _scrollController,
padding: const EdgeInsets.all(16),
children: [
for (final item in _items)
switch (item) {
TextItem() => MessageBubble(
text: item.text,
isUser: item.isUser,
),
},
],
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _textController,
onSubmitted: (_) => _addMessage(),
decoration: const InputDecoration(
hintText: 'Enter a message',
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _addMessage,
child: const Text('Send'),
),
],
),
),
),
],
),
);
}
}
const systemInstruction = '''
## PERSONA
You are an expert task planner.
## GOAL
Work with me to produce a list of tasks that I should do today, and then track
the completion status of each one.
## RULES
Talk with me only about tasks that I should do today.
Do not engage in conversation about any other topic.
Do not offer suggestions unless I ask for them.
Do not offer encouragement unless I ask for it.
Do not offer advice unless I ask for it.
Do not offer opinions unless I ask for them.
## PROCESS
### Planning
* Ask me for information about tasks that I should do today.
* Synthesize a list of tasks from that information.
* Ask clarifying questions if you need to.
* When you have a list of tasks that you think I should do today, present it
to me for review.
* Respond to my suggestions for changes, if I have any, until I accept the
list.
### Tracking
* Once the list is accepted, ask me to let you know when individual tasks are
complete.
* If I tell you a task is complete, mark it as complete.
* Once all tasks are complete, send a message acknowledging that, and then
end the conversation.
''';
आपने अभी जिस main.dart फ़ाइल को कॉपी करके चिपकाया है वह Firebase AI Logic और systemInstruction में मौजूद प्रॉम्प्ट का इस्तेमाल करके, बुनियादी ChatSession सेट अप करती है. यह बातचीत के क्रम को मैनेज करता है. इसके लिए, यह TextItem एलिमेंट की सूची बनाए रखता है और उन्हें उपयोगकर्ता की क्वेरी के साथ दिखाता है. इसके लिए, यह उस MessageBubble विजेट का इस्तेमाल करता है जिसे आपने पहले बनाया था.
आगे बढ़ने से पहले, यहां दी गई कुछ चीज़ें देख लें:
initStateतरीके से, Firebase AI Logic से कनेक्शन सेट अप किया जाता है.- ऐप्लिकेशन, एजेंट को मैसेज भेजने के लिए
TextFieldऔर एक बटन उपलब्ध कराता है. _addMessageतरीके में, उपयोगकर्ता का मैसेज एजेंट को भेजा जाता है._itemsसूची में, बातचीत का इतिहास सेव किया जाता है.- मैसेज,
ListViewमेंMessageBubbleविजेट का इस्तेमाल करके दिखाए जाते हैं.
ऐप्लिकेशन को टेस्ट करना
ऐसा करने के बाद, अब ऐप्लिकेशन को चलाया जा सकता है और उसकी जांच की जा सकती है.
flutter run -d chrome
आज आपको जो काम करने हैं उनके बारे में एजेंट से चैट करें. सिर्फ़ टेक्स्ट पर आधारित यूज़र इंटरफ़ेस (यूआई) से काम हो सकता है. हालाँकि, GenUI से काम को ज़्यादा आसानी से और तेज़ी से किया जा सकता है.
4. GenUI पैकेज इंटिग्रेट करना
अब समय आ गया है कि सामान्य टेक्स्ट से जनरेटिव यूज़र इंटरफ़ेस पर अपग्रेड किया जाए. आपको GenUI Conversation, Catalog, और SurfaceController ऑब्जेक्ट के लिए, Firebase मैसेजिंग लूप को बदलना होगा. इससे एआई मॉडल को चैट स्ट्रीम में असली Flutter विजेट बनाने की अनुमति मिलती है.

genui पैकेज में पांच क्लास उपलब्ध हैं. इस कोडलैब में इनका इस्तेमाल किया जाएगा:
SurfaceControllerमॉडल से जनरेट किए गए मैप के यूज़र इंटरफ़ेस (यूआई) को स्क्रीन पर दिखाता है.A2uiTransportAdapter, GenUI के इंटरनल अनुरोधों को किसी भी बाहरी लैंग्वेज मॉडल से जोड़ता है.Conversationआपके Flutter ऐप्लिकेशन के लिए, कंट्रोलर और ट्रांसपोर्ट अडैप्टर को एक ही यूनिफ़ाइड एपीआई के साथ रैप करता है.Catalogमें, भाषा मॉडल के लिए उपलब्ध विजेट और प्रॉपर्टी के बारे में बताया गया है.Surfaceएक ऐसा विजेट है जो मॉडल से जनरेट किया गया यूज़र इंटरफ़ेस (यूआई) दिखाता है.
जनरेट किए गए Surface को दिखाने के लिए तैयार रहें
मौजूदा कोड में एक TextItem क्लास शामिल है. यह बातचीत में मौजूद किसी एक टेक्स्ट मैसेज को दिखाती है. एजेंट की बनाई गई Surface को दिखाने के लिए, एक और क्लास जोड़ें:
class SurfaceItem extends ConversationItem {
final String surfaceId;
SurfaceItem({required this.surfaceId});
}
GenUI बिल्डिंग ब्लॉक शुरू करना
lib/main.dart में सबसे ऊपर, genui लाइब्रेरी इंपोर्ट करें:
import 'package:genui/genui.dart' hide TextPart;
import 'package:genui/genui.dart' as genui;
genui पैकेज और firebase_ai पैकेज, दोनों में TextPart क्लास शामिल है. इस तरह से genui को इंपोर्ट करने पर, TextPart के वर्शन को genui.TextPart के तौर पर नेमस्पेस किया जाता है. इससे नाम के टकराव से बचा जा सकता है.
_chatSession के बाद _MyHomePageState में मुख्य फ़ंक्शनल कंट्रोलर का एलान करें:
class _MyHomePageState extends State<MyHomePage> {
// ... existing members
late final ChatSession _chatSession;
// Add GenUI controllers
late final SurfaceController _controller;
late final A2uiTransportAdapter _transport;
late final Conversation _conversation;
late final Catalog catalog;
इसके बाद, GenUI लाइब्रेरी के कंट्रोलर तैयार करने के लिए, initState को अपडेट करें.
initState से इस लाइन को हटाएं:
_chatSession.sendMessage(Content.text(systemInstruction));
इसके बाद, यह कोड जोड़ें:
@override
void initState() {
// ... existing code ...
// Initialize the GenUI Catalog.
// The genui package provides a default set of primitive widgets (like text
// and basic buttons) out of the box using this class.
catalog = BasicCatalogItems.asCatalog();
// Create a SurfaceController to manage the state of generated surfaces.
_controller = SurfaceController(catalogs: [catalog]);
// Create a transport adapter that will process messages to and from the
// agent, looking for A2UI messages.
_transport = A2uiTransportAdapter(onSend: _sendAndReceive);
// Link the transport and SurfaceController together in a Conversation,
// which provides your app a unified API for interacting with the agent.
_conversation = Conversation(
controller: _controller,
transport: _transport,
);
}
यह कोड, कंट्रोलर और अडैप्टर को मैनेज करने वाला Conversation फ़साड बनाता है. इस बातचीत से आपके ऐप्लिकेशन को इवेंट की एक स्ट्रीम मिलती है. इसका इस्तेमाल करके, यह पता लगाया जा सकता है कि एजेंट क्या बना रहा है. साथ ही, इससे एजेंट को मैसेज भेजने का तरीका भी मिलता है.
इसके बाद, बातचीत के इवेंट के लिए लिसनर बनाएं. इनमें, टेक्स्ट मैसेज और गड़बड़ियों के साथ-साथ, प्लैटफ़ॉर्म से जुड़े इवेंट भी शामिल हैं:
@override
void initState() {
// ... existing code ...
// Listen to GenUI stream events to update the UI
_conversation.events.listen((event) {
setState(() {
switch (event) {
case ConversationSurfaceAdded added:
_items.add(SurfaceItem(surfaceId: added.surfaceId));
_scrollToBottom();
case ConversationSurfaceRemoved removed:
_items.removeWhere(
(item) =>
item is SurfaceItem && item.surfaceId == removed.surfaceId,
);
case ConversationContentReceived content:
_items.add(TextItem(text: content.text, isUser: false));
_scrollToBottom();
case ConversationError error:
debugPrint('GenUI Error: ${error.error}');
default:
}
});
});
}
आखिर में, सिस्टम प्रॉम्प्ट बनाएं और उसे एजेंट को भेजें:
@override
void initState() {
// ... existing code ...
// Create the system prompt for the agent, which will include this app's
// system instruction as well as the schema for the catalog.
final promptBuilder = PromptBuilder.chat(
catalog: catalog,
systemPromptFragments: [systemInstruction],
);
// Send the prompt into the Conversation, which will subsequently route it
// to Firebase using the transport mechanism.
_conversation.sendRequest(
ChatMessage.system(promptBuilder.systemPromptJoined()),
);
}
डिसप्ले प्लैटफ़ॉर्म
इसके बाद, _items सूची में SurfaceItem दिखाने के लिए, ListView के build तरीके को अपडेट करें:
Expanded(
child: ListView(
controller: _scrollController,
padding: const EdgeInsets.all(16),
children: [
for (final item in _items)
switch (item) {
TextItem() => MessageBubble(
text: item.text,
isUser: item.isUser,
),
// New!
SurfaceItem() => Surface(
surfaceContext: _controller.contextFor(
item.surfaceId,
),
),
},
],
),
),
Surface विजेट का कंस्ट्रक्टर, एक surfaceContext लेता है. इससे यह पता चलता है कि इसे कौनसी सतह दिखानी है. पहले बनाया गया SurfaceController, _controller, हर प्लैटफ़ॉर्म के लिए परिभाषा और स्थिति देता है. साथ ही, यह पक्का करता है कि अपडेट होने पर यह फिर से बन जाए.
GenUI को Firebase AI Logic से कनेक्ट करना
genui पैकेज में "Bring Your Own Model" अप्रोच का इस्तेमाल किया जाता है. इसका मतलब है कि आपके पास यह कंट्रोल होता है कि आपके अनुभव को बेहतर बनाने के लिए, कौनसे एलएलएम का इस्तेमाल किया जाए. इस मामले में, Firebase के एआई लॉजिक का इस्तेमाल किया जा रहा है. हालांकि, इस पैकेज को अलग-अलग एजेंट और प्रोवाइडर के साथ काम करने के लिए बनाया गया है.
इस सुविधा के साथ कुछ ज़िम्मेदारियां भी जुड़ी हैं: आपको genui पैकेज से जनरेट हुए मैसेज को अपने चुने हुए एजेंट को भेजना होगा. साथ ही, एजेंट के जवाबों को genui में वापस भेजना होगा.
इसके लिए, आपको पिछले चरण के कोड में बताए गए _sendAndReceive तरीके को तय करना होगा. MyHomePageState में यह कोड जोड़ें:
Future<void> _sendAndReceive(ChatMessage msg) async {
final buffer = StringBuffer();
// Reconstruct the message part fragments
for (final part in msg.parts) {
if (part.isUiInteractionPart) {
buffer.write(part.asUiInteractionPart!.interaction);
} else if (part is genui.TextPart) {
buffer.write(part.text);
}
}
if (buffer.isEmpty) {
return;
}
final text = buffer.toString();
// Send the string to Firebase AI Logic.
final response = await _chatSession.sendMessage(Content.text(text));
if (response.text?.isNotEmpty ?? false) {
// Feed the response back into GenUI's transportation layer
_transport.addChunk(response.text!);
}
}
जब भी genui पैकेज को एजेंट को कोई मैसेज भेजना होगा, तब वह इस तरीके का इस्तेमाल करेगा. मेथड के आखिर में addChunk को कॉल करने से, एजेंट के जवाब को वापस genui पैकेज में भेज दिया जाता है. इससे, पैकेज को जवाब को प्रोसेस करने और यूज़र इंटरफ़ेस (यूआई) जनरेट करने की अनुमति मिलती है.
आखिर में, अपनी मौजूदा _addMessage विधि को इस नए वर्शन से पूरी तरह बदलें, ताकि यह सीधे Firebase के बजाय Conversation में मैसेज रूट कर सके:
Future<void> _addMessage() async {
final text = _textController.text;
if (text.trim().isEmpty) {
return;
}
_textController.clear();
setState(() {
_items.add(TextItem(text: text, isUser: true));
});
_scrollToBottom();
// Send the user's input through GenUI instead of directly to Firebase.
await _conversation.sendRequest(ChatMessage.user(text));
}
हो गया! ऐप्लिकेशन को फिर से चलाने की कोशिश करें. टेक्स्ट मैसेज के अलावा, आपको एजेंट के जनरेट किए गए यूज़र इंटरफ़ेस (यूआई) भी दिखेंगे. जैसे, बटन, टेक्स्ट विजेट वगैरह.
एजेंट को यूज़र इंटरफ़ेस (यूआई) को किसी खास तरीके से दिखाने के लिए भी कहा जा सकता है. उदाहरण के लिए, "मुझे अपने टास्क कॉलम में दिखाओ. साथ ही, हर टास्क को 'पूरा हुआ' के तौर पर मार्क करने के लिए एक बटन भी दिखाओ" जैसा मैसेज आज़माएं.
5. इंतज़ार की स्थिति जोड़ना
एलएलएम जनरेशन एसिंक्रोनस होता है. जवाब मिलने तक, चैट इंटरफ़ेस को इनपुट बटन बंद करने होंगे. साथ ही, प्रोग्रेस इंडिकेटर दिखाना होगा, ताकि उपयोगकर्ता को पता चल सके कि GenUI कॉन्टेंट बना रहा है. अच्छी बात यह है कि genui पैकेज में एक Listenable उपलब्ध है. इसका इस्तेमाल करके, बातचीत की स्थिति को ट्रैक किया जा सकता है. उस ConversationState वैल्यू में एक isWaiting प्रॉपर्टी शामिल होती है. इससे यह तय किया जाता है कि मॉडल कॉन्टेंट जनरेट कर रहा है या नहीं.
इनपुट कंट्रोल को ValueListenableBuilder से रैप करें
_conversation.state सुनने के लिए, lib/main.dart के सबसे नीचे Row को रैप करने वाला ValueListenableBuilder बनाएं. इसमें आपका TextField और ElevatedButton शामिल होता है. state.isWaiting की जांच करके, मॉडल के कॉन्टेंट जनरेट करने के दौरान इनपुट देने की सुविधा बंद की जा सकती है.
ValueListenableBuilder<ConversationState>(
valueListenable: _conversation.state,
builder: (context, state, child) {
return Row(
children: [
Expanded(
child: TextField(
controller: _textController,
// Also disable the Enter key submission when waiting!
onSubmitted: state.isWaiting ? null : (_) => _addMessage(),
decoration: const InputDecoration(
hintText: 'Enter a message',
),
),
),
const SizedBox(width: 8),
ElevatedButton(
// Disable the send button when the model is generating
onPressed: state.isWaiting ? null : _addMessage,
child: const Text('Send'),
),
],
);
},
),
प्रोग्रेस बार जोड़ना
मुख्य Column विजेट को Stack के अंदर रैप करें. इसके बाद, LinearProgressIndicator को उस स्टैक के दूसरे चाइल्ड के तौर पर जोड़ें. इसे सबसे नीचे ऐंकर किया गया है. सेट अप पूरा करने के बाद, आपके Scaffold का body ऐसा दिखना चाहिए:
body: Stack( // New!
children: [
Column(
children: [
Expanded(
child: ListView(
controller: _scrollController,
padding: const EdgeInsets.all(16),
children: [
for (final item in _items)
switch (item) {
TextItem() => MessageBubble(
text: item.text,
isUser: item.isUser,
),
SurfaceItem() => Surface(
surfaceContext: _controller.contextFor(
item.surfaceId,
),
),
},
],
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ValueListenableBuilder<ConversationState>(
valueListenable: _conversation.state,
builder: (context, state, child) {
return Row(
children: [
Expanded(
child: TextField(
controller: _textController,
onSubmitted:
state.isWaiting ? null : (_) => _addMessage(),
decoration: const InputDecoration(
hintText: 'Enter a message',
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: state.isWaiting ? null : _addMessage,
child: const Text('Send'),
),
],
);
},
),
),
),
],
),
// Listen to the state again, this time to render a progress indicator
ValueListenableBuilder<ConversationState>(
valueListenable: _conversation.state,
builder: (context, state, child) {
if (state.isWaiting) {
return const LinearProgressIndicator();
}
return const SizedBox.shrink();
},
),
],
),
6. GenUI Surface को बनाए रखना
अब तक, टास्क की सूची को स्क्रोल की जा सकने वाली चैट स्ट्रीम में रेंडर किया जाता था. इसमें हर नए मैसेज या सर्फ़ेस को सूची में जोड़ा जाता था. अगले चरण में, आपको यह दिखेगा कि किसी सरफेस का नाम कैसे रखा जाता है और उसे यूज़र इंटरफ़ेस (यूआई) में किसी खास जगह पर कैसे दिखाया जाता है.
सबसे पहले, main.dart के सबसे ऊपर, void main() से पहले, एक कॉन्स्टेंट का एलान करें, ताकि उसे सर्फ़ेस आईडी के तौर पर इस्तेमाल किया जा सके:
const taskDisplaySurfaceId = 'task_display';
दूसरा, Conversation लिसनर में switch स्टेटमेंट को अपडेट करें, ताकि यह पक्का किया जा सके कि उस आईडी वाला कोई भी सर्फ़ेस, _items में न जोड़ा जाए:
case ConversationSurfaceAdded added:
if (added.surfaceId != taskDisplaySurfaceId) {
_items.add(SurfaceItem(surfaceId: added.surfaceId));
_scrollToBottom();
}
इसके बाद, अपने विजेट ट्री के लेआउट स्ट्रक्चर को खोलें, ताकि चैट लॉग के ठीक ऊपर पिन किए गए सर्फ़ेस के लिए जगह बनाई जा सके. इन दोनों विजेट को मुख्य Column के पहले चाइल्ड कॉम्पोनेंट के तौर पर जोड़ें:
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.topLeft,
child: Surface(
surfaceContext: _controller.contextFor(
taskDisplaySurfaceId,
),
),
),
),
const Divider(),
अब तक, आपके एजेंट को अपनी ज़रूरत के हिसाब से, सर्फ़ेस बनाने और उनका इस्तेमाल करने की पूरी आज़ादी थी. इसे ज़्यादा सटीक निर्देश देने के लिए, आपको सिस्टम प्रॉम्प्ट पर फिर से जाना होगा. systemInstruction कॉन्स्टेंट में सेव किए गए प्रॉम्प्ट के आखिर में, यह ## USER INTERFACE सेक्शन जोड़ें:
const systemInstruction = '''
// ... existing prompt content ...
## USER INTERFACE
* To display the list of tasks create one and only one instance of the
TaskDisplay catalog item. Use "$taskDisplaySurfaceId" as its surface ID.
* Update $taskDisplaySurfaceId as necessary when the list changes.
* $taskDisplaySurfaceId must include a button for each task that I can use
to mark it complete. When I use that button to mark a task complete, it
should send you a message indicating what I've done.
* Avoid repeating the same information in a single message.
* When responding with text, rather than A2UI messages, be brief.
''';
अपने एजेंट को यूज़र इंटरफ़ेस (यूआई) के कॉम्पोनेंट इस्तेमाल करने के बारे में साफ़ तौर पर निर्देश देना ज़रूरी है. साथ ही, यह भी बताना ज़रूरी है कि उन्हें कब और कैसे इस्तेमाल करना है. एजेंट को किसी कैटलॉग आइटम और सर्फ़ेस आईडी का इस्तेमाल करने के लिए कहकर (और एक ही इंस्टेंस का दोबारा इस्तेमाल करने के लिए कहकर), यह पक्का किया जा सकता है कि वह आपकी पसंद के मुताबिक इंटरफ़ेस बनाए.
अभी और काम करना बाकी है. हालांकि, अपने ऐप्लिकेशन को फिर से चलाकर देखा जा सकता है. इससे आपको दिखेगा कि एजेंट, यूज़र इंटरफ़ेस (यूआई) में सबसे ऊपर टास्क डिसप्ले सरफेस बना रहा है.
7. अपनी पसंद के मुताबिक कैटलॉग विजेट बनाना
फ़िलहाल, TaskDisplay कैटलॉग आइटम मौजूद नहीं है. अगले कुछ चरणों में, आपको इन समस्याओं को ठीक करने का तरीका बताया जाएगा. इसके लिए, आपको एक डेटा स्कीमा, उस स्कीमा को पार्स करने के लिए एक क्लास, एक विजेट, और कैटलॉग आइटम बनाना होगा.
सबसे पहले, task_display.dart नाम की फ़ाइल बनाएं और इसमें ये इंपोर्ट जोड़ें:
import 'package:flutter/material.dart';
import 'package:genui/genui.dart';
import 'package:json_schema_builder/json_schema_builder.dart';
डेटा स्कीमा बनाना
इसके बाद, उस डेटा स्कीमा को तय करें जिसे एजेंट, टास्क डिसप्ले बनाने के लिए उपलब्ध कराएगा. इस प्रोसेस में, json_schema_builder पैकेज के कुछ खास कंस्ट्रक्टर का इस्तेमाल किया जाता है. हालांकि, असल में आपको सिर्फ़ एक JSON स्कीमा तय करना होता है. इसका इस्तेमाल, एजेंट को भेजे जाने वाले और एजेंट से मिलने वाले मैसेज में किया जाता है.
सबसे पहले, कॉम्पोनेंट के नाम का इस्तेमाल करके एक बुनियादी S.object बनाएं:
final taskDisplaySchema = S.object(
properties: {
'component': S.string(enumValues: ['TaskDisplay']),
},
);
इसके बाद, स्कीमा प्रॉपर्टी में title, tasks, name, isCompleted, और completeAction जोड़ें.
final taskDisplaySchema = S.object(
properties: {
'component': S.string(enumValues: ['TaskDisplay']),
'title': S.string(description: 'The title of the task list'),
'tasks': S.list(
description: 'A list of tasks to be completed today',
items: S.object(
properties: {
'name': S.string(description: 'The name of the task to be completed'),
'isCompleted': S.boolean(
description: 'Whether the task is completed',
),
'completeAction': A2uiSchemas.action(
description:
'The action performed when the user has completed the task.',
),
},
),
),
},
);
completeAction प्रॉपर्टी देखें. इसे A2uiSchemas.action की मदद से बनाया जाता है. यह स्कीमा प्रॉपर्टी के लिए कंस्ट्रक्टर है, जो A2UI ऐक्शन को दिखाता है. स्कीमा में कोई ऐक्शन जोड़ने का मतलब है कि ऐप्लिकेशन, एजेंट को यह बता रहा है, "जब तुम मुझे कोई टास्क दो, तो मुझे उस ऐक्शन का नाम और मेटाडेटा भी दो जिसका इस्तेमाल करके मैं तुम्हें बता सकूं कि टास्क पूरा हो गया है." इसके बाद, जब उपयोगकर्ता किसी चेकबॉक्स पर टैप करेगा, तब ऐप्लिकेशन उस कार्रवाई को शुरू करेगा.
इसके बाद, स्कीमा में required फ़ील्ड जोड़ें. इनसे एजेंट को हर बार कुछ प्रॉपर्टी की जानकारी भरने के लिए कहा जाता है. इस मामले में, हर प्रॉपर्टी ज़रूरी है!
final taskDisplaySchema = S.object(
properties: {
'component': S.string(enumValues: ['TaskDisplay']),
'title': S.string(description: 'The title of the task list'),
'tasks': S.list(
description: 'A list of tasks to be completed today',
items: S.object(
properties: {
'name': S.string(description: 'The name of the task to be completed'),
'isCompleted': S.boolean(
description: 'Whether the task is completed',
),
'completeAction': A2uiSchemas.action(
description:
'The action performed when the user has completed the task.',
),
},
// New!
required: ['name', 'isCompleted', 'completeAction'],
),
),
},
// New!
required: ['title', 'tasks'],
);
डेटा पार्स करने वाली क्लास बनाना
इस कॉम्पोनेंट के इंस्टेंस बनाते समय, एजेंट स्कीमा से मेल खाने वाला डेटा भेजेगा. आने वाले JSON को टाइप किए गए Dart ऑब्जेक्ट में पार्स करने के लिए, दो क्लास जोड़ें. ध्यान दें कि _TaskDisplayData रूट स्ट्रक्चर को कैसे हैंडल करता है. साथ ही, इनर ऐरे पार्सिंग को _TaskData को कैसे सौंपता है.
class _TaskData {
final String name;
final bool isCompleted;
final String actionName;
final JsonMap actionContext;
_TaskData({
required this.name,
required this.isCompleted,
required this.actionName,
required this.actionContext,
});
factory _TaskData.fromJson(Map<String, Object?> json) {
try {
final action = json['completeAction']! as JsonMap;
final event = action['event']! as JsonMap;
return _TaskData(
name: json['name'] as String,
isCompleted: json['isCompleted'] as bool,
actionName: event['name'] as String,
actionContext: event['context'] as JsonMap,
);
} catch (e) {
throw Exception('Invalid JSON for _TaskData: $e');
}
}
}
class _TaskDisplayData {
final String title;
final List<_TaskData> tasks;
_TaskDisplayData({required this.title, required this.tasks});
factory _TaskDisplayData.fromJson(Map<String, Object?> json) {
try {
return _TaskDisplayData(
title: (json['title'] as String?) ?? 'Tasks',
tasks: (json['tasks'] as List<Object?>)
.map((e) => _TaskData.fromJson(e as Map<String, Object?>))
.toList(),
);
} catch (e) {
throw Exception('Invalid JSON for _TaskDisplayData: $e');
}
}
}
अगर आपने पहले Flutter का इस्तेमाल किया है, तो ये क्लास शायद आपकी बनाई गई क्लास से मिलती-जुलती हों. ये JsonMap स्वीकार करते हैं और JSON से पार्स किए गए डेटा वाला एक टाइप किया गया ऑब्जेक्ट दिखाते हैं.
_TaskData में actionName और actionContext फ़ील्ड देखें. इन्हें JSON की completeAction प्रॉपर्टी से निकाला जाता है. इनमें कार्रवाई का नाम और डेटा कॉन्टेक्स्ट (GenUI के डेटा मॉडल में कार्रवाई की जगह का रेफ़रंस) शामिल होता है. इनका इस्तेमाल बाद में UserActionEvent बनाने के लिए किया जाएगा.
डेटा मॉडल, सभी डाइनैमिक यूज़र इंटरफ़ेस (यूआई) स्टेट के लिए एक सेंट्रलाइज़्ड, ऑब्ज़र्वेबल स्टोर होता है. इसे genui लाइब्रेरी मैनेज करती है. जब एजेंट, कैटलॉग से कोई यूज़र इंटरफ़ेस (यूआई) कॉम्पोनेंट बनाता है, तो वह कॉम्पोनेंट के स्कीमा से मेल खाने वाला डेटा ऑब्जेक्ट भी बनाता है. इस डेटा ऑब्जेक्ट को क्लाइंट के डेटा मॉडल में सेव किया जाता है, ताकि इसका इस्तेमाल विजेट बनाने के लिए किया जा सके. साथ ही, एजेंट को भेजे जाने वाले बाद के मैसेज में इसका रेफ़रंस दिया जा सके. जैसे, completeAction को विजेट से कनेक्ट किया जा रहा है.
विजेट जोड़ें
अब सूची दिखाने के लिए, एक विजेट बनाएं. इसे _TaskDisplayData क्लास का इंस्टेंस और टास्क पूरा होने पर कॉल करने के लिए कॉलबैक स्वीकार करना चाहिए.
class _TaskDisplay extends StatelessWidget {
final _TaskDisplayData data;
final void Function(_TaskData) onCompleteTask;
const _TaskDisplay({required this.data, required this.onCompleteTask});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
data.title,
style: Theme.of(context).textTheme.titleLarge,
),
),
...data.tasks.map(
(task) => CheckboxListTile(
title: Text(
task.name,
style: TextStyle(
decoration: task.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
value: task.isCompleted,
onChanged: task.isCompleted
? null
: (val) {
if (val == true) {
onCompleteTask(task);
}
},
),
),
],
);
}
}
CatalogItem बनाएं
स्कीमा, पार्सर, और विजेट बनाने के बाद, अब इन सभी को एक साथ जोड़ने के लिए CatalogItem बनाया जा सकता है.
task_display.dart के सबसे नीचे, taskDisplay को टॉप-लेवल वैरिएबल के तौर पर बनाएं. इसके बाद, आने वाले JSON को पार्स करने के लिए _TaskDisplayData का इस्तेमाल करें. साथ ही, _TaskDisplay विजेट का इंस्टेंस बनाएं.
final taskDisplay = CatalogItem(
name: 'TaskDisplay',
dataSchema: taskDisplaySchema,
widgetBuilder: (itemContext) {
final json = itemContext.data as Map<String, Object?>;
final data = _TaskDisplayData.fromJson(json);
return _TaskDisplay(
data: data,
onCompleteTask: (task) async {
// We will implement this next!
},
);
},
);
onCompleteTask को लागू करना
विजेट के काम करने के लिए, यह ज़रूरी है कि जब कोई टास्क पूरा हो जाए, तो यह एजेंट को इसकी सूचना दे. टास्क के डेटा से completeAction का इस्तेमाल करके इवेंट बनाने और उसे भेजने के लिए, खाली onCompleteTask प्लेसहोल्डर को इस कोड से बदलें.
onCompleteTask: (task) async {
// A data context is a reference to a location in the data model. This line
// turns that reference into a concrete data object that the agent can use.
// It's kind of like taking a pointer and replacing it with the value it
// points to.
final JsonMap resolvedContext = await resolveContext(
itemContext.dataContext,
task.actionContext,
);
// Dispatch an event back to the agent, letting it know a task was completed.
// This will be sent to the agent in an A2UI message that includes the name
// of the action, the surface ID, and the resolved data context.
itemContext.dispatchEvent(
UserActionEvent(
name: task.actionName,
sourceComponentId: itemContext.id,
context: resolvedContext,
),
);
}
कैटलॉग आइटम रजिस्टर करना
आखिर में, main.dart खोलें, नई फ़ाइल इंपोर्ट करें, और उसे कैटलॉग के अन्य आइटम के साथ रजिस्टर करें.
इस इंपोर्ट को lib/main.dart में सबसे ऊपर जोड़ें:
import 'task_display.dart';
अपने initState() फ़ंक्शन में मौजूद catalog = BasicCatalogItems.asCatalog(); को इससे बदलें:
// The Catalog is immutable, so use copyWith to create a new version
// that includes our custom catalog item along with the basics.
catalog = BasicCatalogItems.asCatalog().copyWith(newItems: [taskDisplay]);
आपका काम पूरा हुआ! बदलाव देखने के लिए, ऐप्लिकेशन को हॉट रीस्टार्ट करें.
8. एजेंट से इंटरैक्ट करने के अलग-अलग तरीके आज़माएं

आपने कैटलॉग में नया विजेट जोड़ लिया है और ऐप्लिकेशन के यूज़र इंटरफ़ेस (यूआई) में इसके लिए जगह बना ली है. अब एजेंट के साथ काम करने का समय आ गया है. GenUI का एक मुख्य फ़ायदा यह है कि यह आपके डेटा के साथ इंटरैक्ट करने के दो तरीके उपलब्ध कराता है: बटन और चेकबॉक्स जैसे ऐप्लिकेशन यूज़र इंटरफ़ेस (यूआई) के ज़रिए और ऐसे एजेंट के ज़रिए जो नैचुरल लैंग्वेज को समझता है और डेटा के बारे में तर्क दे सकता है. दोनों को आज़माकर देखें!
- टेक्स्ट फ़ील्ड का इस्तेमाल करके, तीन या चार टास्क के बारे में बताएं. इसके बाद, उन्हें सूची में देखें.
- किसी टास्क को पूरा या अधूरा के तौर पर टॉगल करने के लिए, चेकबॉक्स का इस्तेमाल करें.
- पांच से छह कामों की सूची बनाएं. इसके बाद, एजेंट को उन कामों को हटाने के लिए कहें जिनके लिए आपको कहीं जाना पड़ता है.
- एजेंट को बार-बार किए जाने वाले टास्क की सूची को अलग-अलग आइटम के तौर पर बनाने के लिए कहें ("मुझे माँ, पापा, और दादी के लिए हॉलिडे कार्ड खरीदना है. इनके लिए अलग-अलग टास्क बनाओ.").
- एजेंट से कहें कि वह सभी टास्क को 'पूरा हो गया' या 'पूरा नहीं हुआ' के तौर पर मार्क करे या पहले दो या तीन टास्क को पूरा होने के तौर पर मार्क करे.
9. बधाई हो
बधाई हो! आपने Generative UI और Flutter का इस्तेमाल करके, एआई की मदद से टास्क ट्रैक करने वाला ऐप्लिकेशन बनाया है.
आपको क्या सीखने को मिला
- Flutter Firebase SDK का इस्तेमाल करके, Google के फ़ाउंडेशन मॉडल के साथ इंटरैक्ट करना
- इस कुकी का इस्तेमाल, GenUI का इस्तेमाल करके Gemini से जनरेट किए गए इंटरैक्टिव सर्फ़ेस को रेंडर करने के लिए किया जाता है
- पहले से तय किए गए स्टैटिक रेंडरिंग आईडी का इस्तेमाल करके, लेआउट में पिन करने की सुविधा
- बेहतर इंटरैक्शन लूप के लिए, कस्टम स्कीमा और विजेट कैटलॉग डिज़ाइन करना