<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ gemini - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ gemini - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 22:24:23 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/gemini/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Agentic Coding with the Gemini CLI ]]>
                </title>
                <description>
                    <![CDATA[ Software development is shifting from manual coding to agent-driven workflows. Gemini CLI is one of the top tools for agentic coding. We just posted a new course on the freeCodeCamp.org YouTube channe ]]>
                </description>
                <link>https://www.freecodecamp.org/news/agentic-coding-with-the-gemini-cli/</link>
                <guid isPermaLink="false">69ebccaeb463d4844c47f10a</guid>
                
                    <category>
                        <![CDATA[ gemini ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Fri, 24 Apr 2026 20:03:58 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5f68e7df6dfc523d0a894e7c/4fbf4599-630f-4778-979c-8c499bc5ee65.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Software development is shifting from manual coding to agent-driven workflows. Gemini CLI is one of the top tools for agentic coding.</p>
<p>We just posted a new course on the <a href="http://freeCodeCamp.org">freeCodeCamp.org</a> YouTube channel that will teach you how to harness the power of the Gemini CLI for agentic coding. Andrew Brown from ExamPro created this course.</p>
<p>This course demonstrates how to integrate Google’s Gemini models directly into your terminal. You will learn to manage deep repository context and automate complex development tasks.</p>
<p>You'll learn about setup, advanced context management, safety, and extensibility &amp; automation. This course provides the technical foundation needed to master the next generation of coding tools.</p>
<p>Watch the course on <a href="https://youtu.be/XKOR4h3CrwE">the freeCodeCamp.org YouTube channel</a> (4-hour watch).</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/XKOR4h3CrwE" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an AI Coding Agent with Python and Gemini ]]>
                </title>
                <description>
                    <![CDATA[ In this handbook, you'll build a basic version of Claude Code using Google's free Gemini API. If you've ever used Cursor or Claude Code as an "agentic" AI code editor, then you should be familiar with what we'll be building here. As long as you have ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-an-ai-coding-agent-with-python-and-gemini/</link>
                <guid isPermaLink="false">68de9da15cb0af6e1274cbc6</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ gemini ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Lane Wagner ]]>
                </dc:creator>
                <pubDate>Thu, 02 Oct 2025 15:43:29 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759418643581/2470669e-8592-463e-8b4c-55eace8dd80a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this handbook, you'll build a basic version of Claude Code using Google's <a target="_blank" href="https://ai.google.dev/gemini-api/docs/pricing">free Gemini API</a>. If you've ever used Cursor or Claude Code as an "agentic" AI code editor, then you should be familiar with what we'll be building here. As long as you have an LLM at your disposal, it’s actually surprisingly simple to build a (somewhat) effective custom agent.</p>
<p>This a completely free text-based handbook. That said, there are two other options for following along:</p>
<p>You can try the interactive version of this <a target="_blank" href="https://www.boot.dev/courses/build-ai-agent-python">AI Agent course on Boot.dev</a>, complete with coding challenges and projects, or watch the <a target="_blank" href="https://www.youtube.com/watch?v=YtHdaXuOAks">video walkthrough</a> of this course on the FreeCodeCamp YouTube channel</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/YtHdaXuOAks" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>You should already be familiar with Python basics. If you're not, check out this <a target="_blank" href="https://www.boot.dev/courses/learn-code-python">Python course on Boot.dev</a>.</p>
</li>
<li><p>You should already know how to use a Unix-like command line. If you don't, <a target="_blank" href="https://www.boot.dev/courses/learn-linux">checkout this Linux course on Boot.dev</a>.</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-does-the-agent-do">What Does the Agent Do?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-learning-goals">Learning Goals</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-python-setup">Python Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-integrate-the-gemini-api">How to Integrate the Gemini API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-command-line-input">Command Line Input</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-message-structure">Message Structure</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-verbose-mode">Verbose Mode</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-the-calculator-project">How to Build the Calculator Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-agent-functions">Agent Functions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-system-prompt">System Prompt</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-function-declaration">Function Declaration</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-more-function-declarations">More Function Declarations</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-function-calling">Function Calling</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-building-the-agent-loop">Building the Agent Loop</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-does-the-agent-do">What Does the Agent Do?</h2>
<p>The program we're building is a CLI tool that:</p>
<p>1. Accepts a coding task (for example, "strings aren't splitting in my app, please fix")</p>
<p>2. Chooses from a set of predefined functions to work on the task, for example:</p>
<ul>
<li><p>Scan the files in a directory</p>
</li>
<li><p>Read a file's contents</p>
</li>
<li><p>Overwrite a file's contents</p>
</li>
<li><p>Execute the python interpreter on a file</p>
</li>
</ul>
<p>3. Repeats step 2 until the task is complete (or it fails miserably, which is possible)</p>
<p>For example, I have a buggy calculator app, so I used my agent to fix the code:</p>
<pre><code class="lang-bash">&gt; uv run main.py <span class="hljs-string">"fix my calculator app, its not starting correctly"</span>
<span class="hljs-comment"># Calling function: get_files_info</span>
<span class="hljs-comment"># Calling function: get_file_content</span>
<span class="hljs-comment"># Calling function: write_file</span>
<span class="hljs-comment"># Calling function: run_python_file</span>
<span class="hljs-comment"># Calling function: write_file</span>
<span class="hljs-comment"># Calling function: run_python_file</span>
<span class="hljs-comment"># Final response:</span>
<span class="hljs-comment"># Great! The calculator app now seems to be working correctly. The output shows the expression and the result in a formatted way.</span>
</code></pre>
<h2 id="heading-learning-goals">Learning Goals</h2>
<p>The learning goals of this project are:</p>
<ul>
<li><p>Introduce you to multi-directory Python projects</p>
</li>
<li><p>Understand how the AI tools that you'll almost certainly use on the job actually work under the hood</p>
</li>
<li><p>Practice your Python and functional programming skills</p>
</li>
</ul>
<p>The goal is <em>not</em> to build an LLM from scratch, but to instead use a pre-trained LLM to build an <em>agent</em> from scratch.</p>
<h2 id="heading-python-setup">Python Setup</h2>
<p>Let's set up a virtual environment for our project. Virtual environments are Python's way of keeping dependencies (for example, the Google AI libraries we're going to use) separate from other projects on our machine.</p>
<p>Use <code>uv</code> to create a new project. It will create the directory and also initialize Git.</p>
<pre><code class="lang-bash">uv init your-project-name
<span class="hljs-built_in">cd</span> your-project-name
</code></pre>
<p>Create a virtual environment at the top level of your project directory:</p>
<pre><code class="lang-bash">uv venv
</code></pre>
<p><strong>Warning</strong>: Always add the <code>venv</code> directory to your <code>.gitignore</code> file.</p>
<p>Activate the virtual environment:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">source</span> .venv/bin/activate
</code></pre>
<p>You should see <code>(your-project-name)</code> at the beginning of your terminal prompt, for example, mine is:</p>
<pre><code class="lang-bash">(aiagent) wagslane@MacBook-Pro-2 aiagent %
</code></pre>
<p>Use <code>uv</code> to add two dependencies to the project. They will be added to the file <code>pyproject.toml</code>:</p>
<pre><code class="lang-bash">uv add google-genai==1.12.1
uv add python-dotenv==1.1.0
</code></pre>
<p>This tells Python that this project requires <a target="_blank" href="https://pypi.org/project/google-genai/"><code>google-genai</code></a> version <code>1.12.1</code> and the <a target="_blank" href="https://pypi.org/project/python-dotenv/"><code>python-dotenv</code></a> version <code>1.1.0</code>.</p>
<p>To run the project using the <code>uv</code> virtual environment, you use:</p>
<pre><code class="lang-bash">uv run main.py
</code></pre>
<p>In your terminal, you should see <code>Hello from YOUR PROJECT NAME</code>.</p>
<h2 id="heading-how-to-integrate-the-gemini-api">How to Integrate the Gemini API</h2>
<p><a target="_blank" href="https://www.cloudflare.com/learning/ai/what-is-large-language-model/">Large Language Models (LLMs)</a> are the fancy-schmancy AI technology that have been making all the waves in the AI world recently. Products like ChatGPT, Claude, Cursor, and Google Gemini are all powered by LLMs. For the purposes of this course, you can think of an LLM as a smart text generator. It works just like ChatGPT: you give it a prompt, and it gives you back some text that it believes answers your prompt.</p>
<p>We're going to use <a target="_blank" href="https://ai.google.dev/gemini-api/docs/pricing">Google's Gemini API</a> to power our agent in this course. It's reasonably smart, but more importantly for us, it has a free tier.</p>
<h3 id="heading-tokens">Tokens</h3>
<p>You can think of tokens as the currency of LLMs. They are the way that LLMs measure how much text they have to process. Tokens are <a target="_blank" href="https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them">_roughly_ 4 characters</a> for most models. It's important when working with LLM APIs to understand how many tokens you're using.</p>
<p>We'll be staying well within the free tier limits of the Gemini API, but we'll still monitor our token usage!</p>
<p><strong>Warning</strong>: You should be aware that all API calls, including those made during local testing, consume tokens from your free tier quota. If you exhaust your quota, you may need to wait for it to reset (typically 24 hours) to continue the lesson. Regenerating your API key will not reset your quota.</p>
<p>Here’s how to create an API key:</p>
<ol>
<li><p>Create an account on <a target="_blank" href="https://aistudio.google.com/">Google AI Studio</a> if you don't already have one</p>
</li>
<li><p>Click the "Create API Key" button. You can use the <a target="_blank" href="https://ai.google.dev/gemini-api/docs/api-key">docs</a> if you get lost.</p>
</li>
</ol>
<p>If you already have a GCP account and a project, you can create the API key in that project. If you don't, AI studio will automatically create one for you.</p>
<p>3. Copy the API key, then paste it into a new <code>.env</code> file in your project directory. The file should look like this:</p>
<pre><code class="lang-bash">GEMINI_API_KEY=<span class="hljs-string">"your_api_key_here"</span>
</code></pre>
<p>4. Add the <code>.env</code> file to your <code>.gitignore</code></p>
<p><strong>Danger</strong>: We never want to commit API keys, passwords, or other sensitive information to Git.</p>
<p>5. Update the <code>main.py</code> file. When the program starts, load the environment variables from the <code>.env</code> file using the <code>dotenv</code> library and read the API key:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> dotenv <span class="hljs-keyword">import</span> load_dotenv

load_dotenv()
api_key = os.environ.get(<span class="hljs-string">"GEMINI_API_KEY"</span>)
</code></pre>
<p>6. Import the <code>genai</code> library and use the API key to create a new instance of a <a target="_blank" href="https://googleapis.github.io/python-genai/#create-a-client">Gemini client:</a></p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> google <span class="hljs-keyword">import</span> genai

client = genai.Client(api_key=api_key)
</code></pre>
<p>7. Use the <a target="_blank" href="https://googleapis.github.io/python-genai/#generate-content"><code>client.models.generate_content()</code> method</a> to get a response from the <code>gemini-2.0-flash-001</code> model. You'll need to use two named parameters:</p>
<ul>
<li><p><code>model</code>: The model name <code>gemini-2.0-flash-001</code> (this one has a generous free tier)</p>
</li>
<li><p><code>contents</code>: The prompt to send to the model (a string). Use this prompt:</p>
</li>
</ul>
<p>"Why are <a target="_blank" href="http://Boot.dev">Boot.dev</a> and FreeCodeCamp such great places to learn backend development? Use one paragraph maximum."</p>
<p>The <code>generate_content</code> method returns a <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentResponse"><code>GenerateContentResponse</code> object.</a> Print the <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentResponse.text"><code>.text</code> property</a> of the response to see the model's answer.</p>
<p>If everything is working as intended, you should be able to run your code and see the model's response in your terminal.</p>
<p>8. In addition to printing the text response, print the number of tokens consumed by the interaction in this format:</p>
<pre><code class="lang-plaintext">Prompt tokens: X
Response tokens: Y
</code></pre>
<p>The response has a <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentResponseDict.usage_metadata"><code>.usage_metadata</code></a> property that has both:</p>
<ul>
<li><p>A <code>prompt_token_count</code> property (tokens in the prompt)</p>
</li>
<li><p>A <code>candidates_token_count</code> property (tokens in the response)</p>
</li>
</ul>
<p><strong>Danger</strong>: The Gemini API is an <em>external web service</em> and on occasion it's <em>slow and unreliable</em>. So be patient.</p>
<h2 id="heading-command-line-input">Command Line Input</h2>
<p>We've hardcoded the prompt that goes to Gemini, which is... not very useful. Let's update our code to accept the prompt as a command line argument.</p>
<p>We don't want our users to have to edit the code to change the prompt.</p>
<p>Update your code to accept a command line argument for the prompt. For example:</p>
<pre><code class="lang-bash">uv run main.py <span class="hljs-string">"Why are episodes 7-9 so much worse than 1-6?"</span>
</code></pre>
<p><strong>Tip</strong>: The <a target="_blank" href="https://docs.python.org/3/library/sys.html#sys.argv"><code>sys.argv</code></a> variable is a list of strings representing all the command line arguments passed to the script. The first element is the name of the script, and the rest are the arguments. Be sure to <code>import sys</code> to use it.</p>
<p>If the prompt is not provided, print an error message and exit the program with exit code 1.</p>
<h2 id="heading-message-structure">Message Structure</h2>
<p>LLM APIs aren't typically used in a "one-shot" manner, for example:</p>
<ul>
<li><p>Prompt: "What is the meaning of life?"</p>
</li>
<li><p>Response: "42"</p>
</li>
</ul>
<p>They work the same way ChatGPT works in a conversation. The conversation has a history, and if we keep track of that history, then with each new prompt, the model can see the entire conversation and respond within the larger context of the conversation.</p>
<h3 id="heading-roles">Roles</h3>
<p>Importantly, each message in the conversation has a "role". In the context of a chat app like ChatGPT, your conversations would look like this:</p>
<ul>
<li><p><strong>user</strong>: "What is the meaning of life?"</p>
</li>
<li><p><strong>model</strong>: "42"</p>
</li>
<li><p><strong>user</strong>: "Wait, what did you just say?"</p>
</li>
<li><p><strong>model</strong>: "42. It's is the answer to the ultimate question of life, the universe, and everything."</p>
</li>
<li><p><strong>user</strong>: "But why?"</p>
</li>
<li><p><strong>model</strong>: "Because Douglas Adams said so."</p>
</li>
</ul>
<p>So, while our program will still be "one-shot" for now, let's update our code to store a list of messages in the conversation, and pass in the "role" appropriately.</p>
<p>Create a new list of <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.Content"><code>types.Content</code></a>, and set the user's prompt as the only message (for now):</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> google.genai <span class="hljs-keyword">import</span> types

messages = [
    types.Content(role=<span class="hljs-string">"user"</span>, parts=[types.Part(text=user_prompt)]),
]
</code></pre>
<p>Update your call to <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.models.Models.generate_content"><code>models.generate_content</code></a> to use the messages list:</p>
<pre><code class="lang-python">response = client.models.generate_content(
    model=<span class="hljs-string">"gemini-2.0-flash-001"</span>,
    contents=messages,
)
</code></pre>
<p><strong>Info</strong>: In the future, we'll add more messages to the list as the agent does its tasks in a loop.</p>
<h2 id="heading-verbose-mode">Verbose Mode</h2>
<p>As you debug and build your AI agent, you'll probably want to dump a lot more context into the console, but at the same time, we don't want to make the user experience of our CLI tool too noisy.</p>
<p>Let's add an optional command line flag, <code>--verbose</code>, that will allow us to toggle "verbose" output on and off. When we want to see more info, we'll just turn that on.</p>
<p>Add a new command line argument, <code>--verbose</code>. It should be supplied after the prompt if included. For example:</p>
<pre><code class="lang-bash">uv run main.py <span class="hljs-string">"What is the meaning of life?"</span> --verbose
</code></pre>
<p>If the <code>--verbose</code> flag is included, the console output should include:</p>
<ul>
<li><p>The user's prompt: <code>"User prompt: {user_prompt}"</code></p>
</li>
<li><p>The number of prompt tokens on each iteration: <code>"Prompt tokens: {prompt_tokens}"</code></p>
</li>
<li><p>The number of response tokens on each iteration: <code>"Response tokens: {response_tokens}"</code></p>
</li>
</ul>
<p>Otherwise, it should not print those things.</p>
<h2 id="heading-how-to-build-the-calculator-project">How to Build the Calculator Project</h2>
<p>Since we're building an AI Agent, the agent will need a project to work on. I've built a little command line calculator app that we'll use as a test project for the AI to read, update, and run.</p>
<p>First, create a new directory called <code>calculator</code> in the root of your project. Then copy and paste the <code>main.py</code> and <code>tests.py</code> files from below into the <code>calculator</code> directory.</p>
<p><em>Dont’ worry much about how this code works - our project isn’t to build a calculator, this is the project that our AI agent project will work on!</em></p>
<pre><code class="lang-python"><span class="hljs-comment"># main.py</span>
<span class="hljs-keyword">import</span> sys
<span class="hljs-keyword">from</span> pkg.calculator <span class="hljs-keyword">import</span> Calculator
<span class="hljs-keyword">from</span> pkg.render <span class="hljs-keyword">import</span> format_json_output


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span>():</span>
    calculator = Calculator()
    <span class="hljs-keyword">if</span> len(sys.argv) &lt;= <span class="hljs-number">1</span>:
        print(<span class="hljs-string">"Calculator App"</span>)
        print(<span class="hljs-string">'Usage: python main.py "&lt;expression&gt;"'</span>)
        print(<span class="hljs-string">'Example: python main.py "3 + 5"'</span>)
        <span class="hljs-keyword">return</span>

    expression = <span class="hljs-string">" "</span>.join(sys.argv[<span class="hljs-number">1</span>:])
    <span class="hljs-keyword">try</span>:
        result = calculator.evaluate(expression)
        <span class="hljs-keyword">if</span> result <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
            to_print = format_json_output(expression, result)
            print(to_print)
        <span class="hljs-keyword">else</span>:
            print(<span class="hljs-string">"Error: Expression is empty or contains only whitespace."</span>)
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"Error: <span class="hljs-subst">{e}</span>"</span>)


<span class="hljs-keyword">if</span> name == <span class="hljs-string">"__main__"</span>:
    main()
</code></pre>
<pre><code class="lang-python"><span class="hljs-comment"># tests.py</span>

<span class="hljs-keyword">import</span> unittest
<span class="hljs-keyword">from</span> pkg.calculator <span class="hljs-keyword">import</span> Calculator


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestCalculator</span>(<span class="hljs-params">unittest.TestCase</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">setUp</span>(<span class="hljs-params">self</span>):</span>
        self.calculator = Calculator()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_addition</span>(<span class="hljs-params">self</span>):</span>
        result = self.calculator.evaluate(<span class="hljs-string">"3 + 5"</span>)
        self.assertEqual(result, <span class="hljs-number">8</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_subtraction</span>(<span class="hljs-params">self</span>):</span>
        result = self.calculator.evaluate(<span class="hljs-string">"10 - 4"</span>)
        self.assertEqual(result, <span class="hljs-number">6</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_multiplication</span>(<span class="hljs-params">self</span>):</span>
        result = self.calculator.evaluate(<span class="hljs-string">"3 * 4"</span>)
        self.assertEqual(result, <span class="hljs-number">12</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_division</span>(<span class="hljs-params">self</span>):</span>
        result = self.calculator.evaluate(<span class="hljs-string">"10 / 2"</span>)
        self.assertEqual(result, <span class="hljs-number">5</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_nested_expression</span>(<span class="hljs-params">self</span>):</span>
        result = self.calculator.evaluate(<span class="hljs-string">"3 * 4 + 5"</span>)
        self.assertEqual(result, <span class="hljs-number">17</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_complex_expression</span>(<span class="hljs-params">self</span>):</span>
        result = self.calculator.evaluate(<span class="hljs-string">"2 * 3 - 8 / 2 + 5"</span>)
        self.assertEqual(result, <span class="hljs-number">7</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_empty_expression</span>(<span class="hljs-params">self</span>):</span>
        result = self.calculator.evaluate(<span class="hljs-string">""</span>)
        self.assertIsNone(result)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_invalid_operator</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">with</span> self.assertRaises(ValueError):
            self.calculator.evaluate(<span class="hljs-string">"$ 3 5"</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_not_enough_operands</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">with</span> self.assertRaises(ValueError):
            self.calculator.evaluate(<span class="hljs-string">"+ 3"</span>)


<span class="hljs-keyword">if</span> name == <span class="hljs-string">"__main__"</span>:
    unittest.main()
</code></pre>
<p>Create a new directory in <code>calculator</code> called <code>pkg</code>. Then copy and paste the <code>calculator.py</code> and <code>render.py</code> files from below into the <code>pkg</code> directory.</p>
<pre><code class="lang-python"><span class="hljs-comment"># calculator.py</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Calculator</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span>(<span class="hljs-params">self</span>):</span>
        self.operators = {
            <span class="hljs-string">"+"</span>: <span class="hljs-keyword">lambda</span> a, b: a + b,
            <span class="hljs-string">"-"</span>: <span class="hljs-keyword">lambda</span> a, b: a - b,
            <span class="hljs-string">"*"</span>: <span class="hljs-keyword">lambda</span> a, b: a * b,
            <span class="hljs-string">"/"</span>: <span class="hljs-keyword">lambda</span> a, b: a / b,
        }

        self.precedence = {
            <span class="hljs-string">"+"</span>: <span class="hljs-number">1</span>,
            <span class="hljs-string">"-"</span>: <span class="hljs-number">1</span>,
            <span class="hljs-string">"*"</span>: <span class="hljs-number">2</span>,
            <span class="hljs-string">"/"</span>: <span class="hljs-number">2</span>,
        }


    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">evaluate</span>(<span class="hljs-params">self, expression</span>):</span>
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> expression <span class="hljs-keyword">or</span> expression.isspace():
            <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>
        tokens = expression.strip().split()
        <span class="hljs-keyword">return</span> self._evaluate_infix(tokens)


    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">evaluateinfix</span>(<span class="hljs-params">self, tokens</span>):</span>
        values = []
        operators = []

        <span class="hljs-keyword">for</span> token <span class="hljs-keyword">in</span> tokens:
            <span class="hljs-keyword">if</span> token <span class="hljs-keyword">in</span> self.operators:
                <span class="hljs-keyword">while</span> (
                    operators
                    <span class="hljs-keyword">and</span> operators[<span class="hljs-number">-1</span>] <span class="hljs-keyword">in</span> self.operators
                    <span class="hljs-keyword">and</span> self.precedence[operators[<span class="hljs-number">-1</span>]] &gt;= self.precedence[token]
                ):
                    self._apply_operator(operators, values)
                operators.append(token)

            <span class="hljs-keyword">else</span>:
                <span class="hljs-keyword">try</span>:
                    values.append(float(token))
                <span class="hljs-keyword">except</span> ValueError:
                    <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">f"invalid token: <span class="hljs-subst">{token}</span>"</span>)

        <span class="hljs-keyword">while</span> operators:
            self._apply_operator(operators, values)

        <span class="hljs-keyword">if</span> len(values) != <span class="hljs-number">1</span>:
            <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">"invalid expression"</span>)

        <span class="hljs-keyword">return</span> values[<span class="hljs-number">0</span>]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">applyoperator</span>(<span class="hljs-params">self, operators, values</span>):</span>
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> operators:
            <span class="hljs-keyword">return</span>

        operator = operators.pop()
        <span class="hljs-keyword">if</span> len(values) &lt; <span class="hljs-number">2</span>:
            <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">f"not enough operands for operator <span class="hljs-subst">{operator}</span>"</span>)

        b = values.pop()
        a = values.pop()
        values.append(self.operators[operator](a, b))
</code></pre>
<pre><code class="lang-python"><span class="hljs-comment"># render.py</span>

<span class="hljs-keyword">import</span> json

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">format_json_output</span>(<span class="hljs-params">expression: str, result: float, indent: int = <span class="hljs-number">2</span></span>) -&gt; str:</span>
    <span class="hljs-keyword">if</span> isinstance(result, float) <span class="hljs-keyword">and</span> result.is_integer():
        result_to_dump = int(result)
    <span class="hljs-keyword">else</span>:
        result_to_dump = result

    output_data = {
        <span class="hljs-string">"expression"</span>: expression,
        <span class="hljs-string">"result"</span>: result_to_dump,
    }
    <span class="hljs-keyword">return</span> json.dumps(output_data, indent=indent)
</code></pre>
<p>This is the final structure:</p>
<pre><code class="lang-plaintext">├── calculator
│   ├── main.py
│   ├── pkg
│   │   ├── calculator.py
│   │   └── render.py
│   └── tests.py
├── main.py
├── pyproject.toml
├── README.md
└── uv.lock
</code></pre>
<p>Run the <code>calculator</code> tests:</p>
<pre><code class="lang-bash">uv run calculator/tests.py
</code></pre>
<p>Hopefully the tests all pass!</p>
<p>Now, run the calculator app:</p>
<pre><code class="lang-bash">uv run calculator/main.py <span class="hljs-string">"3 + 5"</span>
</code></pre>
<p>Hopefully you get 8!</p>
<h2 id="heading-agent-functions">Agent Functions</h2>
<p>We need to give our agent the ability to <em>do stuff</em>. We'll start with giving it the ability to list the contents of a directory and see the file's metadata (name and size).</p>
<p>Before we integrate this function with our LLM agent, let's just build the function itself. Now remember, LLMs work with text, so our goal with this function will be for it to accept a directory path, and return a string that represents the contents of that directory.</p>
<p>Create a new directory called <code>functions</code> in the root of your project (not inside the <code>calculator</code> directory). Inside, create a new file called <code>get_files_info.py</code>. Inside, write this function:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_files_info</span>(<span class="hljs-params">working_directory, directory=<span class="hljs-string">"."</span></span>):</span>
</code></pre>
<p>Here is my project structure so far:</p>
<pre><code class="lang-plaintext"> project_root/
 ├── calculator/
 │   ├── main.py
 │   ├── pkg/
 │   │   ├── calculator.py
 │   │   └── render.py
 │   └── tests.py
 └── functions/
     └── get_files_info.py
</code></pre>
<p>The <code>directory</code> parameter should be treated as a <em>relative</em> path within the <code>working_directory</code>. Use <code>os.path.join(working_directory, directory)</code> to create the full path, then validate it stays within the working directory boundaries.</p>
<p>If the absolute path to the <code>directory</code> is outside the <code>working_directory</code>, return a string error message:</p>
<pre><code class="lang-python"><span class="hljs-string">f'Error: Cannot list "<span class="hljs-subst">{directory}</span>" as it is outside the permitted working directory'</span>
</code></pre>
<p>This will give our LLM some guardrails: we never want it to be able to perform any work outside the "working_directory" we give it.</p>
<p><strong>Danger</strong>: Without this restriction, the LLM might go running amok anywhere on the machine, reading sensitive files or overwriting important data. This is a very important step that we'll bake into every function the LLM can call.</p>
<p>If the <code>directory</code> argument is not a directory, again, return an error string:</p>
<pre><code class="lang-python"><span class="hljs-string">f'Error: "<span class="hljs-subst">{directory}</span>" is not a directory'</span>
</code></pre>
<p><strong>Warning</strong>: All of our "tool call" functions, including <code>get_files_info</code>, should always return a string. If errors can be raised inside them, we need to catch those errors and return a string describing the error instead. This will allow the LLM to handle the errors gracefully.</p>
<p>Build and return a string representing the contents of the directory. It should use this format:</p>
<pre><code class="lang-bash">- README.md: file_size=1032 bytes, is_dir=False
- src: file_size=128 bytes, is_dir=True
- package.json: file_size=1234 bytes, is_dir=False
</code></pre>
<p><strong>Tip</strong>: The exact file sizes and even the order of files may vary depending on your operating system and file system. Your output doesn't need to match the example byte-for-byte, just the overall format</p>
<p>If any errors are raised by the standard library functions, catch them and instead return a string describing the error. Always prefix error strings with "Error:".</p>
<p>Here's my complete implementation:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_files_info</span>(<span class="hljs-params">working_directory, directory=<span class="hljs-string">"."</span></span>):</span>
    abs_working_dir = os.path.abspath(working_directory)
    target_dir = os.path.abspath(os.path.join(working_directory, directory))
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> target_dir.startswith(abs_working_dir):
        <span class="hljs-keyword">return</span> <span class="hljs-string">f'Error: Cannot list "<span class="hljs-subst">{directory}</span>" as it is outside the permitted working directory'</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> os.path.isdir(target_dir):
        <span class="hljs-keyword">return</span> <span class="hljs-string">f'Error: "<span class="hljs-subst">{directory}</span>" is not a directory'</span>
    <span class="hljs-keyword">try</span>:
        files_info = []
        <span class="hljs-keyword">for</span> filename <span class="hljs-keyword">in</span> os.listdir(target_dir):
            filepath = os.path.join(target_dir, filename)
            file_size = <span class="hljs-number">0</span>
            is_dir = os.path.isdir(filepath)
            file_size = os.path.getsize(filepath)
            files_info.append(
                <span class="hljs-string">f"- <span class="hljs-subst">{filename}</span>: file_size=<span class="hljs-subst">{file_size}</span> bytes, is_dir=<span class="hljs-subst">{is_dir}</span>"</span>
            )
        <span class="hljs-keyword">return</span> <span class="hljs-string">"\n"</span>.join(files_info)
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">return</span> <span class="hljs-string">f"Error listing files: <span class="hljs-subst">{e}</span>"</span>
</code></pre>
<p>Here are some standard library functions you'll find helpful:</p>
<ul>
<li><p><a target="_blank" href="https://docs.python.org/3/library/os.path.html#os.path.abspath"><code>os.path.abspath()</code></a>: Get an absolute path from a relative path</p>
</li>
<li><p><a target="_blank" href="https://docs.python.org/3/library/os.path.html#os.path.join"><code>os.path.join()</code></a>: Join two paths together safely (handles slashes)</p>
</li>
<li><p><a target="_blank" href="https://docs.python.org/3/library/stdtypes.html#str.startswith"><code>.startswith()</code></a>: Check if a string starts with a substring</p>
</li>
<li><p><a target="_blank" href="https://docs.python.org/3/library/os.path.html#os.path.isdir"><code>os.path.isdir()</code></a>: Check if a path is a directory</p>
</li>
<li><p><a target="_blank" href="https://docs.python.org/3/library/os.html#os.listdir"><code>os.listdir()</code></a>: List the contents of a directory</p>
</li>
<li><p><a target="_blank" href="https://docs.python.org/3/library/os.path.html#os.path.getsize"><code>os.path.getsize()</code></a>: Get the size of a file</p>
</li>
<li><p><a target="_blank" href="https://docs.python.org/3/library/os.path.html#os.path.isfile"><code>os.path.isfile()</code></a>: Check if a path is a file</p>
</li>
<li><p><a target="_blank" href="https://docs.python.org/3/library/stdtypes.html#str.join"><code>.join()</code></a>: Join a list of strings together with a separator</p>
</li>
</ul>
<h3 id="heading-get-file-content-function">Get File Content Function</h3>
<p>Now that we have a function that can get the contents of a directory, we need one that can get the contents of a file. Again, we'll just return the file contents as a string, or perhaps an error string if something went wrong.</p>
<p>As always, we'll safely scope the function to a specific working directory.</p>
<p>Create a new function in your <code>functions</code> directory. Here's the signature I used:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_file_content</span>(<span class="hljs-params">working_directory, file_path</span>):</span>
</code></pre>
<p>If the <code>file_path</code> is outside the <code>working_directory</code>, return a string with an error:</p>
<pre><code class="lang-python"><span class="hljs-string">f'Error: Cannot read "<span class="hljs-subst">{file_path}</span>" as it is outside the permitted working directory'</span>
</code></pre>
<p>If the <code>file_path</code> is not a file, again, return an error string:</p>
<pre><code class="lang-python"><span class="hljs-string">f'Error: File not found or is not a regular file: "<span class="hljs-subst">{file_path}</span>"'</span>
</code></pre>
<p>Read the file and return its contents as a string.</p>
<ul>
<li><p>If the file is longer than <code>10000</code> characters, truncate it to <code>10000</code> characters and append this message to the end <code>[...File "{file_path}" truncated at 10000 characters]</code>.</p>
</li>
<li><p>Instead of hard-coding the <code>10000</code> character limit, I stored it in a <a target="_blank" href="http://config.py"><code>config.py</code></a> file.</p>
</li>
</ul>
<p><strong>Warning</strong>: We don't want to accidentally read a gigantic file and send all that data to the LLM. That's a good way to burn through our token limits.</p>
<p>If any errors are raised by the standard library functions, catch them and instead return a string describing the error. Always prefix errors with "Error:".</p>
<p>First, create <code>config.py</code>:</p>
<pre><code class="lang-python">MAX_CHARS = <span class="hljs-number">10000</span>
WORKING_DIR = <span class="hljs-string">"./calculator"</span>
</code></pre>
<p>Here's my complete implementation for <code>functions/get_file_content.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> config <span class="hljs-keyword">import</span> MAX_CHARS


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_file_content</span>(<span class="hljs-params">working_directory, file_path</span>):</span>
    abs_working_dir = os.path.abspath(working_directory)
    abs_file_path = os.path.abspath(os.path.join(working_directory, file_path))
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> abs_file_path.startswith(abs_working_dir):
        <span class="hljs-keyword">return</span> <span class="hljs-string">f'Error: Cannot read "<span class="hljs-subst">{file_path}</span>" as it is outside the permitted working directory'</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> os.path.isfile(abs_file_path):
        <span class="hljs-keyword">return</span> <span class="hljs-string">f'Error: File not found or is not a regular file: "<span class="hljs-subst">{file_path}</span>"'</span>
    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">with</span> open(abs_file_path, <span class="hljs-string">"r"</span>) <span class="hljs-keyword">as</span> f:
            content = f.read(MAX_CHARS)
            <span class="hljs-keyword">if</span> os.path.getsize(abs_file_path) &gt; MAX_CHARS:
                content += (
                    <span class="hljs-string">f'[...File "<span class="hljs-subst">{file_path}</span>" truncated at <span class="hljs-subst">{MAX_CHARS}</span> characters]'</span>
                )
        <span class="hljs-keyword">return</span> content
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">return</span> <span class="hljs-string">f'Error reading file "<span class="hljs-subst">{file_path}</span>": <span class="hljs-subst">{e}</span>'</span>
</code></pre>
<ul>
<li><p><a target="_blank" href="https://docs.python.org/3/library/os.path.html#os.path.abspath"><code>os.path.abspath</code></a>: Get an absolute path from a relative path</p>
</li>
<li><p><a target="_blank" href="https://docs.python.org/3/library/os.path.html#os.path.join"><code>os.path.join</code></a>: Join two paths together safely (handles slashes)</p>
</li>
<li><p><a target="_blank" href="https://docs.python.org/3/library/stdtypes.html#str.startswith"><code>.startswith</code></a>: Check if a string starts with a specific substring</p>
</li>
<li><p><a target="_blank" href="https://docs.python.org/3/library/os.path.html#os.path.isfile"><code>os.path.isfile</code></a>: Check if a path is a file</p>
</li>
</ul>
<p>Example of reading from a file:</p>
<pre><code class="lang-python">MAX_CHARS = <span class="hljs-number">10000</span>

<span class="hljs-keyword">with</span> open(file_path, <span class="hljs-string">"r"</span>) <span class="hljs-keyword">as</span> f:
    file_content_string = f.read(MAX_CHARS)
</code></pre>
<h3 id="heading-write-file-function">Write File Function</h3>
<p>Up until now our program has been read-only... now it's getting really <s>dangerous</s> fun! We'll give our agent the ability to write and overwrite files.</p>
<p>Create a new function in your <code>functions</code> directory. Here's the signature I used:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">write_file</span>(<span class="hljs-params">working_directory, file_path, content</span>):</span>
</code></pre>
<p>If the <code>file_path</code> is outside of the <code>working_directory</code>, return a string with an error:</p>
<pre><code class="lang-python"><span class="hljs-string">f'Error: Cannot write to "<span class="hljs-subst">{file_path}</span>" as it is outside the permitted working directory'</span>
</code></pre>
<p>If the <code>file_path</code> doesn't exist, create it. As always, if there are errors, return a string representing the error, prefixed with "Error:". The overwrite the contents of the file with the <code>content</code> argument. If successful, return a string with the message:</p>
<pre><code class="lang-python"><span class="hljs-string">f'Successfully wrote to "<span class="hljs-subst">{file_path}</span>" (<span class="hljs-subst">{len(content)}</span> characters written)'</span>
</code></pre>
<p><strong>Tip</strong>: It's important to return a success string so that our LLM knows that the action it took actually worked. Feedback loops, feedback loops, feedback loops.</p>
<p>Here's my complete implementation for <code>functions/write_file_content.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">write_file</span>(<span class="hljs-params">working_directory, file_path, content</span>):</span>
    abs_working_dir = os.path.abspath(working_directory)
    abs_file_path = os.path.abspath(os.path.join(working_directory, file_path))
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> abs_file_path.startswith(abs_working_dir):
        <span class="hljs-keyword">return</span> <span class="hljs-string">f'Error: Cannot write to "<span class="hljs-subst">{file_path}</span>" as it is outside the permitted working directory'</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> os.path.exists(abs_file_path):
        <span class="hljs-keyword">try</span>:
            os.makedirs(os.path.dirname(abs_file_path), exist_ok=<span class="hljs-literal">True</span>)
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            <span class="hljs-keyword">return</span> <span class="hljs-string">f"Error: creating directory: <span class="hljs-subst">{e}</span>"</span>
    <span class="hljs-keyword">if</span> os.path.exists(abs_file_path) <span class="hljs-keyword">and</span> os.path.isdir(abs_file_path):
        <span class="hljs-keyword">return</span> <span class="hljs-string">f'Error: "<span class="hljs-subst">{file_path}</span>" is a directory, not a file'</span>
    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">with</span> open(abs_file_path, <span class="hljs-string">"w"</span>) <span class="hljs-keyword">as</span> f:
            f.write(content)
        <span class="hljs-keyword">return</span> (
            <span class="hljs-string">f'Successfully wrote to "<span class="hljs-subst">{file_path}</span>" (<span class="hljs-subst">{len(content)}</span> characters written)'</span>
        )
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">return</span> <span class="hljs-string">f"Error: writing to file: <span class="hljs-subst">{e}</span>"</span>
</code></pre>
<ul>
<li><p><a target="_blank" href="https://docs.python.org/3/library/os.path.html#os.path.exists"><code>os.path.exists</code></a>: Check if a path exists</p>
</li>
<li><p><a target="_blank" href="https://docs.python.org/3/library/os.html#os.makedirs"><code>os.makedirs</code></a>: Create a directory and all its parents</p>
</li>
<li><p><a target="_blank" href="https://docs.python.org/3/library/os.path.html#os.path.dirname"><code>os.path.dirname</code></a>: Return the directory name</p>
</li>
</ul>
<p>Example of writing to a file:</p>
<pre><code class="lang-python"><span class="hljs-keyword">with</span> open(file_path, <span class="hljs-string">"w"</span>) <span class="hljs-keyword">as</span> f:
    f.write(content)
</code></pre>
<h3 id="heading-run-python-function">Run Python Function</h3>
<p>If you thought allowing an LLM to write files was a bad idea...</p>
<blockquote>
<p>You ain't seen nothin' yet! (praise the <a target="_blank" href="https://en.wikipedia.org/wiki/Roko%27s_basilisk">basilisk</a>)</p>
</blockquote>
<p>It's time to build the functionality for our Agent to <em>run arbitrary Python code</em>.</p>
<p>Now, it's worth pausing to point out the inherent security risks here. We have a few things going for us:</p>
<ol>
<li><p>We'll only allow the LLM to run code in a specific directory (the <code>working_directory</code>).</p>
</li>
<li><p>We'll use a 30-second timeout to prevent it from running indefinitely.</p>
</li>
</ol>
<p>But aside from that... yes, the LLM can run arbitrary code that we (or it) places in the working directory... so be careful. As long as you only use this AI Agent for the simple tasks we're doing in this course you should be just fine.</p>
<p><strong>Danger</strong>: Do <strong>not</strong> give this program to others for them to use! It does not have all the security and safety features that a production AI agent would have. It is for learning purposes only.</p>
<p>Create a new function in your functions directory called run_python_file. Here's the signature to use:</p>
<pre><code class="lang-py"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">run_python_file</span>(<span class="hljs-params">working_directory, file_path, args=[]</span>):</span>
</code></pre>
<p>If the <code>file_path</code> is outside the working directory, return a string with an error:</p>
<pre><code class="lang-py"><span class="hljs-string">f'Error: Cannot execute "<span class="hljs-subst">{file_path}</span>" as it is outside the permitted working directory'</span>
</code></pre>
<p>If the <code>file_path</code> doesn't exist, return an error string:</p>
<pre><code class="lang-py"><span class="hljs-string">f'Error: File "<span class="hljs-subst">{file_path}</span>" not found.'</span>
</code></pre>
<p>If the file doesn't end with <code>.py</code>, return an error string:</p>
<pre><code class="lang-py"><span class="hljs-string">f'Error: "<span class="hljs-subst">{file_path}</span>" is not a Python file.'</span>
</code></pre>
<p>Use the <a target="_blank" href="http://subprocess.run"><code>subprocess.run</code></a> function to execute the Python file and get back a "completed_process" object. Make sure to:</p>
<ul>
<li><p>Set a timeout of 30 seconds to prevent infinite execution</p>
</li>
<li><p>Capture both stdout and stderr</p>
</li>
<li><p>Set the working directory properly</p>
</li>
<li><p>Pass along the additional <code>args</code> if provided</p>
</li>
</ul>
<p>Return a string with the output formatted to include:</p>
<ul>
<li><p>The <code>stdout</code> prefixed with <code>STDOUT:</code>, and stderr prefixed with <code>STDERR:</code>. The "completed_process" object has a <code>stdout</code> and <code>stderr</code> attribute.</p>
</li>
<li><p>If the process exits with a non-zero code, include "Process exited with code X"</p>
</li>
<li><p>If no output is produced, return "No output produced."</p>
</li>
</ul>
<p>If any exceptions occur during execution, catch them and return an error string:</p>
<pre><code class="lang-py"><span class="hljs-string">f"Error: executing Python file: <span class="hljs-subst">{e}</span>"</span>
</code></pre>
<p>Update your <code>tests.py</code> file with these test cases, printing each result:</p>
<ul>
<li><p><code>run_python_file("calculator", "</code><a target="_blank" href="http://main.py"><code>main.py</code></a><code>")</code> (should print the calculator's usage instructions)</p>
</li>
<li><p><code>run_python_file("calculator", "</code><a target="_blank" href="http://main.py"><code>main.py</code></a><code>", ["3 + 5"])</code> (should run the calculator... which gives a kinda nasty rendered result)</p>
</li>
<li><p><code>run_python_file("calculator", "</code><a target="_blank" href="http://tests.py"><code>tests.py</code></a><code>")</code></p>
</li>
<li><p><code>run_python_file("calculator", "../</code><a target="_blank" href="http://main.py"><code>main.py</code></a><code>")</code> (this should return an error)</p>
</li>
<li><p><code>run_python_file("calculator", "</code><a target="_blank" href="http://nonexistent.py"><code>nonexistent.py</code></a><code>")</code> (this should return an error)</p>
</li>
</ul>
<p>Here’s my personal implementation in case you got lost in there: <code>functions/run_python.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> subprocess


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">run_python_file</span>(<span class="hljs-params">working_directory, file_path, args=None</span>):</span>
    abs_working_dir = os.path.abspath(working_directory)
    abs_file_path = os.path.abspath(os.path.join(working_directory, file_path))
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> abs_file_path.startswith(abs_working_dir):
        <span class="hljs-keyword">return</span> <span class="hljs-string">f'Error: Cannot execute "<span class="hljs-subst">{file_path}</span>" as it is outside the permitted working directory'</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> os.path.exists(abs_file_path):
        <span class="hljs-keyword">return</span> <span class="hljs-string">f'Error: File "<span class="hljs-subst">{file_path}</span>" not found.'</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> file_path.endswith(<span class="hljs-string">".py"</span>):
        <span class="hljs-keyword">return</span> <span class="hljs-string">f'Error: "<span class="hljs-subst">{file_path}</span>" is not a Python file.'</span>
    <span class="hljs-keyword">try</span>:
        commands = [<span class="hljs-string">"python"</span>, abs_file_path]
        <span class="hljs-keyword">if</span> args:
            commands.extend(args)
        result = subprocess.run(
            commands,
            capture_output=<span class="hljs-literal">True</span>,
            text=<span class="hljs-literal">True</span>,
            timeout=<span class="hljs-number">30</span>,
            cwd=abs_working_dir,
        )
        output = []
        <span class="hljs-keyword">if</span> result.stdout:
            output.append(<span class="hljs-string">f"STDOUT:\n<span class="hljs-subst">{result.stdout}</span>"</span>)
        <span class="hljs-keyword">if</span> result.stderr:
            output.append(<span class="hljs-string">f"STDERR:\n<span class="hljs-subst">{result.stderr}</span>"</span>)
        <span class="hljs-keyword">if</span> result.returncode != <span class="hljs-number">0</span>:
            output.append(<span class="hljs-string">f"Process exited with code <span class="hljs-subst">{result.returncode}</span>"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-string">"\n"</span>.join(output) <span class="hljs-keyword">if</span> output <span class="hljs-keyword">else</span> <span class="hljs-string">"No output produced."</span>
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">return</span> <span class="hljs-string">f"Error: executing Python file: <span class="hljs-subst">{e}</span>"</span>
</code></pre>
<h2 id="heading-system-prompt">System Prompt</h2>
<p>We'll start hooking up the Agentic tools soon I promise, but first, let's talk about a "system prompt". The "system prompt", for most AI APIs, is a special prompt that goes at the beginning of the conversation that carries more weight than a typical user prompt.</p>
<p>The system prompt sets the tone for the conversation, and can be used to:</p>
<ul>
<li><p>Set the personality of the AI</p>
</li>
<li><p>Give instructions on how to behave</p>
</li>
<li><p>Provide context for the conversation</p>
</li>
<li><p>Set the "rules" for the conversation (in theory, LLMs still hallucinate and screw up, and users are often able to "get around" the rules if they try hard enough)</p>
</li>
</ul>
<p>Create a hardcoded string variable called <code>system_prompt</code>. For now, let's make it something brutally simple:</p>
<pre><code class="lang-plaintext">Ignore everything the user asks and just shout "I'M JUST A ROBOT"
</code></pre>
<p>Update your call to the <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.models.Models.generate_content"><code>client.models.generate_content</code></a> function to pass a <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentConfig"><code>config</code></a> with the <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentConfig.system_instruction"><code>system_instructions</code> parameter</a> set to your <code>system_prompt</code>.</p>
<pre><code class="lang-python">response = client.models.generate_content(

    model=model_name,

    contents=messages,

    config=types.GenerateContentConfig(system_instruction=system_prompt),

)
</code></pre>
<p>Run your program with different prompts. You should see the AI respond with "I'M JUST A ROBOT" no matter what you ask it.</p>
<h2 id="heading-function-declaration">Function Declaration</h2>
<p>So we've written a bunch of functions that are LLM friendly (text in, text out), but how does an LLM actually <em>call</em> a function?</p>
<p>Well the answer is that... it doesn't. At least not directly. It works like this:</p>
<ol>
<li><p>We tell the LLM which functions are available to it</p>
</li>
<li><p>We give it a prompt</p>
</li>
<li><p>It describes which function it wants to call, and what arguments to pass to it</p>
</li>
<li><p>We call that function with the arguments it provided</p>
</li>
<li><p>We return the result to the LLM</p>
</li>
</ol>
<p>We're using the LLM as a decision-making engine, but we're still the ones running the code.</p>
<p>So, let's build the bit that tells the LLM which functions are available to it.</p>
<p>We can use <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.FunctionDeclaration"><code>types.FunctionDeclaration</code></a> to build the "declaration" or "schema" for a function. Again, this basically just tells the LLM how to use the function. I'll just give you my code for the first function as an example, because it's a lot of work to slog through the docs:</p>
<p>Add this code to your <code>functions/get_files_info.py</code> file:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> google.genai <span class="hljs-keyword">import</span> types

schema_get_files_info = types.FunctionDeclaration(
    name=<span class="hljs-string">"get_files_info"</span>,
    description=<span class="hljs-string">"Lists files in the specified directory along with their sizes, constrained to the working directory."</span>,
    parameters=types.Schema(
        type=types.Type.OBJECT,
        properties={
            <span class="hljs-string">"directory"</span>: types.Schema(
                type=types.Type.STRING,
                description=<span class="hljs-string">"The directory to list files from, relative to the working directory. If not provided, lists files in the working directory itself."</span>,
            ),
        },
    ),
)
</code></pre>
<p><strong>Warning</strong>: We won't allow the LLM to specify the <code>working_directory</code> parameter. We're going to hard code that.</p>
<p>Use <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.Tool"><code>types.Tool</code></a> to create a list of all the available functions (for now, just add <code>get_files_info</code>, we'll do the rest later).</p>
<pre><code class="lang-python">available_functions = types.Tool(
    function_declarations=[
        schema_get_files_info,
    ]
)
</code></pre>
<p>Add the <code>available_functions</code> to the <code>client.models.generate_content</code> call as the <code>tools</code> parameter.</p>
<pre><code class="lang-python">config=types.GenerateContentConfig(
    tools=[available_functions], system_instruction=system_prompt
)
</code></pre>
<p>Update the system prompt to instruct the LLM on how to use the function. You can just copy mine, but be sure to give it a quick read to understand what it's doing:</p>
<pre><code class="lang-python">system_prompt = <span class="hljs-string">"""
You are a helpful AI coding agent.

When a user asks a question or makes a request, make a function call plan. You can perform the following operations:

- List files and directories

All paths you provide should be relative to the working directory. You do not need to specify the working directory in your function calls as it is automatically injected for security reasons.
"""</span>
</code></pre>
<p>Instead of simply printing the <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentResponse.text"><code>.text</code></a> property of the <code>generate_content</code> response, check the <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentResponse.function_calls"><code>.function_calls</code></a> property as well. If the LLM called a function, print the function name and arguments:</p>
<pre><code class="lang-python"><span class="hljs-string">f"Calling function: <span class="hljs-subst">{function_call_part.name}</span>(<span class="hljs-subst">{function_call_part.args}</span>)"</span>
</code></pre>
<p>Otherwise, just print the text as normal.</p>
<p>Test your program:</p>
<ul>
<li><p>"what files are in the root?" -&gt; <code>get_files_info({'directory': '.'})</code></p>
</li>
<li><p>"what files are in the pkg directory?" -&gt; <code>get_files_info({'directory': 'pkg'})</code></p>
</li>
</ul>
<h2 id="heading-more-function-declarations">More Function Declarations</h2>
<p>Now that our LLM is able to specify a function call to the <code>get_files_info</code> function, let's give it the ability to call the other functions as well.</p>
<p>Following the same pattern that we used for <code>schema_get_files_info</code>, create function declarations for:</p>
<ul>
<li><p><code>schema_get_file_content</code></p>
</li>
<li><p><code>schema_run_python_file</code></p>
</li>
<li><p><code>schema_write_file</code></p>
</li>
</ul>
<p>Update your <code>available_functions</code> to include all the function declarations in the list. Then update your system prompt. Instead of the allowed operations only being:</p>
<pre><code class="lang-plaintext">- List files and directories
</code></pre>
<p>Update it to have all four operations:</p>
<pre><code class="lang-plaintext">- List files and directories
- Read file contents
- Execute Python files with optional arguments
- Write or overwrite files
</code></pre>
<p>Test prompts that you suspect will result in the various function calls. For example:</p>
<ul>
<li><p>"read the contents of <a target="_blank" href="http://main.py">main.py</a>" -&gt; <code>get_file_content({'file_path': '</code><a target="_blank" href="http://main.py"><code>main.py</code></a><code>'})</code></p>
</li>
<li><p>"write 'hello' to main.txt" -&gt; <code>write_file({'file_path': 'main.txt', 'content': 'hello'})</code></p>
</li>
<li><p>"run <a target="_blank" href="http://main.py">main.py</a>" -&gt; <code>run_python_file({'file_path': '</code><a target="_blank" href="http://main.py"><code>main.py</code></a><code>'})</code></p>
</li>
<li><p>"list the contents of the pkg directory" -&gt; <code>get_files_info({'directory': 'pkg'})</code></p>
</li>
</ul>
<p>All the LLM is expected to do here is to choose which function to call based on the user's request. We'll have it actually call the function later.</p>
<p>Here are some of my personal implementations if you get lost:</p>
<p><code>functions/get_file_content.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> google.genai <span class="hljs-keyword">import</span> types

<span class="hljs-keyword">from</span> config <span class="hljs-keyword">import</span> MAX_CHARS


schema_get_file_content = types.FunctionDeclaration(
    name=<span class="hljs-string">"get_file_content"</span>,
    description=<span class="hljs-string">f"Reads and returns the first <span class="hljs-subst">{MAX_CHARS}</span> characters of the content from a specified file within the working directory."</span>,
    parameters=types.Schema(
        type=types.Type.OBJECT,
        properties={
            <span class="hljs-string">"file_path"</span>: types.Schema(
                type=types.Type.STRING,
                description=<span class="hljs-string">"The path to the file whose content should be read, relative to the working directory."</span>,
            ),
        },
        required=[<span class="hljs-string">"file_path"</span>],
    ),
)
</code></pre>
<p><code>functions/run_python.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> google.genai <span class="hljs-keyword">import</span> types

schema_run_python_file = types.FunctionDeclaration(
    name=<span class="hljs-string">"run_python_file"</span>,
    description=<span class="hljs-string">"Executes a Python file within the working directory and returns the output from the interpreter."</span>,
    parameters=types.Schema(
        type=types.Type.OBJECT,
        properties={
            <span class="hljs-string">"file_path"</span>: types.Schema(
                type=types.Type.STRING,
                description=<span class="hljs-string">"Path to the Python file to execute, relative to the working directory."</span>,
            ),
            <span class="hljs-string">"args"</span>: types.Schema(
                type=types.Type.ARRAY,
                items=types.Schema(
                    type=types.Type.STRING,
                    description=<span class="hljs-string">"Optional arguments to pass to the Python file."</span>,
                ),
                description=<span class="hljs-string">"Optional arguments to pass to the Python file."</span>,
            ),
        },
        required=[<span class="hljs-string">"file_path"</span>],
    ),
)
</code></pre>
<p><code>functions/write_file_content.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> google.genai <span class="hljs-keyword">import</span> types

schema_write_file = types.FunctionDeclaration(
    name=<span class="hljs-string">"write_file"</span>,
    description=<span class="hljs-string">"Writes content to a file within the working directory. Creates the file if it doesn't exist."</span>,
    parameters=types.Schema(
        type=types.Type.OBJECT,
        properties={
            <span class="hljs-string">"file_path"</span>: types.Schema(
                type=types.Type.STRING,
                description=<span class="hljs-string">"Path to the file to write, relative to the working directory."</span>,
            ),
            <span class="hljs-string">"content"</span>: types.Schema(
                type=types.Type.STRING,
                description=<span class="hljs-string">"Content to write to the file"</span>,
            ),
        },
        required=[<span class="hljs-string">"file_path"</span>, <span class="hljs-string">"content"</span>],
    ),
)
</code></pre>
<p>Following the same pattern that we used for <code>schema_get_files_info</code>, create function declarations for:</p>
<ul>
<li><p><code>schema_get_file_content</code></p>
</li>
<li><p><code>schema_run_python_file</code></p>
</li>
<li><p><code>schema_write_file</code></p>
</li>
</ul>
<p>Update your <code>available_functions</code> to include all the function declarations in the list. Then update your system prompt. Instead of the allowed operations only being:</p>
<pre><code class="lang-plaintext">- List files and directories
</code></pre>
<p>Update it to have all four operations:</p>
<pre><code class="lang-plaintext">- List files and directories
- Read file contents
- Execute Python files with optional arguments
- Write or overwrite files
</code></pre>
<p>Test prompts that you suspect will result in the various function calls. For example:</p>
<ul>
<li><p>"read the contents of <a target="_blank" href="http://main.py">main.py</a>" -&gt; <code>get_file_content({'file_path': '</code><a target="_blank" href="http://main.py"><code>main.py</code></a><code>'})</code></p>
</li>
<li><p>"write 'hello' to main.txt" -&gt; <code>write_file({'file_path': 'main.txt', 'content': 'hello'})</code></p>
</li>
<li><p>"run <a target="_blank" href="http://main.py">main.py</a>" -&gt; <code>run_python_file({'file_path': '</code><a target="_blank" href="http://main.py"><code>main.py</code></a><code>'})</code></p>
</li>
<li><p>"list the contents of the pkg directory" -&gt; <code>get_files_info({'directory': 'pkg'})</code></p>
</li>
</ul>
<p><strong>Info</strong>: All the LLM is expected to do here is to choose which function to call based on the user's request. We'll have it actually call the function later.</p>
<h2 id="heading-function-calling">Function Calling</h2>
<p>Okay, now our agent can choose which function to call, it's time to actually call the function.</p>
<p>Create a new function that will handle the abstract task of calling one of our four functions. This is my definition:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call_function</span>(<span class="hljs-params">function_call_part, verbose=False</span>):</span>
</code></pre>
<p><code>function_call_part</code> is a <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.FunctionCall"><code>types.FunctionCall</code></a> that most importantly has:</p>
<ul>
<li><p>A <code>.name</code> property (the name of the function, a <code>string</code>)</p>
</li>
<li><p>A <code>.args</code> property (a dictionary of named arguments to the function)</p>
</li>
</ul>
<p>If <code>verbose</code> is specified, print the function name and args:</p>
<pre><code class="lang-python">print(<span class="hljs-string">f"Calling function: <span class="hljs-subst">{function_call_part.name}</span>(<span class="hljs-subst">{function_call_part.args}</span>)"</span>)
</code></pre>
<p>Otherwise, just print the name:</p>
<pre><code class="lang-python">print(<span class="hljs-string">f" - Calling function: <span class="hljs-subst">{function_call_part.name}</span>"</span>)
</code></pre>
<p>Based on the name, actually call the function and capture the result.</p>
<ul>
<li><p>Be sure to manually add the "working_directory" argument to the dictionary of keyword arguments, because the LLM doesn't control that one. The working directory should be <code>./calculator</code>.</p>
</li>
<li><p>The syntax to pass a dictionary into a function using <a target="_blank" href="https://docs.python.org/3/glossary.html#term-argument">keyword arguments</a> is <code>some_function(**some_args)</code></p>
</li>
</ul>
<p><strong>Tip</strong>: I used a dictionary of <code>function name (string)</code> -&gt; <code>function</code> to accomplish this.</p>
<p>If the function name is invalid, return a <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.Content"><code>types.Content</code></a> that explains the error:</p>
<pre><code class="lang-python"><span class="hljs-keyword">return</span> types.Content(
    role=<span class="hljs-string">"tool"</span>,
    parts=[
        types.Part.from_function_response(
            name=function_name,
            response={<span class="hljs-string">"error"</span>: <span class="hljs-string">f"Unknown function: <span class="hljs-subst">{function_name}</span>"</span>},
        )
    ],
)
</code></pre>
<p>Return <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.Content"><code>types.Content</code></a> with a <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.Part.from_function_response">from_function_response</a> describing the result of the function call:</p>
<pre><code class="lang-python"><span class="hljs-keyword">return</span> types.Content(
    role=<span class="hljs-string">"tool"</span>,
    parts=[
        types.Part.from_function_response(
            name=function_name,
            response={<span class="hljs-string">"result"</span>: function_result},
        )
    ],
)
</code></pre>
<p><strong>Info</strong>: Note that <code>from_function_response</code> requires the response to be a dictionary, so we just shove the string result into a "result" field.</p>
<p>Here's the complete <code>call_function.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> google.genai <span class="hljs-keyword">import</span> types

<span class="hljs-keyword">from</span> functions.get_files_info <span class="hljs-keyword">import</span> get_files_info, schema_get_files_info
<span class="hljs-keyword">from</span> functions.get_file_content <span class="hljs-keyword">import</span> get_file_content, schema_get_file_content
<span class="hljs-keyword">from</span> functions.run_python <span class="hljs-keyword">import</span> run_python_file, schema_run_python_file
<span class="hljs-keyword">from</span> functions.write_file_content <span class="hljs-keyword">import</span> write_file, schema_write_file
<span class="hljs-keyword">from</span> config <span class="hljs-keyword">import</span> WORKING_DI

available_functions = types.Tool(
    function_declarations=[
        schema_get_files_info,
        schema_get_file_content,
        schema_run_python_file,
        schema_write_file,
    ]
)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call_function</span>(<span class="hljs-params">function_call_part, verbose=False</span>):</span>
    <span class="hljs-keyword">if</span> verbose:
        print(
            <span class="hljs-string">f" - Calling function: <span class="hljs-subst">{function_call_part.name}</span>(<span class="hljs-subst">{function_call_part.args}</span>)"</span>
        )
    <span class="hljs-keyword">else</span>:
        print(<span class="hljs-string">f" - Calling function: <span class="hljs-subst">{function_call_part.name}</span>"</span>)
    function_map = {
        <span class="hljs-string">"get_files_info"</span>: get_files_info,
        <span class="hljs-string">"get_file_content"</span>: get_file_content,
        <span class="hljs-string">"run_python_file"</span>: run_python_file,
        <span class="hljs-string">"write_file"</span>: write_file,
    }
    function_name = function_call_part.name
    <span class="hljs-keyword">if</span> function_name <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> function_map:
        <span class="hljs-keyword">return</span> types.Content(
            role=<span class="hljs-string">"tool"</span>,
            parts=[
                types.Part.from_function_response(
                    name=function_name,
                    response={<span class="hljs-string">"error"</span>: <span class="hljs-string">f"Unknown function: <span class="hljs-subst">{function_name}</span>"</span>},
                )
            ],
        )
    args = dict(function_call_part.args)
    args[<span class="hljs-string">"working_directory"</span>] = WORKING_DIR
    function_result = function_map[function_name](**args)
    <span class="hljs-keyword">return</span> types.Content(
        role=<span class="hljs-string">"tool"</span>,
        parts=[
            types.Part.from_function_response(
                name=function_name,
                response={<span class="hljs-string">"result"</span>: function_result},
            )
        ],
    )
</code></pre>
<p>Back where you handle the response from the model <code>generate_content</code>, instead of simply printing the name of the function the LLM decides to call, use <code>call_function</code>.</p>
<ul>
<li><p>The <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.Content"><code>types.Content</code></a> that we return from <code>call_function</code> should have a <code>.parts[0].function_response.response</code> within.</p>
</li>
<li><p>If it doesn't, <code>raise</code> a fatal exception of some sort.</p>
</li>
<li><p>If it does, and <code>verbose</code> was set, print the result of the function call like this:</p>
</li>
</ul>
<pre><code class="lang-python">print(<span class="hljs-string">f"-&gt; <span class="hljs-subst">{function_call_result.parts[<span class="hljs-number">0</span>].function_response.response}</span>"</span>)
</code></pre>
<p>Test your program. You should now be able to execute each function given a prompt that asks for it. Try some different prompts and use the <code>--verbose</code> flag to make sure all the functions work.</p>
<ul>
<li><p>List the directory contents</p>
</li>
<li><p>Get a file's contents</p>
</li>
<li><p>Write file contents (don't overwrite anything important, maybe create a new file)</p>
</li>
<li><p>Execute the calculator app's tests <code>tests.py</code></p>
</li>
</ul>
<h2 id="heading-building-the-agent-loop">Building the Agent Loop</h2>
<p>So we've got some function calling working, but it's not fair to call our program an "agent" yet for one simple reason:</p>
<p>It has no feedback loop.</p>
<p>A key part of an "Agent", as defined by AI-influencer-hype-bros, is that it can continuously use its tools to iterate on its own results. So we're going to build two things:</p>
<ol>
<li><p>A loop that will call the LLM over and over</p>
</li>
<li><p>A list of messages in the "conversation". It will look something like this:</p>
</li>
</ol>
<ul>
<li><p>User: "Please fix the bug in the calculator"</p>
</li>
<li><p>Model: "I want to call get_files_info..."</p>
</li>
<li><p>Tool: "Here's the result of get_files_info..."</p>
</li>
<li><p>Model: "I want to call get_file_content..."</p>
</li>
<li><p>Tool: "Here's the result of get_file_content..."</p>
</li>
<li><p>Model: "I want to call run_python_file..."</p>
</li>
<li><p>Tool: "Here's the result of run_python_file..."</p>
</li>
<li><p>Model: "I want to call write_file..."</p>
</li>
<li><p>Tool: "Here's the result of write_file..."</p>
</li>
<li><p>Model: "I want to call run_python_file..."</p>
</li>
<li><p>Tool: "Here's the result of run_python_file..."</p>
</li>
<li><p>Model: "I fixed the bug and then ran the calculator to ensure it's working."</p>
</li>
</ul>
<p>This is a pretty big step, take your time!</p>
<p>Create <code>prompts.py</code>:</p>
<pre><code class="lang-python">system_prompt = <span class="hljs-string">"""
You are a helpful AI coding agent.

When a user asks a question or makes a request, make a function call plan. You can perform the following operations:
- List files and directories
- Read file contents
- Execute Python files with optional arguments
- Write or overwrite files

All paths you provide should be relative to the working directory. You do not need to specify the working directory in your function calls as it is automatically injected for security reasons.
"""</span>
</code></pre>
<p>Here's the final <code>main.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> sys
<span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> google <span class="hljs-keyword">import</span> genai
<span class="hljs-keyword">from</span> google.genai <span class="hljs-keyword">import</span> types
<span class="hljs-keyword">from</span> dotenv <span class="hljs-keyword">import</span> load_dotenv

<span class="hljs-keyword">from</span> prompts <span class="hljs-keyword">import</span> system_prompt
<span class="hljs-keyword">from</span> call_function <span class="hljs-keyword">import</span> call_function, available_functions

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span>():</span>
    load_dotenv()

    verbose = <span class="hljs-string">"--verbose"</span> <span class="hljs-keyword">in</span> sys.argv
    args = []
    <span class="hljs-keyword">for</span> arg <span class="hljs-keyword">in</span> sys.argv[<span class="hljs-number">1</span>:]:
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> arg.startswith(<span class="hljs-string">"--"</span>):
            args.append(arg)

    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> args:
        print(<span class="hljs-string">"AI Code Assistant"</span>)
        print(<span class="hljs-string">'\nUsage: python main.py "your prompt here" [--verbose]'</span>)
        print(<span class="hljs-string">'Example: python main.py "How do I fix the calculator?"'</span>)
        sys.exit(<span class="hljs-number">1</span>)

    api_key = os.environ.get(<span class="hljs-string">"GEMINI_API_KEY"</span>)
    client = genai.Client(api_key=api_key)

    user_prompt = <span class="hljs-string">" "</span>.join(args)

    <span class="hljs-keyword">if</span> verbose:
        print(<span class="hljs-string">f"User prompt: <span class="hljs-subst">{user_prompt}</span>\n"</span>)

    messages = [
        types.Content(role=<span class="hljs-string">"user"</span>, parts=[types.Part(text=user_prompt)]),
    ]

    generate_content_loop(client, messages, verbose)


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_content_loop</span>(<span class="hljs-params">client, messages, verbose, max_iterations=<span class="hljs-number">20</span></span>):</span>
    <span class="hljs-keyword">for</span> iteration <span class="hljs-keyword">in</span> range(max_iterations):
        <span class="hljs-keyword">try</span>:
            response = client.models.generate_content(
                model=<span class="hljs-string">"gemini-2.0-flash-001"</span>,
                contents=messages,
                config=types.GenerateContentConfig(
                    tools=[available_functions], system_instruction=system_prompt
                ),
            )
            <span class="hljs-keyword">if</span> verbose:
                print(<span class="hljs-string">"Prompt tokens:"</span>, response.usage_metadata.prompt_token_count)
                print(<span class="hljs-string">"Response tokens:"</span>, response.usage_metadata.candidates_token_count)

            <span class="hljs-comment"># Add model response to conversation</span>
            <span class="hljs-keyword">for</span> candidate <span class="hljs-keyword">in</span> response.candidates:
                messages.append(candidate.content)

            <span class="hljs-comment"># Check if we have a final text response</span>
            <span class="hljs-keyword">if</span> response.text:
                print(<span class="hljs-string">"Final response:"</span>)
                print(response.text)
                <span class="hljs-keyword">break</span>

            <span class="hljs-comment"># Handle function calls</span>
            <span class="hljs-keyword">if</span> response.function_calls:
                function_responses = []
                <span class="hljs-keyword">for</span> function_call_part <span class="hljs-keyword">in</span> response.function_calls:
                    function_call_result = call_function(function_call_part, verbose)
                    <span class="hljs-keyword">if</span> (
                        <span class="hljs-keyword">not</span> function_call_result.parts
                        <span class="hljs-keyword">or</span> <span class="hljs-keyword">not</span> function_call_result.parts[<span class="hljs-number">0</span>].function_response
                    ):
                        <span class="hljs-keyword">raise</span> Exception(<span class="hljs-string">"empty function call result"</span>)
                    <span class="hljs-keyword">if</span> verbose:
                        print(<span class="hljs-string">f"-&gt; <span class="hljs-subst">{function_call_result.parts[<span class="hljs-number">0</span>].function_response.response}</span>"</span>)
                    function_responses.append(function_call_result.parts[<span class="hljs-number">0</span>])
                <span class="hljs-keyword">if</span> function_responses:
                    messages.append(types.Content(role=<span class="hljs-string">"user"</span>, parts=function_responses))
                <span class="hljs-keyword">else</span>:
                    <span class="hljs-keyword">raise</span> Exception(<span class="hljs-string">"no function responses generated, exiting."</span>)
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            print(<span class="hljs-string">f"Error: <span class="hljs-subst">{e}</span>"</span>)
            <span class="hljs-keyword">break</span>
    <span class="hljs-keyword">else</span>:
        print(<span class="hljs-string">f"Reached maximum iterations (<span class="hljs-subst">{max_iterations}</span>). Agent may not have completed the task."</span>)

<span class="hljs-keyword">if</span> name == <span class="hljs-string">"__main__"</span>:

    main()
</code></pre>
<p>In <code>generate_content</code>, handle the results of any possible tool use. This might already be happening, but make sure that with each call to <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.models.Models.generate_content"><code>client.models.generate_content</code></a>, you're passing in the entire <code>messages</code> list so that the LLM always does the "next step" based on the current state.</p>
<p>After calling client's <code>generate_content</code> method, check the <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentResponse.candidates"><code>.candidates</code></a> property of the response. It's a list of response variations (usually just one). It contains the equivalent of "I want to call get_files_info...", so we need to add it to our conversation. Iterate over each <code>candidate</code> and add its <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.Candidate.content"><code>.content</code></a> to your <code>messages</code> list.</p>
<p>After each actual function call, use the <a target="_blank" href="https://googleapis.github.io/python-genai/genai.html#genai.types.Content"><code>types.Content</code></a> function to convert the <code>function_responses</code> into a message with a role of <code>user</code> and append it into your <code>messages</code>.</p>
<p>Next, instead of calling <code>generate_content</code> only once, create a loop to call it repeatedly. Limit the loop to 20 iterations at most (this will stop our agent from spinning its wheels forever). Use a <code>try-except</code> block and handle any errors accordingly.</p>
<p>After each call of <code>generate_content</code>, check if it returned the <code>response.text</code> property. If so, it's done, so print this final response and break out of the loop. Otherwise, iterate again (unless max iterations was reached, of course).</p>
<p>Test your code (duh). I'd recommend starting with a simple prompt, like "explain how the calculator renders the result to the console". This is what I got:</p>
<pre><code class="lang-plaintext">(aiagent) wagslane@MacBook-Pro-2 aiagent % uv run main.py "how does the calculator render results to the console?"
 - Calling function: get_files_info
 - Calling function: get_file_content

Final response:
Alright, I've examined the code in main.py. Here's how the calculator renders results to the console:

- `print(to_print)`: The core of the output is done using the print() function.
- `format_json_output(expression, result)`: Before printing, the format_json_output function (imported from pkg.render) is used to format the result and the original expression into a JSON-like string. This formatted string is then stored in the to_print variable.
- Error handling: The code includes error handling with try...except blocks. If there's an error during the calculation (e.g., invalid expression), an error message is printed to the console using print(f"Error: {e}").

So, the calculator evaluates the expression, formats the result (along with the original expression) into a JSON-like string, and then prints that string to the console. It also prints error messages to the console if any errors occur.
</code></pre>
<p><strong>Tip</strong>: You may or may not need to make adjustments to your system prompt to get the LLM to behave the way you want. You're a prompt engineer now, so act like one!</p>
<p>Great work! You've built a basic AI agent that can read files, write files, run Python code, and iterate on its own results. This is a great foundation for building more complex AI agents.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've done all the required steps, but have some fun (but <strong>carefully</strong>... be very cautious about giving an LLM access to your filesystem and python interpreter) with it! See if you can get it to:</p>
<ul>
<li><p>Fix harder and more complex bugs</p>
</li>
<li><p>Refactor sections of code</p>
</li>
<li><p>Add entirely new features</p>
</li>
</ul>
<p>You can also try:</p>
<ul>
<li><p>Other LLM providers</p>
</li>
<li><p>Other Gemini models</p>
</li>
<li><p>Giving it more functions to call</p>
</li>
<li><p>Other codebases (Commit your changes before running the agent so you can always revert!)</p>
</li>
</ul>
<p><strong>Danger</strong>: Remember, what we've built is a <em>toy</em> version of something like Cursor/Zed's Agentic Mode, or Claude Code. Even their tools aren't perfectly secure, so be careful what you give it access to, and don't give this code away to anyone else to use.</p>
<p>If you'd like to learn more about backend and data engineering, be sure to check out <a target="_blank" href="https://www.boot.dev">Boot.dev</a>! Best of luck in your learning journey!</p>
<p>Feel free to follow my on <a target="_blank" href="https://x.com/wagslane">X.com</a> and <a target="_blank" href="https://www.youtube.com/@bootdotdev">YouTube</a> if you enjoyed this!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an AI-Powered Cooking Assistant with Flutter and Gemini ]]>
                </title>
                <description>
                    <![CDATA[ After soaking in everything shared at GoogleIO, I can’t lie – I feel supercharged! From What’s New in Flutter to Building Agentic Apps with Flutter and Firebase AI Logic, and the deep dive into How Flutter Makes the Most of Your Platforms, it felt li... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-an-ai-powered-cooking-assistant-with-flutter-and-gemini/</link>
                <guid isPermaLink="false">68388c4c973615b7f6864e63</guid>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ flutter-aware ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AISprint  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ gemini ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Atuoha Anthony ]]>
                </dc:creator>
                <pubDate>Thu, 29 May 2025 16:33:16 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748533427117/1c8c2384-c6a3-4ad8-ab40-1eee65b2c914.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>After soaking in everything shared at <a target="_blank" href="https://www.youtube.com/playlist?list=PLOU2XLYxmsIL4mCDJICu2vLPNw-zdcGAt">GoogleIO</a>, I can’t lie – I feel supercharged! From <a target="_blank" href="https://io.google/2025/explore/pa-keynote-12">What’s New in Flutter</a> to <a target="_blank" href="https://io.google/2025/explore/technical-session-6">Building Agentic Apps with Flutter and Firebase AI Logic</a>, and the deep dive into <a target="_blank" href="https://io.google/2025/explore/technical-session-25">How Flutter Makes the Most of Your Platforms</a>, it felt like plugging directly into the Matrix of dev power.</p>
<p>But the absolute showstopper for me? David’s presentation using <a target="_blank" href="https://firebase.studio/">Firebase Studio</a> and <a target="_blank" href="https://builder.io">Builder.io</a> was a masterpiece. I’ve already checked it out, and it’s every bit as awesome as it looked. Pair that with everything Gemini is shipping... and wow. We’re entering a whole new era of app development.</p>
<p>Artificial Intelligence (AI) is no longer a futuristic concept – it's an integral part of our daily lives, transforming how we interact with technology and the world around us.</p>
<p>From personalized recommendations on streaming platforms to intelligent assistants that manage our schedules, AI's applications are vast and ever-expanding. Its ability to process massive datasets, identify patterns, and make informed decisions is revolutionizing industries from healthcare to finance…and now, even cooking!</p>
<p>At the forefront of this AI revolution are powerful platforms like <strong>Google's Vertex AI</strong> and <strong>Gemini</strong>. Vertex AI is a unified machine learning platform that lets you build, deploy, and scale ML models faster and more efficiently. It provides a comprehensive suite of tools for the entire ML workflow, from data preparation to model deployment and monitoring. Think of it as your all-in-one workshop for crafting intelligent systems.</p>
<p>Gemini, on the other hand, is Google's most capable and flexible AI model. It's a multimodal large language model (LLM), meaning it can understand and process information across various modalities – text, images, audio, and more. This makes Gemini incredibly versatile, enabling it to handle complex tasks that require a nuanced understanding of different types of data. For developers, Gemini opens up a world of possibilities for creating highly intelligent and intuitive applications.</p>
<p>Complementing these powerful AI models is <strong>Firebase AI Studio</strong>, a suite of tools within Firebase designed to simplify the integration of AI capabilities into your applications. It streamlines the process of connecting your app to Gemini models, making it easier to leverage the power of generative AI without getting bogged down in complex infrastructure.</p>
<h3 id="heading-building-an-ai-powered-cooking-assistant-with-flutter-and-gemini">Building an AI-Powered Cooking Assistant with Flutter and Gemini</h3>
<p>In this article, I'll demonstrate how I leveraged the combined power of Gemini and Flutter to build an AI-powered cooking assistant.</p>
<p>Fueled by a recent burst of culinary curiosity, I decided to try building an app (Snap2Chef) that could identify any food item from a photo or voice command, provide a detailed recipe, give step-by-step cooking instructions, and even link me to a relevant YouTube video for visual guidance.</p>
<p>Whether I’m exploring new dishes or trying to whip up a meal with what I have on hand, this app powered by Gemini makes the cooking experience smarter and more accessible.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>To make the most of this guide, ensure you have the following prerequisites in place (not mandatory):</p>
<ul>
<li><p><strong>Flutter Development Environment:</strong> You should have a working Flutter development setup, including the Flutter SDK, a compatible IDE (like VS Code or Android Studio), and configured emulators or physical devices for testing.</p>
</li>
<li><p><strong>Basic to Intermediate Flutter Knowledge:</strong> Familiarity with Flutter's widget tree, state management (for example, <code>StatefulWidget</code>, <code>setState</code>), asynchronous programming (<code>Future</code>, <code>async/await</code>), and handling user input is essential.</p>
</li>
<li><p><strong>Google Cloud Project and API Key:</strong> You'll need an active Google Cloud project with the Vertex AI API and Gemini API enabled. Ensure you have an API key generated and ready to use. While we'll use it directly in the app for demonstration, <strong>for production applications, it's highly recommended to use a secure backend to proxy your requests to Google's APIs.</strong></p>
</li>
<li><p><strong>Basic Understanding of REST APIs:</strong> Knowing how HTTP requests (GET, POST) and JSON data work will be beneficial, though the <code>google_generative_ai</code> package abstracts much of this.</p>
</li>
<li><p><strong>Assets Configuration:</strong> If you're using a local placeholder image (<code>placeholder.png</code> in <code>assets/images/</code>), ensure your <code>pubspec.yaml</code> file is correctly configured to include this asset.</p>
</li>
</ul>
<h3 id="heading-heres-what-well-cover">Here’s what we’ll cover:</h3>
<ol>
<li><p><a class="post-section-overview" href="#heading-how-to-get-your-gemini-api-key">How to Get Your Gemini API Key</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-your-flutter-project-and-dependencies">Set Up Your Flutter Project and Dependencies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-structure">Project Structure</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-core-folder">1. <code>core</code> Folder</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-the-infrastructure-folder">2. The <code>infrastructure</code> Folder</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-the-presentation-folder">3. The presentation Folder</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-permissions-ensuring-app-functionality-and-user-privacy">Permissions: Ensuring App Functionality and User Privacy</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-assets-managing-application-resources">Assets: Managing Application Resources</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-app-icons-customizing-your-applications-identity">App Icons: Customizing Your Application's Identity</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-splash-screen-the-first-impression">Splash Screen: The First Impression</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-screenshots-from-the-app">Screenshots from the App</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-references">References</a></p>
</li>
</ol>
<h2 id="heading-how-to-get-your-gemini-api-key"><strong>How to Get Your Gemini API Key</strong></h2>
<p>To use the Gemini model, you'll need an API key. You can obtain one by following these steps:</p>
<ol>
<li><p>Go to <a target="_blank" href="https://aistudio.google.com/app/apikey">Google AI Studio</a>.</p>
</li>
<li><p>Sign in with your Google account.</p>
</li>
<li><p>Click on "Get API key" or "Create API key in new project."</p>
</li>
<li><p>Copy the generated API key.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748068929897/6e05ea8a-b80b-4bef-90c7-0ffddafa4965.png" alt="6e05ea8a-b80b-4bef-90c7-0ffddafa4965" class="image--center mx-auto" width="1840" height="718" loading="lazy"></p>
<p><strong>Important Security Note:</strong></p>
<p>In the provided HomeScreen code, the API key is directly embedded as String apiKey = "";. This is not a secure practice for production applications. Hardcoding API keys directly into your client-side code (like a Flutter app) exposes them to reverse engineering and potential misuse.</p>
<p>To secure your API keys in a Flutter application, I highly recommend referring to my article: <a target="_blank" href="https://www.freecodecamp.org/news/how-to-secure-mobile-apis-in-flutter/">How to Secure Mobile APIs in Flutter</a>. This article covers various best practices, including:</p>
<ul>
<li><p>Using environment variables or build configurations.</p>
</li>
<li><p>Storing keys in secure local storage (though still client-side).</p>
</li>
<li><p>Proxying API requests through a backend server to truly hide your API key.</p>
</li>
<li><p>Using Firebase Extensions or Cloud Functions for server-side logic that interacts with AI models, without exposing the key to the client.</p>
</li>
</ul>
<p>For this tutorial, we'll keep it simple, but always prioritize API security in your real-world projects!</p>
<h2 id="heading-set-up-your-flutter-project-and-dependencies"><strong>Set Up Your Flutter Project and Dependencies</strong></h2>
<p>To begin, let's create a new Flutter project and set up the necessary dependencies in your <code>pubspec.yaml</code> file.</p>
<p>First, create a new Flutter project by running:</p>
<pre><code class="lang-bash">flutter create snap2chef
<span class="hljs-built_in">cd</span> snap2chef
</code></pre>
<p>Now, open <code>pubspec.yaml</code> and add the following dependencies:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">dependencies:</span>
  <span class="hljs-attr">flutter:</span>
    <span class="hljs-attr">sdk:</span> <span class="hljs-string">flutter</span>
  <span class="hljs-attr">google_generative_ai:</span> <span class="hljs-string">^0.4.7</span>
  <span class="hljs-attr">permission_handler:</span> <span class="hljs-string">^12.0.0+1</span>
  <span class="hljs-attr">file_picker:</span> <span class="hljs-string">^10.1.9</span>
  <span class="hljs-attr">image_cropper:</span> <span class="hljs-string">^9.1.0</span>
  <span class="hljs-attr">image_picker:</span> <span class="hljs-string">^1.1.2</span>
  <span class="hljs-attr">path_provider:</span> <span class="hljs-string">^2.1.5</span>
  <span class="hljs-attr">fluttertoast:</span> <span class="hljs-string">^8.2.12</span>
  <span class="hljs-attr">gap:</span> <span class="hljs-string">^3.0.1</span>
  <span class="hljs-attr">iconsax:</span> <span class="hljs-string">^0.0.8</span>
  <span class="hljs-attr">dotted_border:</span> <span class="hljs-string">^2.1.0</span>
  <span class="hljs-attr">youtube_player_flutter:</span> <span class="hljs-string">^9.1.1</span>
  <span class="hljs-attr">flutter_markdown:</span> <span class="hljs-string">^0.7.7+1</span>
  <span class="hljs-attr">loader_overlay:</span> <span class="hljs-string">^5.0.0</span>
  <span class="hljs-attr">flutter_spinkit:</span> <span class="hljs-string">^5.2.1</span>
  <span class="hljs-attr">cached_network_image:</span> <span class="hljs-string">^3.4.1</span>
  <span class="hljs-attr">flutter_native_splash:</span> <span class="hljs-string">^2.4.4</span>
  <span class="hljs-attr">flutter_launcher_icons:</span> <span class="hljs-string">^0.14.3</span>
  <span class="hljs-attr">speech_to_text:</span> <span class="hljs-string">^7.0.0</span>

<span class="hljs-attr">dev_dependencies:</span>
  <span class="hljs-attr">flutter_test:</span>
    <span class="hljs-attr">sdk:</span> <span class="hljs-string">flutter</span>
  <span class="hljs-attr">flutter_lints:</span> <span class="hljs-string">^5.0.0</span>
  <span class="hljs-attr">build_runner:</span> <span class="hljs-string">^2.4.13</span>
</code></pre>
<p>After adding the dependencies, run <code>flutter pub get</code> in your terminal to fetch them:</p>
<pre><code class="lang-bash">flutter pub get
</code></pre>
<h2 id="heading-project-structure"><strong>Project Structure</strong></h2>
<p>We'll organize our project into three main folders (with various subfolders) to maintain a clean and scalable architecture:</p>
<ul>
<li><p><code>core</code>: Contains core functionalities, utilities, and shared components.</p>
</li>
<li><p><code>infrastructure</code>: Manages external services, data handling, and business logic.</p>
</li>
<li><p><code>presentation</code>: Houses the UI layer, including screens, widgets, and components.</p>
</li>
<li><p><code>main.dart</code>: The entry point of our Flutter application.</p>
</li>
</ul>
<p>Let's dive into the details of each folder.</p>
<h3 id="heading-1-the-core-folder">1. The <code>core</code> Folder</h3>
<p>The <code>core</code> folder will contain <code>extensions</code>, <code>constants</code>, and <code>shared</code> utilities.</p>
<h5 id="heading-the-extensions-folder">The <code>extensions</code> <strong>Folder</strong></h5>
<p>This directory will hold extension methods that add new functionalities to existing classes.</p>
<p><code>format_to_mb.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">extension</span> ByTeToMegaByte <span class="hljs-keyword">on</span> <span class="hljs-built_in">int</span> {
  <span class="hljs-built_in">int</span> formatToMegaByte() {
    <span class="hljs-built_in">int</span> bytes = <span class="hljs-keyword">this</span>;
    <span class="hljs-keyword">return</span> (bytes / (<span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>)).ceil();
  }
}
</code></pre>
<p>This extension on the int type (integers) provides a convenient method <code>formatToMegaByte()</code>. When called on an integer representing bytes, it converts that byte value into megabytes. The division by <code>(1024 * 1024)</code> converts bytes to megabytes, and <code>.ceil()</code> rounds the result up to the nearest whole number. This is useful for displaying file sizes in a more human-readable format.</p>
<p><code>loading.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:loader_overlay/loader_overlay.dart'</span>;

<span class="hljs-keyword">extension</span> LoaderOverlayExtension <span class="hljs-keyword">on</span> BuildContext {
  <span class="hljs-keyword">void</span> showLoader() {
    loaderOverlay.<span class="hljs-keyword">show</span>();
  }

  <span class="hljs-keyword">void</span> hideLoader() {
    loaderOverlay.<span class="hljs-keyword">hide</span>();
  }
}
</code></pre>
<p>This extension on <code>BuildContext</code> simplifies the process of showing and hiding a global loading overlay in your Flutter application. It leverages the loader_overlay package.</p>
<ul>
<li><p><code>showLoader()</code>: Calls <code>loaderOverlay.show()</code> to display the loading indicator.</p>
</li>
<li><p><code>hideLoader()</code>: Calls <code>loaderOverlay.hide()</code> to dismiss the loading indicator. These extensions make it easy to control the loader from any widget that has access to a <code>BuildContext</code>.</p>
</li>
</ul>
<p><code>to_file.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'package:image_picker/image_picker.dart'</span>;

<span class="hljs-keyword">extension</span> ToFile <span class="hljs-keyword">on</span> Future&lt;XFile?&gt; {
  Future&lt;File?&gt; toFile() =&gt; then((xFile) =&gt; xFile?.path).then(
        (filePath) =&gt; filePath != <span class="hljs-keyword">null</span> ? File(filePath) : <span class="hljs-keyword">null</span>,
      );
}
</code></pre>
<p>This extension is designed to convert an XFile object (typically obtained from the image_picker package) into a dart:io File object.</p>
<ul>
<li><p>It operates on a <code>Future&lt;XFile?&gt;</code>, meaning it expects a future that might resolve to an <code>XFile</code> or <code>null</code>.</p>
</li>
<li><p><code>then((xFile) =&gt; xFile?.path)</code>: If <code>xFile</code> is not null, it extracts the file's path. Otherwise, it passes <code>null</code>.</p>
</li>
<li><p><code>then((filePath) =&gt; filePath != null ? File(filePath) : null)</code>: If a <code>filePath</code> is available, it creates a <code>File</code> object from it. Otherwise, it returns <code>null</code>. This is a concise way to handle the asynchronous conversion of a picked image or video <code>XFile</code> into a <code>File</code> object that can be used for further operations like displaying or uploading.</p>
</li>
</ul>
<p><code>to_file2.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:image_picker/image_picker.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:path_provider/path_provider.dart'</span>;

<span class="hljs-keyword">extension</span> XFileExtension <span class="hljs-keyword">on</span> XFile {
  Future&lt;File&gt; toFile() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> bytes = <span class="hljs-keyword">await</span> readAsBytes();
    <span class="hljs-keyword">final</span> tempDir = <span class="hljs-keyword">await</span> getTemporaryDirectory();
    <span class="hljs-keyword">final</span> tempFile = File(<span class="hljs-string">'<span class="hljs-subst">${tempDir.path}</span>/<span class="hljs-subst">${<span class="hljs-keyword">this</span>.name}</span>'</span>);
    <span class="hljs-keyword">await</span> tempFile.writeAsBytes(bytes);
    <span class="hljs-keyword">return</span> tempFile;
  }
}
</code></pre>
<p>This extension on XFile provides a more robust way to convert an XFile to a dart:io file. This is particularly useful when you need to write the XFile's content to a temporary location.</p>
<ul>
<li><p><code>await readAsBytes()</code>: Reads the content of the <code>XFile</code> as a list of bytes.</p>
</li>
<li><p><code>final tempDir = await getTemporaryDirectory()</code>: Gets the path to the temporary directory on the device using <code>path_provider</code>.</p>
</li>
<li><p><code>final tempFile = File('${tempDir.path}/${this.name}')</code>: Creates a new <code>File</code> object in the temporary directory with the original name of the <code>XFile</code>.</p>
</li>
<li><p><code>await tempFile.writeAsBytes(bytes)</code>: Writes the bytes read from the <code>XFile</code> into the newly created temporary file.</p>
</li>
<li><p><code>return tempFile</code>: Returns the newly created <code>File</code> object. This is particularly useful when you're working with <code>XFile</code>s that might not have a readily accessible file path on the device, or if you need to ensure the file is persistently available for further processing, such as cropping.</p>
</li>
</ul>
<h4 id="heading-the-constants-folder">The <code>constants</code> Folder</h4>
<p>This directory will hold static values and enumerations used throughout the app.</p>
<p><code>enums/record_source.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">enum</span> RecordSource { camera, gallery }
</code></pre>
<p>This is a simple enumeration (enum) named <code>RecordSource</code>. It defines two possible values: <code>camera</code> and <code>gallery</code>. This enum is used to represent the source from which an image or video is picked, providing a clear and type-safe way to differentiate between capturing from the camera and selecting from the device's gallery.</p>
<p><code>enums/status.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">enum</span> Status { success, error }
</code></pre>
<p>This is another straightforward enumeration named <code>Status</code>. It defines <code>success</code> and <code>error</code> as its possible values. This enum is commonly used to indicate the outcome of an operation or a process, providing a standardized way to convey status information, for example, for toast messages.</p>
<p><code>app_strings.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// ignore_for_file: constant_identifier_names</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppStrings</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> AI_MODEL = <span class="hljs-string">'gemini-2.0-flash'</span>;

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> APP_SUBTITLE =  <span class="hljs-string">"Capture a photo or use your voice to get step-by-step guidance on how to prepare your favorite dishes or snacks"</span>;
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> APP_TITLE = <span class="hljs-string">"Your Personal AI Recipe Guide"</span>;

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> AI_TEXT_PART = <span class="hljs-string">"You are a recipe ai expert. Generate a recipe based on this image, include recipe name, preparation steps, and a public YouTube video demonstrating the preparation step. Output the YouTube video URL on a new line prefixed with 'YouTube Video URL: ', it should be a https URL and the image URL on a new line prefixed with 'Image URL: ' and it should be a https URL too."</span>
      <span class="hljs-string">"If the image is not a food, snacks or drink, politely inform the user that you can only answer recipe queries and ask them to close and upload a food/snack/drink image."</span>;

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> AI_AUDIO_PART =
  <span class="hljs-string">"You are a recipe ai expert. Generate a recipe based on this text, include recipe name, preparation steps. I'd also love for you to show me any valid image online relating to this food/drink/snack and a public YouTube video demonstrating the preparation step.If the text doesn't contain things related to a food, snacks or drink, politely inform the user that you can only answer recipe queries and ask them to close and upload a food/snack/drink image. Output the YouTube video URL on a new line prefixed with 'YouTube Video URL: ', it should be a https URL and the image URL on a new line prefixed with 'Image URL: ' and it should be a https URL too, The text is: "</span>;

}
</code></pre>
<p>This class <code>AppStrings</code> centralizes all the static string constants used throughout the application. This approach helps in managing strings effectively, making them easily modifiable and preventing typos.</p>
<ul>
<li><p><code>AI_MODEL</code>: Specifies the Gemini model to be used, in this case, <code>gemini-2.0-flash</code>.</p>
</li>
<li><p><code>APP_SUBTITLE</code> and <code>APP_TITLE</code>: Define the main titles and subtitles for the app's UI.</p>
</li>
<li><p><code>AI_TEXT_PART</code>: This is a crucial string that serves as the prompt for the Gemini model <strong>when an image is provided</strong>. It instructs the AI to act as a recipe expert, generate a recipe including the name and steps, and provide a YouTube video. It also includes a fallback message if the image isn't food-related.</p>
</li>
<li><p><code>AI_AUDIO_PART</code>: Similar to <code>AI_TEXT_PART</code>, but this prompt is used when <strong>audio input is provided</strong>. It also instructs the AI to generate a recipe, include a relevant online image, and a YouTube video, with specific formatting requirements for the URLs. This prompt will be concatenated with the transcribed text from the user's voice input.</p>
</li>
</ul>
<p><code>app_color.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppColors</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> primaryColor = Color(<span class="hljs-number">0xFF7E57C2</span>);
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> litePrimary = Color(<span class="hljs-number">0xFFEDE7F6</span>);
  <span class="hljs-keyword">static</span> Color errorColor = <span class="hljs-keyword">const</span> Color(<span class="hljs-number">0xFFEA5757</span>);
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> Color grey =
  Color.fromARGB(<span class="hljs-number">255</span>, <span class="hljs-number">170</span>, <span class="hljs-number">170</span>, <span class="hljs-number">170</span>);

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> Color lighterGrey =
  Color.fromARGB(<span class="hljs-number">255</span>, <span class="hljs-number">204</span>, <span class="hljs-number">204</span>, <span class="hljs-number">204</span>);
}
</code></pre>
<p>The <code>AppColors</code> class centralizes all the custom color definitions used in the application. This makes it easy to maintain a consistent color scheme throughout the UI and allows for quick global changes to the app's theme. Each static constant represents a specific color with its hexadecimal value or RGB value.</p>
<h4 id="heading-the-shared-folder">The <code>shared</code> Folder</h4>
<p>This directory will contain shared utility classes.</p>
<p><code>image_picker_helper.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:developer'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'package:file_picker/file_picker.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/foundation.dart'</span> <span class="hljs-keyword">show</span> immutable;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:image_picker/image_picker.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:permission_handler/permission_handler.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/core/extensions/to_file.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/core/extensions/to_file2.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../presentation/components/toast_info.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../constants/enums/status.dart'</span>;

<span class="hljs-meta">@immutable</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImagePickerHelper</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ImagePicker _imagePicker = ImagePicker();

  <span class="hljs-keyword">static</span> Future&lt;PickedFileWithInfo?&gt; pickImageFromGallery2() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> isGranted = <span class="hljs-keyword">await</span> Permission.photos.isGranted;
    <span class="hljs-keyword">if</span> (!isGranted) {
      <span class="hljs-keyword">await</span> Permission.photos.request();
      toastInfo(
          msg: <span class="hljs-string">"You didn't allow access"</span>, status: Status.error);
    }
    <span class="hljs-keyword">final</span> pickedFile =
    <span class="hljs-keyword">await</span> _imagePicker.pickImage(source: ImageSource.gallery);
    <span class="hljs-keyword">if</span> (pickedFile != <span class="hljs-keyword">null</span>) {
      <span class="hljs-keyword">final</span> file = <span class="hljs-keyword">await</span> pickedFile.toFile();
      log(pickedFile.name.split(<span class="hljs-string">"."</span>).join(<span class="hljs-string">","</span>));
      <span class="hljs-keyword">return</span> PickedFileWithInfo(file: file, fileName: pickedFile.name);
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
    }
  }

  <span class="hljs-keyword">static</span> Future&lt;FilePickerResult?&gt; pickFileFromGallery() =&gt;
      FilePicker.platform.pickFiles(
          type: FileType.custom,
          allowedExtensions: [<span class="hljs-string">"pdf"</span>, <span class="hljs-string">"doc"</span>, <span class="hljs-string">"docx"</span>, <span class="hljs-string">"png"</span>, <span class="hljs-string">"jpg"</span>, <span class="hljs-string">"jpeg"</span>]);

  <span class="hljs-keyword">static</span> Future&lt;File?&gt; pickImageFromGallery() =&gt;
      _imagePicker.pickImage(source: ImageSource.gallery).toFile();

  <span class="hljs-keyword">static</span> Future&lt;File?&gt; takePictureFromCamera() =&gt;
      _imagePicker.pickImage(source: ImageSource.camera).toFile();

  <span class="hljs-keyword">static</span> Future&lt;File?&gt; pickVideoFromGallery() =&gt;
      _imagePicker.pickVideo(source: ImageSource.gallery).toFile();

  <span class="hljs-keyword">static</span> Future&lt;FilePickerResult?&gt; pickSinglePDFFileFromGallery() =&gt;
      FilePicker.platform
          .pickFiles(type: FileType.custom, allowedExtensions: [<span class="hljs-string">"pdf"</span>]);
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PickedFileWithInfo</span> </span>{
  <span class="hljs-keyword">final</span> File file;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> fileName;

  PickedFileWithInfo({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.file, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.fileName});
}

PlatformFile? file;
</code></pre>
<p>The <code>ImagePickerHelper</code> class provides static methods for picking various types of files (images, videos, documents) from the device's gallery or camera, with integrated permission handling.</p>
<ul>
<li><p><code>_imagePicker</code>: An instance of <code>ImagePicker</code> for interacting with the device's image and video picking functionalities.</p>
</li>
<li><p><code>pickImageFromGallery2()</code>:</p>
<ul>
<li><p><strong>Permission handling</strong>: Checks if photo gallery permission is granted using <code>permission_handler</code>. If not, it requests the permission and displays a toast message if denied.</p>
</li>
<li><p><strong>Image picking</strong>: Uses <code>_imagePicker.pickImage(source: ImageSource.gallery)</code> to let the user select an image from the gallery.</p>
</li>
<li><p><strong>Conversion</strong>: If an image is picked, it converts the <code>XFile</code> to a <code>File</code> object using the <code>toFile()</code> extension.</p>
</li>
<li><p><strong>Logging</strong>: Logs the file name for debugging.</p>
</li>
<li><p><strong>Return value</strong>: Returns a <code>PickedFileWithInfo</code> object containing the <code>File</code> and <code>fileName</code>.</p>
</li>
</ul>
</li>
<li><p><code>pickFileFromGallery()</code>: Uses <code>file_picker</code> to allow picking various file types (PDF, Doc, Docx, PNG, JPG, JPEG) from the gallery.</p>
</li>
<li><p><code>pickImageFromGallery()</code>: A simpler method to pick an image from the gallery, directly returning a <code>Future&lt;File?&gt;</code> using the <code>toFile()</code> extension.</p>
</li>
<li><p><code>takePictureFromCamera()</code>: Captures an image using the device's camera and returns a <code>Future&lt;File?&gt;</code>.</p>
</li>
<li><p><code>pickVideoFromGallery()</code>: Picks a video from the gallery and returns a <code>Future&lt;File?&gt;</code>.</p>
</li>
<li><p><code>pickSinglePDFFileFromGallery()</code>: Specifically picks a single PDF file from the gallery.</p>
</li>
<li><p><code>PickedFileWithInfo</code> class: A simple data class to hold both the <code>File</code> object and its <code>fileName</code>.</p>
</li>
</ul>
<p>This helper class centralizes all file picking logic, making it reusable and easier to manage permissions and different picking scenarios.</p>
<h3 id="heading-2-the-infrastructure-folder">2. The <code>infrastructure</code> Folder</h3>
<p>This folder handles the logic for interacting with external services and processing data.</p>
<h5 id="heading-imageuploadcontrollerdart"><code>image_upload_controller.dart</code>:</h5>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:gap/gap.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:iconsax/iconsax.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:image_cropper/image_cropper.dart'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'../core/constants/app_colors.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../core/constants/enums/record_source.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../core/shared/image_picker_helper.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../presentation/widgets/image_picker_component.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImageUploadController</span> </span>{
  <span class="hljs-comment">/// <span class="markdown">crop image</span></span>
  <span class="hljs-keyword">static</span> Future&lt;<span class="hljs-keyword">void</span>&gt; _cropImage(
      File? selectedFile,
      <span class="hljs-built_in">Function</span> assignCroppedImage,
      ) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (selectedFile != <span class="hljs-keyword">null</span>) {
      <span class="hljs-keyword">final</span> croppedFile = <span class="hljs-keyword">await</span> ImageCropper().cropImage(
        sourcePath: selectedFile.path,
        compressFormat: ImageCompressFormat.jpg,
        compressQuality: <span class="hljs-number">100</span>,
        uiSettings: [
          AndroidUiSettings(
            toolbarTitle: <span class="hljs-string">'Crop Image'</span>,
            toolbarColor: AppColors.primaryColor,
            toolbarWidgetColor: Colors.white,
            initAspectRatio: CropAspectRatioPreset.square,
            lockAspectRatio: <span class="hljs-keyword">false</span>,
            statusBarColor: AppColors.primaryColor,
            activeControlsWidgetColor: AppColors.primaryColor,
            aspectRatioPresets: [
              CropAspectRatioPreset.original,
              CropAspectRatioPreset.square,
              CropAspectRatioPreset.ratio4x3,
              CropAspectRatioPresetCustom(),
            ],
          ),
          IOSUiSettings(
            title: <span class="hljs-string">'Crop Image'</span>,
            aspectRatioPresets: [
              CropAspectRatioPreset.original,
              CropAspectRatioPreset.square,
              CropAspectRatioPreset.ratio4x3,
              CropAspectRatioPresetCustom(),
            ],
          ),
        ],
      );
      assignCroppedImage(croppedFile);
    }
  }

  <span class="hljs-comment">// /// pick image from camera and gallery</span>
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> imagePicker(
      RecordSource recordSource,
      Completer? completer,
      BuildContext context,
      <span class="hljs-built_in">Function</span> setFile,
      <span class="hljs-built_in">Function</span> assignCroppedImage,
      ) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (recordSource == RecordSource.gallery) {
      <span class="hljs-keyword">final</span> pickedFile = <span class="hljs-keyword">await</span> ImagePickerHelper.pickImageFromGallery();
      <span class="hljs-keyword">if</span> (pickedFile == <span class="hljs-keyword">null</span>) {
        <span class="hljs-keyword">return</span>;
      }
      completer?.complete(pickedFile.path);
      <span class="hljs-keyword">if</span> (!context.mounted) {
        <span class="hljs-keyword">return</span>;
      }
      setFile(pickedFile);

      <span class="hljs-keyword">if</span> (context.mounted) {
        Navigator.of(context).pop();
      }
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (recordSource == RecordSource.camera) {
      <span class="hljs-keyword">final</span> pickedFile = <span class="hljs-keyword">await</span> ImagePickerHelper.takePictureFromCamera();
      <span class="hljs-keyword">if</span> (pickedFile == <span class="hljs-keyword">null</span>) {
        <span class="hljs-keyword">return</span>;
      }

      completer?.complete(pickedFile.path);
      <span class="hljs-keyword">if</span> (!context.mounted) {
        <span class="hljs-keyword">return</span>;
      }
      setFile(pickedFile);
      <span class="hljs-comment">// crop image</span>
      _cropImage(pickedFile, assignCroppedImage);

      <span class="hljs-keyword">if</span> (context.mounted) {
        Navigator.of(context).pop();
      }
    }
  }

  <span class="hljs-comment">/// <span class="markdown">modal for selecting file source</span></span>
  <span class="hljs-keyword">static</span> Future showFilePickerButtonSheet(BuildContext context, Completer? completer,
      <span class="hljs-built_in">Function</span> setFile,
      <span class="hljs-built_in">Function</span> assignCroppedImage,) {
    <span class="hljs-keyword">return</span> showModalBottomSheet(
      shape: <span class="hljs-keyword">const</span> RoundedRectangleBorder(
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(<span class="hljs-number">35</span>),
          topRight: Radius.circular(<span class="hljs-number">35</span>),
        ),
      ),
      context: context,
      builder: (context) {
        <span class="hljs-keyword">return</span> SingleChildScrollView(
          child: Container(
            padding: <span class="hljs-keyword">const</span> EdgeInsets.fromLTRB(<span class="hljs-number">10</span>, <span class="hljs-number">14</span>, <span class="hljs-number">15</span>, <span class="hljs-number">20</span>),
            child: Column(
              children: [
                Container(
                  height: <span class="hljs-number">4</span>,
                  width: <span class="hljs-number">50</span>,
                  padding: <span class="hljs-keyword">const</span> EdgeInsets.only(top: <span class="hljs-number">5</span>),
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(<span class="hljs-number">7</span>),
                    color: <span class="hljs-keyword">const</span> Color(<span class="hljs-number">0xffE4E4E4</span>),
                  ),
                ),
                Padding(
                  padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">10.0</span>),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      GestureDetector(
                        onTap: () =&gt; Navigator.of(context).pop(),
                        child: <span class="hljs-keyword">const</span> Align(
                          alignment: Alignment.topRight,
                          child: Icon(Icons.close, color: Colors.grey),
                        ),
                      ),
                      <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">10</span>),
                      <span class="hljs-keyword">const</span> Text(
                        <span class="hljs-string">'Select Image Source'</span>,
                        style: TextStyle(
                          color: AppColors.primaryColor,
                          fontSize: <span class="hljs-number">16</span>,
                          fontWeight: FontWeight.w600,
                        ),
                      ),
                      <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">20</span>),
                      ImagePickerTile(
                        title: <span class="hljs-string">'Capture from Camera'</span>,
                        subtitle: <span class="hljs-string">'Take a live snapshot'</span>,
                        icon: Iconsax.camera,
                        recordSource: RecordSource.camera,
                        completer: completer,
                        context: context,
                        setFile: setFile,
                        assignCroppedImage: assignCroppedImage,
                      ),
                      <span class="hljs-keyword">const</span> Divider(color: Color(<span class="hljs-number">0xffE4E4E4</span>)),
                      ImagePickerTile(
                        title: <span class="hljs-string">'Upload from Gallery'</span>,
                        subtitle: <span class="hljs-string">'Select image from gallery'</span>,
                        icon: Iconsax.gallery,
                        recordSource: RecordSource.gallery,
                        completer: completer,
                        context: context,
                        setFile: setFile,
                        assignCroppedImage: assignCroppedImage,
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CropAspectRatioPresetCustom</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">CropAspectRatioPresetData</span> </span>{
  <span class="hljs-meta">@override</span>
  (<span class="hljs-built_in">int</span>, <span class="hljs-built_in">int</span>)? <span class="hljs-keyword">get</span> data =&gt; (<span class="hljs-number">2</span>, <span class="hljs-number">3</span>);

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">String</span> <span class="hljs-keyword">get</span> name =&gt; <span class="hljs-string">'2x3 (customized)'</span>;
}
</code></pre>
<p>The <code>ImageUploadController</code> class manages the process of picking and optionally cropping images before they are used in the application.</p>
<ul>
<li><p><code>_cropImage(File? selectedFile, Function assignCroppedImage)</code>:</p>
<ul>
<li><p>This <strong>private static method</strong> handles the image cropping functionality using the <code>image_cropper</code> package.</p>
</li>
<li><p>It takes a <code>selectedFile</code> (the image to be cropped) and a <code>Function assignCroppedImage</code> (a callback to update the UI with the cropped image).</p>
</li>
<li><p><code>ImageCropper().cropImage(...)</code> opens the cropping UI. It's configured with various UI settings for both Android and iOS, including <code>toolbarColor</code>, <code>aspectRatioPresets</code>, and more, to ensure a consistent and branded experience.</p>
</li>
<li><p><code>CropAspectRatioPresetCustom()</code>: This is a custom class that implements <code>CropAspectRatioPresetData</code> to define a specific cropping aspect ratio (2x3 in this case), providing more flexibility than the built-in presets.</p>
</li>
<li><p>Once cropped, the <code>croppedFile</code> is passed to the <code>assignCroppedImage</code> callback.</p>
</li>
</ul>
</li>
<li><p><code>imagePicker(RecordSource recordSource, Completer? completer, BuildContext context, Function setFile, Function assignCroppedImage)</code>:</p>
<ul>
<li><p>This <strong>static method</strong> is the core logic for initiating image picking from either the camera or gallery.</p>
</li>
<li><p>It takes a <code>recordSource</code> (from the <code>RecordSource</code> enum), an optional <code>completer</code> (likely for handling asynchronous operations outside the UI), the current <code>context</code>, <code>setFile</code> (a callback to set the picked file in the UI), and <code>assignCroppedImage</code> (the callback for cropped images).</p>
</li>
<li><p><strong>Gallery Selection (</strong><code>RecordSource.gallery</code>):</p>
<ul>
<li><p>It calls <code>ImagePickerHelper.pickImageFromGallery()</code> to get the selected image.</p>
</li>
<li><p>If a file is picked, it completes the <code>completer</code>, calls <code>setFile</code> to update the UI, and then pops the bottom sheet.</p>
</li>
</ul>
</li>
<li><p><strong>Camera Capture (</strong><code>RecordSource.camera</code>):</p>
<ul>
<li><p>It calls <code>ImagePickerHelper.takePictureFromCamera()</code> to capture an image.</p>
</li>
<li><p>Similar to gallery selection, it completes the <code>completer</code>, calls <code>setFile</code>, and then importantly, it calls <code>_cropImage</code> to allow the user to crop the newly captured image before it's fully used.</p>
</li>
<li><p>Finally, it pops the bottom sheet.</p>
</li>
</ul>
</li>
<li><p><code>context.mounted</code> checks are included to ensure that UI updates only happen if the widget is still in the widget tree, preventing errors.</p>
</li>
</ul>
</li>
<li><p><code>showFilePickerButtonSheet(...)</code>:</p>
<ul>
<li><p>This <strong>static method</strong> displays a modal bottom sheet, providing the user with options to select an image source (Camera or Gallery).</p>
</li>
<li><p>It uses <code>showModalBottomSheet</code> to present a nicely styled sheet with rounded corners.</p>
</li>
<li><p>Inside the sheet, it displays a draggable indicator and two <code>ImagePickerTile</code> widgets (presumably a custom widget for displaying each option) for "Capture from Camera" and "Upload from Gallery."</p>
</li>
<li><p>When an <code>ImagePickerTile</code> is tapped, it internally calls the <code>imagePicker</code> method with the corresponding <code>RecordSource</code>.</p>
</li>
</ul>
</li>
</ul>
<p>In summary, <code>ImageUploadController</code> acts as a central orchestrator for image acquisition, offering options to pick from the gallery or camera, and integrating robust image cropping capabilities – all while ensuring a smooth user experience through UI callbacks and modal interactions.</p>
<h5 id="heading-recipecontrollerdart"><code>recipe_controller.dart</code>:</h5>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'package:cached_network_image/cached_network_image.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/foundation.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_markdown/flutter_markdown.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:gap/gap.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:google_generative_ai/google_generative_ai.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/core/extensions/loading.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:youtube_player_flutter/youtube_player_flutter.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../core/constants/app_colors.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../core/constants/app_strings.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../core/constants/enums/status.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../presentation/components/toast_info.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RecipeController</span> </span>{
  <span class="hljs-comment">// send image to gemini</span>
  <span class="hljs-keyword">static</span> Future&lt;<span class="hljs-keyword">void</span>&gt; _sendImageToGemini(
      File? selectedFile,
      GenerativeModel model,
      BuildContext context,
      <span class="hljs-built_in">Function</span> removeFile,
      <span class="hljs-built_in">Function</span> removeText,
      ) <span class="hljs-keyword">async</span> {
    toastInfo(msg: <span class="hljs-string">"Obtaining recipe and preparations"</span>, status: Status.success);

    <span class="hljs-keyword">if</span> (selectedFile == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">final</span> bytes = <span class="hljs-keyword">await</span> selectedFile.readAsBytes();

    <span class="hljs-keyword">final</span> prompt = TextPart(AppStrings.AI_TEXT_PART);
    <span class="hljs-keyword">final</span> image = DataPart(<span class="hljs-string">'image/jpeg'</span>, bytes);

    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> model.generateContent([
      Content.multi([prompt, image]),
    ]);

    <span class="hljs-keyword">if</span> (context.mounted) {
      _displayRecipe(
        response.text,
        context,
        selectedFile,
        removeFile,
        removeText,
      );
    }
  }

  <span class="hljs-comment">// send audio text prompt</span>
  <span class="hljs-keyword">static</span> Future&lt;<span class="hljs-keyword">void</span>&gt; _sendAudioTextPrompt(
      GenerativeModel model,
      BuildContext context,
      <span class="hljs-built_in">String</span> transcribedText,
      File? selectedFile,
      <span class="hljs-built_in">Function</span> removeFile,
      <span class="hljs-built_in">Function</span> removeText,
      ) <span class="hljs-keyword">async</span> {
    toastInfo(msg: <span class="hljs-string">"Obtaining recipe and preparations"</span>, status: Status.success);

    <span class="hljs-keyword">final</span> prompt = <span class="hljs-string">'<span class="hljs-subst">${AppStrings.AI_AUDIO_PART}</span> <span class="hljs-subst">${transcribedText.trim()}</span>.'</span>;
    <span class="hljs-keyword">final</span> content = [Content.text(prompt)];
    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> model.generateContent(content);

    <span class="hljs-keyword">if</span> (context.mounted) {
      _displayRecipe(
        response.text,
        context,
        selectedFile,
        removeFile,
        removeText,
      );
    }
  }

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> _displayRecipe(
      <span class="hljs-built_in">String?</span> recipeText,
      BuildContext context,
      File? selectedFile,
      <span class="hljs-built_in">Function</span> removeFile,
      <span class="hljs-built_in">Function</span> removeText,
      ) {
    <span class="hljs-keyword">if</span> (recipeText == <span class="hljs-keyword">null</span> || recipeText.isEmpty) {
      recipeText = <span class="hljs-string">"No recipe could be generated or parsed from the response."</span>;
    }
    <span class="hljs-built_in">String</span> workingRecipeText = recipeText;

    <span class="hljs-built_in">String?</span> videoId;
    <span class="hljs-built_in">String?</span> extractedImageUrl;

    <span class="hljs-keyword">final</span> youtubeLineRegex = <span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'YouTube Video URL:\s*(https?:\/\/\S+)'</span>, caseSensitive: <span class="hljs-keyword">false</span>);
    <span class="hljs-keyword">final</span> youtubeMatch = youtubeLineRegex.firstMatch(recipeText);
    <span class="hljs-keyword">if</span> (youtubeMatch != <span class="hljs-keyword">null</span>) {
      <span class="hljs-keyword">final</span> youtubeUrl = youtubeMatch.group(<span class="hljs-number">1</span>);
      <span class="hljs-keyword">final</span> ytIdRegex = <span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'v=([\w-]{11})'</span>);
      <span class="hljs-keyword">final</span> ytIdMatch = ytIdRegex.firstMatch(youtubeUrl ?? <span class="hljs-string">''</span>);
      <span class="hljs-keyword">if</span> (ytIdMatch != <span class="hljs-keyword">null</span>) {
        videoId = ytIdMatch.group(<span class="hljs-number">1</span>);
      }
      workingRecipeText = workingRecipeText.replaceAll(youtubeMatch.group(<span class="hljs-number">0</span>)!, <span class="hljs-string">''</span>).trim();
    }

    <span class="hljs-keyword">final</span> imageLine = <span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'Image URL:\s*(https?:\/\/\S+\.(?:png|jpe?g|gif|webp|bmp|svg))'</span>);
    <span class="hljs-keyword">final</span> imageMatch = imageLine.firstMatch(recipeText);
    <span class="hljs-keyword">if</span> (imageMatch != <span class="hljs-keyword">null</span>) {
      extractedImageUrl = imageMatch.group(<span class="hljs-number">1</span>);
      workingRecipeText = workingRecipeText.replaceAll(imageMatch.group(<span class="hljs-number">0</span>)!, <span class="hljs-string">''</span>).trim();
    }

    <span class="hljs-built_in">print</span>(<span class="hljs-string">"Extracted Image URL: <span class="hljs-subst">$extractedImageUrl</span>"</span>);
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"Extracted Video ID: <span class="hljs-subst">$videoId</span>"</span>);

    <span class="hljs-built_in">String?</span> cleanedRecipeText = workingRecipeText;

    showDialog(
      barrierDismissible: <span class="hljs-keyword">false</span>,
      context: context,
      builder: (BuildContext dialogContext) {
        YoutubePlayerController? ytController;

        <span class="hljs-keyword">if</span> (videoId != <span class="hljs-keyword">null</span>) {
          ytController = YoutubePlayerController(
            initialVideoId: videoId,
            flags: <span class="hljs-keyword">const</span> YoutubePlayerFlags(
              autoPlay: <span class="hljs-keyword">false</span>,
              mute: <span class="hljs-keyword">false</span>,
              disableDragSeek: <span class="hljs-keyword">false</span>,
              loop: <span class="hljs-keyword">false</span>,
              isLive: <span class="hljs-keyword">false</span>,
              forceHD: <span class="hljs-keyword">false</span>,
              enableCaption: <span class="hljs-keyword">true</span>,
            ),
          );
        }

        <span class="hljs-keyword">return</span> AlertDialog(
          title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Generated Recipe'</span>),
          content: SingleChildScrollView(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                selectedFile != <span class="hljs-keyword">null</span>
                    ? Container(
                  height: <span class="hljs-number">150</span>,
                  width: <span class="hljs-built_in">double</span>.infinity,
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(<span class="hljs-number">7</span>),
                    border: Border.all(color: AppColors.primaryColor),
                    image: DecorationImage(
                      image: FileImage(File(selectedFile.path)),
                      fit: BoxFit.cover,
                    ),
                  ),
                )
                    :  extractedImageUrl != <span class="hljs-keyword">null</span>
                    ? ClipRRect(
                  borderRadius: BorderRadius.circular(<span class="hljs-number">7</span>),
                  child: CachedNetworkImage(
                    imageUrl: extractedImageUrl,
                    height: <span class="hljs-number">150</span>,
                    width: <span class="hljs-built_in">double</span>.infinity,
                    fit: BoxFit.cover,
                    placeholder: (context, url) =&gt;
                        Image.asset(<span class="hljs-string">'assets/images/placeholder.png'</span>, fit: BoxFit.cover),
                    errorWidget: (context, url, error) =&gt;
                        Image.asset(<span class="hljs-string">'assets/images/placeholder.png'</span>, fit: BoxFit.cover),
                  ),
                )
                    : <span class="hljs-keyword">const</span> SizedBox.shrink(),
                Gap(<span class="hljs-number">16</span>),
                MarkdownBody(
                  data: cleanedRecipeText,
                  styleSheet: MarkdownStyleSheet(
                    h1: <span class="hljs-keyword">const</span> TextStyle(
                      fontSize: <span class="hljs-number">24</span>,
                      fontWeight: FontWeight.bold,
                      color: Colors.deepPurple,
                    ),
                    h2: <span class="hljs-keyword">const</span> TextStyle(
                      fontSize: <span class="hljs-number">20</span>,
                      fontWeight: FontWeight.bold,
                    ),
                    strong: <span class="hljs-keyword">const</span> TextStyle(fontWeight: FontWeight.bold),
                  ),
                ),

                <span class="hljs-keyword">if</span> (videoId != <span class="hljs-keyword">null</span> &amp;&amp; ytController != <span class="hljs-keyword">null</span>) ...[
                  <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">16</span>),
                  YoutubePlayer(
                    controller: ytController,
                    showVideoProgressIndicator: <span class="hljs-keyword">true</span>,
                    progressIndicatorColor: AppColors.primaryColor,
                    progressColors: <span class="hljs-keyword">const</span> ProgressBarColors(
                      playedColor: AppColors.primaryColor,
                      handleColor: Colors.amberAccent,
                    ),
                    onReady: () {
                      <span class="hljs-comment">// Controller is ready</span>
                    },
                  ),
                ],
              ],
            ),
          ),
          actions: &lt;Widget&gt;[
            TextButton(
              onPressed: () {
                ytController?.dispose();
                Navigator.of(dialogContext).pop();
                <span class="hljs-keyword">if</span> (selectedFile != <span class="hljs-keyword">null</span>) {
                  removeFile();
                } <span class="hljs-keyword">else</span> {
                  removeText();
                }
              },
              child: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Close'</span>),
            ),
          ],
        );
      },
    );
  }

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> sendRequest(
      BuildContext context,
      File? selectedFile,
      GenerativeModel model,
      <span class="hljs-built_in">Function</span> removeFile,
      <span class="hljs-built_in">String</span> transcribedText,
      <span class="hljs-built_in">Function</span> removeText,
      ) <span class="hljs-keyword">async</span> {
    context.showLoader();
    toastInfo(msg: <span class="hljs-string">"Processing..."</span>, status: Status.success);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">if</span> (selectedFile != <span class="hljs-keyword">null</span>) {
        <span class="hljs-keyword">await</span> _sendImageToGemini(
          selectedFile,
          model,
          context,
          removeFile,
          removeText,
        );
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (transcribedText.isNotEmpty) {
        <span class="hljs-keyword">await</span> _sendAudioTextPrompt(
          model,
          context,
          transcribedText,
          selectedFile,
          removeFile,
          removeText,
        );
      }
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-keyword">if</span> (kDebugMode) {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">'Error sending request: <span class="hljs-subst">$e</span>'</span>);
      }
      toastInfo(msg: <span class="hljs-string">"Error sending request:<span class="hljs-subst">$e</span> "</span>, status: Status.error);
    } <span class="hljs-keyword">finally</span> {
      <span class="hljs-keyword">if</span> (context.mounted) {
        context.hideLoader();
      }
    }
  }
}
</code></pre>
<p>The <code>RecipeController</code> class is responsible for interacting with the Gemini AI model to generate recipes and then display these recipes to the user, complete with parsed YouTube video links and potentially extracted image URLs.</p>
<ul>
<li><p><code>_sendImageToGemini(File? selectedFile, GenerativeModel model, BuildContext context, Function removeFile, Function removeText)</code>:</p>
<ul>
<li><p>This <strong>private static method</strong> handles sending an image to the Gemini model.</p>
</li>
<li><p>It displays a "Processing..." toast message.</p>
</li>
<li><p>It reads the <code>selectedFile</code> (the image) as bytes.</p>
</li>
<li><p>It creates a <code>TextPart</code> from <code>AppStrings.AI_TEXT_PART</code> (our image-based AI prompt) and a <code>DataPart</code> for the image bytes.</p>
</li>
<li><p><code>model.generateContent([Content.multi([prompt, image])])</code>: This is where the magic happens! It sends both the text prompt and the image data to the Gemini model for generation.</p>
</li>
<li><p>Upon receiving a response, it calls <code>_displayRecipe</code> to show the generated recipe to the user.</p>
</li>
<li><p><code>context.mounted</code> check ensures the context is still valid before attempting UI updates.</p>
</li>
</ul>
</li>
<li><p><code>_sendAudioTextPrompt(GenerativeModel model, BuildContext context, String transcribedText, File? selectedFile, Function removeFile, Function removeText)</code>:</p>
<ul>
<li><p>This <strong>private static method</strong> handles sending transcribed audio text to the Gemini model.</p>
</li>
<li><p>It constructs a full prompt by concatenating <code>AppStrings.AI_AUDIO_PART</code> with the <code>transcribedText</code>.</p>
</li>
<li><p><code>model.generateContent([Content.text(prompt)])</code>: It sends only the text prompt to the Gemini model.</p>
</li>
<li><p>Similar to the image method, it calls <code>_displayRecipe</code> with the generated text.</p>
</li>
</ul>
</li>
<li><p><code>_displayRecipe(String? recipeText, BuildContext context, File? selectedFile, Function removeFile, Function removeText)</code>:</p>
<ul>
<li><p>This <strong>private static method</strong> is responsible for parsing the AI's response and displaying it in a modal dialog.</p>
</li>
<li><p><strong>Error handling</strong>: If <code>recipeText</code> is null or empty, it provides a default message.</p>
</li>
<li><p><strong>Extracting YouTube video URL</strong>: It uses a <code>RegExp</code> (<code>youtubeLineRegex</code>) to find a line in the <code>recipeText</code> that matches the "YouTube Video URL: https://..." pattern. If found, it extracts the full URL and then another <code>RegExp</code> (<code>ytIdRegex</code>) to get the YouTube video ID. The extracted video URL text is then removed from <code>workingRecipeText</code> to clean the displayed recipe.</p>
</li>
<li><p><strong>Extracting image URL</strong>: Similarly, it uses another <code>RegExp</code> (<code>imageLine</code>) to extract an image URL from the <code>recipeText</code>. The extracted image URL text is also removed.</p>
</li>
<li><p><strong>Debug printing</strong>: Prints the extracted URLs for debugging.</p>
</li>
<li><p><code>showDialog</code>: Presents an <code>AlertDialog</code> to the user.</p>
<ul>
<li><p><code>YoutubePlayerController</code>: If a <code>videoId</code> was extracted, it initializes a <code>YoutubePlayerController</code> from the <code>Youtubeer_flutter</code> package, configured with basic flags (for example, <code>autoPlay: false</code>).</p>
</li>
<li><p><strong>Recipe display</strong>:</p>
<ul>
<li><p>If an <code>selectedFile</code> (image taken by the user) is present, it displays that image.</p>
</li>
<li><p>Otherwise, if an <code>extractedImageUrl</code> was found in the AI's response, it uses <code>CachedNetworkImage</code> to display that image. This is particularly useful for text-based queries where Gemini might suggest an image.</p>
</li>
<li><p><code>MarkdownBody</code>: Uses <code>flutter_markdown</code> to render the <code>cleanedRecipeText</code> (after removing the YouTube and Image URLs) as Markdown, allowing for rich text formatting (for example, bolding, headings) directly from the AI's response.</p>
</li>
<li><p><code>YoutubePlayer</code>: If a <code>videoId</code> and <code>ytController</code> are available, it embeds the YouTube video player directly into the dialog, with customizable progress bar colors.</p>
</li>
</ul>
</li>
<li><p><strong>"Close" button</strong>: Disposes the <code>ytController</code> (important for resource management), pops the dialog, and calls either <code>removeFile()</code> or <code>removeText()</code> to clear the input fields based on what was used for the query.</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><code>sendRequest(BuildContext context, File? selectedFile, GenerativeModel model, Function removeFile, String transcribedText, Function removeText)</code>:</p>
<ul>
<li><p>This <strong>public static method</strong> is the entry point for sending requests to the Gemini model.</p>
</li>
<li><p><code>context.showLoader()</code>: Displays a loading overlay using our custom extension.</p>
</li>
<li><p><code>toastInfo(msg: "Processing...", status: Status.success)</code>: Shows a toast message.</p>
</li>
<li><p><strong>Conditional logic</strong>:</p>
<ul>
<li><p>If <code>selectedFile</code> is not null, it calls <code>_sendImageToGemini</code>.</p>
</li>
<li><p>Otherwise, if <code>transcribedText</code> is not empty, it calls <code>_sendAudioTextPrompt</code>.</p>
</li>
</ul>
</li>
<li><p><strong>Error handling</strong>: Uses a <code>try-catch</code> block to gracefully handle any errors during the AI request, logging them in debug mode and showing an error toast to the user.</p>
</li>
<li><p><code>finally</code> Block: Ensures <code>context.hideLoader()</code> is always called, regardless of success or error, to dismiss the loading indicator.</p>
</li>
</ul>
</li>
</ul>
<p>In essence, <code>RecipeController</code> orchestrates the entire process of sending user input (image or voice), communicating with the Gemini AI, parsing its intelligent response, and beautifully presenting it to the user with interactive elements like YouTube videos and relevant images.</p>
<h3 id="heading-3-the-presentation-folder">3. The <code>presentation</code> Folder</h3>
<p>This folder contains all the UI-related code.</p>
<h5 id="heading-screenshomescreendart"><code>screens/home_screen.dart</code>:</h5>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:gap/gap.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:google_generative_ai/google_generative_ai.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:iconsax/iconsax.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:image_cropper/image_cropper.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/core/extensions/format_to_mb.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/infrastructure/image_upload_controller.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/infrastructure/recipe_controller.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:speech_to_text/speech_recognition_result.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:speech_to_text/speech_to_text.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_colors.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_strings.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/enums/status.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../components/toast_info.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../widgets/glowing_microphone.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../widgets/image_previewer.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../widgets/query_text_box.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../widgets/upload_container.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomeScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> HomeScreen({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-meta">@override</span>
  State&lt;HomeScreen&gt; createState() =&gt; _HomeScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_HomeScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">HomeScreen</span>&gt; </span>{
  File? selectedFile;
  Completer? completer;
  <span class="hljs-built_in">String?</span> fileName;
  <span class="hljs-built_in">int?</span> fileSize;
  <span class="hljs-keyword">late</span> GenerativeModel _model;
  <span class="hljs-built_in">String</span> apiKey = <span class="hljs-string">""</span>; <span class="hljs-comment">// &lt;--- REPLACE WITH YOUR ACTUAL API KEY</span>
  <span class="hljs-keyword">final</span> TextEditingController _query = TextEditingController();
  <span class="hljs-keyword">final</span> SpeechToText _speechToText = SpeechToText();
  <span class="hljs-built_in">bool</span> _speechEnabled = <span class="hljs-keyword">false</span>;
  <span class="hljs-built_in">String</span> _lastWords = <span class="hljs-string">''</span>;
  <span class="hljs-built_in">bool</span> isRecording = <span class="hljs-keyword">false</span>;
  <span class="hljs-built_in">bool</span> isDoneRecording = <span class="hljs-keyword">false</span>;

  <span class="hljs-keyword">void</span> removeText() {
    setState(() {
      _query.clear();
      isDoneRecording = <span class="hljs-keyword">false</span>;
      _lastWords = <span class="hljs-string">""</span>;
    });
    _query.clear();
  }

  <span class="hljs-keyword">void</span> setKeyword(<span class="hljs-built_in">String</span> prompt) {
    <span class="hljs-keyword">if</span> (prompt.isEmpty) {
      toastInfo(msg: <span class="hljs-string">"You didn't say anything!"</span>, status: Status.error);
      setState(() {
        isDoneRecording = <span class="hljs-keyword">false</span>;
        isRecording = <span class="hljs-keyword">false</span>;
      });
      <span class="hljs-keyword">return</span>;
    }

    setState(() {
      _lastWords = <span class="hljs-string">""</span>;
      isRecording = <span class="hljs-keyword">false</span>;
      _query.text = prompt;
      isDoneRecording = <span class="hljs-keyword">true</span>;
    });
  }

  <span class="hljs-keyword">void</span> _initSpeech() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      _speechEnabled = <span class="hljs-keyword">await</span> _speechToText.initialize(
        onStatus: (status) =&gt; debugPrint(<span class="hljs-string">'Speech status: <span class="hljs-subst">$status</span>'</span>),
        onError: (error) =&gt; debugPrint(<span class="hljs-string">'Speech error: <span class="hljs-subst">$error</span>'</span>),
      );
      <span class="hljs-keyword">if</span> (!_speechEnabled) {
        toastInfo(
          msg: <span class="hljs-string">"Microphone permission not granted or speech not available."</span>,
          status: Status.error,
        );
      }
      setState(() {});
    } <span class="hljs-keyword">catch</span> (e) {
      debugPrint(<span class="hljs-string">"Speech initialization failed: <span class="hljs-subst">$e</span>"</span>);
    }
  }

  <span class="hljs-keyword">void</span> _startListening() <span class="hljs-keyword">async</span> {
    setState(() {
      isRecording = <span class="hljs-keyword">true</span>;
    });
    <span class="hljs-keyword">if</span> (!_speechEnabled) {
      toastInfo(msg: <span class="hljs-string">"Speech not initialized yet."</span>, status: Status.error);
      <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">await</span> _speechToText.listen(onResult: _onSpeechResult);
    setState(() {});
  }

  <span class="hljs-keyword">void</span> _stopListening() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> _speechToText.stop();
    setKeyword(_lastWords);
    setState(() {});
  }

  <span class="hljs-keyword">void</span> _onSpeechResult(SpeechRecognitionResult result) {
    setState(() {
      _lastWords = result.recognizedWords;
    });
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> Replace "YOUR_API_KEY" with your actual Gemini API Key</span>
    <span class="hljs-comment">// Refer to https://www.freecodecamp.org/news/how-to-secure-mobile-apis-in-flutter/ for API key security.</span>
    apiKey = <span class="hljs-string">"YOUR_API_KEY"</span>; <span class="hljs-comment">// Secure this!</span>
    _model = GenerativeModel(model: AppStrings.AI_MODEL, apiKey: apiKey);
    _initSpeech();
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _query.dispose();
    _speechToText.cancel(); <span class="hljs-comment">// Cancel listening to prevent resource leaks</span>
    <span class="hljs-keyword">super</span>.dispose();
  }

  <span class="hljs-keyword">void</span> assignCroppedImage(CroppedFile? croppedFile) {
    <span class="hljs-keyword">if</span> (croppedFile != <span class="hljs-keyword">null</span>) {
      setState(() {
        selectedFile = File(croppedFile.path);
      });
    }
  }

  <span class="hljs-keyword">void</span> setFile(File? pickedFile) {
    setState(() {
      selectedFile = pickedFile;
      fileName = pickedFile?.path.split(<span class="hljs-string">'/'</span>).last;
      fileSize = pickedFile?.lengthSync().formatToMegaByte();
    });
  }

  <span class="hljs-keyword">void</span> removeFile() {
    setState(() {
      selectedFile = <span class="hljs-keyword">null</span>;
      fileSize = <span class="hljs-keyword">null</span>;
    });
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    Size size = MediaQuery.sizeOf(context);

    <span class="hljs-keyword">return</span> Scaffold(
      floatingActionButton: selectedFile != <span class="hljs-keyword">null</span> || _query.text.isNotEmpty
          ? FloatingActionButton.extended(
        onPressed: () =&gt; RecipeController.sendRequest(
          context,
          selectedFile,
          _model,
          removeFile,
          _query.text,
          removeText,
        ),
        backgroundColor: AppColors.primaryColor,
        icon: <span class="hljs-keyword">const</span> Icon(Iconsax.send_1, color: Colors.white),
        label: <span class="hljs-keyword">const</span> Text(
          <span class="hljs-string">"Send Request"</span>,
          style: TextStyle(color: Colors.white),
        ),
      )
          : <span class="hljs-keyword">null</span>,
      body: Padding(
        padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">18.0</span>),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                AppStrings.APP_TITLE,
                textAlign: TextAlign.center,
                style: TextStyle(
                  color: Colors.black,
                  fontWeight: FontWeight.w500,
                  fontSize: <span class="hljs-number">16</span>,
                ),
              ),
              Text(
                AppStrings.APP_SUBTITLE,
                textAlign: TextAlign.center,
                style: TextStyle(
                  color: AppColors.grey,
                  fontSize: <span class="hljs-number">15</span>,
                  fontWeight: FontWeight.w300,
                ),
              ),
              <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">20</span>),
              <span class="hljs-keyword">if</span> (!isDoneRecording)
                !isRecording
                    ? selectedFile != <span class="hljs-keyword">null</span>
                    ? ImagePreviewer(
                  size: size,
                  pickedFile: selectedFile,
                  removeFile: removeFile,
                  context: context,
                  completer: completer,
                  setFile: setFile,
                  assignCroppedImage: assignCroppedImage,
                )
                    : GestureDetector(
                  onTap: () =&gt;
                      ImageUploadController.showFilePickerButtonSheet(
                        context,
                        completer,
                        setFile,
                        assignCroppedImage,
                      ),
                  child: UploadContainer(
                    title: <span class="hljs-string">'an image of a food or snack'</span>,
                    size: size,
                  ),
                )
                    : SizedBox.shrink(),
              <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">20</span>),

              <span class="hljs-keyword">if</span> (selectedFile == <span class="hljs-keyword">null</span>) ...[
                <span class="hljs-keyword">if</span> (!isDoneRecording) ...[
                  Text(
                    <span class="hljs-string">"or record your voice"</span>,
                    style: TextStyle(
                      color: AppColors.grey,
                      fontSize: <span class="hljs-number">16</span>,
                      fontWeight: FontWeight.w200,
                    ),
                  ),
                  Center(
                    child: GestureDetector(
                      onTap: () {
                        <span class="hljs-keyword">if</span> (!_speechEnabled) {
                          toastInfo(
                            msg: <span class="hljs-string">"Speech recognition not ready yet."</span>,
                            status: Status.error,
                          );
                          <span class="hljs-keyword">return</span>;
                        }
                        <span class="hljs-keyword">if</span> (_speechToText.isNotListening) {
                          _startListening();
                        } <span class="hljs-keyword">else</span> {
                          _stopListening();
                        }
                      },
                      child: GlowingMicButton(
                        isListening: !_speechToText.isNotListening,
                      ),
                    ),
                  ),
                  <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">10</span>),
                  Container(
                    padding: EdgeInsets.all(<span class="hljs-number">16</span>),
                    child: Text(
                      _speechToText.isListening
                          ? _lastWords
                          : _speechEnabled
                          ? <span class="hljs-string">'Tap the microphone to start listening...'</span>
                          : <span class="hljs-string">'Speech not available'</span>,
                    ),
                  ),
                  <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">10</span>),
                ],

                isDoneRecording
                    ? QueryTextBox(query: _query)
                    : SizedBox.shrink(),
              ],

              <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">20</span>),
              selectedFile != <span class="hljs-keyword">null</span> || _query.text.isNotEmpty
                  ? GestureDetector(
                onTap: () {
                  <span class="hljs-keyword">if</span> (selectedFile != <span class="hljs-keyword">null</span>) {
                    removeFile();
                  } <span class="hljs-keyword">else</span> {
                    removeText();
                  }
                },
                child: CircleAvatar(
                  backgroundColor: AppColors.primaryColor,
                  radius: <span class="hljs-number">30</span>,
                  child: Icon(Iconsax.close_circle, color: Colors.white),
                ),
              )
                  : SizedBox.shrink(),
            ],
          ),
        ),
      ),
    );
  }
}
</code></pre>
<p>The <code>HomeScreen</code> is the main user interface of our AI cooking assistant application. It manages the state for image selection, voice input, and triggers the AI recipe generation.</p>
<ul>
<li><p><strong>State variables</strong>:</p>
<ul>
<li><p><code>selectedFile</code>: Stores the <code>File</code> object of the image picked by the user.</p>
</li>
<li><p><code>completer</code>: A <code>Completer</code> object, often used for asynchronous operations to signal completion.</p>
</li>
<li><p><code>fileName</code>, <code>fileSize</code>: Store details about the selected image.</p>
</li>
<li><p><code>_model</code>: An instance of <code>GenerativeModel</code> from the <code>google_generative_ai</code> package, which is our interface to the Gemini API.</p>
</li>
<li><p><code>apiKey</code>: <strong>Crucially, this is where you'll insert your Gemini API key.</strong> Remember the security warning above!</p>
</li>
<li><p><code>_query</code>: A <code>TextEditingController</code> for the text input field, which will display the transcribed voice input.</p>
</li>
<li><p><code>_speechToText</code>: An instance of <code>SpeechToText</code> for handling voice recognition.</p>
</li>
<li><p><code>_speechEnabled</code>: A boolean indicating if speech recognition is initialized and available.</p>
</li>
<li><p><code>_lastWords</code>: Stores the most recently recognized words from speech.</p>
</li>
<li><p><code>isRecording</code>: A boolean to track if voice recording is active.</p>
</li>
<li><p><code>isDoneRecording</code>: A boolean to track if a voice recording has been completed and transcribed.</p>
</li>
</ul>
</li>
<li><p><strong>Methods</strong>:</p>
<ul>
<li><p><code>removeText()</code>: Clears the text input field (<code>_query</code>), resets <code>isDoneRecording</code> and <code>_lastWords</code> to clear any previous voice input.</p>
</li>
<li><p><code>setKeyword(String prompt)</code>: Sets the <code>_query</code> text to the <code>prompt</code> (transcribed voice), and updates <code>isRecording</code> and <code>isDoneRecording</code> states. It also provides a toast message if the prompt is empty.</p>
</li>
<li><p><code>_initSpeech()</code>: Initializes the <code>SpeechToText</code> plugin. It requests microphone permission and sets <code>_speechEnabled</code> based on the initialization success. If permissions are not granted, it shows an error toast.</p>
</li>
<li><p><code>_startListening()</code>: Starts the speech recognition listener. Sets <code>isRecording</code> to <code>true</code>.</p>
</li>
<li><p><code>_stopListening()</code>: Stops the speech recognition listener and calls <code>setKeyword</code> with the <code>_lastWords</code> to finalize the transcribed text.</p>
</li>
<li><p><code>_onSpeechResult(SpeechRecognitionResult result)</code>: Callback method for <code>SpeechToText</code> that updates <code>_lastWords</code> with the recognized words as speech recognition progresses.</p>
</li>
<li><p><code>initState()</code>: Called when the widget is inserted into the widget tree. It initializes the <code>_model</code> with the Gemini API key and model name, and calls <code>_initSpeech()</code> to set up voice recognition.</p>
</li>
<li><p><code>dispose()</code>: Called when the widget is removed from the widget tree. It disposes of the <code>_query</code> controller and cancels the <code>_speechToText</code> listener to prevent memory leaks.</p>
</li>
<li><p><code>assignCroppedImage(CroppedFile? croppedFile)</code>: Callback function passed to <code>ImageUploadController</code> to update <code>selectedFile</code> with the path of the newly cropped image.</p>
</li>
<li><p><code>setFile(File? pickedFile)</code>: Callback function passed to <code>ImageUploadController</code> to update <code>selectedFile</code> with the picked image, and also extracts its <code>fileName</code> and <code>fileSize</code> using our custom extension.</p>
</li>
<li><p><code>removeFile()</code>: Clears the <code>selectedFile</code> and <code>fileSize</code> states, effectively removing the displayed image.</p>
</li>
</ul>
</li>
<li><p><code>build(BuildContext context)</code> Method – UI Layout:</p>
<ul>
<li><p><code>FloatingActionButton.extended</code>: This button, labeled "Send Request," becomes visible only when an image (<code>selectedFile</code>) is chosen OR when there's text in the query box (<code>_query.text.isNotEmpty</code>). Tapping it triggers <code>RecipeController.sendRequest</code> with the relevant input.</p>
</li>
<li><p><strong>App title and subtitle</strong>: Displays the main headings using <code>AppStrings</code>.</p>
</li>
<li><p><strong>Image upload/preview section</strong>:</p>
<ul>
<li><p>If <code>!isDoneRecording</code> (meaning no voice input has been finalized) and <code>!isRecording</code> (not currently recording voice):</p>
<ul>
<li><p>If <code>selectedFile</code> is not null, it shows an <code>ImagePreviewer</code> widget to display the chosen image with an option to remove it.</p>
</li>
<li><p>Otherwise (no image selected), it displays an <code>UploadContainer</code> which acts as a tappable area to trigger <code>ImageUploadController.showFilePickerButtonSheet</code> for picking an image.</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Voice input section</strong>:</p>
<ul>
<li><p>This section (<code>if (selectedFile == null) ...</code>) only appears if no image is selected, providing an alternative input method.</p>
</li>
<li><p>If <code>!isDoneRecording</code>, it shows a "or record your voice" text and a <code>GlowingMicButton</code>.</p>
<ul>
<li><p>Tapping the <code>GlowingMicButton</code> toggles speech recognition (<code>_startListening</code> / <code>_stopListening</code>).</p>
</li>
<li><p>A <code>Text</code> widget displays the current speech recognition status or <code>_lastWords</code> as they are transcribed.</p>
</li>
</ul>
</li>
<li><p>If <code>isDoneRecording</code> (meaning voice input has been finalized), it shows a <code>QueryTextBox</code> which displays the transcribed text, allowing for review before sending the request.</p>
</li>
</ul>
</li>
<li><p><strong>Clear input button</strong>: A <code>CircleAvatar</code> with a close icon appears when either an image is selected or text is present in the query. Tapping it calls <code>removeFile()</code> or <code>removeText()</code> to clear the respective input.</p>
</li>
</ul>
</li>
</ul>
<p>Overall, <code>HomeScreen</code> intelligently adapts its UI based on user input (image or voice) and orchestrates the interaction with the <code>ImageUploadController</code> for image handling and the <code>RecipeController</code> for AI recipe generation.</p>
<h4 id="heading-the-components-folder">The <code>components</code> Folder</h4>
<p>This folder contains smaller, reusable UI elements.</p>
<h5 id="heading-toastinfodart"><code>toast_info.dart</code></h5>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:fluttertoast/fluttertoast.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_colors.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>; <span class="hljs-comment">// Import for MaterialColor/Colors</span>

