একটি জেনারেটিভ UI (GenUI) অ্যাপ তৈরি করুন

১. ভূমিকা

এই কোডল্যাবে, আপনি ফ্লাটার, ফায়ারবেস এআই লজিক এবং নতুন genui প্যাকেজ ব্যবহার করে একটি টাস্ক লিস্ট অ্যাপ তৈরি করবেন। আপনি একটি টেক্সট-ভিত্তিক চ্যাট অ্যাপ দিয়ে শুরু করবেন, এজেন্টকে নিজস্ব ইউআই (UI) তৈরির ক্ষমতা দিতে জেনইউআই (GenUI) দিয়ে এটিকে আপগ্রেড করবেন এবং সবশেষে আপনার নিজস্ব কাস্টম, ইন্টারেক্টিভ ইউআই কম্পোনেন্ট তৈরি করবেন যা আপনি এবং এজেন্ট সরাসরি নিয়ন্ত্রণ করতে পারবেন।

ক্রোমে চলমান একটি টাস্ক লিস্ট অ্যাপ

আপনি যা করবেন

  • ফ্লাটার এবং ফায়ারবেস এআই লজিক ব্যবহার করে একটি সাধারণ চ্যাট ইন্টারফেস তৈরি করুন।
  • এআই-চালিত সারফেস তৈরি করতে genui প্যাকেজটি ইন্টিগ্রেট করুন।
  • অ্যাপটি যখন এজেন্টের কাছ থেকে প্রতিক্রিয়ার জন্য অপেক্ষা করবে, তা বোঝানোর জন্য একটি প্রোগ্রেস বার যোগ করুন।
  • একটি নামযুক্ত সারফেস তৈরি করুন এবং UI-এর একটি নির্দিষ্ট স্থানে তা প্রদর্শন করুন।
  • একটি কাস্টম GenUI ক্যাটালগ কম্পোনেন্ট তৈরি করুন যা আপনাকে টাস্কগুলো কীভাবে উপস্থাপন করা হবে তার উপর নিয়ন্ত্রণ দেবে।

আপনার যা যা লাগবে

এই কোডল্যাবটি মধ্যবর্তী স্তরের ফ্লাটার ডেভেলপারদের জন্য।

২. শুরু করার আগে

ফ্লাটার প্রজেক্টটি সেট আপ করুন

আপনার টার্মিনাল খুলুন এবং একটি নতুন প্রজেক্ট তৈরি করতে flutter create চালান:

flutter create intro_to_genui
cd intro_to_genui

আপনার ফ্লাটার প্রজেক্টে প্রয়োজনীয় ডিপেন্ডেন্সিগুলো যোগ করুন:

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_ai প্যাকেজটি ব্যবহার করার জন্য, আপনাকে প্রথমে আপনার প্রোজেক্টে Firebase AI Logic সক্রিয় করতে হবে।

  1. Firebase কনসোলে Firebase AI Logic- এ যান।
  2. নির্দেশিত কর্মপ্রবাহটি চালু করতে ' শুরু করুন ' বোতামে ক্লিক করুন।
  3. আপনার প্রজেক্ট সেট আপ করার জন্য স্ক্রিনে দেওয়া নির্দেশাবলী অনুসরণ করুন।

আরও তথ্যের জন্য, ফ্লাটার অ্যাপে ফায়ারবেস যুক্ত করার নির্দেশাবলী দেখে নিন।

এপিআইগুলো সক্রিয় হয়ে গেলে, FlutterFire CLI ব্যবহার করে আপনার ফ্লাটার অ্যাপে ফায়ারবেস ইনিশিয়ালাইজ করুন:

flutterfire configure

আপনার Firebase প্রজেক্টটি নির্বাচন করুন এবং আপনার নির্দিষ্ট প্ল্যাটফর্মগুলোর (যেমন, Android, iOS, web) জন্য এটি কনফিগার করতে নির্দেশাবলী অনুসরণ করুন। এই কোডল্যাবটি আপনার কম্পিউটারে শুধু Flutter SDK এবং Chrome ইনস্টল থাকলেই সম্পন্ন করা যাবে, তবে অ্যাপটি অন্যান্য প্ল্যাটফর্মেও কাজ করবে।

৩. একটি প্রাথমিক চ্যাট ইন্টারফেসের কাঠামো তৈরি করুন।

জেনারেটিভ UI চালু করার আগে, আপনার অ্যাপের একটি ভিত্তি প্রয়োজন: Firebase AI Logic দ্বারা চালিত একটি সাধারণ টেক্সট-ভিত্তিক চ্যাট অ্যাপ্লিকেশন। দ্রুত শুরু করার জন্য, আপনি চ্যাট ইন্টারফেসের সম্পূর্ণ সেটআপটি কপি-পেস্ট করবেন।