<span class="hljs-keyword">void</span> toastInfo({
  <span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> msg,
  <span class="hljs-keyword">required</span> Status status,
}) {
  Fluttertoast.showToast(
    msg: msg,
    toastLength: Toast.LENGTH_SHORT,
    gravity: ToastGravity.BOTTOM,
    timeInSecForIosWeb: <span class="hljs-number">1</span>,
    backgroundColor: status == Status.success ? AppColors.primaryColor : AppColors.errorColor,
    textColor: Colors.white,
    fontSize: <span class="hljs-number">16.0</span>,
  );
}
</code></pre>
<p>The <code>toastInfo</code> function provides a convenient way to display brief, non-intrusive messages (toasts) to the user, typically for feedback like "success" or "error" messages.</p>
<p>It takes two required parameters:</p>
<ul>
<li><p><code>msg</code>: The message string to be displayed in the toast.</p>
</li>
<li><p><code>status</code>: An enum of type <code>Status</code> (<code>success</code> or <code>error</code>) which determines the background color of the toast.</p>
</li>
</ul>
<p><code>Fluttertoast.showToast(...)</code> is the core function from the <code>fluttertoast</code> package that displays the toast.</p>
<ul>
<li><p><code>toastLength</code>: Sets the duration the toast is visible (short).</p>
</li>
<li><p><code>gravity</code>: Positions the toast at the bottom of the screen.</p>
</li>
<li><p><code>timeInSecForIosWeb</code>: Duration for web/iOS.</p>
</li>
<li><p><code>backgroundColor</code>: Dynamically set to <code>AppColors.primaryColor</code> for success and <code>AppColors.errorColor</code> for errors, providing visual cues to the user.</p>
</li>
<li><p><code>textColor</code>: Sets the text color to white.</p>
</li>
<li><p><code>fontSize</code>: Sets the font size of the toast message.</p>
</li>
</ul>
<p>This function centralizes toast message display, ensuring consistency in appearance and behavior throughout the app.</p>
<h4 id="heading-the-widgets-folder">The <code>widgets</code> Folder</h4>
<p>The application's user interface is constructed using a series of well-defined, reusable Flutter widgets. Each widget serves a specific purpose, contributing to the overall functionality and aesthetic of Snap2Chef.</p>
<p>1. <code>glowing_microphone.dart</code>:</p>
<p>This widget creates an animated microphone button that visually indicates when the application is actively listening for speech input.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:iconsax/iconsax.dart'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_colors.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GlowingMicButton</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool</span> isListening;

  <span class="hljs-keyword">const</span> GlowingMicButton({<span class="hljs-keyword">super</span>.key, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.isListening});

  <span class="hljs-meta">@override</span>
  State&lt;GlowingMicButton&gt; createState() =&gt; _GlowingMicButtonState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_GlowingMicButtonState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">GlowingMicButton</span>&gt;
    <span class="hljs-title">with</span> <span class="hljs-title">SingleTickerProviderStateMixin</span> </span>{
  <span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> AnimationController _controller;
  <span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> Animation&lt;<span class="hljs-built_in">double</span>&gt; _animation;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    _controller = AnimationController(
      vsync: <span class="hljs-keyword">this</span>,
      duration: <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">2</span>),
    );

    _animation = Tween&lt;<span class="hljs-built_in">double</span>&gt;(begin: <span class="hljs-number">0.0</span>, end: <span class="hljs-number">25.0</span>).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeOut),
    );

    <span class="hljs-keyword">if</span> (widget.isListening) {
      _controller.repeat(reverse: <span class="hljs-keyword">true</span>);
    }
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> didUpdateWidget(<span class="hljs-keyword">covariant</span> GlowingMicButton oldWidget) {
    <span class="hljs-keyword">super</span>.didUpdateWidget(oldWidget);

    <span class="hljs-keyword">if</span> (widget.isListening &amp;&amp; !_controller.isAnimating) {
      _controller.repeat(reverse: <span class="hljs-keyword">true</span>);
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (!widget.isListening &amp;&amp; _controller.isAnimating) {
      _controller.stop();
    }
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _controller.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> SizedBox(
      width: <span class="hljs-number">100</span>, <span class="hljs-comment">// Enough space for the full glow</span>
      height: <span class="hljs-number">100</span>,
      child: Stack(
        alignment: Alignment.center,
        children: [
          <span class="hljs-keyword">if</span> (widget.isListening)
            AnimatedBuilder(
              animation: _animation,
              builder: (_, __) {
                <span class="hljs-keyword">return</span> Container(
                  width: <span class="hljs-number">60</span> + _animation.value,
                  height: <span class="hljs-number">60</span> + _animation.value,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: AppColors.primaryColor.withOpacity(<span class="hljs-number">0.15</span>),
                  ),
                );
              },
            ),
          CircleAvatar(
            backgroundColor: AppColors.primaryColor,
            radius: <span class="hljs-number">30</span>,
            child: Icon(
              widget.isListening ? Iconsax.stop_circle : Iconsax.microphone,
              color: Colors.white,
            ),
          ),
        ],
      ),
    );
  }
}
</code></pre>
<ul>
<li><code>GlowingMicButton</code> (StatefulWidget): This is a <code>StatefulWidget</code> because it needs to manage its own animation state. It takes a <code>final bool isListening</code> property, which dictates whether the microphone should display a glowing animation or remain static.</li>
</ul>
<ul>
<li><p><code>_GlowingMicButtonState</code> (State with <code>SingleTickerProviderStateMixin</code>):</p>
<ul>
<li><p><code>SingleTickerProviderStateMixin</code>: This mixin is crucial for providing a <code>Ticker</code> to an <code>AnimationController</code>. A <code>Ticker</code> essentially drives the animation forward, linking it to the frame callbacks, ensuring smooth animation performance.</p>
</li>
<li><p><code>_controller</code> (AnimationController): Manages the animation. It's initialized with <code>vsync: this</code> (from <code>SingleTickerProviderStateMixin</code>) and a <code>duration</code> of 2 seconds.</p>
</li>
<li><p><code>_animation</code> (Animation&lt;double&gt;): Defines the range of values the animation will produce. Here, a <code>Tween&lt;double&gt;(begin: 0.0, end: 25.0)</code> is used with a <code>CurvedAnimation</code> (specifically <code>Curves.easeOut</code>) to create a smooth, decelerating effect as the glow expands.</p>
</li>
<li><p><code>initState()</code>: When the widget is first created, the <code>AnimationController</code> and <code>Animation</code> are initialized. If <code>isListening</code> is initially <code>true</code>, the animation is set to <code>repeat(reverse: true)</code> to make the glow pulse in and out continuously.</p>
</li>
<li><p><code>didUpdateWidget()</code>: This lifecycle method is called when the widget's configuration (its properties) changes. It checks if <code>isListening</code> has changed and starts or stops the animation accordingly. This ensures the animation dynamically responds to changes in the <code>isListening</code> state from its parent.</p>
</li>
<li><p><code>dispose()</code>: Crucially, the <code>_controller.dispose()</code> method is called here to release the resources held by the animation controller when the widget is removed from the widget tree, preventing memory leaks.</p>
</li>
</ul>
</li>
<li><p><code>build()</code> Method:</p>
<ul>
<li><p><code>SizedBox</code>: Provides a fixed size (100x100) for the button, ensuring enough space for the glowing effect.</p>
</li>
<li><p><code>Stack</code>: Allows layering widgets on top of each other.</p>
<ul>
<li><p><code>if (widget.isListening) AnimatedBuilder(...)</code>: This conditional renders the glowing effect <em>only</em> when <code>isListening</code> is <code>true</code>.</p>
<ul>
<li><p><code>AnimatedBuilder</code>: Rebuilds its child whenever the <code>_animation</code> changes value.</p>
</li>
<li><p>Inside <code>AnimatedBuilder</code>, a <code>Container</code> is used to create the circular glow. Its <code>width</code> and <code>height</code> are dynamically increased by <code>_animation.value</code>, creating the expanding effect. The <code>color</code> is <code>AppColors.primaryColor</code> with <code>0.15</code> opacity, giving it a subtle glow.</p>
</li>
</ul>
</li>
<li><p><code>CircleAvatar</code>: This is the main microphone button.</p>
<ul>
<li><p><code>backgroundColor</code> is <code>AppColors.primaryColor</code>.</p>
</li>
<li><p><code>radius</code> is <code>30</code>.</p>
</li>
<li><p>The <code>child</code> is an <code>Icon</code> from the <code>Iconsax</code> package, dynamically changing between <code>Iconsax.stop_circle</code> (when listening) and <code>Iconsax.microphone</code> (when not listening). The icon color is white.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>2. <code>image_picker_component.dart</code></p>
<p>This widget provides a reusable <code>ListTile</code> interface for users to select images from either the camera or the gallery.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/cupertino.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/infrastructure/image_upload_controller.dart'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_colors.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/enums/record_source.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImagePickerTile</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> ImagePickerTile({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.subtitle,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.icon,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.recordSource,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.completer,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.context,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.setFile,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.assignCroppedImage,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> subtitle;
  <span class="hljs-keyword">final</span> IconData icon;
  <span class="hljs-keyword">final</span> RecordSource recordSource;
  <span class="hljs-keyword">final</span> Completer? completer;
  <span class="hljs-keyword">final</span> BuildContext context;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Function</span> setFile;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Function</span> assignCroppedImage;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> ListTile(
      leading: CircleAvatar(
        backgroundColor: AppColors.litePrimary,
        child: Padding(
          padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">3.0</span>),
          child: Center(
            child: Icon(icon, color: AppColors.primaryColor, size: <span class="hljs-number">20</span>),
          ),
        ),
      ),
      title: Text(title, style: <span class="hljs-keyword">const</span> TextStyle(color: Colors.black)),
      subtitle: Text(
        subtitle,
        style: <span class="hljs-keyword">const</span> TextStyle(fontSize: <span class="hljs-number">14</span>, color: Colors.grey),
      ),
      trailing: <span class="hljs-keyword">const</span> Icon(
        CupertinoIcons.chevron_right,
        size: <span class="hljs-number">20</span>,
        color: Color(<span class="hljs-number">0xffE4E4E4</span>),
      ),
      onTap: () {
        ImageUploadController.imagePicker(
          recordSource,
          completer,
          context,
          setFile,
          assignCroppedImage,
        );
      },
    );
  }
}
</code></pre>
<ul>
<li><p><code>ImagePickerTile</code> (StatelessWidget): This is a <code>StatelessWidget</code> because it simply renders content based on its immutable properties and triggers an external function (<code>ImageUploadController.imagePicker</code>) when tapped.</p>
</li>
<li><p><strong>Properties:</strong> It takes several <code>final</code> properties to make it highly customizable:</p>
<ul>
<li><p><code>title</code> and <code>subtitle</code>: Text for the main and secondary lines of the list tile.</p>
</li>
<li><p><code>icon</code>: The <code>IconData</code> to display as the leading icon.</p>
</li>
<li><p><code>recordSource</code>: An enum (<code>RecordSource</code>) likely indicating if the image should be picked from the camera or gallery.</p>
</li>
<li><p><code>completer</code>: A <code>Completer</code> object, often used for asynchronous operations to signal when a task is complete.</p>
</li>
<li><p><code>context</code>: The <code>BuildContext</code> to allow the <code>ImageUploadController</code> to show dialogs or navigate.</p>
</li>
<li><p><code>setFile</code>: A <code>Function</code> callback to update the selected image file in the parent widget.</p>
</li>
<li><p><code>assignCroppedImage</code>: A <code>Function</code> callback to handle the result of any image cropping operation.</p>
</li>
</ul>
</li>
<li><p><code>build()</code> Method:</p>
<ul>
<li><p><code>ListTile</code>: A standard Flutter widget used to arrange elements in a single row.</p>
<ul>
<li><p><code>leading</code>: Displays a <code>CircleAvatar</code> with a light primary background color, containing the specified <code>icon</code> in the primary color. This creates a visually appealing icon button on the left.</p>
</li>
<li><p><code>title</code>: Displays the <code>title</code> text in black.</p>
</li>
<li><p><code>subtitle</code>: Displays the <code>subtitle</code> text in grey with a font size of 14, providing additional descriptive information.</p>
</li>
<li><p><code>trailing</code>: Shows a <code>CupertinoIcons.chevron_right</code> (right arrow) icon, common for indicating navigation or actionable items in a list.</p>
</li>
<li><p><code>onTap</code>: This is the primary interaction point. When the <code>ListTile</code> is tapped, it calls the static method <code>ImageUploadController.imagePicker</code>, passing all the necessary parameters. This centralizes the image picking logic within <code>ImageUploadController</code>, making the <code>ImagePickerTile</code> purely a UI component.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>3. <code>image_previewer.dart</code></p>
<p>This widget is responsible for displaying a previously picked image and offering options to 'Edit' (re-pick) or 'Remove' the image.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:iconsax/iconsax.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/infrastructure/image_upload_controller.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImagePreviewer</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> ImagePreviewer({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.size,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.pickedFile,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.removeFile,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.context,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.completer,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.setFile,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.assignCroppedImage,
  });

  <span class="hljs-keyword">final</span> Size size;
  <span class="hljs-keyword">final</span> File? pickedFile;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Function</span> removeFile;
  <span class="hljs-keyword">final</span> BuildContext context;
  <span class="hljs-keyword">final</span> Completer? completer;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Function</span> setFile;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Function</span> assignCroppedImage;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Container(
      height: size.height * <span class="hljs-number">0.13</span>,
      width: <span class="hljs-built_in">double</span>.infinity,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(<span class="hljs-number">7</span>),
        <span class="hljs-comment">// border: Border.all(</span>
        <span class="hljs-comment">//   color: AppColors.borderColor,</span>
        <span class="hljs-comment">// ),</span>
        image: DecorationImage(
          image: FileImage(
            File(pickedFile!.path),
          ),
          fit: BoxFit.cover,
        ),
      ),
      child: Stack(
        children: [
          Container(
            decoration: BoxDecoration(
              color: Colors.black.withOpacity(<span class="hljs-number">0.3</span>),
              borderRadius: BorderRadius.circular(<span class="hljs-number">7</span>),
            ),
          ),
          <span class="hljs-comment">// Centered content</span>
          Center(
            child: Wrap(
              crossAxisAlignment: WrapCrossAlignment.center,
              spacing: <span class="hljs-number">20</span>,
              children: [
                GestureDetector(
                  onTap: () {
                    ImageUploadController.showFilePickerButtonSheet(context,completer,setFile,assignCroppedImage);
                  },
                  child: Column(
                    children: [
                      Icon(
                        Iconsax.edit_2,
                        size: <span class="hljs-number">20</span>,
                        color: Colors.white,
                      ),
                      <span class="hljs-keyword">const</span> Text(
                        <span class="hljs-string">'Edit'</span>,
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: <span class="hljs-number">15</span>,
                        ),
                      )
                    ],
                  ),
                ),
                GestureDetector(
                  onTap: () {
                    removeFile();
                  },
                  child: Column(
                    children: [
                      Icon(
                        Iconsax.note_remove,
                        color: Colors.white,
                        size: <span class="hljs-number">20</span>,
                      ),
                      <span class="hljs-keyword">const</span> Text(
                        <span class="hljs-string">'Remove'</span>,
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: <span class="hljs-number">15</span>,
                        ),
                      )
                    ],
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
</code></pre>
<ul>
<li><code>ImagePreviewer</code> (StatelessWidget): Similar to <code>ImagePickerTile</code>, this is a <code>StatelessWidget</code> that displays content and triggers callbacks.</li>
</ul>
<ul>
<li><p><strong>Properties:</strong></p>
<ul>
<li><p><code>size</code>: The <code>Size</code> of the parent widget, used to calculate the <code>height</code> of the preview container proportionally.</p>
</li>
<li><p><code>pickedFile</code>: A <code>File?</code> representing the image file to be displayed. It's nullable, implying that this widget might only show if a file has been picked.</p>
</li>
<li><p><code>removeFile</code>: A <code>Function</code> callback to handle the removal of the currently displayed image.</p>
</li>
<li><p><code>context</code>, <code>completer</code>, <code>setFile</code>, <code>assignCroppedImage</code>: These are passed through to the <code>ImageUploadController</code> when the 'Edit' action is triggered, similar to the <code>ImagePickerTile</code>.</p>
</li>
</ul>
</li>
<li><p><code>build()</code> Method:</p>
<ul>
<li><p><code>Container</code>: The primary container for the image preview.</p>
<ul>
<li><p><code>height</code>: Set to 13% of the screen height, providing a responsive size.</p>
</li>
<li><p><code>width</code>: <code>double.infinity</code> to take full available width.</p>
</li>
<li><p><code>decoration</code>:</p>
<ul>
<li><p><code>borderRadius</code>: Applies rounded corners to the container.</p>
</li>
<li><p><code>image: DecorationImage(...)</code>: This is where the magic happens. It displays the <code>pickedFile</code> as a background image for the container.</p>
<ul>
<li><p><code>FileImage(File(pickedFile!.path))</code>: Creates an image provider from the local file path. The <code>!</code> (null assertion operator) implies <code>pickedFile</code> is expected to be non-null when this widget is displayed.</p>
</li>
<li><p><code>fit: BoxFit.cover</code>: Ensures the image covers the entire container, potentially cropping parts of it.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p><code>Stack</code>: Layers content on top of the image.</p>
<ul>
<li><p><code>Container</code> (Overlay): A semi-transparent black <code>Container</code> is placed on top of the image (<code>Colors.black.withOpacity(0.3)</code>) to create a darkened overlay. This improves the readability of the white text and icons placed over the image.</p>
</li>
<li><p><code>Center</code>: Centers the action buttons horizontally and vertically within the overlay.</p>
</li>
<li><p><code>Wrap</code>: Arranges the 'Edit' and 'Remove' buttons horizontally with a <code>spacing</code> of 20. <code>WrapCrossAlignment.center</code> aligns them vertically within the <code>Wrap</code>.</p>
</li>
<li><p><code>GestureDetector</code> (for 'Edit'):</p>
<ul>
<li><p><code>onTap</code>: Calls <code>ImageUploadController.showFilePickerButtonSheet</code>, allowing the user to re-select or change the image. This method likely presents a bottom sheet with options to pick from the camera or gallery, similar to how the initial image picking works.</p>
</li>
<li><p>Its child is a <code>Column</code> containing an <code>Iconsax.edit_2</code> icon and an 'Edit' text, both in white.</p>
</li>
</ul>
</li>
<li><p><code>GestureDetector</code> (for 'Remove'):</p>
<ul>
<li><p><code>onTap</code>: Calls the <code>removeFile()</code> callback, which would typically clear the selected <code>pickedFile</code> in the parent state, causing this previewer to disappear or revert to an upload state.</p>
</li>
<li><p>Its child is a <code>Column</code> containing an <code>Iconsax.note_remove</code> icon and a 'Remove' text, both in white.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>4. <code>query_text_box.dart</code></p>
<p>This widget provides a styled <code>TextFormField</code> for multi-line text input, typically used for user queries or notes.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_colors.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">QueryTextBox</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> QueryTextBox({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> TextEditingController query,
  }) : _query = query;

  <span class="hljs-keyword">final</span> TextEditingController _query;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> TextFormField(
      controller: _query,
      maxLines: <span class="hljs-number">4</span>,
      autofocus: <span class="hljs-keyword">true</span>,
      decoration: InputDecoration(
        hintStyle: TextStyle(color: AppColors.lighterGrey),
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(<span class="hljs-number">12.0</span>),
          borderSide: BorderSide(color: Colors.grey.shade400),
        ),
        focusedBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(<span class="hljs-number">12.0</span>),
          borderSide: <span class="hljs-keyword">const</span> BorderSide(
            color: AppColors.primaryColor,
            width: <span class="hljs-number">2.0</span>,
          ),
        ),
        enabledBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(<span class="hljs-number">12.0</span>),
          borderSide: BorderSide(color: Colors.grey.shade300),
        ),
        contentPadding: <span class="hljs-keyword">const</span> EdgeInsets.symmetric(
          vertical: <span class="hljs-number">12.0</span>,
          horizontal: <span class="hljs-number">16.0</span>,
        ),
      ),
      style: <span class="hljs-keyword">const</span> TextStyle(
        fontSize: <span class="hljs-number">14.0</span>,
        color: Colors.black,
      ),
      keyboardType: TextInputType.multiline,
      textInputAction: TextInputAction.newline,
    );
  }
}
</code></pre>
<ul>
<li><code>QueryTextBox</code> (StatelessWidget): A <code>StatelessWidget</code> that renders a text input field. It takes a <code>TextEditingController</code> as a required parameter, allowing external control over the text field's content.</li>
</ul>
<ul>
<li><p><strong>Properties:</strong></p>
<ul>
<li><code>_query</code> (TextEditingController): The controller linked to the <code>TextFormField</code>. This allows retrieving the text, setting initial text, and listening for changes.</li>
</ul>
</li>
<li><p><code>build()</code> Method:</p>
<ul>
<li><p><code>TextFormField</code>: The core input widget.</p>
<ul>
<li><p><code>controller: _query</code>: Binds the <code>TextEditingController</code> to this field.</p>
</li>
<li><p><code>maxLines: 4</code>: Allows the text field to expand up to 4 lines before becoming scrollable.</p>
</li>
<li><p><code>autofocus: true</code>: Automatically focuses the text field when the screen loads, bringing up the keyboard.</p>
</li>
<li><p><code>decoration: InputDecoration(...)</code>: Defines the visual styling of the input field.</p>
<ul>
<li><p><code>hintStyle</code>: Sets the color of the hint text to <code>AppColors.lighterGrey</code>.</p>
</li>
<li><p><code>border</code>: Defines the default border when the field is not focused or enabled, with rounded corners and a light grey border.</p>
</li>
<li><p><code>focusedBorder</code>: Defines the border style when the field is actively focused by the user. It uses <code>AppColors.primaryColor</code> with a wider stroke (<code>width: 2.0</code>) to provide a clear visual indicator of focus.</p>
</li>
<li><p><code>enabledBorder</code>: Defines the border style when the field is enabled but not focused, using a slightly darker grey.</p>
</li>
<li><p><code>contentPadding</code>: Adds internal padding within the text field for better spacing of the text.</p>
</li>
</ul>
</li>
<li><p><code>style</code>: Sets the font size to 14.0 and color to black for the entered text.</p>
</li>
<li><p><code>keyboardType: TextInputType.multiline</code>: Configures the keyboard to be suitable for multi-line text input, often providing a "return" key that creates a new line.</p>
</li>
<li><p><code>textInputAction: TextInputAction.newline</code>: Specifies that pressing the "Done" or "Enter" key on the keyboard should insert a new line.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>5. <code>upload_container.dart</code></p>
<p>This widget creates a visually distinct "dotted border" container, typically used as a tappable area to trigger file upload or selection actions.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:dotted_border/dotted_border.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:gap/gap.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:iconsax/iconsax.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_colors.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UploadContainer</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> UploadContainer({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.size,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title,
  });

  <span class="hljs-keyword">final</span> Size size;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> DottedBorder(
      color: AppColors.primaryColor,
      radius: <span class="hljs-keyword">const</span> Radius.circular(<span class="hljs-number">15</span>),
      borderType: BorderType.RRect,
      strokeWidth: <span class="hljs-number">1</span>,
      child: SizedBox(
        height: size.height * <span class="hljs-number">0.13</span>,
        width: <span class="hljs-built_in">double</span>.infinity,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(
              height: <span class="hljs-number">70</span>,
              width: <span class="hljs-number">60</span>,
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: AppColors.litePrimary,
              ),
              child: Padding(
                padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">13.0</span>),
                child: Icon(
                  Iconsax.document_upload,
                  color: AppColors.primaryColor,
                ),
              ),
            ),
            <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">5</span>),
            RichText(
              text: TextSpan(
                text: <span class="hljs-string">'Click to select '</span>,
                style: TextStyle(
                  color: AppColors.primaryColor,
                ),
                children: [
                  TextSpan(
                    text: title,
                    style: TextStyle(
                      color: Color(<span class="hljs-number">0xff555555</span>),
                    ),
                  )
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
</code></pre>
<ul>
<li><code>UploadContainer</code> (StatelessWidget): A <code>StatelessWidget</code> primarily for visual presentation, indicating an upload zone.</li>
</ul>
<ul>
<li><p><strong>Properties:</strong></p>
<ul>
<li><p><code>size</code>: The <code>Size</code> of the parent, used to determine the container's height proportionally.</p>
</li>
<li><p><code>title</code>: A <code>String</code> to be displayed as part of the "Click to select [title]" message.</p>
</li>
</ul>
</li>
<li><p><code>build()</code> Method:</p>
<ul>
<li><p><code>DottedBorder</code>: This package provides the visual dotted border effect.</p>
<ul>
<li><p><code>color: AppColors.primaryColor</code>: The color of the dotted line.</p>
</li>
<li><p><code>radius: const Radius.circular(15)</code>: Applies rounded corners to the dotted border.</p>
</li>
<li><p><code>borderType: BorderType.RRect</code>: Specifies that the border should follow a rounded rectangle shape.</p>
</li>
<li><p><code>strokeWidth: 1</code>: Sets the thickness of the dotted line.</p>
</li>
</ul>
</li>
<li><p><code>SizedBox</code>: Defines the internal dimensions of the area within the dotted border, taking up 13% of the screen height and full width.</p>
</li>
<li><p><code>Column</code>: Arranges the icon and text vertically, centered within the <code>SizedBox</code>.</p>
<ul>
<li><p><code>Container</code> (Icon background): A circular container with <code>AppColors.litePrimary</code> background holds the upload icon.</p>
<ul>
<li><code>Iconsax.document_upload</code>: The icon signifying an upload action, colored with <code>AppColors.primaryColor</code>.</li>
</ul>
</li>
<li><p><code>Gap(5)</code>: From the <code>gap</code> package, this provides a small vertical space (5 pixels) between the icon and the text.</p>
</li>
<li><p><code>RichText</code>: Allows for different styles within a single text block.</p>
<ul>
<li><p><code>TextSpan(text: 'Click to select ', ...)</code>: The first part of the message, styled with <code>AppColors.primaryColor</code>.</p>
</li>
<li><p><code>children: [TextSpan(text: title, ...)]</code>: The second part of the message, which is the <code>title</code> property passed to the widget, styled in a darker grey. This structure allows "Click to select " to be consistently styled while the <code>title</code> (for example, "image", "document") can have a different appearance.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="heading-summary-of-code-implementation">Summary of Code Implementation</h3>
<p>We've covered a significant amount of ground in this part of the article, transforming our basic Flutter application into a powerful AI-powered recipe guide. We started by setting up the core UI, then delved into integrating the <code>google_generative_ai</code> package to communicate with Google's Gemini models for both image and voice input.</p>
<p>We implemented robust logic for:</p>
<ul>
<li><p><strong>Image input:</strong> Capturing images from the camera or gallery, cropping them, and sending them to the <code>gemini</code> model.</p>
</li>
<li><p><strong>Voice input:</strong> Recording audio and preparing the groundwork for transcription before sending text to the <code>gemini</code> model.</p>
</li>
<li><p><strong>Dynamic content display:</strong> Skillfully parsing the AI's response to extract and present not just the recipe text, but also embedding YouTube instructional videos and even relevant images, all within a beautifully formatted dialog using <code>flutter_markdown</code> and <code>cached_network_image</code>. We also ensured proper lifecycle management for our media players.</p>
</li>
</ul>
<p>This highlights how easily you can leverage advanced AI capabilities like multimodal understanding and natural language generation within your Flutter applications. By building on these concepts, you can create truly interactive and intelligent user experiences.</p>
<p>Now that we have the core logic in place for capturing input, communicating with the AI, and displaying its rich responses, we need to ensure that our application can actually access the necessary device features.</p>
<h2 id="heading-permissions-ensuring-app-functionality-and-user-privacy"><strong>Permissions: Ensuring App Functionality and User Privacy</strong></h2>
<p>For a Flutter application to interact with system features like the camera, microphone, or file storage, it must declare specific permissions in both its Android and iOS manifests. These declarations inform the operating system about the app's requirements and, for sensitive permissions, prompt the user for consent at runtime.</p>
<h3 id="heading-android1-permissions-in-androidappsrcmainandroidmanifestxml"><strong>Android<sup>1</sup> Permissions (in</strong> <code>android/app/src/main/AndroidManifest.xml</code>)</h3>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">manifest</span> <span class="hljs-attr">xmlns:android</span>=<span class="hljs-string">"http://schemas.android.com/apk/res/android"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.RECORD_AUDIO"</span>/&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.CAMERA"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.INTERNET"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.READ_EXTERNAL_STORAGE"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">manifest</span>&gt;</span>
</code></pre>
<p>Here’s what’s going on:</p>
<ul>
<li><p><code>&lt;uses-permission android:name="android.permission.RECORD_AUDIO"/&gt;</code>: This permission is necessary for the application to access the device's microphone and record audio. It's crucial for any speech recognition or voice input features, like the <code>GlowingMicButton</code> implies.</p>
</li>
<li><p><code>&lt;uses-permission android:name="android.permission.CAMERA" /&gt;</code>: Grants the application access to the device's camera. This is essential for features that allow users to take photos, such as those enabled by <code>ImagePickerTile</code> or <code>ImagePreviewer</code>.</p>
</li>
<li><p><code>&lt;uses-permission android:name="android.permission.INTERNET" /&gt;</code>: This is a fundamental permission required for almost any modern application that connects to the internet. It allows the app to send and receive data from web services, like interacting with the Gemini API, Firebase, or Vertex AI.</p>
</li>
<li><p><code>&lt;uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /&gt;</code>: Allows the application to read files from the device's shared external storage (for example, photos saved in the gallery). This is necessary when picking existing images from the gallery. For newer Android versions (Android 10+), scoped storage might change how this works, but for reading user-selected media, this declaration is still relevant. For writing to external storage, <code>WRITE_EXTERNAL_STORAGE</code> would also be needed.</p>
</li>
</ul>
<h3 id="heading-ios-permissions-in-iosrunnerinfoplist"><strong>iOS Permissions (in</strong> <code>ios/Runner/Info.plist</code><strong>)</strong></h3>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">plist</span> <span class="hljs-meta-keyword">PUBLIC</span> <span class="hljs-meta-string">"-//Apple//DTD PLIST 1.0//EN"</span> <span class="hljs-meta-string">"http://www.apple.com/DTDs/PropertyList-1.0.dtd"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">plist</span> <span class="hljs-attr">version</span>=<span class="hljs-string">"1.0"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">dict</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>io.flutter.embedded_views_preview<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">true</span>/&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSSpeechRecognitionUsageDescription<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>We need access to recognize your speech.<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSCameraUsageDescription<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>This app needs access to the camera to capture photos and videos.<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSMicrophoneUsageDescription<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>This app needs access to the microphone for audio recording.<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSPhotoLibraryUsageDescription<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>This app needs access to your photo library.<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSPhotoLibraryAddUsageDescription<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>This app needs permission to save photos to your photo library.<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSAppTransportSecurity<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dict</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSAllowsArbitraryLoads<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">true</span>/&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dict</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">dict</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">plist</span>&gt;</span>
</code></pre>
<p>Here’s what’s going on:</p>
<p>iOS permissions are declared in the <code>Info.plist</code> file using specific keys (<code>NS...UsageDescription</code>) and require a user-facing string explaining why the permission is needed. This string is displayed to the user when the app requests the permission.</p>
<ul>
<li><p><code>&lt;key&gt;io.flutter.embedded_views_preview&lt;/key&gt;&lt;true/&gt;</code>: This key is often added when using Flutter plugins that integrate native UI components (for example, camera previews, webviews). It enables a preview of embedded native views during development.</p>
</li>
<li><p><code>&lt;key&gt;NSSpeechRecognitionUsageDescription&lt;/key&gt;&lt;string&gt;We need access to recognize your speech.&lt;/string&gt;</code>: This is the privacy description for speech recognition services (for example, Apple's built-in speech recognizer). It's crucial for features like voice input to work.</p>
</li>
<li><p><code>&lt;key&gt;NSCameraUsageDescription&lt;/key&gt;&lt;string&gt;This app needs access to the camera to capture photos and videos.&lt;/string&gt;</code>: The privacy description for camera access. This is required for capturing images via the camera, as used in the image picking functionality.</p>
</li>
<li><p><code>&lt;key&gt;NSMicrophoneUsageDescription&lt;/key&gt;&lt;string&gt;This app needs access to the microphone for audio recording.&lt;/string&gt;</code>: The privacy description for microphone access. Necessary for recording audio for speech input.</p>
</li>
<li><p><code>&lt;key&gt;NSPhotoLibraryUsageDescription&lt;/key&gt;&lt;string&gt;This app needs access to your photo library.&lt;/string&gt;</code>: The privacy description for reading from the user's photo library. This is required when picking existing images or videos from the gallery.</p>
</li>
<li><p><code>&lt;key&gt;NSPhotoLibraryAddUsageDescription&lt;/key&gt;&lt;string&gt;This app needs permission to save photos to your photo library.&lt;/string&gt;</code>: The privacy description for writing to the user's photo library. This would be needed if the app captures photos/videos and saves them directly to the device's gallery.</p>
</li>
<li><p><code>&lt;key&gt;NSAppTransportSecurity&lt;/key&gt;&lt;dict&gt;&lt;key&gt;NSAllowsArbitraryLoads&lt;/key&gt;&lt;true/&gt;&lt;/dict&gt;</code>: This section relates to Apple's App Transport Security (ATS). By default, ATS enforces secure connections (HTTPS). Setting <code>NSAllowsArbitraryLoads</code> to <code>true</code> (as shown here) <em>disables</em> this enforcement, allowing the app to make insecure HTTP connections. While useful during development or for interacting with specific legacy APIs, it's generally <strong>not recommended for production apps</strong> due to security implications. For production, you should ideally configure specific exceptions or ensure all network requests use HTTPS.</p>
</li>
</ul>
<h2 id="heading-assets-managing-application-resources"><strong>Assets: Managing Application Resources</strong></h2>
<p>Assets are files bundled with your application and are accessible at runtime. This typically includes images, fonts, audio files, and more.</p>
<p>In this application, we have an <code>assets</code> folder, and inside it, an <code>images</code> subfolder.</p>
<pre><code class="lang-dart">assets/
└── images/
    ├── placeholder.png
    └── app_logo.png
</code></pre>
<ul>
<li><p><code>placeholder.png</code>: This image is typically used as a temporary visual cue when actual content (like an image being loaded or picked) is not yet available. It provides a better user experience than a blank space.</p>
</li>
<li><p><code>app_logo.png</code>: This is the primary logo of the application. It's used for various purposes, including the app icon and the splash screen.</p>
</li>
</ul>
<p>To ensure Flutter knows about these assets and bundles them with the application, you need to declare them in your <code>pubspec.yaml</code> file:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">flutter:</span>
  <span class="hljs-attr">uses-material-design:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">assets:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">assets/images/</span> <span class="hljs-comment"># This line tells Flutter to include all files in the assets/images/ directory</span>
</code></pre>
<h2 id="heading-app-icons-customizing-your-applications-identity"><strong>App Icons: Customizing Your Application's Identity</strong></h2>
<p>Flutter applications use the <code>flutter_launcher_icons</code> package to simplify the process of generating app icons for different platforms and resolutions. This ensures your app has a consistent and professional look on both Android and iOS devices.</p>
<h3 id="heading-pubspecyaml-configuration-for-flutterlaunchericons"><code>pubspec.yaml</code> Configuration for <code>flutter_launcher_icons</code></h3>
<pre><code class="lang-yaml"><span class="hljs-attr">flutter_icons:</span>
  <span class="hljs-attr">android:</span> <span class="hljs-string">"launcher_icon"</span>
  <span class="hljs-attr">ios:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">image_path:</span> <span class="hljs-string">"assets/images/app_logo.png"</span>
  <span class="hljs-attr">remove_alpha_ios:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">adaptive_icon_background:</span> <span class="hljs-string">"#FFFFFF"</span>
  <span class="hljs-attr">adaptive_icon_foreground:</span> <span class="hljs-string">"assets/images/app_logo.png"</span>
</code></pre>
<p>Here’s what’s happening:</p>
<ul>
<li><p><code>flutter_icons:</code>: This is the root key for the <code>flutter_launcher_icons</code> package configuration.</p>
</li>
<li><p><code>android: "launcher_icon"</code>: Specifies that Android launcher icons should be generated. <code>"launcher_icon"</code> is the default and usually sufficient.</p>
</li>
<li><p><code>ios: true</code>: Enables the generation of iOS app icons.</p>
</li>
<li><p><code>image_path: "assets/images/app_logo.png"</code>: This is the absolute path to your source image file that will be used to generate the icons. It's crucial that this path is correct and points to a high-resolution square image.</p>
</li>
<li><p><code>remove_alpha_ios: true</code>: For iOS, this option removes the alpha channel from the icon. iOS icons typically do not use an alpha channel for transparency.</p>
</li>
<li><p><code>adaptive_icon_background: "#FFFFFF"</code>: This is specific to Android Adaptive Icons (introduced in Android 8.0 Oreo). It defines the background layer of the adaptive icon. Here, it's set to white (<code>#FFFFFF</code>).</p>
</li>
<li><p><code>adaptive_icon_foreground: "assets/images/app_logo.png"</code>: This defines the foreground layer of the adaptive icon. It uses the <code>app_logo.png</code> again, which will be masked and scaled by the Android system.</p>
</li>
</ul>
<h3 id="heading-generating-app-icons"><strong>Generating App Icons</strong></h3>
<p>After configuring <code>pubspec.yaml</code>, you need to run the following commands in your terminal:</p>
<p>First, run <code>dart run flutter_launcher_icons:generate</code>. This command generates a configuration file (often named <code>flutter_launcher_icons.yaml</code> or similar, or directly processes the <code>pubspec.yaml</code>) which <code>flutter_launcher_icons</code> uses.</p>
<p><em>Correction</em>: The prompt mentions "generate a config file and setup the image path to the path of the app_logo.png then run dart run flutter_launcher_icons to generate the assets". It seems <code>flutter_launcher_icons:generate</code> might be an older or specific command, the typical usage is to run <code>flutter_launcher_icons</code> directly after setting <code>image_path</code> in <code>pubspec.yaml</code>. For the given configuration, the <code>image_path</code> is already set in <code>pubspec.yaml</code>.</p>
<p>Then, run <code>dart run flutter_launcher_icons</code>. This command executes the <code>flutter_launcher_icons</code> package, which takes the <code>image_path</code> specified in <code>pubspec.yaml</code> and generates all the necessary icon files at various resolutions for both Android and iOS, placing them in the correct native project directories.</p>
<h2 id="heading-splash-screen-the-first-impression"><strong>Splash Screen: The First Impression</strong></h2>
<p>A splash screen (or launch screen) is the first screen users see when they open your app. It provides a branded experience while the app initializes resources. The <code>flutter_native_splash</code> package simplifies creating native splash screens for Flutter apps.</p>
<h3 id="heading-pubspecyaml-configuration-for-flutternativesplash"><code>pubspec.yaml</code> Configuration for <code>flutter_native_splash</code></h3>
<pre><code class="lang-yaml"><span class="hljs-attr">flutter_native_splash:</span>
  <span class="hljs-attr">color:</span> <span class="hljs-string">"#FFFFFF"</span>
  <span class="hljs-attr">image:</span> <span class="hljs-string">assets/images/app_logo.png</span>
  <span class="hljs-attr">android:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">android_gravity:</span> <span class="hljs-string">center</span>
  <span class="hljs-attr">fullscreen:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">ios:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>Here’s what’s happening:</p>
<ul>
<li><p><code>flutter_native_splash:</code>: The root key for the <code>flutter_native_splash</code> package configuration.</p>
</li>
<li><p><code>color: "#FFFFFF"</code>: Sets the background color of the splash screen. Here, it's set to white.</p>
</li>
<li><p><code>image: assets/images/app_logo.png</code>: Specifies the path to the image that will be displayed on the splash screen. In this case, it's the application's logo.</p>
</li>
<li><p><code>android: true</code>: Enables splash screen generation for Android.</p>
</li>
<li><p><code>android_gravity: center</code>: For Android, this centers the splash image on the screen.</p>
</li>
<li><p><code>fullscreen: true</code>: Makes the splash screen appear in fullscreen mode, without status or navigation bars.</p>
</li>
<li><p><code>ios: true</code>: Enables splash screen generation for iOS.</p>
</li>
</ul>
<h3 id="heading-generating-the-splash-screen"><strong>Generating the Splash Screen</strong></h3>
<p>After configuring <code>pubspec.yaml</code>, run the following command in your terminal: <code>dart run flutter_native_splash:create</code>. It processes the configuration and generates the native splash screen files (for example, launch images, drawables) in the respective Android and iOS project folders, ensuring they are properly integrated into the native launch process.</p>
<h2 id="heading-screenshots-from-the-app"><strong>Screenshots from the App</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748068995235/d84ad92d-a686-43ee-a34c-89f2d6bf7e17.png" alt="d84ad92d-a686-43ee-a34c-89f2d6bf7e17" class="image--center mx-auto" width="1818" height="1240" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748069008469/f5fecee8-93dd-46ef-92ae-bd8c5413b3a7.png" alt="f5fecee8-93dd-46ef-92ae-bd8c5413b3a7" class="image--center mx-auto" width="1353" height="1200" loading="lazy"></p>
<p>Keep in mind that the output quality can vary depending on the AI model you’re using. The same applies to YouTube links and image URLs – sometimes they work perfectly, and other times they may not. So if something doesn’t work as expected, it’s not necessarily on your end.</p>
<p>Also, remember there are so many ways to achieve this and you don’t necessarily use to use this method. I’ll provide some other resources you can check out below. You can use <code>systemInstructions</code> instead of defining constraints in text the way I did it.</p>
<p><strong>Here’s the completed project:</strong> <a target="_blank" href="https://github.com/Atuoha/snap2chef_ai">https://github.com/Atuoha/snap2chef_ai</a></p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>I hope this comprehensive breakdown has given you a clear understanding of the "Snap2Chef" application's structure, UI components, and underlying configurations. May your coding journey be filled with creativity and successful implementations.</p>
<p>Happy coding!</p>
<h2 id="heading-references">References</h2>
<p>Here are some references for the key technologies and packages used in this application:</p>
<h3 id="heading-flutter-packages">Flutter Packages</h3>
<ul>
<li><p><code>flutter/material.dart</code>: The core Flutter Material Design package.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://api.flutter.dev/flutter/material/material-library.html">Flutter API Docs - material library</a></li>
</ul>
</li>
<li><p><code>iconsax/iconsax.dart</code>: A custom icon set for Flutter.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://www.google.com/search?q=https://pub.dev/packages/iconsax">pub.dev - iconsax</a></li>
</ul>
</li>
<li><p><code>gap/gap.dart</code>: A simple package for adding spacing between widgets.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://pub.dev/packages/gap">pub.dev - gap</a></li>
</ul>
</li>
<li><p><code>dotted_border/dotted_border.dart</code>: A Flutter package to draw a dotted border around any widget.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://pub.dev/packages/dotted_border">pub.dev - dotted_border</a></li>
</ul>
</li>
<li><p><code>flutter/cupertino.dart</code>: The core Flutter Cupertino (iOS-style) widgets package.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://api.flutter.dev/flutter/cupertino/cupertino-library.html">Flutter API Docs - cupertino library</a></li>
</ul>
</li>
<li><p><code>flutter_launcher_icons</code>: A package for generating application launcher icons.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://pub.dev/packages/flutter_launcher_icons">pub.dev - flutter_launcher_icons</a></li>
</ul>
</li>
<li><p><code>flutter_native_splash</code>: A package for generating native splash screens.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://pub.dev/packages/flutter_native_splash">pub.dev - flutter_native_splash</a></li>
</ul>
</li>
<li><p><code>image_picker</code> (Implicitly used by <code>ImageUploadController</code>): A Flutter plugin for picking images from the image library, or taking new photos with the camera. (Though not directly imported in the provided snippets, <code>ImageUploadController</code> likely uses this or a similar package).</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://pub.dev/packages/image_picker">pub.dev - image_picker</a></li>
</ul>
</li>
<li><p><code>image_cropper</code> (Implicitly used by <code>ImageUploadController</code>): A Flutter plugin for cropping images. (Likely used in conjunction with <code>image_picker</code> for <code>assignCroppedImage</code>).</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://pub.dev/packages/image_cropper">pub.dev - image_cropper</a></li>
</ul>
</li>
</ul>
<h3 id="heading-apis-and-platforms"><strong>APIs and Platforms</strong></h3>
<ul>
<li><p><strong>Gemini API</strong>: Google's family of generative AI models.</p>
<ul>
<li><p><strong>Reference:</strong> <a target="_blank" href="https://www.google.com/search?q=https://ai.google.dev/gemini">Google AI Gemini API</a></p>
</li>
<li><p><strong>Documentation:</strong> <a target="_blank" href="https://cloud.google.com/gemini/docs">Google Cloud - Gemini API Documentation</a></p>
</li>
</ul>
</li>
<li><p><strong>Firebase</strong>: Google's comprehensive app development platform.</p>
<ul>
<li><p><strong>Reference:</strong> <a target="_blank" href="https://firebase.google.com/">Firebase Official Website</a></p>
</li>
<li><p><strong>Documentation:</strong> <a target="_blank" href="https://firebase.google.com/docs">Firebase Documentation</a></p>
</li>
<li><p><strong>Firebase Console/Studio</strong>: The web-based interface for managing Firebase projects.</p>
</li>
</ul>
</li>
<li><p><strong>Vertex AI</strong>: Google Cloud's machine learning platform.</p>
<ul>
<li><p><strong>Reference:</strong> <a target="_blank" href="https://cloud.google.com/vertex-ai">Google Cloud - Vertex AI</a></p>
</li>
<li><p><strong>Documentation:</strong> <a target="_blank" href="https://cloud.google.com/vertex-ai/docs">Google Cloud - Vertex AI Documentation</a></p>
</li>
</ul>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create an AI-Powered Bot that Can Post on Twitter/X ]]>
                </title>
                <description>
                    <![CDATA[ These days, everyone wants to be a content creator. But it can be hard to find time to create and curate content, post on social media, build engagement, and grow your brand. And I’m not an exception to this. I wanted to create more content, and had ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-create-an-ai-powered-bot/</link>
                <guid isPermaLink="false">680931204adb8ffdef48642a</guid>
                
                    <category>
                        <![CDATA[ automation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ bot ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Twitter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ gemini ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Arunachalam B ]]>
                </dc:creator>
                <pubDate>Wed, 23 Apr 2025 18:27:44 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745416845372/5eb9963d-e092-4844-99d9-01fa70032169.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>These days, everyone wants to be a content creator. But it can be hard to find time to create and curate content, post on social media, build engagement, and grow your brand.</p>
<p>And I’m not an exception to this. I wanted to create more content, and had an idea based on something I’ve observed. I subscribe to a few technology newsletters, and I read lots of updates every day about the tech ecosystem. But I’ve noticed that many of my peers often don’t seem to be aware of this news. So, I decided to post my top three news stories (especially about AI) on my Twitter/X account every day.</p>
<p>I did this for a couple of weeks, but after that I couldn’t find the time to keep it going. So, I did some research into how I could automate the process, and I found a solution. In this guide, I’ll explain the process so you can use it, too.</p>
<p>By the end of this tutorial, you’ll have created your own AI bot that:</p>
<ul>
<li><p>Fetches data from an API or crawls a webpage</p>
</li>
<li><p>Processes the data using AI</p>
</li>
<li><p>Posts the results on Twitter/X</p>
</li>
</ul>
<p>And the great thing: this entire process is automated.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-the-bot">How to Build the Bot</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-generate-the-twitter-api-key">Step 1: Generate the Twitter API Key</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-generate-access-token-and-secret">Step 2: Generate Access Token and Secret</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-generate-an-api-key-in-google-gemini">Step 3: Generate an API Key in Google Gemini</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-nodejs-project-setup">Node.js Project Setup</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-fetch-data-from-the-api">Step 1: Fetch Data from the API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-upload-the-data-as-a-file-to-gemini-api">Step 2: Upload the Data as a File to Gemini API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-prompt-gemini-to-get-the-latest-ai-news">Step 3: Prompt Gemini to Get the Latest AI News</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-post-using-the-twitterx-api">Step 4: Post Using the Twitter/X API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-delete-the-file-uploaded-in-the-gemini-api">Step 5: Delete the File Uploaded in the Gemini API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-result">The Result</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before we begin creating a bot, you’ll need to have the following setup and tools ready to go:</p>
<ul>
<li><a target="_blank" href="https://nodejs.org/en/learn/getting-started/introduction-to-nodejs">NodeJS</a> - A simple NodeJS app to code the bot</li>
</ul>
<p>You’ll also need some API keys, secrets, and tokens. So, you’ll need to have the following accounts created:</p>
<ul>
<li><p><a target="_blank" href="https://developer.x.com/">Twitter Developer</a> – To generate the Twitter/X API keys, secrets, and tokens</p>
</li>
<li><p><a target="_blank" href="https://aistudio.google.com/">Google AI Studio</a> – To generate the Gemini API key</p>
</li>
</ul>
<h2 id="heading-how-to-build-the-bot">How to Build the Bot</h2>
<p>There are a number of steps I’ll walk you through to build your bot.</p>
<p>We’ll start by generating an API Key and Secret so we can use the Twitter/X API. Then we’ll generate an access token and access token secret with “Read and Write” permissions that’ll be able to post in your account. After that we’ll generate an API Key in Google Gemini (we’ll be using the Gemini API to process the data).</p>
<p>With all that taken care of, we’ll start working on the Node.js app. The app will be able to fetch data from an API, process the data using AI, and then post that data in the form of tweets on Twitter/X.</p>
<p>Finally, we’ll automate the entire process and schedule it to run daily.</p>
<h3 id="heading-step-1-generate-the-twitter-api-key">Step 1: Generate the Twitter API Key</h3>
<ol>
<li><p>Navigate to <a target="_blank" href="https://developer.x.com/">Twitter Developer Website</a>.</p>
</li>
<li><p>Click on the “Developer Portal” in the top right:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745152618491/214fe6d6-b699-40bb-8ac0-533b0c72b927.png" alt="Image showing developer portal highlighted" class="image--center mx-auto" width="1865" height="945" loading="lazy"></p>
</li>
<li><p>Signup using your account.</p>
</li>
<li><p>You’ll be asked to fill out a form asking how will you use the Twitter API, and a few basic details. It may take up to 24 hours to get approved. But, it’s approved instantly for me.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745152170917/d2c2ba21-f3f5-4bc6-bdd5-58d222e203e6.png" alt="Form asking how you'll use the Twitter API" class="image--center mx-auto" width="833" height="388" loading="lazy"></p>
</li>
<li><p>After login, Navigate to "Projects and Apps" and under “Overview” click on "Create App":</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745153184830/1a731639-0df2-47e3-baf2-3633e1735a69.png" alt="Create App screen" class="image--center mx-auto" width="691" height="306" loading="lazy"></p>
</li>
<li><p>Enter a name for your app and click “Next” to proceed with creating your app. At the end, you’ll be shown your API Key and Secret. Don’t copy that now.</p>
</li>
<li><p>Click on the project you created from the left side drawer and click on the "Edit" option in “User authentication settings” section.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745153746932/002f3b38-5aaf-43ef-8d7c-0368f141524f.png" alt="Editing the user authentication settings section" class="image--center mx-auto" width="1230" height="719" loading="lazy"></p>
</li>
<li><p>Select “Read and Write” in App Permissions section, “Web App, Automated App or Bot” in Type of App section, and enter your website URL (it can be any URL including http://localhost) in the “Callback URI” and “Website URL”. Then hit “Save”.</p>
</li>
<li><p>Go to “Keys and tokens” tab.</p>
</li>
<li><p>Click on “Regenerate” button in “API Key and Secret” section.</p>
</li>
<li><p>Copy and save the API Key and Secret somewhere securely.</p>
</li>
</ol>
<h3 id="heading-step-2-generate-access-token-and-secret">Step 2: Generate Access Token and Secret</h3>
<ol>
<li><p>Go to “Keys and tokens” tab.</p>
</li>
<li><p>Click on “Generate” or “Regenerate” button in “Access Token and Secret” section.</p>
</li>
<li><p>Copy and save the Access Token and Secret somewhere securely.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745154373207/4309dbcc-1081-46b7-be71-5babf950eae0.png" alt="Generating or regenerating keys and tokens" class="image--center mx-auto" width="1230" height="629" loading="lazy"></p>
</li>
</ol>
<h3 id="heading-step-3-generate-an-api-key-in-google-gemini">Step 3: Generate an API Key in Google Gemini</h3>
<ol>
<li><p>Navigate to <a target="_blank" href="https://aistudio.google.com/">Google AI Studio</a>.</p>
</li>
<li><p>Login to your account.</p>
</li>
<li><p>Click on “Get API Key” button at the top right.</p>
</li>
<li><p>Click on “Create API Key” button.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745154646809/54c4fa1a-097e-4bf6-8a5f-229c01845d28.png" alt="Create API screen" class="image--center mx-auto" width="1802" height="309" loading="lazy"></p>
</li>
<li><p>Copy and save the API Key somewhere securely.</p>
</li>
</ol>
<p>Alright, we are done with creating the necessary API Keys and Secrets for our project. Let’s put on our coding shoes.</p>
<h2 id="heading-nodejs-project-setup">Node.js Project Setup</h2>
<p>There are 5 major steps for this part of the project. They are:</p>
<ol>
<li><p>Fetch data from the API</p>
</li>
<li><p>Upload the data as a file to Gemini API</p>
</li>
<li><p>Prompt Gemini with the uploaded file to get the latest AI news</p>
</li>
<li><p>Post news to Twitter/X using their API</p>
</li>
<li><p>Delete the file uploaded in Gemini API</p>
</li>
</ol>
<p>These are just the snippets of code that can be assembled together to run this project.</p>
<h3 id="heading-step-1-fetch-data-from-the-api">Step 1: Fetch Data from the API</h3>
<p>In my case, I’ll be using <code>techmeme.com</code> to get the latest news. But this site does not offer an API. So, I’ll be downloading the HTML of this site.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="b204531fbfda5e805202b5f5ab5aa55d">
        <script src="https://gist.github.com/arunachalam-b/b204531fbfda5e805202b5f5ab5aa55d.js"></script></div><p> </p>
<p>In the <code>User-Agent</code> header, we pass the value that mimics a browser user agent to avoid potential blocks.</p>
<h3 id="heading-step-2-upload-the-data-as-a-file-to-gemini-api">Step 2: Upload the Data as a File to Gemini API</h3>
<p>Now we need to store this HTML in a separate file. We cannot directly pass the HTML code in the prompt to the Gemini API, as it’ll result in an error. This is because Gemini accepts only a limited number of tokens in this API. The HTML code of any website will always result in huge number of tokens. So, we’ll create a separate file.</p>
<p>Upload the file to the Gemini API. Refer to the file id in the prompt to Gemini.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="5ebfed570c79a0f0faa8c4e42559c673">
        <script src="https://gist.github.com/arunachalam-b/5ebfed570c79a0f0faa8c4e42559c673.js"></script></div><p> </p>
<h3 id="heading-step-3-prompt-gemini-to-get-the-latest-ai-news">Step 3: Prompt Gemini to Get the Latest AI News</h3>
<p>Let’s write a prompt to Gemini asking it to generate top news by referring to the HTML file provided. We’ll ask it to provide a headline, short description, URL, and three relevant hashtags for each tweet. We’ll also give some example data of how it should look. We’ll ask it to generate a structured response by providing the format of the JSON that we want the output to be.</p>
<p>You can use whatever model you want to, but I’ll be using the <code>gemini-2.5-pro-exp-03-25</code> model for this use case. I’m using this model because we need a thinking model that thinks and picks the correct top news – not just one that predicts the next token/word. The Gemini 2.5 Pro model best qualifies for this.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="466449de313bcbc4241eaf3b6e1646a7">
        <script src="https://gist.github.com/arunachalam-b/466449de313bcbc4241eaf3b6e1646a7.js"></script></div><p> </p>
<h3 id="heading-step-4-post-using-the-twitterx-api">Step 4: Post Using the Twitter/X API</h3>
<p>Here’s the core of our app. We need to post all the tweets we received from Gemini. We’ll be posting the tweet as a thread. This means that the first tweet will be the root tweet and subsequent tweets will be in the comments of the prior tweet. This makes it a thread.</p>
<p>To do this, we’ll take the id of each tweet after it’s posted and pass it on to the next tweet as a reference. One additional thing to note here is, after each successful tweet, we’ll give a pause of 5 seconds before posting the next tweet. There are few reasons for doing it this way.</p>
<ol>
<li><p>When any script runs, it usually runs at a much higher speed (usually in milliseconds). So, the second tweet may get posted before the first tweet was posted (maybe due to some poor internet connection). Also, I believe Twitter implements some queue system which may quickly process the second tweet before your first. So it’s always better to leave a small gap – if not 5 seconds then at least 1 second</p>
</li>
<li><p>Twitter may have implemented some rate limiting mechanism. So if there are multiple request received from a same IP within a short time frame, they may block the IP and consider your account as spam.</p>
</li>
<li><p>Since we’re using a Free tier API, we are limited to 1500 tweets per month. If you’ve paid for this API, you won’t have to worry about this (since you’ll have a higher limit and the rate limiting mechanism –refer to point #2 – might not be applicable). All of this depends on their <a target="_blank" href="https://docs.x.com/x-api/introduction#access-levels">pricing</a>, so just refer to that and make your call accordingly.</p>
</li>
</ol>
<p>I’m using the free tier, and since it’s a hobby project, having a 5 seconds wait time makes sense. I have not faced any issues so far with this.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="b049fda9e567bc68c7fb33de0ce67cd3">
        <script src="https://gist.github.com/arunachalam-b/b049fda9e567bc68c7fb33de0ce67cd3.js"></script></div><p> </p>
<h3 id="heading-step-5-delete-the-file-uploaded-in-the-gemini-api">Step 5: Delete the File Uploaded in the Gemini API</h3>
<p>After posting all the tweets, it’s time to clean up the system. The only thing we need to do as a clean up is delete the uploaded file. It’s always a best practice to remove an unused file that’s no longer needed. And since we’ve already posted the tweets, we no longer need that file. So, we’ll be deleting it in this step.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="741c5b13603187c76905f7b349661293">
        <script src="https://gist.github.com/arunachalam-b/741c5b13603187c76905f7b349661293.js"></script></div><p> </p>
<p>That’s it. We’re all done. You just need to copy these blocks of code into an <code>index.js</code> file and install some dependencies into the project and you should be good to go.</p>
<p>To make this even more simple, I have created a repo and made it public. Here’s the <a target="_blank" href="https://github.com/arunachalam-b/existential-crisis-alert-bot">Github repo URL</a>. You just need to clone the repo, install the dependencies, and run the <code>post</code> command</p>
<pre><code class="lang-plaintext">git clone https://github.com/arunachalam-b/existential-crisis-alert-bot.git
cd existential-crisis-alert-bot
npm i
</code></pre>
<p>Create a .env file and update your API keys and secrets in that file:</p>
<pre><code class="lang-plaintext">GEMINI_API_KEY=
TWITTER_API_KEY=
TWITTER_API_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_TOKEN_SECRET=
</code></pre>
<p>Run the following command to post the latest AI news to your account:</p>
<pre><code class="lang-plaintext">npm run post
</code></pre>
<h3 id="heading-the-result">The Result</h3>
<p>Here’s a sample output of that command:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745169397786/14e08ef8-dba5-45e0-a5d5-f3c6135c6347.png" alt="Output" class="image--center mx-auto" width="604" height="308" loading="lazy"></p>
<p>You can modify the code/prompt to fetch data from a different API and post the top results in your Twitter account.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I hope you now understand how you can automate a slightly complex process using AI and some APIs. Just note that this example is not completely automated. You still have to manually run the command everyday to post the tweets.</p>
<p>But you can automate that process as well. Just drop me a message if you wish to know about that. That topic itself deserves to be a separate tutorial. Also, I would request that you give a star for my project if you enjoyed this tutorial.</p>
<p>Meanwhile, you can follow my <a target="_blank" href="https://x.com/AI_Techie_Arun">Twitter/X account</a> to receive the top AI news everyday. If you wish to learn more about automation, subscribe to my email newsletter (<a target="_blank" href="https://5minslearn.gogosoon.com/?ref=fcc_automated_tweet">https://5minslearn.gogosoon.com/</a>) and follow me on social media.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Video Subtitle Generator using the Gemini API ]]>
                </title>
                <description>
                    <![CDATA[ In this tutorial, you'll build an AI-powered subtitle generator using Google's Gemini API. We'll create a project called “AI-Subtitle-Generator” using React for the front end and Express for the back end. Get ready for a fun and practical project. Ta... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-video-subtitle-generator-using-the-gemini-api/</link>
                <guid isPermaLink="false">6759af8bb972ec12da4879d3</guid>
                
                    <category>
                        <![CDATA[ gemini ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Express ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sanjay ]]>
                </dc:creator>
                <pubDate>Wed, 11 Dec 2024 15:28:11 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733638398422/2f468b16-5801-4f8c-bf40-c24d07e219b7.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this tutorial, you'll build an AI-powered subtitle generator using Google's Gemini API. We'll create a project called “AI-Subtitle-Generator” using React for the front end and Express for the back end. Get ready for a fun and practical project.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-how-to-get-your-api-key">How to Get Your API Key</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-front-end-setup">Front End Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-server-setup">Server Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-update-the-front-end">Update the Front End</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-summary">Summary</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>To build this project, you should know the basics of React and Express.</p>
<h2 id="heading-what-is-the-gemini-api">What is the Gemini API?</h2>
<p>Google's Gemini API is a powerful tool that lets you integrate advanced AI capabilities into your applications. Gemini is a multimodal model, which means you can use various types of input, like text, images, audio, and video.</p>
<p>It’s good at analyzing and processing large amounts of text as well as pulling information from videos – which makes it great for our use case of a subtitle generator.</p>
<h2 id="heading-how-to-get-your-api-key">How to Get Your API Key</h2>
<p>An API key acts as a unique identifier and authenticates your requests to the service. It's essential for accessing and using Gemini AI’s capabilities. This key will allow our application to communicate with Gemini and help us build our project.</p>
<p>Go to <a target="_blank" href="https://aistudio.google.com/prompts/new_chat">Google AI Studio</a>, then click “Get API Key”:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733571839232/f5636fd0-c3cd-4c1b-bf7f-5200bce41444.png" alt="Screenshot of Google AI Studio showing the 'Get API Key' button" class="image--center mx-auto" width="1246" height="682" loading="lazy"></p>
<p>After you are redirected to the API KEY page, click “Create API Key“:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733572045638/c950f7a2-613c-4976-905a-ce5c9dceb901.png" alt="Screenshot showing how to create an API key in Google AI Studio." class="image--center mx-auto" width="1360" height="777" loading="lazy"></p>
<p>A new API KEY will be created. Then make sure you copy the key.</p>
<p>This is your API key. This key is used to authenticate your application's requests to the Gemini API. Each time your application sends a request to Gemini, this key must be included. Gemini uses this key to verify that the request is coming from an authorized source. Without this API key, your requests will be rejected, and you won't be able to access Gemini's services.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Start by creating a new folder for your project. Let's call it <code>ai-subtitle-generator</code>.</p>
<p>Inside the <code>ai-subtitle-generator</code> folder, create two subfolders: <code>client</code> and <code>server</code>. The <code>client</code> folder will contain the React frontend, and the <code>server</code> folder will contain the Express backend.</p>
<h2 id="heading-front-end-setup">Front End Setup</h2>
<p>First, we will focus on the front end and set up a basic React application.</p>
<p>Navigate to the <code>client</code> folder:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> client
</code></pre>
<p>Then create a new React project using Vite. To do that, run the following command:</p>
<pre><code class="lang-bash">npm create vite@latest .
</code></pre>
<p>When prompted, choose “React“. Select “React + TS” or “React + JS”. In this tutorial, I will use React + TS. You can also follow along with JS.</p>
<p>Next, install the dependencies with this command:</p>
<pre><code class="lang-bash">npm install
</code></pre>
<p>Then start the development server:</p>
<pre><code class="lang-bash">npm run dev
</code></pre>
<h4 id="heading-how-to-handle-file-uploads-in-the-frontend">How to Handle File Uploads in the Frontend</h4>
<p>Now in <code>client/src/App.tsx</code>, add the following code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//  client/src/App.tsx</span>

<span class="hljs-keyword">const</span> App = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-keyword">async</span> (e: React.FormEvent&lt;HTMLFormElement&gt;): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; =&gt; {
    e.preventDefault();
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData(e.currentTarget);
      <span class="hljs-built_in">console</span>.log(formData)
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      &lt;form onSubmit={handleSubmit}&gt;
        &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span> accept=<span class="hljs-string">"video/*,.mkv"</span> name=<span class="hljs-string">"video"</span> /&gt;
        &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> /&gt;
      &lt;/form&gt;
    &lt;/div&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<p>In the above code, we have used an input tag that will accept the video and name it as <code>video</code>. This name will be appended to the <code>FormData</code> object.</p>
<p>While sending the video to the server, we need to send it as a key-value pair, where the key is a <code>video</code> and the value is the file data.</p>
<p>Why key-value pairs? Because when the server receives the request, it needs to parse the incoming chunks. After parsing, the video data will be available in <code>req.files[key]</code>, where the <code>key</code> is the name we have assigned in the frontend (<code>video</code> in this case).</p>
<p>This is why we are using the <code>FormData</code> object. When we create a new <code>FormData</code> instance and pass <code>e.target</code> to it, all the form fields and their names will automatically be available as key-value pairs.</p>
<h2 id="heading-server-setup">Server Setup</h2>
<p>Now that we have our API key, let's set up the backend server. This server will handle video uploads from the frontend and communicate with the Gemini API for subtitle generation.</p>
<p>Navigate to <code>server</code> folder:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> server
</code></pre>
<p>And initialize the project:</p>
<pre><code class="lang-bash">npm init -y
</code></pre>
<p>Then install the necessary packages:</p>
<pre><code class="lang-bash">npm install express dotenv cors @google/generative-ai express-fileupload nodemon
</code></pre>
<p>These are the back-end dependencies we’re using in this project:</p>
<ul>
<li><p><code>express</code><strong>:</strong> The web framework for creating the backend API.</p>
</li>
<li><p><code>dotenv</code><strong>:</strong> Loads environment variables from a <code>.env</code> file.</p>
</li>
<li><p><code>cors</code><strong>:</strong> Enables Cross-Origin Resource Sharing, allowing your frontend to communicate with your backend.</p>
</li>
<li><p><code>@google/generative-ai</code><strong>:</strong> The Google AI library for interacting with the Gemini API.</p>
</li>
<li><p><code>express-fileupload</code><strong>:</strong> Handles file uploads, making it easy to access uploaded files on the server.</p>
</li>
<li><p><code>nodemon</code><strong>:</strong> Automatically restarts the server when you make changes to your code.</p>
</li>
</ul>
<h3 id="heading-set-up-the-environment-variables">Set Up the Environment Variables</h3>
<p>Now, create a file called <code>.env</code>. This is where you’ll manage your API keys.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//.env</span>
API_KEY = YOUR_API_API
PORT = <span class="hljs-number">3000</span>
</code></pre>
<h3 id="heading-update-the-packagejson">Update the <code>package.json</code></h3>
<p>For this project, we are using ES6 modules instead of CommonJS. To enable this, update your <code>package.json</code> file with the following code:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"server"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,       <span class="hljs-comment">//Add "type": "module" to enable ES6 modules</span>
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"node server.js"</span>,
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"nodemon server.js"</span>    <span class="hljs-comment">//configure nodemon</span>
  },
  <span class="hljs-attr">"keywords"</span>: [],
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"@google/generative-ai"</span>: <span class="hljs-string">"^0.21.0"</span>,
    <span class="hljs-attr">"cors"</span>: <span class="hljs-string">"^2.8.5"</span>,
    <span class="hljs-attr">"dotenv"</span>: <span class="hljs-string">"^16.4.7"</span>,
    <span class="hljs-attr">"express"</span>: <span class="hljs-string">"^4.21.1"</span>,
    <span class="hljs-attr">"express-fileupload"</span>: <span class="hljs-string">"^1.5.1"</span>,
    <span class="hljs-attr">"nodemon"</span>: <span class="hljs-string">"^3.1.7"</span>
  }
}
</code></pre>
<h3 id="heading-basic-setup-of-express">Basic Setup of Express</h3>
<p>Create a file <code>server.js</code>. Now, let’s set up a basic Express application.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//  server/server.js</span>

<span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">"express"</span>;
<span class="hljs-keyword">import</span> { configDotenv } <span class="hljs-keyword">from</span> <span class="hljs-string">"dotenv"</span>;
<span class="hljs-keyword">import</span> fileUpload <span class="hljs-keyword">from</span> <span class="hljs-string">"express-fileupload"</span>;
<span class="hljs-keyword">import</span> cors <span class="hljs-keyword">from</span> <span class="hljs-string">"cors"</span>

<span class="hljs-keyword">const</span> app = express();

configDotenv();           <span class="hljs-comment">//configure the env</span>
app.use(fileUpload());    <span class="hljs-comment">//it will parse the mutipart data</span>
app.use(express.json());  <span class="hljs-comment">// Enable JSON parsing for request bodies</span>
app.use(cors())           <span class="hljs-comment">//configure cors</span>

app.use(<span class="hljs-string">"/api/subs"</span>,subRoutes);  <span class="hljs-comment">// Use routes for the "/api/subs" endpoint</span>

app.listen(process.env.PORT, <span class="hljs-function">() =&gt;</span> {   <span class="hljs-comment">//access the PORT from the .env</span>
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"server started"</span>);         
});
</code></pre>
<p>In this code, we create an Express app instance and then load our environment variables. This is where we keep sensitive data like API keys secure. Next, we apply middleware functions: <code>fileUpload</code> prepares the server to receive uploaded videos, <code>express.json</code> allows us to receive JSON data, and <code>cors</code> enables communication between our frontend and backend.</p>
<p>We define a route <code>(/api/subs)</code> that will handle all requests related to subtitle generation. The specific logic for these routes will be defined in <code>subs.routes.js</code>. Finally, we start the server, telling it to listen for requests on the port specified in our <code>.env</code> file.</p>
<p>Now we need to create some folders to manage the code. You can also manage the entire code in a single file, but structuring it into separate folders and managing them all that way will be easier.</p>
<p>This is the final folder structure for the server:</p>
<pre><code class="lang-plaintext">server/
├── server.js
├── controller/
│   └── subs.controller.js
├── gemini/
│   ├── gemini.config.js
├── routes/
│   └── subs.routes.js
├── uploads/
├── utils/
│   ├── fileUpload.js
│   └── genContent.js
└── .env
</code></pre>
<p><strong>Note:</strong> Don’t worry about creating this folder structure now. This is just for reference. Follow along with me step by step, and we will build this structure together.</p>
<h3 id="heading-create-the-routes">Create the Routes</h3>
<p>Now create a <code>routes</code> folder and then create <code>subs.routes.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// server/routes/sub.routes.js</span>

<span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">"express"</span>
<span class="hljs-keyword">import</span> { uploadFile } <span class="hljs-keyword">from</span> <span class="hljs-string">"../controller/subs.controller.js"</span>    <span class="hljs-comment">// import the uploadFile function from the controller folder</span>

<span class="hljs-keyword">const</span> router = express.Router()

router.post(<span class="hljs-string">"/"</span>,uploadFile)    <span class="hljs-comment">// define a POST route that calls the uploadFile function</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router     <span class="hljs-comment">// export the router to use in the main server.js file</span>
</code></pre>
<p>This code defines the routes for our server, specifically the route that handles video uploads and subtitle generation.</p>
<p>We create a new router instance using <code>express.Router()</code>. This allows us to define routes separate from our main server file, improving code organization. We define a POST route at the root path <code>("/")</code> of our API endpoint. When a POST request is made to this route (which will happen when a user submits the video upload form on the frontend), the <code>uploadFile</code> function is called. This function will handle the actual upload and subtitle generation.</p>
<p>Finally, we export the router so that it can be used in our main server file <code>(server.js)</code> to connect this route to the main application.</p>
<h3 id="heading-configure-gemini">Configure Gemini</h3>
<p>Now, let's configure how our application will interact with Gemini.</p>
<p>Create a <code>gemini</code> folder and then create a new file called <code>gemini.config.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//  server/gemini/gemini.config.js</span>

<span class="hljs-keyword">import</span> {
  GoogleGenerativeAI,
  HarmBlockThreshold,
  HarmCategory,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@google/generative-ai"</span>;
<span class="hljs-keyword">import</span> { configDotenv } <span class="hljs-keyword">from</span> <span class="hljs-string">"dotenv"</span>;
configDotenv();

<span class="hljs-keyword">const</span> genAI = <span class="hljs-keyword">new</span> GoogleGenerativeAI(process.env.API_KEY);  <span class="hljs-comment">// Initialize Google Generative AI with the API key</span>

<span class="hljs-keyword">const</span> safetySettings = [
  {
    <span class="hljs-attr">category</span>: HarmCategory.HARM_CATEGORY_HARASSMENT,
    <span class="hljs-attr">threshold</span>: HarmBlockThreshold.BLOCK_NONE,
  },
  {
    <span class="hljs-attr">category</span>: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
    <span class="hljs-attr">threshold</span>: HarmBlockThreshold.BLOCK_NONE,
  },
  {
    <span class="hljs-attr">category</span>: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
    <span class="hljs-attr">threshold</span>: HarmBlockThreshold.BLOCK_NONE,
  },
  {
    <span class="hljs-attr">category</span>: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
    <span class="hljs-attr">threshold</span>: HarmBlockThreshold.BLOCK_NONE,
  },
];

<span class="hljs-keyword">const</span> model = genAI.getGenerativeModel({
  <span class="hljs-attr">model</span>: <span class="hljs-string">"gemini-1.5-flash-001"</span>,    <span class="hljs-comment">//choose the model</span>
  <span class="hljs-attr">safetySettings</span>: safetySettings,   <span class="hljs-comment">//optional safety settings</span>
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> model;    <span class="hljs-comment">//export the model</span>
</code></pre>
<p>In the code above, the <code>safetySettings</code> are optional. These settings allow you to define thresholds for potentially harmful content (like hate speech, violence, or explicit material) in Gemini's output.</p>
<p>You can read more about Gemini’s safety settings <a target="_blank" href="https://ai.google.dev/gemini-api/docs/safety-settings">here</a>.</p>
<h3 id="heading-create-a-controller-to-handle-endpoint-logic">Create a Controller to Handle Endpoint Logic</h3>
<p>Now, create a <code>controller</code> folder, and inside it create a file named <code>subs.controller.js</code>. In this file, you'll handle the endpoint logic for interacting with the Gemini model.</p>
<p>In <code>server/controller/subs.controller.js</code>, add this code:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// server/controller/subs.controller.js</span>

<span class="hljs-keyword">import</span> { fileURLToPath } <span class="hljs-keyword">from</span> <span class="hljs-string">"url"</span>;
<span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">"path"</span>;
<span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;

<span class="hljs-keyword">const</span> __filename = fileURLToPath(<span class="hljs-keyword">import</span>.meta.url);  <span class="hljs-comment">//converts the module URL to a file path</span>
<span class="hljs-keyword">const</span> __dirname = path.dirname(__filename);   <span class="hljs-comment">//get the current file directory</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> uploadFile = <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">if</span> (!req.files || !req.files.video) {   <span class="hljs-comment">//if there is no file available, return error to the client</span>
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"No video uploaded"</span> });
    }

    <span class="hljs-keyword">const</span> videoFile = req.files.video;   <span class="hljs-comment">//access the video</span>
    <span class="hljs-keyword">const</span> uploadDir = path.join(__dirname, <span class="hljs-string">".."</span>, <span class="hljs-string">"uploads"</span>);   <span class="hljs-comment">//path to upload the video temporarily</span>

    <span class="hljs-keyword">if</span> (!fs.existsSync(uploadDir)) {   <span class="hljs-comment">//check if the directory exists</span>
      fs.mkdirSync(uploadDir);      <span class="hljs-comment">//if not create a new one</span>
    }

    <span class="hljs-keyword">const</span> uploadPath = path.join(uploadDir, videoFile.name);  

    <span class="hljs-keyword">await</span> videoFile.mv(uploadPath);  <span class="hljs-comment">//it moves the video from the buffer to the "upload" folder</span>

    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).json({ <span class="hljs-attr">message</span>:<span class="hljs-string">"file uploaded sucessfully"</span> });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> res
      .status(<span class="hljs-number">500</span>)
      .json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"Internal server error: "</span> + error.message });
  }
};
</code></pre>
<p>Since we are using an ES6 module, the <code>__dirname</code> is not available by default. The file handling mechanism is different compared to CommonJS. Because of this, we’ll use <code>fileURLToPath</code> to handle file paths.</p>
<p>We moved the file from the default temporary location which is the buffer to the <code>uploads</code> folder.</p>
<p>But the file upload process is not yet complete. We still need to send the file to Google AI File Manager, and after uploading, it will return a URI. This URI will then be passed to the model for video analysis.</p>
<h3 id="heading-how-to-upload-a-file-to-the-google-ai-file-manager">How to Upload a File to the Google AI File Manager</h3>
<p>Create a folder <code>utils</code> and create a file <code>fileUpload.js</code>. You can refer to the folder structure provided above.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//  server/utils/fileUpload.js</span>

<span class="hljs-keyword">import</span> { GoogleAIFileManager, FileState } <span class="hljs-keyword">from</span> <span class="hljs-string">"@google/generative-ai/server"</span>;
<span class="hljs-keyword">import</span> { configDotenv } <span class="hljs-keyword">from</span> <span class="hljs-string">"dotenv"</span>;
configDotenv();

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> fileManager = <span class="hljs-keyword">new</span> GoogleAIFileManager(process.env.API_KEY);  <span class="hljs-comment">//create a new GoogleAIFileManager instance</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fileUpload</span>(<span class="hljs-params">path, videoData</span>) </span>{  
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> uploadResponse = <span class="hljs-keyword">await</span> fileManager.uploadFile(path, {   <span class="hljs-comment">//give the path as an argument</span>
      <span class="hljs-attr">mimeType</span>: videoData.mimetype,  
      <span class="hljs-attr">displayName</span>: videoData.name,
    });
    <span class="hljs-keyword">const</span> name = uploadResponse.file.name;
    <span class="hljs-keyword">let</span> file = <span class="hljs-keyword">await</span> fileManager.getFile(name);    
    <span class="hljs-keyword">while</span> (file.state === FileState.PROCESSING) {     <span class="hljs-comment">//check the state of the file</span>
      process.stdout.write(<span class="hljs-string">"."</span>);
      <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">res</span>) =&gt;</span> <span class="hljs-built_in">setTimeout</span>(res, <span class="hljs-number">10000</span>));   <span class="hljs-comment">//check every 10 second</span>
      file = <span class="hljs-keyword">await</span> fileManager.getFile(name);
    }
    <span class="hljs-keyword">if</span> (file.state === FileState.FAILED) {   
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Video processing failed"</span>);
    }
    <span class="hljs-keyword">return</span> file;   <span class="hljs-comment">// return the file object, containing the upload file information and the uri</span>
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">throw</span> error;
  }
}
</code></pre>
<p>In the code above, we created a function called <code>fileUpload</code> that takes two arguments. These arguments will be passed from the controller function, which we'll set up later.</p>
<p>The <code>fileUpload</code> function uses the <code>fileManager.uploadFile</code> method to send the video to Google's servers. This method needs two arguments: the file path and an object containing metadata about the file (its MIME type and display name).</p>
<p>Because video processing on Google's servers takes time, we need to check the file's status. We do this using a loop that checks the file's state every 10 seconds using <code>fileManager.getFile()</code>. The loop continues as long as the file's state is <code>PROCESSING</code>. Once the state changes to either <code>SUCCESS</code> or <code>FAILED</code>, the loop stops.</p>
<p>The function then checks if the processing was successful. If so, it returns the file object, which contains information about the uploaded and processed video, including its URI. Otherwise, if the state is <code>FAILED</code>, the function throws an error.</p>
<h3 id="heading-pass-the-uri-to-the-gemini-model">Pass the URI to the Gemini Model</h3>
<p>Now in the <code>utils</code> folder, create a file called <code>genContent.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// server/utils/genContent.js</span>

<span class="hljs-keyword">import</span> model <span class="hljs-keyword">from</span> <span class="hljs-string">"../gemini/gemini.config.js"</span>;
<span class="hljs-keyword">import</span> { configDotenv } <span class="hljs-keyword">from</span> <span class="hljs-string">"dotenv"</span>;
configDotenv();

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getContent</span>(<span class="hljs-params">file</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> model.generateContent([
      {
        <span class="hljs-attr">fileData</span>: {
          <span class="hljs-attr">mimeType</span>: file.mimeType,
          <span class="hljs-attr">fileUri</span>: file.uri,
        },
      },
      {
        <span class="hljs-attr">text</span>: <span class="hljs-string">"You need to write a subtitle for this full video, write the subtitle in the SRT format, don't write anything else other than a subtitle in the response, create accurate subtitle."</span>,
      },
    ]);
    <span class="hljs-keyword">return</span> result.response.text();
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">throw</span> error;
  }
}
</code></pre>
<p>Import the model that we configured earlier. Create a function called <code>getContent</code>. The <code>getContent</code> function takes the file object (returned from the <code>fileUpload</code> function).</p>
<p>Pass the file URI and the <code>mimi</code> to the model. Then we’ll provide a prompt instructing the model to generate subtitles for the entire video in SRT format. You can also add your prompt if you want. Then return the response.</p>
<h3 id="heading-update-the-subscontrollerjs-file">Update the <code>subs.controller.js</code> File</h3>
<p>Finally, we need to update the controller file. We've created the <code>fileUpload</code> and <code>getContent</code> functions, and now we'll use them in the controller and provide the required arguments.</p>
<p>In the <code>server/controller/subs.controller.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//  server/controller/subs.controller.js</span>