অ্যাপটির একটি টেক্সট-ভিত্তিক সংস্করণ

মেসেজ বাবল উইজেট তৈরি করুন

ব্যবহারকারী এবং এজেন্টের পাঠানো টেক্সট মেসেজ দেখানোর জন্য আপনার অ্যাপে একটি উইজেট প্রয়োজন। 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 এ চ্যাট UI প্রয়োগ করুন।

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 তালিকাটিতে কথোপকথনের ইতিহাস সংরক্ষিত থাকে।
  • MessageBubble উইজেট ব্যবহার করে ListView তে বার্তাগুলো প্রদর্শন করা হয়।

অ্যাপটি পরীক্ষা করুন

এটি সম্পন্ন হলে, আপনি এখন অ্যাপটি চালিয়ে পরীক্ষা করতে পারেন।

flutter run -d chrome

আজ আপনি যে কাজগুলো করাতে চান, সে বিষয়ে এজেন্টের সাথে চ্যাট করে দেখুন। যদিও শুধুমাত্র টেক্সট-ভিত্তিক UI দিয়েও কাজটি করা যায়, GenUI এই অভিজ্ঞতাকে আরও সহজ ও দ্রুততর করে তুলতে পারে।

৪. GenUI প্যাকেজটি একীভূত করুন

এখন সাধারণ টেক্সট থেকে জেনারেটিভ UI-তে আপগ্রেড করার সময় এসেছে। আপনি বেসিক ফায়ারবেস মেসেজিং লুপের পরিবর্তে GenUI Conversation , Catalog , এবং SurfaceController অবজেক্ট ব্যবহার করবেন। এর ফলে এআই মডেলটি চ্যাট স্ট্রিমের মধ্যেই আসল ফ্লাটার উইজেট ইনস্ট্যানশিয়েট করতে পারবে।

অ্যাপটির একটি সংস্করণ যেখানে GenUI সমন্বিত আছে

genui প্যাকেজটি পাঁচটি ক্লাস প্রদান করে যা আপনি এই কোডল্যাব জুড়ে ব্যবহার করবেন:

  • SurfaceController মডেল দ্বারা তৈরি UI-কে স্ক্রিনে ম্যাপ করে।
  • A2uiTransportAdapter অভ্যন্তরীণ GenUI অনুরোধগুলিকে যেকোনো বাহ্যিক ভাষা মডেলের সাথে সংযুক্ত করে।
  • Conversation আপনার ফ্লাটার অ্যাপের জন্য কন্ট্রোলার এবং ট্রান্সপোর্ট অ্যাডাপ্টারকে একটি একক, সমন্বিত এপিআই-এর মাধ্যমে আবৃত করে।
  • Catalog ল্যাঙ্গুয়েজ মডেলে উপলব্ধ উইজেট এবং প্রোপার্টিগুলোর বর্ণনা দেয়।
  • Surface হলো এমন একটি উইজেট যা মডেল দ্বারা তৈরি UI প্রদর্শন করে।

তৈরি করা 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 facade তৈরি করে যা কন্ট্রোলার এবং অ্যাডাপ্টার পরিচালনা করে। সেই 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" পদ্ধতি ব্যবহার করে, যার অর্থ হলো আপনিই নিয়ন্ত্রণ করেন কোন LLM আপনার অভিজ্ঞতাকে চালনা করবে। এই ক্ষেত্রে, আপনি Firebase AI Logic ব্যবহার করছেন, কিন্তু প্যাকেজটি বিভিন্ন ধরনের এজেন্ট এবং প্রোভাইডারের সাথে কাজ করার জন্য তৈরি করা হয়েছে।

এই স্বাধীনতার ফলে কিছুটা অতিরিক্ত দায়িত্বও আসে: আপনাকে 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 প্যাকেজে ফেরত পাঠায়, যা প্যাকেজটিকে প্রতিক্রিয়াটি প্রসেস করতে এবং UI তৈরি করতে সাহায্য করে।

অবশেষে, আপনার বিদ্যমান _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));
  }

ব্যাস! অ্যাপটি আবার চালানোর চেষ্টা করুন। টেক্সট মেসেজের পাশাপাশি, আপনি দেখবেন এজেন্টটি বাটন, টেক্সট উইজেট এবং আরও অনেক কিছুর মতো UI সারফেস তৈরি করছে।

আপনি এজেন্টকে UI-টি একটি নির্দিষ্ট উপায়ে প্রদর্শন করতে বলার চেষ্টাও করতে পারেন। উদাহরণস্বরূপ, এই ধরনের একটি বার্তা ব্যবহার করে দেখতে পারেন: "আমার কাজগুলো একটি কলামে দেখান, এবং প্রতিটি কাজ সম্পন্ন হিসেবে চিহ্নিত করার জন্য একটি বাটন রাখুন।"