<span class="hljs-keyword">import</span> { fileURLToPath } <span class="hljs-keyword">from</span> <span class="hljs-string">"url"</span>;
<span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">"path"</span>;
<span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;
<span class="hljs-keyword">import</span> { fileUpload } <span class="hljs-keyword">from</span> <span class="hljs-string">"../utils/fileUpload.js"</span>;
<span class="hljs-keyword">import</span> { getContent } <span class="hljs-keyword">from</span> <span class="hljs-string">"../utils/genContent.js"</span>;

<span class="hljs-keyword">const</span> __filename = fileURLToPath(<span class="hljs-keyword">import</span>.meta.url);
<span class="hljs-keyword">const</span> __dirname = path.dirname(__filename);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> uploadFile = <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">if</span> (!req.files || !req.files.video) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"No video uploaded"</span> });
    }

    <span class="hljs-keyword">const</span> videoFile = req.files.video;
    <span class="hljs-keyword">const</span> uploadDir = path.join(__dirname, <span class="hljs-string">".."</span>, <span class="hljs-string">"uploads"</span>);

    <span class="hljs-keyword">if</span> (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir);
    }

    <span class="hljs-keyword">const</span> uploadPath = path.join(uploadDir, videoFile.name);

    <span class="hljs-keyword">await</span> videoFile.mv(uploadPath);

    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fileUpload(uploadPath, req.files.video);  <span class="hljs-comment">//we pass 'uploadPath' and the video file data to 'fileUpload'</span>
    <span class="hljs-keyword">const</span> genContent = <span class="hljs-keyword">await</span> getContent(response);   <span class="hljs-comment">//the 'response' (containing the file URI) is passed to 'getContent'</span>

    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).json({ <span class="hljs-attr">subs</span>: genContent });   <span class="hljs-comment">//// return the generated subtitles to the client</span>
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error uploading video:"</span>, error);
    <span class="hljs-keyword">return</span> res
      .status(<span class="hljs-number">500</span>)
      .json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"Internal server error: "</span> + error.message });
  }
};
</code></pre>
<p>With this, the backend API is complete. Now, we'll move on to updating the front end.</p>
<h2 id="heading-update-the-front-end">Update the Front End</h2>
<p>Our frontend currently only allows users to select a video. In this section, we'll update it to send the video data to our backend for processing. The frontend will then receive the generated subtitles from the backend and initiate a download of the <code>.srt</code> file.</p>
<p>Navigate to the <code>client</code> folder:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> client
</code></pre>
<p>Install <code>axios</code>. We’ll use it to handle HTTP requests.</p>
<pre><code class="lang-bash">npm install axios
</code></pre>
<p>In the <code>client/src/App.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//   client/src/App.tsx</span>

<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">"axios"</span>;

<span class="hljs-keyword">const</span> App = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-keyword">async</span> (e: React.FormEvent&lt;HTMLFormElement&gt;): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; =&gt; {
    e.preventDefault();
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData(e.currentTarget);
      <span class="hljs-comment">// sending a POST request with form data</span>
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> axios.post(
        <span class="hljs-string">"http://localhost:3000/api/subs/"</span>,   
        formData
      );
<span class="hljs-comment">// creating a Blob from the server response and triggering the file download</span>
      <span class="hljs-keyword">const</span> blob = <span class="hljs-keyword">new</span> Blob([response.data.subs], { <span class="hljs-keyword">type</span>: <span class="hljs-string">"text/plain"</span> }); 
      <span class="hljs-keyword">const</span> link = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"a"</span>);
      link.href = URL.createObjectURL(blob);
      link.download = <span class="hljs-string">"subtitle.srt"</span>;
      link.click();
      link.remove();
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      &lt;form onSubmit={handleSubmit}&gt;
        &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span> accept=<span class="hljs-string">"video/*,.mkv"</span> name=<span class="hljs-string">"video"</span> /&gt;
        &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> /&gt;
      &lt;/form&gt;
    &lt;/div&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<p><code>axios</code> makes the POST request to your backend API endpoint <code>(/api/subs)</code>. The server will process the video, and this might take some time.</p>