৫. অপেক্ষারত অবস্থা যোগ করুন

LLM জেনারেশন অ্যাসিঙ্ক্রোনাস। প্রতিক্রিয়ার জন্য অপেক্ষা করার সময়, চ্যাট ইন্টারফেসকে ইনপুট বাটনগুলো নিষ্ক্রিয় করতে হবে এবং একটি প্রোগ্রেস ইন্ডিকেটর প্রদর্শন করতে হবে, যাতে ব্যবহারকারী জানতে পারে যে GenUI কন্টেন্ট তৈরি করছে। সৌভাগ্যবশত, genui প্যাকেজটি একটি Listenable প্রদান করে যা আপনি কথোপকথনের অবস্থা ট্র্যাক করতে ব্যবহার করতে পারেন। সেই ConversationState ভ্যালুটিতে একটি isWaiting প্রপার্টি রয়েছে, যা মডেলটি কন্টেন্ট তৈরি করছে কিনা তা নির্ধারণ করে।

ইনপুট কন্ট্রোলগুলোকে একটি ValueListenableBuilder দিয়ে মুড়ে দিন।

lib/main.dart ফাইলের একদম নিচে একটি ValueListenableBuilder তৈরি করুন, যা আপনার Row (যেটিতে আপনার TextField এবং ElevatedButton রয়েছে) র‍্যাপ করবে এবং _conversation.state এর তথ্য শুনবে। 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();
      },
    ),
  ],
),

৬. একটি GenUI সারফেস সংরক্ষণ করুন

এখন পর্যন্ত, টাস্ক লিস্টটি স্ক্রলিং চ্যাট স্ট্রিমে রেন্ডার করা হয়েছে, এবং প্রতিটি নতুন মেসেজ বা সারফেস আসার সাথে সাথে তালিকায় যুক্ত হয়েছে। পরবর্তী ধাপে, আপনি দেখতে পাবেন কীভাবে একটি সারফেসের নামকরণ করতে হয় এবং UI-এর মধ্যে একটি নির্দিষ্ট স্থানে তা প্রদর্শন করতে হয়।

প্রথমে, main.dart ফাইলের শুরুতে, void main() ফাংশনের আগে, সারফেস আইডি হিসেবে ব্যবহারের জন্য একটি কনস্ট্যান্ট ডিক্লেয়ার করুন:

const taskDisplaySurfaceId = 'task_display';

দ্বিতীয়ত, Conversation লিসেনারের switch স্টেটমেন্টটি আপডেট করুন, যাতে ওই ID-যুক্ত কোনো সারফেস _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.
''';

আপনার এজেন্টকে কখন এবং কীভাবে UI সারফেস ব্যবহার করতে হবে সে সম্পর্কে স্পষ্ট নির্দেশনা দেওয়া গুরুত্বপূর্ণ। এজেন্টকে একটি নির্দিষ্ট ক্যাটালগ আইটেম ও সারফেস আইডি ব্যবহার করতে (এবং একটিমাত্র ইনস্ট্যান্স পুনরায় ব্যবহার করতে) বলার মাধ্যমে, আপনি নিশ্চিত করতে পারেন যে এটি আপনার কাঙ্ক্ষিত ইন্টারফেসটিই তৈরি করছে।

আরও কাজ বাকি আছে, তবে এজেন্টটি UI-এর শীর্ষে টাস্ক ডিসপ্লে সারফেস তৈরি করছে কিনা তা দেখতে আপনি আপনার অ্যাপটি আবার চালিয়ে দেখতে পারেন।

৭. আপনার নিজস্ব ক্যাটালগ উইজেট তৈরি করুন

এই মুহূর্তে, 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 পার্স করে স্ট্রংলি-টাইপড ডার্ট অবজেক্টে রূপান্তর করার জন্য দুটি ক্লাস যোগ করুন। লক্ষ্য করুন, কীভাবে _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');
    }
  }
}

আপনি যদি আগে ফ্লাটার দিয়ে কিছু তৈরি করে থাকেন, তাহলে এই ক্লাসগুলো সম্ভবত আপনার তৈরি করা ক্লাসগুলোর মতোই। এগুলো একটি JsonMap গ্রহণ করে এবং JSON থেকে পার্স করা ডেটা সম্বলিত একটি স্ট্রংলি-টাইপড অবজেক্ট রিটার্ন করে।

_TaskData এর actionName এবং actionContext ফিল্ডগুলো দেখুন। এগুলো JSON-এর completeAction প্রপার্টি থেকে নেওয়া হয় এবং এতে অ্যাকশনের নাম ও তার ডেটা কনটেক্সট (GenUI-এর ডেটা মডেলে অ্যাকশনটির অবস্থানের একটি রেফারেন্স) থাকে। পরবর্তীতে একটি UserActionEvent তৈরি করতে এগুলো ব্যবহার করা হবে।

ডেটা মডেল হলো সমস্ত ডাইনামিক UI স্টেটের জন্য একটি কেন্দ্রীভূত, পর্যবেক্ষণযোগ্য স্টোর, যা genui লাইব্রেরি দ্বারা পরিচালিত হয়। এজেন্ট যখন ক্যাটালগ থেকে একটি UI কম্পোনেন্ট তৈরি করে, তখন এটি কম্পোনেন্টের স্কিমার সাথে মেলে এমন একটি ডেটা অবজেক্টও তৈরি করে। এই ডেটা অবজেক্টটি ক্লায়েন্টের ডেটা মডেলে সংরক্ষিত থাকে, যাতে এটি উইজেট তৈরি করতে ব্যবহার করা যায় এবং এজেন্টের কাছে পরবর্তী মেসেজগুলিতে (যেমন 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 তৈরি করতে পারেন।

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]);

আপনার কাজ শেষ! পরিবর্তনগুলো দেখতে অ্যাপটি হট রিস্টার্ট করুন।

৮. এজেন্টের সাথে যোগাযোগের বিভিন্ন উপায় নিয়ে পরীক্ষা করুন।

ক্রোমে চলমান একটি টাস্ক লিস্ট অ্যাপ

এখন যেহেতু আপনি ক্যাটালগে নতুন উইজেটটি যোগ করেছেন এবং অ্যাপের UI-তে এর জন্য একটি জায়গা তৈরি করেছেন, তাই এজেন্টের সাথে কাজ করে মজা করার সময় এসেছে। GenUI-এর অন্যতম প্রধান সুবিধা হলো এটি আপনার ডেটার সাথে ইন্টারঅ্যাক্ট করার দুটি উপায় দেয়: বাটন এবং চেকবক্সের মতো অ্যাপ্লিকেশন UI-এর মাধ্যমে, এবং এমন একটি এজেন্টের মাধ্যমে যা স্বাভাবিক ভাষা বোঝে ও ডেটা নিয়ে যুক্তি দিতে পারে। দুটি নিয়েই পরীক্ষা করে দেখুন!

  • টেক্সট ফিল্ডটি ব্যবহার করে তিন-চারটি কাজের বিবরণ দিন এবং দেখুন সেগুলো তালিকায় চলে আসছে।
  • কোনো কাজ সম্পূর্ণ বা অসম্পূর্ণ হিসেবে চিহ্নিত করতে একটি চেকবক্স ব্যবহার করুন।
  • ৫-৬টি কাজের একটি তালিকা তৈরি করুন, তারপর এজেন্টকে বলুন যে কাজগুলোর জন্য আপনাকে কোথাও গাড়ি চালিয়ে যেতে হবে, সেগুলো যেন তালিকা থেকে বাদ দিয়ে দেয়।
  • এজেন্টকে পুনরাবৃত্তিমূলক কাজগুলোর একটি তালিকা আলাদা আলাদা আইটেম হিসেবে তৈরি করতে বলুন ("আমাকে মা, বাবা এবং দিদিমার জন্য একটি ছুটির কার্ড কিনতে হবে। সেগুলোর জন্য আলাদা আলাদা কাজ তৈরি করুন।")।
  • এজেন্টকে বলুন সমস্ত কাজ সমাপ্ত বা অসমাপ্ত হিসেবে চিহ্নিত করতে, অথবা প্রথম দুই বা তিনটিতে টিক চিহ্ন দিতে।

৯. অভিনন্দন

অভিনন্দন! আপনি জেনারেটিভ ইউআই এবং ফ্লাটার ব্যবহার করে একটি এআই-চালিত টাস্ক ট্র্যাকিং অ্যাপ তৈরি করেছেন।

আপনি যা শিখেছেন

  • ফ্লাটার ফায়ারবেস এসডিকে ব্যবহার করে গুগলের ফাউন্ডেশন মডেলগুলোর সাথে ইন্টারঅ্যাক্ট করা
  • GenUI ব্যবহার করে Gemini দ্বারা তৈরি ইন্টারেক্টিভ পৃষ্ঠতল রেন্ডার করা
  • পূর্বনির্ধারিত স্ট্যাটিক রেন্ডারিং আইডি ব্যবহার করে লেআউটে সারফেস পিন করা
  • শক্তিশালী ইন্টারঅ্যাকশন লুপের জন্য কাস্টম স্কিমা এবং উইজেট ক্যাটালগ ডিজাইন করা

রেফারেন্স নথি