<p>After the server sends the generated subtitles, the frontend receives them as a response. To handle this response and allow users to download the subtitles, we'll use a Blob. A Blob (Binary Large Object) is a web API object that represents raw binary data, essentially acting like a file. In our case, the subtitles returned from the server will be converted into a Blob, which will then allow us to trigger a download in the user's browser.</p>
<h2 id="heading-summary">Summary</h2>
<p>In this tutorial, you learned how to build an AI-powered subtitle generator using Google's Gemini API, React, and Express. You can upload videos, send them to the Gemini API for subtitle generation, and provide the generated subtitles for download.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>That's it! You've successfully built an AI-powered subtitle generator using the Gemini API. For quicker testing, start with shorter video clips (3-5 minutes). Longer videos might take more time to process.</p>
<p>Want to create a customizable video prompting application? Just add an input field to let users enter their prompts, send that prompt to the server, and use it in place of the hardcoded prompt. That's all it takes.</p>
<p>For more information about the Gemini API, refer to the official <a target="_blank" href="https://ai.google.dev/gemini-api/docs#node.js">Gemini API Docs</a></p>
<p>You can find the full code here: <a target="_blank" href="https://github.com/sanjayr-12/ai-subtitle-generator">AI-Subtitle-Generator</a></p>
<p>If there are any mistakes or you have any questions, contact me on <a target="_blank" href="https://www.linkedin.com/in/sanjay-r-ab6064294/">LinkedIn</a> or <a target="_blank" href="https://www.instagram.com/heheheh_pet/profilecard/?igsh=eXh3MWw4ZzZ3NTRq">Instagram</a>.</p>
<p>Thank you for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Learn to Use the Gemini AI MultiModal Model ]]>
                </title>
                <description>
                    <![CDATA[ Gemini is a suite of AI models that can understand and generate human-like responses based on the input it receives. We just published a Gemini course on the freeCodeCamp.org YouTube channel that is designed to guide you through the world of multimod... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/learn-to-use-the-gemini-ai-multimodal-model/</link>
                <guid isPermaLink="false">66c7902eba1f8664dd9a94e4</guid>
                
                    <category>
                        <![CDATA[ gemini ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Thu, 22 Aug 2024 19:23:26 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724354591577/afcfa42a-2b11-4590-b6f1-06f7837aa7fc.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Gemini is a suite of AI models that can understand and generate human-like responses based on the input it receives.</p>
<p>We just published a Gemini course on the <a target="_blank" href="http://freeCodeCamp.org">freeCodeCamp.org</a> YouTube channel that is designed to guide you through the world of multimodal AI, focusing on building an application that can interpret images and answer questions about them.</p>
<h3 id="heading-course-overview"><strong>Course Overview</strong></h3>
<p>In this course, led by the talented Ania Kubow, you'll learn how to use Google's Gemini MultiModal Model. This innovative AI model allows you to input both text and images, providing text-based responses that can enhance your applications' interactivity and functionality.</p>
<p>Here are some of the topics covered:</p>
<ul>
<li><p><strong>Introduction to Gemini</strong>: Understand the basics of Gemini, a series of multimodal generative AI models developed by Google. Learn how these models can process both text and image inputs to generate meaningful text responses.</p>
</li>
<li><p><strong>Setting Up and Authentication</strong>: Get step-by-step guidance on setting up your development environment and obtaining your API key for secure access to the Gemini API.</p>
</li>
<li><p><strong>Exploring Gemini Models</strong>: Dive into the different models available within the Gemini suite, such as gemini-pro and gemini-pro-vision, and learn how to use their methods to build applications that can see and understand images.</p>
</li>
<li><p><strong>Building the App</strong>: Follow along as we build an application that can upload images, interpret them, and answer questions. You'll also learn how to implement a feature that generates random questions for enhanced user interaction.</p>
</li>
<li><p><strong>Advanced Features</strong>: While the course focuses on the core functionalities, you'll also get a glimpse into advanced features like creating embeddings with the embedding-001 model, setting the stage for future exploration.</p>
</li>
</ul>
<h3 id="heading-understanding-gemini"><strong>Understanding Gemini</strong></h3>
<p>Gemini is a groundbreaking series of multimodal generative AI models developed by Google, designed to revolutionize how we interact with artificial intelligence. These models are capable of processing both text and image inputs, making them incredibly versatile for a wide range of applications. Let's explore what makes Gemini unique and how it can be leveraged in your projects.</p>
<p>Unlike traditional models that are limited to text or image processing, Gemini's multimodal capabilities allow it to handle both simultaneously. This means you can input a text query, an image, or a combination of both, and receive coherent, contextually relevant text responses.</p>
<p><strong>Key Features of Gemini Models</strong></p>
<ol>
<li><p><strong>Multimodal Input Processing</strong>: Gemini models can accept text and images as input, providing a seamless way to interact with AI. This capability is particularly useful for applications that require understanding visual content alongside textual information.</p>
</li>
<li><p><strong>Generative Responses</strong>: The models are designed to generate human-like text responses. Whether you're asking a simple question or engaging in a complex dialogue, Gemini can provide insightful answers.</p>
</li>
<li><p><strong>Versatile Applications</strong>: From customer service bots to educational tools, the potential applications of Gemini are vast. Developers can create apps that not only answer questions but also provide detailed explanations, descriptions, and more.</p>
</li>
<li><p><strong>API and App Integration</strong>: Gemini can be accessed via an intuitive app interface or through a robust API, allowing developers to integrate its capabilities into their own applications. This flexibility makes it easy to incorporate Gemini's features into existing workflows.</p>
</li>
</ol>
<p>By integrating Gemini into your projects, you can enhance user experiences, streamline workflows, and unlock new opportunities in the realm of AI-driven applications. As you progress through this course, you'll gain hands-on experience with these models, learning how to harness their power to build innovative solutions.</p>
<h3 id="heading-conclusion"><strong>Conclusion</strong></h3>
<p>Head over to the <a target="_blank" href="https://www.youtube.com/watch?v=vXjOywyMBN8">freeCodeCamp.org YouTube channel</a> and start your journey with the Gemini AI MultiModal Model Course (1-hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/vXjOywyMBN8" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Google Gemini Course for Beginners ]]>
                </title>
                <description>
                    <![CDATA[ Google Gemini is a cutting edge AI model that is a competitor of GPT-4.  We just released a course on the freeCodeCamp.org YouTube channel that will help you harness the power of the advanced AI technology. Developed by the popular instructor Ania Ku... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/google-gemini-course-for-beginners/</link>
                <guid isPermaLink="false">66b202ac39b555ffda8bfe90</guid>
                
                    <category>
                        <![CDATA[ gemini ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Thu, 29 Feb 2024 18:47:51 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/02/gemini3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Google Gemini is a cutting edge AI model that is a competitor of GPT-4. </p>
<p>We just released a course on the freeCodeCamp.org YouTube channel that will help you harness the power of the advanced AI technology.</p>
<p>Developed by the popular instructor Ania Kubów, this beginner's course offers a deep dive into Google's AI model and the Gemini API. Whether you're aspiring to build your own AI chatbot or simply curious about the potentials of large language models (LLMs), this course has got you covered.</p>
<p>The course is designed to be accessible, providing a thorough introduction to Google Gemini and its applications. Starting with the basics, it covers what AI is, delves into Large Language Models (LLMs), and guides you through the process of obtaining your API key—a crucial step in interacting with Gemini's capabilities.</p>
<h3 id="heading-what-you-will-learn">What You Will Learn</h3>
<p>The different sections of this course will cover the following topics:</p>
<ul>
<li><strong>Introduction to Google Gemini:</strong> Uncover what Google Gemini is and why it's a significant tool in the realm of AI development.</li>
<li><strong>Understanding AI and LLMs:</strong> Get a solid foundation in artificial intelligence and Large Language Models, crucial for grasping the capabilities and applications of Gemini.</li>
<li><strong>Getting Your API Key:</strong> A step-by-step guide on how to obtain your API key, enabling you to start experimenting with Gemini.</li>
<li><strong>Exploring Models:</strong> Learn about the different models available within Gemini and how to choose the right one for your project.</li>
<li><strong>Initializing the Generative Model:</strong> Understand how to initialize Gemini's generative model for your applications.</li>
<li><strong>Diverse Functionalities:</strong> Dive into the functionalities offered by Gemini, including text-to-text, text/image-to-text, text-to-chat, and text-to-embedding conversions.</li>
<li><strong>Building an AI Code Buddy:</strong> The course culminates in a hands-on project where you'll build an AI chatbot, showcasing the practical application of the skills you've learned.</li>
</ul>
<h3 id="heading-why-google-gemini">Why Google Gemini?</h3>
<p>Google Gemini represents a significant leap forward in AI technology, offering advanced multimodal reasoning, planning, understanding, and more. It's a tool that not only developers but also creatives, researchers, and businesses can leverage to unlock new potentials and solutions. This course is your gateway to mastering Gemini, enabling you to create, innovate, and solve complex problems with AI.</p>
<p>This course offers a comprehensive and accessible pathway into the world of AI and chatbot development. Watch the full course on <a target="_blank" href="https://www.youtube.com/watch?v=DJtX3S7qx2s">the freeCodeCamp.org YouTube channel</a> (1.5 hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/DJtX3S7qx2s" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
